@@ -21,12 +21,13 @@ import { toast } from 'sonner';
2121import { commandManager } from '@/features/graph/commands/command-manager' ;
2222import { AddNodeCommand , AddPreparedEdgeCommand } from '@/features/graph/commands/commands' ;
2323import {
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'
3031import { useGraphShortcuts } from '@/features/graph/ui/use-graph-shortcuts' ;
3132import { useGraphErrors } from '@/hooks/use-graph-errors' ;
3233import { 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 (
0 commit comments