feat: allow deletion of built-in orchestration patterns

Remove the restriction that prevented users from deleting built-in
patterns (those with IDs prefixed 'pattern-'). Built-in patterns are
intended as starting points, not permanent fixtures.

Changes:
- Add deletedBuiltinPatternIds field to WorkspaceState to track which
  built-in patterns the user has removed
- Update mergePatterns to skip deleted built-ins during workspace load,
  preventing them from being re-added
- Remove the isBuiltinPattern guard from AryxAppService.deletePattern;
  when a built-in is deleted, its ID is recorded in the tracking list
- Enable the delete button in SettingsPanel and PatternEditor for all
  patterns (keep the 'Built-in pattern' label for context)
- Add 3 tests covering built-in deletion, custom deletion, and
  selectedPatternId fallback

Users can still restore deleted built-ins via workspace reset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-02 17:31:49 +02:00
co-authored by Copilot
parent e39fffaf3b
commit c0aaffa39f
6 changed files with 63 additions and 14 deletions
+6 -4
View File
@@ -710,13 +710,15 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
}
async deletePattern(patternId: string): Promise<WorkspaceState> {
if (isBuiltinPattern(patternId)) {
throw new Error('Built-in patterns cannot be deleted.');
}
const workspace = await this.loadWorkspace();
workspace.patterns = workspace.patterns.filter((pattern) => pattern.id !== patternId);
if (isBuiltinPattern(patternId)) {
const deletedIds = new Set(workspace.deletedBuiltinPatternIds ?? []);
deletedIds.add(patternId);
workspace.deletedBuiltinPatternIds = [...deletedIds];
}
if (workspace.selectedPatternId === patternId) {
workspace.selectedPatternId = workspace.patterns[0]?.id;
}
+9 -3
View File
@@ -30,13 +30,16 @@ import {
} from '@main/persistence/appPaths';
import { readJsonFile, writeJsonFile } from '@main/persistence/jsonStore';
function mergePatterns(existingPatterns: PatternDefinition[]): PatternDefinition[] {
function mergePatterns(existingPatterns: PatternDefinition[], deletedBuiltinIds: string[]): PatternDefinition[] {
const builtinTimestamp = nowIso();
const builtinPatterns = createBuiltinPatterns(builtinTimestamp);
const builtinIds = new Set(builtinPatterns.map((pattern) => pattern.id));
const deletedSet = new Set(deletedBuiltinIds);
const existingMap = new Map(existingPatterns.map((pattern) => [pattern.id, pattern]));
const mergedBuiltins = builtinPatterns.map((builtin) => {
const mergedBuiltins = builtinPatterns
.filter((builtin) => !deletedSet.has(builtin.id))
.map((builtin) => {
const existing = existingMap.get(builtin.id);
if (!existing) {
return builtin;
@@ -108,9 +111,11 @@ export class WorkspaceRepository {
}));
const settings = normalizeWorkspaceSettings(stored.settings);
const deletedBuiltinPatternIds = stored.deletedBuiltinPatternIds ?? [];
const workspace: WorkspaceState = {
...stored,
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
patterns: mergePatterns(stored.patterns ?? [], deletedBuiltinPatternIds).map((pattern) => ({
...pattern,
approvalPolicy: applyDefaultToolApprovalPolicy(pattern.approvalPolicy),
graph: resolvePatternGraph(pattern),
@@ -118,6 +123,7 @@ export class WorkspaceRepository {
projects,
sessions,
settings,
deletedBuiltinPatternIds,
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
? stored.selectedProjectId
: projects[0]?.id,
+1 -1
View File
@@ -311,7 +311,7 @@ export function PatternEditor({
</div>
</div>
<div className="no-drag flex items-center gap-2">
{!isBuiltin && onDelete && (
{onDelete && (
<button
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[13px] text-[var(--color-status-error)] transition-all duration-200 hover:bg-[var(--color-status-error)]/10"
onClick={onDelete}
+4 -6
View File
@@ -165,12 +165,10 @@ export function SettingsPanel({
onBack={() => setEditingPattern(null)}
onChange={setEditingPattern}
onDelete={
isBuiltin
? undefined
: async () => {
await onDeletePattern(editingPattern.id);
setEditingPattern(null);
}
async () => {
await onDeletePattern(editingPattern.id);
setEditingPattern(null);
}
}
onSave={async () => {
await onSavePattern(editingPattern);
+2
View File
@@ -10,6 +10,8 @@ export interface WorkspaceState {
patterns: PatternDefinition[];
sessions: SessionRecord[];
settings: WorkspaceSettings;
/** IDs of built-in patterns the user has deleted. Prevents re-adding on load. */
deletedBuiltinPatternIds?: string[];
/** Runtime-only MCP probe progress for live UI updates. */
mcpProbingServerIds?: string[];
selectedProjectId?: string;
+41
View File
@@ -59,6 +59,47 @@ function requirePattern(workspace: WorkspaceState, mode: PatternDefinition['mode
return pattern;
}
describe('AryxAppService deletePattern', () => {
test('deletes a built-in pattern and tracks its ID', async () => {
const workspace = createWorkspaceSeed();
const builtinPattern = requirePattern(workspace, 'sequential');
const service = createService(workspace);
const result = await service.deletePattern(builtinPattern.id);
expect(result.patterns.find((p) => p.id === builtinPattern.id)).toBeUndefined();
expect(result.deletedBuiltinPatternIds).toContain(builtinPattern.id);
});
test('deletes a custom pattern without tracking its ID', async () => {
const workspace = createWorkspaceSeed();
const customPattern: PatternDefinition = {
...requirePattern(workspace, 'single'),
id: 'custom-pattern',
name: 'My Custom Pattern',
};
workspace.patterns.push(customPattern);
const service = createService(workspace);
const result = await service.deletePattern('custom-pattern');
expect(result.patterns.find((p) => p.id === 'custom-pattern')).toBeUndefined();
expect(result.deletedBuiltinPatternIds ?? []).not.toContain('custom-pattern');
});
test('selects first remaining pattern when the active pattern is deleted', async () => {
const workspace = createWorkspaceSeed();
const targetPattern = requirePattern(workspace, 'single');
workspace.selectedPatternId = targetPattern.id;
const service = createService(workspace);
const result = await service.deletePattern(targetPattern.id);
expect(result.selectedPatternId).toBe(result.patterns[0]?.id);
expect(result.selectedPatternId).not.toBe(targetPattern.id);
});
});
describe('AryxAppService savePattern', () => {
test('preserves a provided custom graph instead of re-syncing it', async () => {
const workspace = createWorkspaceSeed();