mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-27 23:18:46 +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> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user