Repository navigation
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: bec32d5857804b1388fc031c52a21a53
Branch: dev/1-drawer-edit-overlap
Base: 1db1037
At: 2026-10-29T10:21:15Z
What happens
Flow → Edit shows the graph in the drawer's edit plane, produced by fitPlane(nodes, DRAWER_FLOW_W=364) (plugins/workflow/client.js:1034). fitPlane scales only the centre points (the #1513 rule written for the Settings canvas), so with three or more parallel branches the cards overlap. The branch out-port sits on the card's right edge (.wf-port{right:-5px}), the later card is later in the DOM with the same z-index, and the canvas has no horizontal scroll (.wf-canvas[data-fit="drawer"]{overflow:hidden}) — so clicking r1's out-port hits r2's card instead of starting a wire. The drawer cannot connect branches on a 3-way parallel graph, and the template ships one.
The read-only plane was fixed by #1663; the edit plane still fits on one line on main 1db1037a.
Reproduction (one command, six expected lines)
Root at main 1db1037a, Node ≥18. No browser, no third-party dependency, writes nothing into the repo (mkdtemp, removed at the end).
node --input-type=module -e 'import{readFileSync,mkdtempSync,rmSync}from"node:fs";import{join}from"node:path";import{tmpdir}from"node:os";const M=new Map,R={createElement:()=>null,useState:v=>[v,()=>{}],useEffect:()=>{},useRef:v=>({current:v})};globalThis.window={__ModuleLoader__:{load:({id,factory})=>M.set(id,factory(n=>n==="react"?R:{}))}};(0,eval)(readFileSync("plugins/workflow/client.js","utf8"));const C=M.get("@flybot/workflow"),{createWorkflowService}=await import("./plugins/workflow/src/service.js"),d=mkdtempSync(join(tmpdir(),"p146-")),S=createWorkflowService({dir:d}),ov=(a,b)=>Math.min(a.r,b.r)>Math.max(a.l,b.l)&&Math.min(a.b,b.b)>Math.max(a.t,b.t),bx=n=>({l:n.x-C.NODE_W/2,r:n.x+C.NODE_W/2,t:n.y,b:n.y+C.NODE_H}),D=[],V=[];for(const n of[2,3,4,6]){const ids=Array.from({length:n},(_,i)=>"r"+(i+1));S.create({name:"w"+n,nodes:[{id:"f",name:"F",prompt:"x"},...ids.map(id=>({id,name:id,prompt:"x"})),{id:"j",name:"J",prompt:"x"}],edges:[...ids.map(t=>({from:"f",to:t})),...ids.map(f=>({from:f,to:"j"}))]});const wf=S.state().workflows.find(w=>w.name==="w"+n),L=wf.layers.find(l=>l.length>1),F=new Map(C.fitPlane(wf.nodes,C.DRAWER_FLOW_W).nodes.map(x=>[x.id,x])),B=L.map(id=>bx(F.get(id))),p=[];for(let i=0;i<n;i++)for(let j=i+1;j<n;j++)if(ov(B[i],B[j]))p.push(L[i]+"/"+L[j]+"="+Math.round(Math.min(B[i].r,B[j].r)-Math.max(B[i].l,B[j].l))+"px");D.push(n+"way drawer: overlaps="+p.length+(p.length?" "+p.join(" "):""));const fl=C.flowLayout(wf,368,{}),vb=L.map(id=>{const b=fl.boxes.get(id);return{l:b.left,r:b.left+b.w,t:b.top,b:b.top+b.h}}),q=[];for(let i=0;i<n;i++)for(let j=i+1;j<n;j++)if(ov(vb[i],vb[j]))q.push(L[i]+"/"+L[j]);if(n>3)V.push(n+"way readonly368: overlaps="+q.length+(q.length?" "+q.join(" "):""))}for(const l of[...D,...V])console.log(l);rmSync(d,{recursive:true,force:true});'Expected stdout (exactly six lines, fixed order):
2way drawer: overlaps=0
3way drawer: overlaps=2 r1/r2=82px r2/r3=82px
4way drawer: overlaps=5 r1/r2=111px r1/r3=53px r2/r3=111px r2/r4=53px r3/r4=111px
6way drawer: overlaps=14 r1/r2=134px r1/r3=99px r1/r4=65px r1/r5=30px r2/r3=134px r2/r4=99px r2/r5=65px r2/r6=30px r3/r4=134px r3/r5=99px r3/r6=65px r4/r5=134px r4/r6=99px r5/r6=134px
4way readonly368: overlaps=0
6way readonly368: overlaps=0
Notes:
- The command builds
fork → r1..rN → jointhrough the real service, then calls the client's ownfitPlane(nodes, DRAWER_FLOW_W)(the same call the drawer edit plane makes,plugins/workflow/client.js:1034) andflowLayout(wf,368)(the read-only Flow page), testing every pair of 168×72 cards for a two-dimensional intersection;Npxis the horizontal overlap width. - Reading: drawer 2-way 0 pairs, 3-way and up 2/5/14; at the same width the read-only page is 0 for 4-way and 6-way — so the layout function can spread the cards, this path does not.
- 6-way is the ceiling from
plugins/workflow/src/graph.js:189("A parallel step seats at most 6 Bots"). - Two consecutive runs on the same box:
diffempty, stderr empty, exit 0, under 1s. If numbers differ elsewhere, check the Node version (--input-type=modulewith top-level await).
Expected
Cards in the edit plane never overlap, and every card's out-port / mini-tool centres are on top at their own coordinates, so a wire can be started from any branch.
Scope
plugins/workflow/client.js + tests. Two options:
- A (preferred): the editing branch reuses
flowLayout's column-width / wrapping rule (same formula, wrap per layer), row heights from measured heights; themovegesture becomes a pure delta (no÷scale). - B: keep
fitPlane, give the edit plane horizontal scrolling with a minimum widthmax(364, span + 168 + 24)andoverflow-x:auto, keeping the offset maths.
Either way the read-only plane and the 2-way edit plane must stay byte-identical.
Acceptance:
- 3/4/6-way at 364 and 368: any two cards in the edit plane have zero two-dimensional intersection, and every card's
.wf-portcentre and.wf-minicentre is not inside any other card's rect (DOM order decides the hit); - read-only plane (368, 3/4/6-way) and the 2-way edit plane (364): serialized boxes+wires byte-identical to the base you build on;
- on a 3-way graph, dragging from
r1's out-port tojoinhitsr1(the last card in DOM order at that coordinate, notr2) and addsr1→jointo the edges.
Not in scope: the read-only plane (done in #1663), the sidebar row (#1488), the room/let-go items (#1664, #1666), any host-side change.
Activity
HugoXK commentedon Oct 29, 2026
Release
Written-by: FlyBot Lead
Released: bec32d5857804b1388fc031c52a21a53
Reason: merged as 0312772 (PR #1683)
At: 2026-10-29T13:05:15Z
workflow: keep a parallel step's cards apart in the drawer's Edit pla…
Workflow stage drawer Edit plane: parallel Reviewer/Tester cards over…