From f67fab0816b6b21c37f1305e0d99460a6553a3e4 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 24 Mar 2026 23:41:00 +0100 Subject: [PATCH] fix: edge selection highlighting and group-chat connection rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CSS styling for selected edges (indigo highlight with glow) - Enable orchestrator↔agent connections in group-chat mode - Downgrade disconnected-agent validation from error to warning - Rename addHandoffEdge to addEdge (generic for all modes) - Add tests for group-chat connection rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pattern-graph/PatternGraphCanvas.tsx | 4 +- src/renderer/lib/patternGraph.ts | 8 ++-- src/renderer/styles.css | 6 +++ src/shared/domain/pattern.ts | 17 +++++--- tests/renderer/patternGraph.test.ts | 42 +++++++++++++++++-- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx index 08a971e..cd43164 100644 --- a/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx +++ b/src/renderer/components/pattern-graph/PatternGraphCanvas.tsx @@ -22,7 +22,7 @@ import type { OrchestrationMode, PatternDefinition, PatternGraph } from '@shared import { resolvePatternGraph } from '@shared/domain/pattern'; import type { ModelDefinition } from '@shared/domain/models'; import { - addHandoffEdge, + addEdge, autoLayoutGraph, fromCanvasPositions, isConnectionAllowed, @@ -143,7 +143,7 @@ function PatternGraphCanvasInner({ } if (connection.source && connection.target) { - const updatedGraph = addHandoffEdge(graph, connection.source, connection.target); + const updatedGraph = addEdge(graph, connection.source, connection.target); onGraphChange(updatedGraph); } }, diff --git a/src/renderer/lib/patternGraph.ts b/src/renderer/lib/patternGraph.ts index 8da7688..8cd38a9 100644 --- a/src/renderer/lib/patternGraph.ts +++ b/src/renderer/lib/patternGraph.ts @@ -208,8 +208,10 @@ export function isConnectionAllowed( return false; case 'handoff': return sourceNode.kind === 'agent' && targetNode.kind === 'agent'; - case 'group-chat': - return false; + case 'group-chat': { + const kinds = new Set([sourceNode.kind, targetNode.kind]); + return kinds.has('orchestrator') && kinds.has('agent'); + } default: return false; } @@ -226,7 +228,7 @@ function edgeId(source: string, target: string): string { return `edge-${source}-to-${target}`; } -export function addHandoffEdge(graph: PatternGraph, source: string, target: string): PatternGraph { +export function addEdge(graph: PatternGraph, source: string, target: string): PatternGraph { const newEdge: PatternGraphEdge = { id: edgeId(source, target), source, diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 38e378c..bb7bf7d 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -150,6 +150,12 @@ textarea { stroke-width: 1.5; } +.react-flow__edge.selected .react-flow__edge-path { + stroke: #818cf8; + stroke-width: 2.5; + filter: drop-shadow(0 0 4px rgba(129, 140, 248, 0.4)); +} + .react-flow__edge.animated .react-flow__edge-path { stroke-dasharray: 5; animation: reactflow-dash 0.5s linear infinite; diff --git a/src/shared/domain/pattern.ts b/src/shared/domain/pattern.ts index 833c7f0..927c347 100644 --- a/src/shared/domain/pattern.ts +++ b/src/shared/domain/pattern.ts @@ -531,8 +531,13 @@ function getAgentNodes(graph: PatternGraph): PatternGraphNode[] { return graph.nodes.filter((node) => node.kind === 'agent'); } -function pushGraphIssue(issues: PatternValidationIssue[], message: string, field = 'graph'): void { - issues.push({ level: 'error', field, message }); +function pushGraphIssue( + issues: PatternValidationIssue[], + message: string, + field = 'graph', + level: 'error' | 'warning' = 'error', +): void { + issues.push({ level, field, message }); } function buildAdjacency(graph: PatternGraph): { @@ -779,7 +784,7 @@ function validateHandoffGraph( for (const agentNode of agentNodes) { if (!reachable.has(agentNode.id)) { - pushGraphIssue(issues, `Handoff entry agent must be able to reach "${agentNode.agentId}".`); + pushGraphIssue(issues, `Handoff entry agent must be able to reach "${agentNode.agentId}".`, 'graph', 'warning'); } } @@ -807,7 +812,7 @@ function validateGroupChatGraph( const orchestratorSources = new Set((incoming.get(orchestratorNode.id) ?? []).map((edge) => edge.source)); if (graph.edges.length !== pattern.agents.length * 2 + 2) { - pushGraphIssue(issues, 'Group chat graphs must connect the orchestrator to every participant and then back to user output.'); + pushGraphIssue(issues, 'Group chat graphs must connect the orchestrator to every participant and then back to user output.', 'graph', 'warning'); } if ((outgoing.get(inputNode.id) ?? []).some((edge) => edge.target !== orchestratorNode.id)) { @@ -820,11 +825,11 @@ function validateGroupChatGraph( for (const agentNode of agentNodes) { if (!orchestratorTargets.has(agentNode.id)) { - pushGraphIssue(issues, `Orchestrator must connect to agent "${agentNode.agentId}".`); + pushGraphIssue(issues, `Orchestrator must connect to agent "${agentNode.agentId}".`, 'graph', 'warning'); } if (!orchestratorSources.has(agentNode.id)) { - pushGraphIssue(issues, `Agent "${agentNode.agentId}" must connect back to the orchestrator.`); + pushGraphIssue(issues, `Agent "${agentNode.agentId}" must connect back to the orchestrator.`, 'graph', 'warning'); } } } diff --git a/tests/renderer/patternGraph.test.ts b/tests/renderer/patternGraph.test.ts index 290b13d..235c10c 100644 --- a/tests/renderer/patternGraph.test.ts +++ b/tests/renderer/patternGraph.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test'; import { createBuiltinPatterns, resolvePatternGraph, type PatternDefinition } from '@shared/domain/pattern'; import { addAgentNodeToGraph, - addHandoffEdge, + addEdge, autoLayoutGraph, canMoveSequential, findAgentForNode, @@ -148,19 +148,53 @@ describe('pattern graph connection rules', () => { ); expect(allowed).toBe(false); }); + + test('group-chat mode allows orchestrator-to-agent connections', () => { + const pattern = findPattern('group-chat'); + const graph = resolvePatternGraph(pattern); + const orchestratorNode = graph.nodes.find((n) => n.kind === 'orchestrator')!; + const agentNode = graph.nodes.find((n) => n.kind === 'agent')!; + + const orcToAgent = isConnectionAllowed( + { source: orchestratorNode.id, target: agentNode.id, sourceHandle: null, targetHandle: null }, + 'group-chat', + graph, + ); + expect(orcToAgent).toBe(true); + + const agentToOrc = isConnectionAllowed( + { source: agentNode.id, target: orchestratorNode.id, sourceHandle: null, targetHandle: null }, + 'group-chat', + graph, + ); + expect(agentToOrc).toBe(true); + }); + + test('group-chat mode disallows agent-to-agent connections', () => { + const pattern = findPattern('group-chat'); + const graph = resolvePatternGraph(pattern); + const agentNodes = graph.nodes.filter((n) => n.kind === 'agent'); + + const allowed = isConnectionAllowed( + { source: agentNodes[0]!.id, target: agentNodes[1]!.id, sourceHandle: null, targetHandle: null }, + 'group-chat', + graph, + ); + expect(allowed).toBe(false); + }); }); describe('pattern graph mutation helpers', () => { - test('addHandoffEdge adds a new edge between agent nodes', () => { + test('addEdge adds a new edge between agent nodes', () => { const pattern = findPattern('handoff'); const graph = resolvePatternGraph(pattern); const agentNodes = graph.nodes.filter((n) => n.kind === 'agent'); const initialEdgeCount = graph.edges.length; - const updated = addHandoffEdge(graph, agentNodes[1]!.id, agentNodes[2]!.id); + const updated = addEdge(graph, agentNodes[1]!.id, agentNodes[2]!.id); expect(updated.edges.length).toBe(initialEdgeCount + 1); - const duplicated = addHandoffEdge(updated, agentNodes[1]!.id, agentNodes[2]!.id); + const duplicated = addEdge(updated, agentNodes[1]!.id, agentNodes[2]!.id); expect(duplicated.edges.length).toBe(initialEdgeCount + 1); });