Skip to content

Commit 098c439

Browse files
authored
relayouts the graph using Dagre when a replace change causes node intersections (#459)
* relayouts the graph using Dagre when a `replace` change causes node intersections * Update agents-manage-ui/src/components/graph/graph.tsx
1 parent 6003748 commit 098c439

File tree

3 files changed

+44
-6
lines changed

3 files changed

+44
-6
lines changed

.changeset/blue-lemons-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-manage-ui": patch
3+
---
4+
5+
relayouts the graph using Dagre when a `replace` change causes node intersections

agents-manage-ui/src/components/graph/graph.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import { toast } from 'sonner';
2121
import { commandManager } from '@/features/graph/commands/command-manager';
2222
import { AddNodeCommand, AddPreparedEdgeCommand } from '@/features/graph/commands/commands';
2323
import {
24+
applyDagreLayout,
2425
deserializeGraphData,
2526
type ExtendedFullGraphDefinition,
2627
extractGraphMetadata,
2728
serializeGraphData,
2829
} from '@/features/graph/domain';
29-
import { useGraphActions, useGraphStore } from '@/features/graph/state/use-graph-store';
30+
import { useGraphActions, useGraphStore } from '@/features/graph/state/use-graph-store'
3031
import { useGraphShortcuts } from '@/features/graph/ui/use-graph-shortcuts';
3132
import { useGraphErrors } from '@/hooks/use-graph-errors';
3233
import { useSidePane } from '@/hooks/use-side-pane';
@@ -179,7 +180,7 @@ function Flow({
179180
return lookup;
180181
}, [graph?.agents]);
181182

182-
const { screenToFlowPosition, updateNodeData, fitView } = useReactFlow();
183+
const { screenToFlowPosition, updateNodeData, fitView, getNodes, getEdges, getIntersectingNodes } = useReactFlow();
183184
const { storeNodes, edges, metadata } = useGraphStore((state) => ({
184185
storeNodes: state.nodes,
185186
edges: state.edges,
@@ -188,7 +189,7 @@ function Flow({
188189
const {
189190
setNodes,
190191
setEdges,
191-
onNodesChange,
192+
onNodesChange: storeOnNodesChange,
192193
onEdgesChange,
193194
setMetadata,
194195
setInitial,
@@ -202,6 +203,38 @@ function Flow({
202203
const { nodeId, edgeId, setQueryState, openGraphPane, isOpen } = useSidePane();
203204
const { errors, showErrors, setErrors, clearErrors, setShowErrors } = useGraphErrors();
204205

206+
/**
207+
* Custom `onNodesChange` handler that relayouts the graph using Dagre
208+
* when a `replace` change causes node intersections.
209+
**/
210+
const onNodesChange: typeof storeOnNodesChange = useCallback((changes) => {
211+
storeOnNodesChange(changes);
212+
213+
const replaceChanges = changes.filter(change => change.type === 'replace');
214+
if (!replaceChanges.length) {
215+
return
216+
}
217+
// Using `setTimeout` instead of `requestAnimationFrame` ensures updated node positions are available,
218+
// as `requestAnimationFrame` may run too early, causing `hasIntersections` to incorrectly return false.
219+
setTimeout(() => {
220+
const currentNodes = getNodes();
221+
// Check if any of the replaced nodes are intersecting with others
222+
for (const change of replaceChanges) {
223+
const node = currentNodes.find(n => n.id === change.id);
224+
if (!node) {
225+
continue
226+
}
227+
// Use React Flow's intersection detection
228+
const intersectingNodes = getIntersectingNodes(node);
229+
if (intersectingNodes.length > 0) {
230+
// Apply Dagre layout to resolve intersections
231+
setNodes((prev) => applyDagreLayout(prev, getEdges()))
232+
return // exit loop
233+
}
234+
}
235+
}, 0)
236+
}, [getNodes, getEdges, getIntersectingNodes, setNodes, storeOnNodesChange]);
237+
205238
// biome-ignore lint/correctness/useExhaustiveDependencies: we only want to run this effect on first render
206239
useEffect(() => {
207240
setInitial(

agents-manage-ui/src/features/graph/domain/deserialize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ function calculateNodeHeight(node: Node): number {
6464
return Math.max(height, BASE_NODE_HEIGHT);
6565
}
6666

67-
function applyDagreLayout(nodes: Node[], edges: Edge[]): Node[] {
68-
const g = new (dagre as any).graphlib.Graph();
67+
export function applyDagreLayout(nodes: Node[], edges: Edge[]): Node[] {
68+
const g = new dagre.graphlib.Graph();
6969
g.setGraph({
7070
rankdir: 'TB',
7171
nodesep: 150,
@@ -85,7 +85,7 @@ function applyDagreLayout(nodes: Node[], edges: Edge[]): Node[] {
8585
g.setEdge(edge.source, edge.target);
8686
}
8787

88-
(dagre as any).layout(g);
88+
dagre.layout(g);
8989

9090
return nodes.map((node) => {
9191
const nodeWithPosition = g.node(node.id);

0 commit comments

Comments
 (0)