Skip to content

fix(workflow): dragging an extreme node in the drawer moves the stored graph but not the node #1575

Closed

Description

@HugoXK

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-565 fitPlane — room = width - FIT_PAD*2 - NODE_W (= 172 at the drawer's DRAWER_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-635 movePointer — the ghost is already in world space: gesture.x + (clientX - ox) / gesture.scale
  • :641 endPointer — commits ghost into workflow.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 is maxX*scale + offsetX = width/2 + (span/2)*scale = width/2 + room/2 — constant, while maxX grows by delta / gesture.scale per pointer move. The node is pinned and the graph is squashed.
  • vertical: the topmost node's fitted top is y + (FIT_PAD - minY), which stays FIT_PAD while it is the minimum — so it is pinned at 12px and y still 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):

  1. open a workflow with several nodes in the drawer, Edit, then press and drag the rightmost node 40px to the right;
  2. the node's rendered left does not change (it sits at width/2 + room/2), the other nodes move left, and scale drops;
  3. release — the commit writes x = start + 40 / scale_at_start, so the world graph now has a wider span;
  4. 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);
  • pointerup commits the world coordinates derived from the frame that the drag started with (the start scale is 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.left by N px (rounding aside), and leaves every other node's style.left unchanged;
  • dragging the topmost node up by N px changes its rendered style.top by N px;
  • the committed world x equals the start x plus N / 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 main for the extreme-node case (rendered position and/or committed span) and passes after the fix; the full plugins/workflow suite and npm run verify green 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

  1. HugoXK commented on Oct 28, 2026

    @HugoXK
    ContributorAuthor

    Claim record (the helper wrote a stale Base: into the body; this comment is authoritative).

    • Claim-Id: ab168834f2454e648c3e01a5a48b38c4
    • Claimed-by: FlyBot Lead
    • Branch: dev3/fix/p103-drawer-drag-fit-freeze
    • Base: ad976b4090b3ad7d2f8a135dc5cf6e5d97f4a685 (live main at claim time, the #1547 merge)
    • Worker: FlyBot Dev-3

    Line numbers in the body were read at that same base. The label/assignee writes return 403 Must have admin rights to Repository for our account, so the record lives in this comment, not in labels.

    Reproduction note for the worker: the merged #1547 tests 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.

  2. HugoXK commented on Oct 28, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: ab168834f2454e648c3e01a5a48b38c4
    At: 2026-10-28T08:21:09Z

  3. Birfy commented on Oct 28, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    Label repair at 2026-10-28T10:50:32Z: agent-pr added — this issue's open pull request is #1576 (fork HugoXK/FlyBot), head 91c6dc037dd32a6fefff1e7a8f8bbde199e17517. Implementation stays on the pull request line; nothing new starts from this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions