From 1bc476f10bee271743f3991a67c88bb6807ec524 Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:37:12 -0800 Subject: [PATCH] fix(copilot): panning on workflow (#3057) --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 71 +++++++++---------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 07c8c5468..2e305431a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1800,37 +1800,32 @@ const WorkflowContent = React.memo(() => { ) }, [screenToFlowPosition, handleToolbarDrop]) - /** - * Focus canvas on changed blocks when diff appears. - */ + /** Tracks blocks to pan to after diff updates. */ const pendingZoomBlockIdsRef = useRef | null>(null) - const prevDiffReadyRef = useRef(false) + const seenDiffBlocksRef = useRef>(new Set()) - // Phase 1: When diff becomes ready, record which blocks we want to zoom to - // Phase 2 effect is located after displayNodes is defined (search for "Phase 2") + /** Queues newly changed blocks for viewport panning. */ useEffect(() => { - if (isDiffReady && !prevDiffReadyRef.current && diffAnalysis) { - // Diff just became ready - record blocks to zoom to - const changedBlockIds = [ - ...(diffAnalysis.new_blocks || []), - ...(diffAnalysis.edited_blocks || []), - ] - - if (changedBlockIds.length > 0) { - pendingZoomBlockIdsRef.current = new Set(changedBlockIds) - } else { - // No specific blocks to focus on, fit all after a frame - pendingZoomBlockIdsRef.current = null - requestAnimationFrame(() => { - fitViewToBounds({ padding: 0.1, duration: 600 }) - }) - } - } else if (!isDiffReady && prevDiffReadyRef.current) { - // Diff was cleared (accepted/rejected) - cancel any pending zoom + if (!isDiffReady || !diffAnalysis) { pendingZoomBlockIdsRef.current = null + seenDiffBlocksRef.current.clear() + return } - prevDiffReadyRef.current = isDiffReady - }, [isDiffReady, diffAnalysis, fitViewToBounds]) + + const newBlocks = new Set() + const allBlocks = [...(diffAnalysis.new_blocks || []), ...(diffAnalysis.edited_blocks || [])] + + for (const id of allBlocks) { + if (!seenDiffBlocksRef.current.has(id)) { + newBlocks.add(id) + } + seenDiffBlocksRef.current.add(id) + } + + if (newBlocks.size > 0) { + pendingZoomBlockIdsRef.current = newBlocks + } + }, [isDiffReady, diffAnalysis]) /** Displays trigger warning notifications. */ useEffect(() => { @@ -2238,18 +2233,12 @@ const WorkflowContent = React.memo(() => { }) }, [derivedNodes, blocks, pendingSelection, clearPendingSelection]) - // Phase 2: When displayNodes updates, check if pending zoom blocks are ready - // (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined) + /** Pans viewport to pending blocks once they have valid dimensions. */ useEffect(() => { const pendingBlockIds = pendingZoomBlockIdsRef.current - if (!pendingBlockIds || pendingBlockIds.size === 0) { - return - } + if (!pendingBlockIds || pendingBlockIds.size === 0) return - // Find the nodes we're waiting for const pendingNodes = displayNodes.filter((node) => pendingBlockIds.has(node.id)) - - // Check if all expected nodes are present with valid dimensions const allNodesReady = pendingNodes.length === pendingBlockIds.size && pendingNodes.every( @@ -2261,16 +2250,20 @@ const WorkflowContent = React.memo(() => { ) if (allNodesReady) { - logger.info('Diff ready - focusing on changed blocks', { + logger.info('Focusing on changed blocks', { changedBlockIds: Array.from(pendingBlockIds), foundNodes: pendingNodes.length, }) - // Clear pending state before zooming to prevent re-triggers pendingZoomBlockIdsRef.current = null - // Use requestAnimationFrame to ensure React has finished rendering + + const nodesWithAbsolutePositions = pendingNodes.map((node) => ({ + ...node, + position: getNodeAbsolutePosition(node.id), + })) + requestAnimationFrame(() => { fitViewToBounds({ - nodes: pendingNodes, + nodes: nodesWithAbsolutePositions, duration: 600, padding: 0.1, minZoom: 0.5, @@ -2278,7 +2271,7 @@ const WorkflowContent = React.memo(() => { }) }) } - }, [displayNodes, fitViewToBounds]) + }, [displayNodes, fitViewToBounds, getNodeAbsolutePosition]) /** Handles ActionBar remove-from-subflow events. */ useEffect(() => {