Repository navigation
fix(workflow): dragging an extreme node in the drawer moves the stored graph but not the node #1575
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: ab168834f2454e648c3e01a5a48b38c4
Branch: dev3/fix/p103-drawer-drag-fit-freeze
Base: 9b866a5
At: 2026-10-28T08:12:35Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What is wrong
In the workflow drawer, dragging a node that is the leftmost, rightmost or topmost of the graph does not move it on screen, but it does change the coordinates written to workflow/graph. The graph silently deforms: repeat the drag and the dragged node's world x keeps growing (or y keeps shrinking) while the picture on screen stays put and every other node is compressed toward the centre.
The drawer paints a fitted copy of the graph (#1513, #1547), and the fit is recomputed from a node list that already contains the drag ghost:
plugins/workflow/client.js:614—const nodes = workflow.nodes.map(node => ghost?.[node.id] ? { ...node, ...ghost[node.id] } : node):618—const plane = fitPlane(nodes, DRAWER_FLOW_W), i.e. the ghost is part of the fit input:543-565fitPlane—room = width - FIT_PAD*2 - NODE_W(=172at the drawer'sDRAWER_FLOW_W = 364,FIT_PAD = 12,NODE_W = 168);scale = min(1, room/span);offsetX = width/2 - ((minX+maxX)/2)*scale;offsetY = FIT_PAD - minY:625-635movePointer— the ghost is already in world space:gesture.x + (clientX - ox) / gesture.scale:641endPointer— commitsghostintoworkflow.nodes, so the world change is persisted
Because both scale and offsetX are recomputed from the same list each frame, the extreme node's fitted position is not independent of its own drag:
- horizontal, when
span > room(span=maxX - minX,room=172): fitted left of the max node ismaxX*scale + offsetX = width/2 + (span/2)*scale = width/2 + room/2— constant, whilemaxXgrows bydelta / gesture.scaleper pointer move. The node is pinned and the graph is squashed. - vertical: the topmost node's fitted top is
y + (FIT_PAD - minY), which staysFIT_PADwhile it is the minimum — so it is pinned at12pxandystill decreases. - a node that is not an extreme of the graph is dragged correctly: the span does not change, so the fit does not change.
Reproduction
At main ad976b4090b3ad7d2f8a135dc5cf6e5d97f4a685 (the #1547 merge), with a workflow whose nodes are a horizontal chain wide enough for span > 172, in the drawer (DRAWER_FLOW_W = 364):
- open a workflow with several nodes in the drawer,
Edit, then press and drag the rightmost node 40px to the right; - the node's rendered
leftdoes not change (it sits atwidth/2 + room/2), the other nodes move left, andscaledrops; - release — the commit writes
x = start + 40 / scale_at_start, so the world graph now has a wider span; - repeat: the span grows every time while the screen barely changes.
Same for the leftmost node dragged left and the topmost node dragged up (FIT_PAD pinning). Dragging a middle node is correct today and must stay correct.
The merged #1547 tests assert the committed payload (test/client.test.js:234, 200 + 48/fit.scale) and never assert a rendered position, so 124 green workflow tests do not cover this.
Expected
During a drag the fit is a fixed frame: it is computed from workflow.nodes only, and the ghost is drawn inside that same frame. Then
- the dragged node follows the pointer 1:1 in screen space, including when it is the leftmost/rightmost/topmost node;
- every other node's rendered position is unchanged for the whole gesture (no re-centring, no re-scaling under the pointer);
pointerupcommits the world coordinates derived from the frame that the drag started with (the startscaleis already stored in the gesture,:634);- a writable pointer still cannot produce a graph whose stored coordinates do not match what the user saw.
Scope
plugins/workflow/client.js and plugins/workflow/test/client.test.js only. No server change, no new endpoint, no change to fitPlane's contract for the Settings canvas (:1116-1130 has its own plane), no change to what a pointerup commits for a correct middle-node drag.
Acceptance
- dragging the rightmost node right by N px changes its rendered
style.leftby N px (rounding aside), and leaves every other node'sstyle.leftunchanged; - dragging the topmost node up by N px changes its rendered
style.topby N px; - the committed world
xequals the startxplusN / scale_at_start, and does not depend on intermediate frames; - the existing payload assertion (
test/client.test.js:234) stays green, and a middle-node drag behaves exactly as today; - red first: at least one test that fails on
mainfor the extreme-node case (rendered position and/or committed span) and passes after the fix; the fullplugins/workflowsuite andnpm run verifygreen at the tip.
Not in scope
- a zoom / Fit ↔ 100% toggle in the drawer, and the overlap of parallel branches at low scale (that is a separate usability proposal, not this bug);
- Tidy/re-layout, undo, and the Settings canvas.
Activity
HugoXK commentedon Oct 28, 2026
Claim record (the helper wrote a stale
Base:into the body; this comment is authoritative).ab168834f2454e648c3e01a5a48b38c4dev3/fix/p103-drawer-drag-fit-freezead976b4090b3ad7d2f8a135dc5cf6e5d97f4a685(livemainat claim time, the#1547merge)Line numbers in the body were read at that same base. The label/assignee writes return 403
Must have admin rights to Repositoryfor our account, so the record lives in this comment, not in labels.Reproduction note for the worker: the merged
#1547tests assert the committed payload only (plugins/workflow/test/client.test.js:234), so the failure has to be pinned with a rendered-position assertion, not a payload one. Please record the actual measured numbers in the PR body rather than a claimed table.HugoXK commentedon Oct 28, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: ab168834f2454e648c3e01a5a48b38c4
At: 2026-10-28T08:21:09Z
Birfy commentedon Oct 28, 2026
Written-by: Triage
Label repair at 2026-10-28T10:50:32Z:
agent-pradded — this issue's open pull request is #1576 (forkHugoXK/FlyBot), head91c6dc037dd32a6fefff1e7a8f8bbde199e17517. Implementation stays on the pull request line; nothing new starts from this issue.fix(workflow): keep the drawer fit fixed while a node is dragged (Thi…
fix(workflow): drawer flowchart lays out from its steps; wires route …