fix(canvas): stop phantom ports and a latched-open action bar - #6688
Conversation
Ports surface on hover from a swell painted on the card border, and that swell was raised with no regard for whether a handle exists behind it. A Response block mounts no source handle, so hovering its edge raised a knob no edge could ever leave from. A trigger mounts no target handle, yet still swelled under a connection dragged from another card, offering a drop it cannot accept. Gate each direction on the handle that backs it, and limit a trigger's own swell to its source edge the way the subflow start node already does. The action bar latched open for the same interaction. Leaving the card arms a retract and installs a pointermove listener to track the pointer across the gap up to the bar; re-entering the bar's band called openHover(), which cancelled the retract AND removed that listener. No further pointerleave can arrive once the pointer is off the node, so nothing was left to close the bar. Keep the listener installed and re-arm the retract when the pointer moves back out. Also clear the magnetized port when the pointer leaves the tracking band onto the action bar: only the in-band path recomputed it, so the last knob stayed pinned at hover amplitude with the pointer nowhere near it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryLow Risk Overview Border swells now respect whether a card can actually start or receive connections via new Action menu swell ( Adds jsdom tests for both behaviors. Reviewed by Cursor Bugbot for commit 961df01. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ef0e5a0. Configure here.
Greptile SummaryThe PR corrects canvas hover affordances and action-menu retraction behavior.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/workflow-renderer/src/workflow-block/use-action-menu-swell.ts | Separates timeout cancellation from listener cleanup so pointer tracking can close the action menu after leaving its hover band. |
| packages/workflow-renderer/src/workflow-block/workflow-block-border.tsx | Gates cursor swells by connection capability and clears stale magnetized-port state outside the tracking band. |
| packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx | Supplies source and target connection capabilities based on the handles rendered by each block type. |
| packages/workflow-renderer/src/workflow-block/use-action-menu-swell.test.tsx | Adds regression coverage for carrying the pointer past the action bar and resting within its hover band. |
| packages/workflow-renderer/src/workflow-block/workflow-block-border-connectable.test.tsx | Adds coverage ensuring unsupported source and target swells are not offered. |
Reviews (2): Last reviewed commit: "fix(canvas): scope the receive gate, cov..." | Re-trigger Greptile
Gating the foreign-drag listener also ran the shared pointer-tracking reset, which belongs to the card's own hover. A trigger sets canReceiveConnection false while canStartConnection stays true, so the reset undid the layout effect's :hover bootstrap and left a card that mounted under the pointer with no source swell until the pointer left and came back. Skip the listener instead; the effect's own cleanup already covers a true-to-false flip. Drop the trigger-only cursorSwellSides restriction. A swell on a trigger's input edge resolves to a source handle, so an edge genuinely can be made there — it was a behavior change beyond the bug, not a phantom. Cover both directions of the swell gate, and use the shared sleep helper in the action-bar test. Hoist the constant connection sides out of the render body so they stop riding the borderPorts dep array.
|
@cursor review |

Two canvas hover bugs from Slack.
Ports that appear where no edge can be made
Hovering a card raises a swell on its border that reads as a connection port. The swell was raised without regard for whether a handle exists behind it:
The two contexts are separate (your own hover is a source affordance; a swell raised while another card is the drag origin is a target affordance), so they are now gated separately on the handle that actually backs each one. A trigger's own swell is additionally limited to its source edge, matching what the subflow start node already does — so it no longer raises a knob on the edge where every other card shows its input.
Action bar staying open after leaving via a port
Leaving a card arms a 100ms retract and installs a
pointermovelistener to track the pointer across the gap up to the bar, which floats above the card. Re-entering the bar's hover band calledopenHover()— which cancelled the retract and removed that listener. Since no furtherpointerleavecan arrive once the pointer is off the node, nothing was left to close the bar and it latched open. The listener now stays installed and the retract is re-armed when the pointer moves back out.Same gesture also left a knob puffed: only the in-band path recomputed the magnetized port, so leaving the tracking band upward onto the action bar pinned the last one at hover amplitude with the pointer nowhere near it.
Type of Change
Testing
Added
use-action-menu-swell.test.tsxcovering both directions of the gap crossing — retracts when the pointer carries on past the bar, stays open when it rests on it. Verified it fails against the old behavior and passes with the fix. Full renderer suite (72) and canvas suite (349) green; type-check and lint clean.Checklist