Skip to content

fix(workflow): stop subflows resizing themselves after every load - #6639

Merged
waleedlatif1 merged 10 commits into
stagingfrom
fix/subflow-resize-settle
Aug 12, 2026
Merged

fix(workflow): stop subflows resizing themselves after every load#6639
waleedlatif1 merged 10 commits into
stagingfrom
fix/subflow-resize-settle

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • A subflow renders at one size on load and then visibly grows a moment later, on every refresh.
  • The container sizes itself from its children. When a child had not yet reported a height it used estimateBlockDimensions instead — a guess of ceil(subBlockCount / 2) rows clamped to [3,7], which reads a 39-field Gmail card as 276px tall against the 112px it actually draws. The container painted that, the real height arrived a frame later, and it resized between the two.
  • A card's height depends on what it actually renders — which rows survive its conditions, whether it draws a summary sentence, and for a reactive field even a credential it has to fetch (useReactiveConditionsuseWorkspaceCredential). The card is the only thing that can know it. So: size only from heights children have themselves reported, and hold the container at its current size until they have.
  • getBlockDimensions keeps the estimate for callers that only need a rough box (clamping a drag, placing a paste) and is now that same lookup plus the fallback, rather than a second copy of the policy.
  • The gate reads layout (in-memory, written by updateBlockLayoutMetrics for cards and updateNodeDimensions for containers), not height/data.height. Those are persisted, so an unreported block can still carry last session's numbers and an unsized inner container hands back its 500x300 default — both of which the gate would otherwise treat as measured, and the second of which made nested containers resize twice.
  • Separately, calculateContainerDimensions counted the container's chrome twice. Child coordinates are relative to the container's own origin and already held clear of the header by clampPositionToContainer, so a child's far edge is the distance to cover and only the trailing padding is owed. Adding the header and leading padding again left every container 66px taller and 16px wider than its contents.

For the reported workflow, the Loop went 300 → 565.5 → 401.5 on load. It now settles once, at 603 × 335.5 — the child's far edge plus exactly one padding.

Type of Change

  • Bug fix

Testing

New unit tests pin the sizing contract; two of them fail on the old math (expected { width: 619, height: 401.5 } to deeply equal { width: 603, height: 335.5 }, and expected 82 to be 16 for the gap under a child). Full utils suite green (62), type-check and lint clean.

The two sizes were derived by running the real functions against the exported workflow state, and cross-checked against the reported screenshots: scaling both by the Gmail card's known 112px height (both at 0.58) gives container heights of ~412 and ~579 against the computed 401.5 and 565.5, with the child's offset staying ~207.5 in both. That also confirms the coordinate convention the padding fix depends on — children are positioned from the container's outer origin.

Not verified in the running app. I brought up dev:full but could not sign in to the local instance, so this is verified by unit test and arithmetic only.

Follow-ups (not in this PR)

  • MCP blocks on the field-row path still resize once. estimateWorkflowBlockDimensions is exact on the sentence path but deliberately biased high on the field-row path, because a mcp-dynamic-args field expands into one row per schema property when the card mounts. The bias is correct for autolayout (a gap beats an overlap) but still moves a container. It is fixable — contrary to that function's doc, the count is derivable from block state, since _toolSchema is an ordinary subblock value — but removing the slack changes autolayout spacing, so it wants its own change.
  • estimateWorkflowBlockDimensions (lib/workflows/autolayout/utils.ts) already estimates from block state rather than type alone, and is far more accurate than estimateBlockDimensions. Routing the remaining estimate callers through it would fix the guess everywhere it is still painted (workflow.tsx:2747 feeds it into React Flow node height, so selection bounds are 276px on a 112px card).
  • preview-workflow.tsx re-implements calculateContainerDimensions — and already uses the corrected formula, independent corroboration that the double-count was a bug.
  • getNodeAbsolutePosition hardcodes headerHeight = 50 / leftPadding = 16 / topPadding = 16 and adds them to a child's position, which contradicts the outer-origin convention the rest of the code uses. It looks like a latent bug for nested containers, but it feeds reparenting math and deserves its own verification.
  • Containers still resize once per load, monotonically. Correcting my own commit message: block.height and container data.width/data.height are persisted — what is missing is the write-back. updateNodeDimensions is a raw store action with no collaborative sync, and the realtime subflow-config handler re-floors data to 500x300, so a container paints the default on load and grows once when its children report. A tall loop still visibly grows from 300px each refresh. Removing that frame means writing the derived size back, which is a bigger change than this one.
  • updateContainerDimensionsDuringMove (the drag path) still feeds calculateContainerDimensions estimates. Plausibly correct — a best-effort box while dragging — but it duplicates the gather loop.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

A container sized itself from its children, and when a child had not yet
reported a height it used `estimateBlockDimensions` in its place — a guess of
`ceil(subBlockCount / 2)` rows, which read a 39-field Gmail card as 276px tall
against the 112px it draws. The container painted that number, the real height
arrived a frame later, and it visibly resized between the two. Nothing is
persisted, so it happened on every refresh.

A card's height depends on what it actually renders — which rows survive its
conditions, whether it draws a summary sentence, and for a reactive field even
a credential it has to fetch — so the card is the only thing that can know it.
Size only from heights the children have themselves reported, and hold the
container at its current size until they have. `getBlockDimensions` keeps the
estimate for the callers that only need a rough box (clamping a drag, placing a
paste) and is now that same lookup plus the fallback.

Also stop `calculateContainerDimensions` counting the container's chrome twice.
Child coordinates are relative to the container's own origin and are already
held clear of the header by `clampPositionToContainer`, so a child's far edge is
the distance to cover and only the trailing padding is owed on top. Adding the
header and leading padding again left every container 66px taller and 16px
wider than its contents.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 12, 2026 8:18pm

Request Review

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core workflow canvas layout (container sizing, block dimension estimates, shared renderer constants); behavior is well covered by new unit tests but affects every loop/parallel/subflow on the editor.

Overview
Fixes subflows visibly resizing after load by sizing children with getBlockMetrics (same state-aware path as rendered cards) instead of type-only estimateBlockDimensions, and by sizing loop/parallel children from the current workflow store snapshot so nested containers don’t use stale inner dimensions during deepest-first resize.

calculateContainerDimensions no longer adds header/leading padding on top of child positions that are already relative to the container origin (via clampPositionToContainer), which had inflated every container by ~66px height and 16px width.

CONTAINER_DIMENSIONS is updated (header 40px, asymmetric padding) and subflow-node-view reads those values for header/content inset so layout math and painted chrome stay in sync. Adds node-position-utils unit tests for the sizing contract; workflow-renderer vitest sets IS_REACT_ACT_ENVIRONMENT to quiet mount-test noise.

Reviewed by Cursor Bugbot for commit a32bbe5. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces type-only child-height estimates with state-aware metrics, makes nested container sizing consume a fresh store snapshot, and aligns container geometry with rendered chrome.

  • Uses getBlockMetrics for regular card sizing and a note-specific fallback.
  • Corrects container padding calculations and centralizes rendered header and gutter dimensions.
  • Adds geometry and renderer mount tests.

Confidence Score: 4/5

The PR is not yet safe to merge because MCP cards with runtime-expanded arguments still cause their parent containers to resize after mounting.

The new state-based estimate counts mcp-dynamic-args as one row, while the mounted card expands each tool-schema property into a row and reports a larger height, preserving the visible post-load resize on that current workflow path.

Files Needing Attention: apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts Introduces state-aware, fresh-snapshot sizing, but runtime-expanded MCP arguments still make pre-mount and measured card heights diverge.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils.ts Correctly removes duplicated leading chrome from container bounds while retaining trailing padding and minimum dimensions.
packages/workflow-renderer/src/dimensions.ts Centralizes the container chrome and gutter dimensions shared by layout and rendering.
packages/workflow-renderer/src/subflow/subflow-node-view.tsx Replaces duplicated Tailwind geometry literals with shared container-dimension constants.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils.test.ts Covers the corrected coordinate convention, trailing gaps, defaults, and minimum container bounds.

Reviews (5): Last reviewed commit: "fix(workflow): size an unmeasured note a..." | Re-trigger Greptile

Two holes in the measurement gate, both from reading the wrong field.

`height` is a persisted column and `data.width` / `data.height` persist a
container's last size, so a block that has not reported yet can still carry last
session's numbers — reachable through paste, import, and checkpoint restore. The
gate treated those as reported and sized from them.

Nested containers had it worse: an inner container with an unreported descendant
handed back its 500x300 default as though it were measured, so the outer
container sized to that and resized again once the descendant filled in — the
same two-step this change exists to remove.

`layout` is in-memory only and written by exactly the two places that know: a
card through `updateBlockLayoutMetrics`, a container through
`updateNodeDimensions`. Reading it means "reported during this session" and
nothing else, and an inner container that is still waiting reports null, so the
outer one waits with it.

`getBlockDimensions` keeps the persisted height and the estimate as fallbacks —
its callers only need a rough box, where a stale height still beats a guess.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts Outdated
The gate in the reverted commit held a container at its current size until its
children reported. That is worse than it sounds: the size it holds is the
persisted default of 300, the child needs 335, and so the child hung outside
the container until something forced a resize.

Estimate accurately instead of waiting. `getBlockMetrics` derives a card's
height from the block's own state — the sub-blocks its values leave visible, the
summary sentence, the error row — through the same
`calculateWorkflowBlockDimensions` the card calls, and lands on the height the
card goes on to render: 112px for the Gmail card the type-only estimate put at
276px. The pass before the cards report and the pass after now produce the same
container, so there is nothing to gate and nothing to correct.

This also fixes the guess everywhere else it was painted rather than only in the
container path — `estimateBlockDimensions` fed React Flow's node height for
unmeasured blocks, so selection bounds were 276px around a 112px card.
Left, top and bottom were 16 and the bottom read tighter than either, because
the 50px header sits above the top gap and gives that edge visual weight the
other two do not have. Taking them to 24 leaves the three gutter-only edges
matching and the bottom no longer pinched.

Right stays 80. The container's output handle sits on that edge, so a child
needs clearance there it does not need anywhere else — chrome rather than
gutter, now said so in the type.

Only reachable as a single constant each because the paddings mean what they
say: each is the gap between a child's edge and the container's, counted once.
While the sizing math added the header and leading padding a second time, the
effective bottom gap was spread across three constants and tuning it meant
reasoning about all of them.
The four paddings and the header height existed twice: as
`CONTAINER_DIMENSIONS`, which sizes a container and clamps its children, and
again as Tailwind literals in `subflow-node-view`, which draws the header and
the content box. Nothing kept them in step and they had already drifted — the
view rendering a 40px header against a constant claiming 50, so children were
clamped 10px below where the header actually ends.

The view now renders from the constants, and the constant follows the DOM at 40.

Match the bottom gutter to the right at 80. The two edges that carry chrome are
now the two that are wider: the container's output handle sits on the right, and
the resize grip in the bottom-right corner spans 40px in from both, so a child
at the 24px gutter width could sit underneath it. Left and top are only gutter
and stay at 24.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

…class

The header renders from `CONTAINER_DIMENSIONS.HEADER_HEIGHT` now, so the class
it used to carry is gone. Assert the rendered height against the same constant
the layout math measures against — the two drifting apart is what this whole
change is about, and a utility-class assertion cannot catch that.

Also set `IS_REACT_ACT_ENVIRONMENT`, which these tests have always needed. React
only treats `act` as supported when it can see the flag, so every render logged
"The current testing environment is not configured to support act(...)" — around
forty lines of it per run, burying the actual failure output.
`vitest.setup.ts` is inside the package's tsconfig, so assigning an undeclared
property on `globalThis` failed type-check (TS7017) even though the tests ran.
`calculateLoopDimensions` took child positions from the live store but child
dimensions from the hook's render snapshot, so it was reading two ages of the
same data. `resizeLoopNodes` walks deepest-first: an inner container resized
earlier in the pass was already updated in the live snapshot and still stale in
the closed-over one, so its parent sized against the old inner box and only
caught up on a later render — a nested container visibly resizing twice, which
is the symptom this branch set out to remove.

Take both from the snapshot the function already reads.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts Outdated
Routing every non-container block through `getBlockMetrics` sent notes through
the workflow-card estimate, which counts sub-block rows and an error row a note
does not have. A note that had not reported a height yet got a card's box, so a
container holding one sized itself around the wrong shape.

Give a note its own branch, as the estimate it replaced did: measured height
when there is one, and the height an empty note paints when there is not.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a32bbe5. Configure here.

@waleedlatif1
waleedlatif1 merged commit 128054e into staging Aug 12, 2026
31 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/subflow-resize-settle branch August 12, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant