Skip to content

Commit f1c1e52

Browse files
committed
fix(copilot): reliable zoom to changed blocks after diff applied
1 parent d8d85fc commit f1c1e52

File tree

2 files changed

+58
-30
lines changed

2 files changed

+58
-30
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,51 +1641,36 @@ const WorkflowContent = React.memo(() => {
16411641
}, [screenToFlowPosition, handleToolbarDrop])
16421642

16431643
/**
1644-
* Focus canvas on changed blocks when diff appears
1645-
* Focuses on new/edited blocks rather than fitting the entire workflow
1644+
* Focus canvas on changed blocks when diff appears.
16461645
*/
1646+
const pendingZoomBlockIdsRef = useRef<Set<string> | null>(null)
16471647
const prevDiffReadyRef = useRef(false)
1648+
1649+
// Phase 1: When diff becomes ready, record which blocks we want to zoom to
1650+
// Phase 2 effect is located after displayNodes is defined (search for "Phase 2")
16481651
useEffect(() => {
1649-
// Only focus when diff transitions from not ready to ready
16501652
if (isDiffReady && !prevDiffReadyRef.current && diffAnalysis) {
1653+
// Diff just became ready - record blocks to zoom to
16511654
const changedBlockIds = [
16521655
...(diffAnalysis.new_blocks || []),
16531656
...(diffAnalysis.edited_blocks || []),
16541657
]
16551658

16561659
if (changedBlockIds.length > 0) {
1657-
const allNodes = getNodes()
1658-
const changedNodes = allNodes.filter((node) => changedBlockIds.includes(node.id))
1659-
1660-
if (changedNodes.length > 0) {
1661-
logger.info('Diff ready - focusing on changed blocks', {
1662-
changedBlockIds,
1663-
foundNodes: changedNodes.length,
1664-
})
1665-
requestAnimationFrame(() => {
1666-
fitViewToBounds({
1667-
nodes: changedNodes,
1668-
duration: 600,
1669-
padding: 0.1,
1670-
minZoom: 0.5,
1671-
maxZoom: 1.0,
1672-
})
1673-
})
1674-
} else {
1675-
logger.info('Diff ready - no changed nodes found, fitting all')
1676-
requestAnimationFrame(() => {
1677-
fitViewToBounds({ padding: 0.1, duration: 600 })
1678-
})
1679-
}
1660+
pendingZoomBlockIdsRef.current = new Set(changedBlockIds)
16801661
} else {
1681-
logger.info('Diff ready - no changed blocks, fitting all')
1662+
// No specific blocks to focus on, fit all after a frame
1663+
pendingZoomBlockIdsRef.current = null
16821664
requestAnimationFrame(() => {
16831665
fitViewToBounds({ padding: 0.1, duration: 600 })
16841666
})
16851667
}
1668+
} else if (!isDiffReady && prevDiffReadyRef.current) {
1669+
// Diff was cleared (accepted/rejected) - cancel any pending zoom
1670+
pendingZoomBlockIdsRef.current = null
16861671
}
16871672
prevDiffReadyRef.current = isDiffReady
1688-
}, [isDiffReady, diffAnalysis, fitViewToBounds, getNodes])
1673+
}, [isDiffReady, diffAnalysis, fitViewToBounds])
16891674

16901675
/** Displays trigger warning notifications. */
16911676
useEffect(() => {
@@ -2093,6 +2078,48 @@ const WorkflowContent = React.memo(() => {
20932078
})
20942079
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
20952080

2081+
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
2082+
// (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined)
2083+
useEffect(() => {
2084+
const pendingBlockIds = pendingZoomBlockIdsRef.current
2085+
if (!pendingBlockIds || pendingBlockIds.size === 0) {
2086+
return
2087+
}
2088+
2089+
// Find the nodes we're waiting for
2090+
const pendingNodes = displayNodes.filter((node) => pendingBlockIds.has(node.id))
2091+
2092+
// Check if all expected nodes are present with valid dimensions
2093+
const allNodesReady =
2094+
pendingNodes.length === pendingBlockIds.size &&
2095+
pendingNodes.every(
2096+
(node) =>
2097+
typeof node.width === 'number' &&
2098+
typeof node.height === 'number' &&
2099+
node.width > 0 &&
2100+
node.height > 0
2101+
)
2102+
2103+
if (allNodesReady) {
2104+
logger.info('Diff ready - focusing on changed blocks', {
2105+
changedBlockIds: Array.from(pendingBlockIds),
2106+
foundNodes: pendingNodes.length,
2107+
})
2108+
// Clear pending state before zooming to prevent re-triggers
2109+
pendingZoomBlockIdsRef.current = null
2110+
// Use requestAnimationFrame to ensure React has finished rendering
2111+
requestAnimationFrame(() => {
2112+
fitViewToBounds({
2113+
nodes: pendingNodes,
2114+
duration: 600,
2115+
padding: 0.1,
2116+
minZoom: 0.5,
2117+
maxZoom: 1.0,
2118+
})
2119+
})
2120+
}
2121+
}, [displayNodes, fitViewToBounds])
2122+
20962123
/** Handles ActionBar remove-from-subflow events. */
20972124
useEffect(() => {
20982125
const handleRemoveFromSubflow = (event: Event) => {

apps/sim/hooks/use-canvas-viewport.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback } from 'react'
22
import type { Node, ReactFlowInstance } from 'reactflow'
3+
import { BLOCK_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
34

45
interface VisibleBounds {
56
width: number
@@ -139,8 +140,8 @@ export function useCanvasViewport(reactFlowInstance: ReactFlowInstance | null) {
139140
let maxY = Number.NEGATIVE_INFINITY
140141

141142
nodes.forEach((node) => {
142-
const nodeWidth = node.width ?? 200
143-
const nodeHeight = node.height ?? 100
143+
const nodeWidth = node.width ?? BLOCK_DIMENSIONS.FIXED_WIDTH
144+
const nodeHeight = node.height ?? BLOCK_DIMENSIONS.MIN_HEIGHT
144145

145146
minX = Math.min(minX, node.position.x)
146147
minY = Math.min(minY, node.position.y)

0 commit comments

Comments
 (0)