improvement(workflow): redesigned block cards with edge-following connection swell - #5976
improvement(workflow): redesigned block cards with edge-following connection swell#5976andresdjasso wants to merge 3 commits into
Conversation
Connection anchors: an output now always leaves a card from the right.
The cursor swell lets a drag start on any edge, but the left side is the
input, so anchoring an outgoing edge there drew a line out of the input
port and read as a second input. `normalizeCursorSourceHandleId` resolves
every drag to the right anchor, `normalizePositionedSourceHandleId`
collapses `source-left` alongside the legacy vertical anchors (so data
from the API, an older client, or a stale save self-heals on load), and
only the right-side source anchor is mounted.
Drops in `onConnectEnd` are always source -> target. The branch that
reversed the edge for a drag starting on an input could never run: the
`target` handle is `isConnectableStart={false}` and the positioned side
anchors are `isConnectable={false}`, so React Flow never reports an input
as a drag origin. Removed it and its now-unused imports.
A newly created block is centered once its node mounts and is measured,
so a card added from a drag-release, the block menu, or the toolbar is
never left off-screen or under the editor panel.
The editor panel's block icon uses the same type accent as the card's
badge instead of the block's legacy `bgColor`, which had left the panel
on the old per-integration brand colours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR SummaryHigh Risk Overview Adds a persisted Collapsed-node preview gains type-accent ChipTags, natural-language sentence summaries with inline chips, meta rows, and tighter height math. The action bar moves to a Canvas behavior: camera focus on create/click, arrow-key block navigation, connection-line styling via data attributes, and deploy/run graphite button variants. Tooltips support Reviewed by Cursor Bugbot for commit ace5467. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThe PR redesigns workflow cards around an SVG-painted border and horizontal connection affordances.
Confidence Score: 4/5The realtime edge path needs handle canonicalization before merge to prevent duplicate logical connections during mixed-client sessions. The new source-handle normalization collapses legacy left outputs only in selected client, rendering, and persistence paths, while realtime insertion and duplicate comparison retain raw handles and can accept both left and right representations of one connection. Files Needing Attention: packages/workflow-types/src/workflow.ts and apps/realtime/src/database/operations.ts
|
| Filename | Overview |
|---|---|
| packages/workflow-renderer/src/workflow-block/workflow-block-border.tsx | Introduces the sampled-perimeter SVG border renderer, bulge composition, pointer tracking, and spring animation used by workflow cards. |
| packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx | Rebuilds the shared workflow-card view around the painted border, integrated handles, deterministic layout, error output, and action swell. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx | Integrates the redesigned renderer with live canvas state, connection behavior, block actions, and error-output controls. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx | Normalizes connection handles, updates connection drag behavior and edge layering, and adds measured-node centering and keyboard navigation. |
| packages/workflow-types/src/workflow.ts | Adds shared handle normalization helpers, but realtime ingress and duplicate checks do not apply the same positioned-handle canonicalization. |
| packages/workflow-persistence/src/load.ts | Normalizes legacy positioned handles while loading workflows, exposing duplicate raw handles accepted during mixed-client realtime sessions. |
| packages/workflow-persistence/src/save.ts | Canonicalizes positioned handles before workflow edges are persisted through this save path. |
| apps/realtime/src/database/operations.ts | Persists realtime edge handles without applying the new positioned-handle canonicalization, allowing logically duplicate left/right outputs. |
| apps/sim/executor/execution/edge-manager.ts | Recognizes positioned source handles as ordinary successful output routes while preserving special routing handles. |
| apps/sim/lib/workflows/blocks/deterministic-dimensions.ts | Updates deterministic card-height calculations to match the redesigned rendered content. |
| packages/emcn/src/components/tooltip/tooltip.tsx | Extends tooltip positioning behavior needed by controls placed above the new action swell. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Create[Create workflow block] --> Store[Workflow store]
Store --> Measure[Mount and measure node]
Measure --> Center[Center canvas on new block]
Store --> Card[Workflow block view]
Card --> Border[SVG border renderer]
Border --> Input[Left or right input knob]
Border --> Output[Right output knob]
Output --> Normalize[Normalize source handle]
Normalize --> Edge[Workflow edge]
Edge --> Persist[Realtime and persistence]
Edge --> Execute[Executor edge routing]
Error[Error-output toggle] --> Card
Error --> Persist
Reviews (1): Last reviewed commit: "fix(workflow): keep outputs on the right..." | Re-trigger Greptile
| export function normalizePositionedSourceHandleId<T extends string | null | undefined>( | ||
| handle: T | ||
| ): T | PositionedSourceHandleId { | ||
| return handle === 'source-top' || handle === 'source-bottom' || handle === 'source-left' | ||
| ? getPositionedSourceHandleId('right') | ||
| : handle |
There was a problem hiding this comment.
Realtime bypasses handle canonicalization
During a mixed-client session, an older client can still submit source-left while a current client submits source-right; the realtime database and duplicate-edge paths retain and compare those raw values, so the same logical connection is accepted twice even though this normalization later renders and reloads both as the right-side output.
Knowledge Base Used:
The Start card intermittently collapsed after load, squashing the action-menu tab so its icon row sat over the card. `.workflow-drag-handle` is the host the border renderer measures, and both it and the header row took their height from `blockHeight && blockHeight > 0`. `blockHeight` comes from the deterministic-dimensions pass and is already floored at MIN_PAINTED_HEIGHT (48), but it is absent on the first frames — and with no floor the host collapsed to its natural content height (25.5px for a header-only trigger, exactly the title's line box). The border builds its perimeter from `host.offsetHeight`, so that window painted a sub-floor card: too little straight edge remained on the vertical runs for the action-menu tab, which collapsed into the corner arcs. Whether you saw it depended purely on whether the dimension publish had landed, which is why it reproduced on one workflow and not another. Floor all three: the host, the header row (so `items-center` centres the title and type tag rather than pinning them to the top), and the border's own `offsetHeight` read. Also raise ACTION_MENU_CONTENT_READY_THRESHOLD to 0.9. At 0.8 the 24px icon row was revealed while the swell had only reached 22.4px of its 28px — shorter than the row it contains. Secondary to the above, but a real overflow window on its own. The test now pins the ratio rather than the constant. Tag palette moves to fixed brand values (hex, not derived oklch) with two inks — #F8F8F8 on dark fills, #1A1A1A on light. Tones are renamed to match what they render. `green` (2.55:1) and `orange` (3.15:1) sit under WCAG AA against their paired ink; both are deliberate brand decisions and are documented in the component. Deploy and Run take two new Button variants rather than className overrides, so `tertiary` stays green everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ace5467. Configure here.
| } | ||
| }, | ||
|
|
||
| setBlockErrorEnabled: (id: string, errorEnabled: boolean) => { |
There was a problem hiding this comment.
Error toggle lost on paste
Medium Severity
errorEnabled is not carried through duplicate/paste. batchAddBlocks rebuilds each block without copying it, setBlockErrorEnabled updates only the top-level flag and not data.errorEnabled, and server BATCH_ADD writes data without merging the top-level value the way full save does. A block with Error output on loses that setting when duplicated or pasted.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit ace5467. Configure here.
| if (!handleId?.startsWith(prefix)) return handleId | ||
|
|
||
| return getPositionedSourceHandleId('right') | ||
| } |
There was a problem hiding this comment.
Duplicate success edge handles
Medium Severity
Success outputs can be stored under both source and source-right. The interactive knob still emits source, while cursor-swell drags normalize to source-right. Uniqueness treats those as different handles, so the same source-to-target success link can be created twice and both fire on success.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit ace5467. Configure here.


Redesigns the workflow block card: a single SVG border that follows the cursor along the card edge, connection knobs that swell out of that border, and an action menu that rises out of the top edge instead of floating above it.
What changed
Border renderer (
workflow-block-border.tsx, new) — the card outline is one path. The rounded-rect perimeter is flattened into a 1D arc-length space; every port is a bulge on that line. Bulges combine withmax(union), never sum, so overlapping swells merge instead of spiking. Springs drive the cursor swell and the action-menu tab; the rAF loop self-terminates at rest.Connection model — outputs always leave from the right. A drag begun on the left edge still starts a connection, but its anchor normalizes to
source-right; anchoring one on the left would put an outgoing line on the input port. Inputs remain unlimited fan-in. Legacysource-top/source-bottom/source-leftcollapse tosource-righton persistence.Error output moved to the bottom edge, centered under the error toggle.
Type tags now use fixed brand values (hex, not derived) with two inks —
#F8F8F8on dark fills,#1A1A1Aon light. Tone names match what they render.Deploy / Run take two new
Buttonvariants (graphiteSubtle,graphite) rather than className overrides, sotertiarystays green everywhere else.Card height bug (latest commit)
The Start card intermittently collapsed after load, squashing the action-menu tab so its icon row sat over the card.
.workflow-drag-handleis the host the border measures, and both it and the header row took their height fromblockHeight && blockHeight > 0.blockHeightis already floored atMIN_PAINTED_HEIGHT(48) by the deterministic-dimensions pass — but it is absent on the first frames, and with no floor the host collapsed to its natural content height (25.5px for a header-only trigger, exactly the title's line box). The border builds its perimeter fromhost.offsetHeight, so that window painted a sub-floor card with too little straight edge left for the action-menu tab.Whether you saw it depended purely on whether the dimension publish had landed — which is why it reproduced on one workflow and not another. Floored in all three places.
Known accessibility exceptions
green(2.55:1) andorange(3.15:1) sit under WCAG AA against their paired ink. Both are deliberate brand decisions, documented inchip-tag.tsx. The label is always duplicated by an icon and the block name beside it, so the tag is a redundant cue rather than the sole carrier — but these pairings should not be reused anywhere a label stands alone.Needs review / hand-off
Merge conflict in
apps/sim/lib/workflows/autolayout/core.ts— the only real conflict; everything else auto-merges. Staging refactoredgetSourceHandleYOffsetto take aGraphNodeand readnode.metrics.heightinstead of rawblock.height. This branch changed the same function to anchor side ports at the block's vertical center and to return the full height for the error handle (which moved to the bottom edge, so there is noERROR_BOTTOM_OFFSETto subtract).Recommended resolution: take staging's signature and height source, then apply it to this branch's semantics —
node.metrics.height / 2for centered side ports,node.metrics.heightfor the error handle, subflow containers keepingDEFAULT_Y_OFFSET. Left unresolved deliberately: a wrong call here silently mis-places autolayout edges, and that is best verified by whoever owns autolayout.Canvas performance — the border renderer runs a spring loop per card. It self-terminates at rest and the foreign-drag listener is frame-coalesced, but this has not been profiled on a large graph under sustained interaction. Worth a pass on drag, multi-select, and hover across a few hundred blocks.
horizontalHandlesis vestigial — still persisted, socket-synced and undo/redo-able, but the renderer ignores it. The baseline honored it. Either wire it back up or remove the field.Sub-48px cards —
MIN_PAINTED_HEIGHTis a floor, not a fix. The border produces no usable path below it. Root-causing that would remove the need for the floor entirely.Toolbar and search-modal block colors are unchanged — they read
bgColorfrom each block definition and draw a white glyph, so adopting the new palette needs the fill+ink pairing rather than a swap (three of the new colors are light).