fix: address graph editor UX feedback

- Hide input handle on User Input nodes and output handle on User Output
  nodes (separate userInputNode/userOutputNode node types)
- Enable edge deletion in handoff mode only (Delete key or React Flow UI);
  only agent-to-agent edges are deletable, structural edges are protected
- Block all edge mutations (add/delete) in concurrent and group chat modes
- Add agent as disconnected node without auto-rebuilding the graph
- Add sequential reorder controls (↑↓) in the inspector panel with
  position/order swap and automatic edge rebuilding
- Replace circular group chat layout with vertical column to prevent
  bidirectional edge crossings
- Tighten handoff layout spacing so edges between triage and specialists
  don't overlap nodes
- Use smoothstep edge type for cleaner edge routing across all modes
- Add 6 new tests covering reorder, disconnected add, edge deletion rules,
  and node type assertions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-24 23:09:21 +01:00
co-authored by Copilot
parent 7d078faada
commit c6c8e79e9f
7 changed files with 392 additions and 55 deletions
+19 -16
View File
@@ -210,14 +210,20 @@ function createHandoffGraph(agents: PatternAgentDefinition[]): PatternGraph {
const outputNode: PatternGraphNode = {
id: SYSTEM_NODE_IDS.userOutput,
kind: 'user-output',
position: { x: 860, y: 0 },
position: { x: 700, y: 0 },
};
const entryAgent = agents[0];
const specialistCount = Math.max(agents.length - 1, 1);
const entryNode = entryAgent
? createAgentNode(entryAgent, 0, { x: 220, y: 0 })
? createAgentNode(entryAgent, 0, { x: 200, y: 0 })
: undefined;
// Place specialists in a vertical column with enough spacing so
// bidirectional edges to/from the triage agent don't overlap.
const specialistNodes = agents.slice(1).map((agent, index) =>
createAgentNode(agent, index + 1, { x: 540, y: spreadY(index, Math.max(agents.length - 1, 1), 220) }),
createAgentNode(agent, index + 1, {
x: 460,
y: spreadY(index, specialistCount, 150),
}),
);
const nodes = [inputNode, ...(entryNode ? [entryNode] : []), ...specialistNodes, outputNode];
const edges: PatternGraphEdge[] = [];
@@ -245,24 +251,21 @@ function createGroupChatGraph(agents: PatternAgentDefinition[]): PatternGraph {
const orchestratorNode: PatternGraphNode = {
id: SYSTEM_NODE_IDS.orchestrator,
kind: 'orchestrator',
position: { x: 250, y: 0 },
position: { x: 200, y: 0 },
};
const outputNode: PatternGraphNode = {
id: SYSTEM_NODE_IDS.userOutput,
kind: 'user-output',
position: { x: 900, y: 0 },
position: { x: 660, y: 0 },
};
const centerX = 560;
const centerY = 0;
const radiusX = 190;
const radiusY = 170;
const agentNodes = agents.map((agent, index) => {
const angle = agents.length <= 1 ? 0 : (Math.PI * 2 * index) / agents.length - Math.PI / 2;
return createAgentNode(agent, index, {
x: Math.round(centerX + Math.cos(angle) * radiusX),
y: Math.round(centerY + Math.sin(angle) * radiusY),
});
});
// Place agents in a vertical column to the right of the orchestrator.
// This avoids crossing edges from the bidirectional orchestrator↔agent links.
const agentNodes = agents.map((agent, index) =>
createAgentNode(agent, index, {
x: 440,
y: spreadY(index, Math.max(agents.length, 1), 130),
}),
);
return {
nodes: [inputNode, orchestratorNode, ...agentNodes, outputNode],