From f0d7d445891b5645179c2810dc47bd3575aaffde Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 24 Mar 2026 23:34:38 +0100 Subject: [PATCH] feat: enable edge deletion in group-chat mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend edge deletion to group-chat orchestrator↔agent edges in addition to handoff agent↔agent edges. Structural edges (user-input/output, distributor/collector) remain non-deletable across all modes. Sequential, concurrent, and single modes keep fully structural topologies with no user-deletable edges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pattern-graph/PatternGraphCanvas.tsx | 15 +++------ src/renderer/lib/patternGraph.ts | 32 +++++++++++++------ tests/renderer/patternGraph.test.ts | 18 +++++++++-- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx index f90e70e..08a971e 100644 --- a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx +++ b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx @@ -114,18 +114,11 @@ function PatternGraphCanvasInner({ const handleEdgesChange: OnEdgesChange = useCallback( (changes) => { - // Block all edge deletions in modes that don't support it - if (!isEdgeDeletionAllowed(pattern.mode)) { - const filtered = changes.filter((c) => c.type !== 'remove'); - if (filtered.length > 0) { - onEdgesChange(filtered); - } - return; - } - - // For handoff mode, apply removals to the authoritative graph + // Route edge removals through the authoritative graph. + // Non-deletable edges are already protected by the per-edge `deletable` + // flag, so React Flow will not emit removal changes for them. const removals = changes.filter((c) => c.type === 'remove'); - if (removals.length > 0) { + if (removals.length > 0 && isEdgeDeletionAllowed(pattern.mode)) { let updatedGraph = graph; for (const removal of removals) { if (removal.type === 'remove') { diff --git a/src/renderer/lib/patternGraph.ts b/src/renderer/lib/patternGraph.ts index 3406cd6..8da7688 100644 --- a/src/renderer/lib/patternGraph.ts +++ b/src/renderer/lib/patternGraph.ts @@ -101,18 +101,32 @@ export function toCanvasNodes( }); } -/** Determines whether user-created edges can be deleted in this mode. */ +/** Determines whether an edge can be deleted by the user. + * + * Deletable edges: + * - handoff: agent ↔ agent edges (handoff routes) + * - group-chat: orchestrator ↔ agent edges (participation links) + * + * Structural edges (user-input/output, distributor/collector) are never deletable. + * Sequential, concurrent, and single modes have fully structural topologies. + */ function isEdgeDeletable(edge: PatternGraphEdge, mode: OrchestrationMode, graph: PatternGraph): boolean { - if (mode !== 'handoff') { + const sourceNode = graph.nodes.find((n) => n.id === edge.source); + const targetNode = graph.nodes.find((n) => n.id === edge.target); + if (!sourceNode || !targetNode) { return false; } - const sourceNode = graph.nodes.find((n) => n.id === edge.source); - const targetNode = graph.nodes.find((n) => n.id === edge.target); + if (mode === 'handoff') { + return sourceNode.kind === 'agent' && targetNode.kind === 'agent'; + } - // Only agent↔agent edges in handoff mode are user-deletable; - // system edges (user-input → triage, agent → user-output) are structural. - return sourceNode?.kind === 'agent' && targetNode?.kind === 'agent'; + if (mode === 'group-chat') { + const kinds = new Set([sourceNode.kind, targetNode.kind]); + return kinds.has('orchestrator') && kinds.has('agent'); + } + + return false; } const EDGE_COLORS = { @@ -201,9 +215,9 @@ export function isConnectionAllowed( } } -/** Whether edges can be deleted by the user in this mode. */ +/** Whether any edges can be deleted by the user in this mode. */ export function isEdgeDeletionAllowed(mode: OrchestrationMode): boolean { - return mode === 'handoff'; + return mode === 'handoff' || mode === 'group-chat'; } /* ── Graph mutation helpers ─────────────────────────────────── */ diff --git a/tests/renderer/patternGraph.test.ts b/tests/renderer/patternGraph.test.ts index 7d2c57c..290b13d 100644 --- a/tests/renderer/patternGraph.test.ts +++ b/tests/renderer/patternGraph.test.ts @@ -226,11 +226,11 @@ describe('sequential reorder', () => { }); describe('edge deletion rules', () => { - test('only handoff mode allows edge deletion', () => { + test('handoff and group-chat modes allow edge deletion', () => { expect(isEdgeDeletionAllowed('handoff')).toBe(true); + expect(isEdgeDeletionAllowed('group-chat')).toBe(true); expect(isEdgeDeletionAllowed('sequential')).toBe(false); expect(isEdgeDeletionAllowed('concurrent')).toBe(false); - expect(isEdgeDeletionAllowed('group-chat')).toBe(false); expect(isEdgeDeletionAllowed('single')).toBe(false); }); @@ -248,6 +248,20 @@ describe('edge deletion rules', () => { expect(nonDeletableEdges.length).toBeGreaterThan(0); }); + test('group-chat marks orchestrator↔agent edges as deletable', () => { + const pattern = findPattern('group-chat'); + const graph = resolvePatternGraph(pattern); + const edges = toCanvasEdges(graph, 'group-chat'); + + const deletableEdges = edges.filter((e) => e.deletable); + const nonDeletableEdges = edges.filter((e) => !e.deletable); + + // Orchestrator↔agent edges should be deletable + expect(deletableEdges.length).toBeGreaterThan(0); + // Structural edges (user-input → orchestrator, orchestrator → user-output) + expect(nonDeletableEdges.length).toBeGreaterThan(0); + }); + test('user-input nodes use userInputNode type and user-output use userOutputNode type', () => { const pattern = findPattern('sequential'); const graph = resolvePatternGraph(pattern);