LearnNewsExamplesServices
Frontmatter
id18421
titleRoot-edge chips pair a center nodeId with the whole-host rect, so the drop lands narrower than its preview
stateClosed
labels
bugaiarchitecturedashboard
assigneesneo-opus-vega
createdAtSep 7, 2026, 2:20 AM
updatedAtSep 8, 2026, 7:21 AM
githubUrlhttps://github.com/neomjs/neo/issues/18421
authorneo-opus-vega
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 8, 2026, 7:21 AM

Root-edge chips pair a center nodeId with the whole-host rect, so the drop lands narrower than its preview

Closed Backlog/active-chunk-21 bugaiarchitecturedashboard
neo-opus-vega
neo-opus-vega commented on Sep 7, 2026, 2:20 AM

Context and problem

Reported by @tobiu on merged dev: dock layouts offer a 5-arm cross inside a container and 4 chips at the root edges. The root top/bottom dropzones span the full width and the sides the full height — but dropping at the bottom in apps/workstation does not produce what the preview drew. The side zones win and the dropped zone comes out narrower.

His framing is the right one: "a classic border-layouts problem — which corners get more weight."

The mechanism, located

The semantic model is not at fault. Probed at d9ea7a5b44 with a document whose root was already a horizontal split — preview 6px above the bottom border → previewToOperation → Operations.splitNode:

previewKind edge-bottom   previewTarget split-root   accepted
operation   splitNode     targetNodeId  split-root   orientation vertical
newRoot     split-split-root-0          children [split-root, tabs-gamma-0]
droppedIsDirectRootChild  TRUE

Full width, exactly as previewed. Operations.splitNode wraps the root (if (!parentSlot) doc.root = newSplitId), so nesting order is docking order.

The defect is in src/dashboard/dock/interaction/DragAffordances.mjs#ensureGeometry:

rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone'
    ? (WorkspaceDocument.getZoneNodeId(nodes[…].zones?.center) ?? me.owner.dockModel.root)
    : me.owner.dockModel.root;

When the root is an edge-zone, the chips target its center. splitNode then writes back through setZoneNodeId(parent, 'center', newSplit) — nesting the drop among the side zones, which are its siblings. Meanwhile the chip renders inset at the host rect's edge midpoints.

⭐@neo-gpt-emmy's statement of the invariant is the precise one: "the root chip's nodeId and its rectangle must describe the same dockable boundary. A center nodeId paired with the whole-host rect is the defect." Preview and commit disagree because they are measured against different nodes.

Provenance: the retarget arrived in e9f10ba17e (#17837/#17841), a vocabulary/anatomy refactor — not a fix for a known defect.

The architectural decision

Two corner rules currently coexist, switching on a node type the user cannot see:

root type rule
split tree WPF DockPanel — docking order owns the corner
edge-zone Swing BorderLayout — fixed slots, structure owns it

@tobiu's stated preference — drop bottom → full width; later drop right → right takes full height and bottom narrows — is the DockPanel rule, and the split-tree path already implements it. Decision: keep last-drop-wins, and make it uniform. Three-way agreement (@neo-opus-vega, @neo-gpt, @neo-gpt-emmy).

🔴But "always use document.root" is wrong, and @neo-gpt-emmy's probe found why the conditional exists. On the canonical vessel shape (edge-zone shell → center stack), wrapping the whole shell leaves resolveStackRoot = null. That is live consumed authority — LayoutAdapter.project reads it under enableStackDrag, and Participation rejects whole-stack admission unless it matches groupNodeId. Splitting the center preserves the shell and returns the new split as the stack root.

So one node kind conflates two things: a real arrangement root, and a shell around transferable content. The fix is to resolve an explicit dockable-root boundary from the owner/host contract and measure that node for both preview and commit — never to delete the conditional.

Evidence already gathered

  • @neo-opus-vega: split-tree root chain correct end to end (above).
  • @neo-gpt at d3b7f0c2: edge-zone with center + left bands — splitNode(bottom, target=frame) → V[frame,tabs-b], then splitNode(right, target=currentRoot) → H[previousRoot,tabs-c]. Both validate, frame descriptor byte-identical, JSON round-trip preserves nested child order.
  • @neo-gpt-emmy, three probes: the real dense Workstation initialDocument wraps correctly and createSavedLayout → JSON → restoreSavedLayout preserves the document byte-for-byte (this answers the saved-layout ordering concern — persistence stores the resulting tree, not a drop replay); the vessel resolveStackRoot regression above; and a sole-pane case where wrapping leaves an empty old edge-zone sibling, since normalizeTree retains empty edge-zones.

Not yet covered by any probe: the real producer→preview→commit path, rendered geometry across two sequential drops, and Group window materialization.

Acceptance criteria

  • AC-1: the root chip's nodeId and its rendered rect describe the same boundary; a drop commits against the node the preview measured.
  • AC-2: two sequential root-edge drops assert on the document tree — bottom yields a vertical root [oldRoot, newTabs]; a following right yields a horizontal root [previousRoot, newTabs] with the bottom nested one deeper. Asserting rendered widths alone is insufficient: a wrong tree passes if the renderer compensates.
  • AC-3: one rendered assertion that the bottom pane's width equals the container width after the first drop — the arm that would have caught this report.
  • AC-4: whole-stack projection and admission survive — resolveStackRoot stays non-null for the canonical vessel shape, and Participation still admits a whole-stack transfer after a root edge drop.
  • AC-5: the sole-pane case leaves no empty edge-zone sibling, or the producer rejects the drop earlier — test the path rather than assuming splitNode eliminates the empty half.
  • AC-6: existing inside-container cross behaviour is unchanged; it remains the local tier.

Out of scope

Renderer/CSS sizing compensation, new callback or config layers (no probe indicated one), #18414's placeholder work, the human-overlap matrix, and any change to the edge-zone internal fixed-slot layout — the shell keeps its own arrangement.

Avoided traps

Do not "fix" the report by shrinking the Workstation root preview to match the center — that hides the mismatch instead of resolving which boundary is authoritative. Do not delete the conditional outright (breaks resolveStackRoot). Do not introduce a second projection path or a drop-order replay in persistence.

Decision Record impact: aligned-with ADR 0029 — this states an invariant the drop model already implies rather than amending authority.

Related

Related: #18303 · #18304 · #17837

Live latest-20 open sweep plus a 12-message A2A recency scan run immediately before creation; no equivalent or competing claim. Origin Session ID: 9b1a4410-2864-4d83-9b7b-105972029354