LearnNewsExamplesServices
Frontmatter
id17926
titleA dock composition that omits the tear-out boundary gets header-clamped tab drags
stateClosed
labels
bugaitestingarchitecturecore
assigneesneo-gpt
createdAtAug 31, 2026, 10:43 AM
updatedAtAug 31, 2026, 2:35 PM
githubUrlhttps://github.com/neomjs/neo/issues/17926
authorneo-opus-vega
commentsCount6
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 2:35 PM

A dock composition that omits the tear-out boundary gets header-clamped tab drags

Closed v13.2.0/archive-v13-2-0-chunk-17 bugaitestingarchitecturecore
neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 10:43 AM

The defect

dashboard.dock.projection.LayoutAdapter forwards boundaryContainerId into each tabs node's header sortZoneConfig only when the composition passes dockTearOutBoundaryContainerId (LayoutAdapter.mjs ~1023). Every composition that omits it falls back to the generic container default — container.Base#afterSetSortable creates the sort zone with boundaryContainerId: me.id, the header toolbar's own element — and the base SortZone clamps the drag proxy to that boundary's rect.

Result for a dock host: a tab drag that begins as an in-header sort can never leave the header strip. The proxy pins to the toolbar, and the cross-zone machinery the same zone is supposed to drive degrades — measured on a consumer: the drop indicators render parked and degenerate at the dock corner (w≈11px at the strip's edge) instead of tracking the pointer. A drag that exits the strip on its very first move happens to engage the cross-zone path, which makes the failure look intermittent and gesture-dependent.

apps/workstation never sees this because it passes dockTearOutBoundaryContainerId: me.id (view/Workspace.mjs ~948) — its comment even states the intended semantics: "Exiting a source toolbar remains ordinary cross-zone motion; only leaving this app/window root enters the vessel outcome machine." The first external consumer omitted the option — its name says tear-out, an opt-in feature — and got clamped drags on day one. Witnessed live in a private consumer workspace on current dev; one line (passing the option) fully restores both gesture shapes.

The adapter's fallback comment says omitting the option keeps consumers "byte-identical" — byte-identical to a plain tab container, but for a dock composition the toolbar clamp contradicts the product's core interaction. An option named for tear-out silently governs basic in-window cross-zone drag UX.

Acceptance Criteria

  • A dashboard.dock.Workspace-backed composition that passes no tear-out options gets tab-header sort zones whose drag boundary is the dock workspace root (the Workspace auto-supplies its component id per the ledger); tear-out stays opt-in and plain tab.Container behavior stays untouched. A direct LayoutAdapter.project() consumer owns supplying the workspace-boundary context field itself — omitting both keys retains the header-toolbar clamp for that consumer class, and the option's docs state exactly that.
  • A spec pins the sort-first gesture shape: pointer moves within the strip first, then exits — the cross-zone preview/indicators must engage at the pointer, not park at the strip.
  • dockTearOutBoundaryContainerId docs state which behaviors it governs beyond tear-out, or the boundary default moves out of its shadow so it governs only tear-out.

Contract Ledger (consumed cells)

Cell Producer Behavior Fallback Docs Evidence
(1) tear-out boundary omitted (today's default) container.Base#afterSetSortable merges boundaryContainerId: me.id (the header toolbar) under sortZoneConfig; projection.LayoutAdapter omits the key entirely when context.dockTearOutBoundaryContainerId is null (~:1023) Proxy clamped to the tab-strip rect (main.addon.DragDrop boundary clamp, ~:914), with allowOverdrag: false (gated on enableDockTearOut, ~:1040) — the adapter's own §2.8 comment states the consequence: "while the proxy is clamped to the tab-strip boundary the intersection ratio never drops — the exit cannot fire". Sort-first gestures cannot leave the strip; cross-zone previews degrade (indicators parked degenerate at the strip corner, w≈11px) None — silent The adapter comment calls the fallback "byte-identical" for consumers that omit the option; true only relative to plain tab containers, not to dock semantics Consumer repro (sort-first vs fast-exit gesture, measured); apps/workstation immune because it supplies the option
(2) dockTearOutBoundaryContainerId supplied Composition option → LayoutAdapter ~:1023-1025 → sortZoneConfig.boundaryContainerId Proxy free within the named root; exiting a source toolbar is ordinary cross-zone motion. This key alone does NOT arm tear-out — arming is enableDockTearOut === true, which pairs allowOverdrag + enableProxyToPopup + the window-boundary hysteresis (~:1040-1042) n/a apps/workstation/view/Workspace.mjs ~:948 comment (the §2.8 block) states the intended semantics Workstation (long-standing); the consumer fix passed ONLY this key with tear-out off — cross-zone motion restored, tear-out never armed, both gesture shapes re-verified
(3) Selected mechanic for this ticket dashboard.dock.Workspace supplies its OWN component id into the projection context as the boundary default (a new context field alongside the existing options; el id = component id by the component contract, so it is a real DOM id, unlike context.workspaceId, which is options.workspaceId ?? null — an optional input nothing produces). The adapter resolves sortZoneConfig.boundaryContainerId as explicit dockTearOutBoundaryContainerId first, then the new context field. Direct-adapter consumers (compositions building LayoutAdapter without dashboard.dock.Workspace): the field is part of the ADAPTER's options contract and such a consumer owns supplying it — omitting BOTH keys retains today's toolbar clamp for that consumer class only, and the option's docs must say exactly that (the adapter owns no component, so it cannot manufacture a root id) Omitted-option compositions get workspace-bounded drags — cell (1) collapses into cell (2) with root = workspace. Supplying the option stays byte-identical. Tear-out arming unchanged (still enableDockTearOut). Explicitly NOT via allowOverdrag: that flag is tear-out's pairing switch — flipping it without tear-out removes the in-window clamp semantics instead of widening them. Explicitly NOT via changing container.Base's generic default: plain toolbars keep their clamp The generic toolbar clamp remains for non-dock sortables AC-3 already covers documenting which behaviors the option name governs, or moving the boundary default out of its shadow The consumer fix IS this mechanic applied manually (one composition line); the AC-2 spec pins the sort-first gesture shape so the default cannot regress

Origin

Consumer-workspace P0 regression triage, agent session 914dffcd-0057-46a5-a0c6-9ca807fd5c91. Related: #17862 (claim arbitration), #17841 (dock anatomy refactor), #8093 (unified header SortZone creation — where the container-default boundary comes from).

— Vega (Fable 5, Claude Code) 🌿

tobiu referenced in commit 36332ff - "feat(dashboard): add dock workspace tab drag boundary (#17926) (#17939)" on Aug 31, 2026, 2:35 PM
tobiu closed this issue on Aug 31, 2026, 2:35 PM