mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
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:
@@ -710,13 +710,15 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deletePattern(patternId: string): Promise<WorkspaceState> {
|
async deletePattern(patternId: string): Promise<WorkspaceState> {
|
||||||
if (isBuiltinPattern(patternId)) {
|
|
||||||
throw new Error('Built-in patterns cannot be deleted.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = await this.loadWorkspace();
|
const workspace = await this.loadWorkspace();
|
||||||
workspace.patterns = workspace.patterns.filter((pattern) => pattern.id !== patternId);
|
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) {
|
if (workspace.selectedPatternId === patternId) {
|
||||||
workspace.selectedPatternId = workspace.patterns[0]?.id;
|
workspace.selectedPatternId = workspace.patterns[0]?.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,16 @@ import {
|
|||||||
} from '@main/persistence/appPaths';
|
} from '@main/persistence/appPaths';
|
||||||
import { readJsonFile, writeJsonFile } from '@main/persistence/jsonStore';
|
import { readJsonFile, writeJsonFile } from '@main/persistence/jsonStore';
|
||||||
|
|
||||||
function mergePatterns(existingPatterns: PatternDefinition[]): PatternDefinition[] {
|
function mergePatterns(existingPatterns: PatternDefinition[], deletedBuiltinIds: string[]): PatternDefinition[] {
|
||||||
const builtinTimestamp = nowIso();
|
const builtinTimestamp = nowIso();
|
||||||
const builtinPatterns = createBuiltinPatterns(builtinTimestamp);
|
const builtinPatterns = createBuiltinPatterns(builtinTimestamp);
|
||||||
const builtinIds = new Set(builtinPatterns.map((pattern) => pattern.id));
|
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 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);
|
const existing = existingMap.get(builtin.id);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return builtin;
|
return builtin;
|
||||||
@@ -108,9 +111,11 @@ export class WorkspaceRepository {
|
|||||||
}));
|
}));
|
||||||
const settings = normalizeWorkspaceSettings(stored.settings);
|
const settings = normalizeWorkspaceSettings(stored.settings);
|
||||||
|
|
||||||
|
const deletedBuiltinPatternIds = stored.deletedBuiltinPatternIds ?? [];
|
||||||
|
|
||||||
const workspace: WorkspaceState = {
|
const workspace: WorkspaceState = {
|
||||||
...stored,
|
...stored,
|
||||||
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
|
patterns: mergePatterns(stored.patterns ?? [], deletedBuiltinPatternIds).map((pattern) => ({
|
||||||
...pattern,
|
...pattern,
|
||||||
approvalPolicy: applyDefaultToolApprovalPolicy(pattern.approvalPolicy),
|
approvalPolicy: applyDefaultToolApprovalPolicy(pattern.approvalPolicy),
|
||||||
graph: resolvePatternGraph(pattern),
|
graph: resolvePatternGraph(pattern),
|
||||||
@@ -118,6 +123,7 @@ export class WorkspaceRepository {
|
|||||||
projects,
|
projects,
|
||||||
sessions,
|
sessions,
|
||||||
settings,
|
settings,
|
||||||
|
deletedBuiltinPatternIds,
|
||||||
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
|
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
|
||||||
? stored.selectedProjectId
|
? stored.selectedProjectId
|
||||||
: projects[0]?.id,
|
: projects[0]?.id,
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export function PatternEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="no-drag flex items-center gap-2">
|
<div className="no-drag flex items-center gap-2">
|
||||||
{!isBuiltin && onDelete && (
|
{onDelete && (
|
||||||
<button
|
<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"
|
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}
|
onClick={onDelete}
|
||||||
|
|||||||
@@ -165,12 +165,10 @@ export function SettingsPanel({
|
|||||||
onBack={() => setEditingPattern(null)}
|
onBack={() => setEditingPattern(null)}
|
||||||
onChange={setEditingPattern}
|
onChange={setEditingPattern}
|
||||||
onDelete={
|
onDelete={
|
||||||
isBuiltin
|
async () => {
|
||||||
? undefined
|
await onDeletePattern(editingPattern.id);
|
||||||
: async () => {
|
setEditingPattern(null);
|
||||||
await onDeletePattern(editingPattern.id);
|
}
|
||||||
setEditingPattern(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onSave={async () => {
|
onSave={async () => {
|
||||||
await onSavePattern(editingPattern);
|
await onSavePattern(editingPattern);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface WorkspaceState {
|
|||||||
patterns: PatternDefinition[];
|
patterns: PatternDefinition[];
|
||||||
sessions: SessionRecord[];
|
sessions: SessionRecord[];
|
||||||
settings: WorkspaceSettings;
|
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. */
|
/** Runtime-only MCP probe progress for live UI updates. */
|
||||||
mcpProbingServerIds?: string[];
|
mcpProbingServerIds?: string[];
|
||||||
selectedProjectId?: string;
|
selectedProjectId?: string;
|
||||||
|
|||||||
@@ -59,6 +59,47 @@ function requirePattern(workspace: WorkspaceState, mode: PatternDefinition['mode
|
|||||||
return pattern;
|
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', () => {
|
describe('AryxAppService savePattern', () => {
|
||||||
test('preserves a provided custom graph instead of re-syncing it', async () => {
|
test('preserves a provided custom graph instead of re-syncing it', async () => {
|
||||||
const workspace = createWorkspaceSeed();
|
const workspace = createWorkspaceSeed();
|
||||||
|
|||||||
Reference in New Issue
Block a user