Skip to content

Commit d123719

Browse files
author
Juan Roldan
committed
Merge branch 'worktree-workflow-canvas-engine'
2 parents 2dea2e3 + 5f859bb commit d123719

4 files changed

Lines changed: 88 additions & 6 deletions

File tree

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/orchestrator/server.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,8 +1258,17 @@ export function createServer() {
12581258
if (!managed) { res.status(404).json({ error: 'Agent not found' }); return; }
12591259
const { sourceId, targetId, sourceHandle, condition } = req.body;
12601260
if (!sourceId || !targetId) { res.status(400).json({ error: 'sourceId and targetId required' }); return; }
1261-
const edge = addEdge(req.params.id, req.params.wfId, { sourceId, targetId, sourceHandle, condition });
1262-
res.status(201).json(edge);
1261+
try {
1262+
const edge = addEdge(req.params.id, req.params.wfId, { sourceId, targetId, sourceHandle, condition });
1263+
res.status(201).json(edge);
1264+
} catch (err) {
1265+
const msg = err instanceof Error ? err.message : String(err);
1266+
if (msg.includes('FOREIGN KEY')) {
1267+
res.status(400).json({ error: 'sourceId or targetId does not reference an existing node' });
1268+
} else {
1269+
res.status(500).json({ error: msg });
1270+
}
1271+
}
12631272
});
12641273

12651274
app.delete('/agents/:id/workflows/:wfId/edges/:edgeId', (req, res) => {

packages/frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
"test:e2e": "playwright test"
1313
},
1414
"dependencies": {
15+
"@dagrejs/dagre": "^3.0.0",
1516
"@tailwindcss/typography": "^0.5.19",
17+
"@xyflow/react": "^12.6.0",
1618
"chart.js": "^4.5.1",
1719
"posthog-js": "^1.368.2",
18-
"@xyflow/react": "^12.6.0",
1920
"react": "^18.3.1",
2021
"react-chartjs-2": "^5.3.1",
2122
"react-dom": "^18.3.1",

packages/frontend/src/components/workflow-canvas/WorkflowCanvas.tsx

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
addEdge,
55
useNodesState,
66
useEdgesState,
7+
useReactFlow,
8+
ReactFlowProvider,
79
Controls,
810
MiniMap,
911
Background,
@@ -13,6 +15,7 @@ import {
1315
type Node,
1416
} from '@xyflow/react';
1517
import '@xyflow/react/dist/style.css';
18+
import dagre from '@dagrejs/dagre';
1619

1720
import { nodeTypes } from './nodes';
1821
import { NodeConfigPanel } from './NodeConfigPanel';
@@ -93,22 +96,61 @@ function fromReactFlowEdges(edges: Edge[]): Omit<WorkflowEdge, 'workflowId'>[] {
9396
}));
9497
}
9598

99+
const NODE_WIDTH = 200;
100+
const NODE_HEIGHT = 80;
101+
102+
function layoutGraph(nodes: Node[], edges: Edge[]): Node[] {
103+
const g = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
104+
g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 100, marginx: 40, marginy: 40 });
105+
106+
for (const node of nodes) {
107+
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
108+
}
109+
for (const edge of edges) {
110+
g.setEdge(edge.source, edge.target);
111+
}
112+
113+
dagre.layout(g);
114+
115+
return nodes.map(node => {
116+
const pos = g.node(node.id);
117+
return { ...node, position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 } };
118+
});
119+
}
120+
96121
let nodeIdCounter = 0;
97122

98-
export function WorkflowCanvas({ agentId, workflowId }: Props) {
123+
function WorkflowCanvasInner({ agentId, workflowId }: Props) {
99124
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
100125
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
101126
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
102127
const [saving, setSaving] = useState(false);
103128
const [dirty, setDirty] = useState(false);
104129
const loaded = useRef(false);
130+
const { fitView } = useReactFlow();
131+
132+
const applyLayout = useCallback(() => {
133+
setNodes(nds => {
134+
const laid = layoutGraph(nds, edges);
135+
setTimeout(() => fitView({ padding: 0.2, duration: 300 }), 50);
136+
return laid;
137+
});
138+
setDirty(true);
139+
}, [edges, fitView]);
105140

106141
// Load graph
107142
useEffect(() => {
108143
fetchWorkflowGraph(agentId, workflowId).then(graph => {
109144
if (graph.nodes.length > 0) {
110-
setNodes(graph.nodes.map(toReactFlowNode));
111-
setEdges(graph.edges.map(toReactFlowEdge));
145+
const rfNodes = graph.nodes.map(toReactFlowNode);
146+
const rfEdges = graph.edges.map(toReactFlowEdge);
147+
const allAtOrigin = rfNodes.every(n => n.position.x === 0 && n.position.y === 0);
148+
if (allAtOrigin) {
149+
setNodes(layoutGraph(rfNodes, rfEdges));
150+
} else {
151+
setNodes(rfNodes);
152+
}
153+
setEdges(rfEdges);
112154
} else {
113155
// Empty graph — seed with trigger + end
114156
const triggerId = crypto.randomUUID();
@@ -227,6 +269,12 @@ export function WorkflowCanvas({ agentId, workflowId }: Props) {
227269
{nodes.length} nodes · {edges.length} edges
228270
{dirty && <span className="text-warning ml-2">• unsaved</span>}
229271
</span>
272+
<button
273+
className={buttonGhost + ' !text-[10px]'}
274+
onClick={applyLayout}
275+
>
276+
Auto Layout
277+
</button>
230278
<button
231279
className={buttonGhost + ' !text-[10px]'}
232280
onClick={handleSave}
@@ -291,3 +339,11 @@ export function WorkflowCanvas({ agentId, workflowId }: Props) {
291339
</div>
292340
);
293341
}
342+
343+
export function WorkflowCanvas(props: Props) {
344+
return (
345+
<ReactFlowProvider>
346+
<WorkflowCanvasInner {...props} />
347+
</ReactFlowProvider>
348+
);
349+
}

0 commit comments

Comments
 (0)