Context
Measured on a consumer whose multi-window dock looked finished and had two entire gestures dead.
Workspace#getDockProjectionOptions is the base hook that arms tear-out:
getDockProjectionOptions() {
return this.enableDockTearOutLifecycle
? {enableDockTearOut: true, ...this.tearOutHandlers}
: {}
}It is also the natural hook for a host to override — it is the documented way to thread projection options. A host that overrides it and returns its own object, without spreading super, discards enableDockTearOut and the whole handler bundle.
Nothing reports it. The projection succeeds, the dock renders, and the pop-out action still appears — so the host's own acceptance signal says multi-window works.
Live latest-open sweep: latest 20 open issues at 2026-09-03T11:16:37Z; no equivalent. A2A claim sweep: no competing claim.
The Problem
Two gestures die, and the third keeps working in a way that hides them.
- Dragging a tab out of the dock becomes unreachable.
enableDockTearOut is what sets allowOverdrag and enableProxyToPopup on every tab sort zone (projection/LayoutAdapter.mjs:1253-1255). Without it the proxy stays clamped to its tab strip, and the adapter's own comment states the consequence exactly: "the boundary-exit grammar reads the LIVE proxy rect, and while the proxy is clamped to the tab-strip boundary the intersection ratio never drops — the exit cannot fire." The user cannot drag past the boundary at all; it does not read as disabled, it reads as broken.
- A dragged popup finds no target.
crossWindowSortGroup becomes sortGroup on the zone (:1256), and manager.DragCoordinator keys its registry by sort group — sortZones.get(sortGroup)?.get(windowId). A null group means the zone never registers, so a popup dragged over the window highlights no drop areas.
- The pop-out ACTION still renders, because
dockPopOutActionActive derives from tearOutHandlers on the instance, not from the projection context. So the one path that does not read the context keeps working, and it is the path a host is most likely to test first.
That last point is what makes this worth a ticket rather than a consumer footnote: the failure is not just silent, it is masked by a working sibling. A host can pop a pane out, close it, watch it come home, and reasonably conclude the feature is delivered.
This is the #18151 class with an extra turn of the screw — the engine does not merely ask a host to re-derive what it knows, it hands the host a way to drop it and then reports success.
The Architectural Reality
src/dashboard/dock/Workspace.mjs — getDockProjectionOptions() is the sole carrier of enableDockTearOut and tearOutHandlers into the context.
src/dashboard/dock/projection/LayoutAdapter.mjs:1253-1256 — where the three zone flags are derived from it.
Workspace#dockPopOutActionActive — derived from the instance bundle, which is why the action survives the loss.
- The same method's JSDoc already documents that two related inconsistencies "throw at projection rather than silently unaddressing an action". The mechanism this ticket needs is therefore already precedented in the very method that loses the options.
The Fix
Preferred: throw at projection when enableDockTearOutLifecycle is true but the resolved projection context carries no enableDockTearOut. Same shape, same place, same rationale as the two violations already guarded there — a host that meant to enable tear-out learns immediately, and a host that meant to disable it turns the lifecycle flag off rather than dropping the options.
Alternative, if the throw is judged too strict: apply the base wiring outside the overridable hook, so the host's override composes with it rather than replacing it. Cleaner in that a host cannot get it wrong, but it makes the hook's return value no longer the whole context, which is a bigger contract change than the guard.
Either way the host-facing rule wants stating in the hook's JSDoc: an override must spread super.
Acceptance Criteria
Out of Scope
crossWindowSortGroup having no engine default. A sort group is a deployment-wide name a host must choose, so requiring it is correct; this ticket is about the options the engine does own being droppable.
- The vessel conversion thresholds.
- Whether
dockPopOutActionActive should derive from the context instead of the instance — arguably it should, so the action tracks the same truth as the gestures, but that is a separate design question and changing it would alter when the action hides.
Avoided Traps
- Documenting it and calling it done. The reason this survived in a consumer for a full session is not absent documentation, it is that the pop-out action kept working and supplied a false pass. A doc line does not reach someone who already believes the feature is delivered.
- Treating it as consumer error. It is, locally — the override should have spread
super. But an engine that routes its own load-bearing wiring through a host-overridable hook, and then reports success when the host replaces it, has handed out the footgun. The guard is the engine's to add.
- Guarding on the handler bundle instead of the context. Checking
tearOutHandlers would pass in exactly the broken case: the bundle is on the instance and intact. The context is the thing that must be inspected.
Decision Record impact
none — a projection-time guard consistent with two existing ones in the same method.
Related
- #18151 — the class: the dock asking consumers to supply or preserve what only the engine has.
- #18153 / #18159 / #18160 — the pop-out action's own availability chain, which is what masks this.
Handoff Retrieval Hints
query_raw_memories: "tear-out opt-ins lost when a host overrides getDockProjectionOptions without super"
- Anchors:
Workspace#getDockProjectionOptions, LayoutAdapter zone flags at :1253-1256, DragCoordinator.sortZones, dockPopOutActionActive
Origin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d
Context
Measured on a consumer whose multi-window dock looked finished and had two entire gestures dead.
Workspace#getDockProjectionOptionsis the base hook that arms tear-out:getDockProjectionOptions() { return this.enableDockTearOutLifecycle ? {enableDockTearOut: true, ...this.tearOutHandlers} : {} }It is also the natural hook for a host to override — it is the documented way to thread projection options. A host that overrides it and returns its own object, without spreading
super, discardsenableDockTearOutand the whole handler bundle.Nothing reports it. The projection succeeds, the dock renders, and the pop-out action still appears — so the host's own acceptance signal says multi-window works.
Live latest-open sweep: latest 20 open issues at 2026-09-03T11:16:37Z; no equivalent. A2A claim sweep: no competing claim.
The Problem
Two gestures die, and the third keeps working in a way that hides them.
enableDockTearOutis what setsallowOverdragandenableProxyToPopupon every tab sort zone (projection/LayoutAdapter.mjs:1253-1255). Without it the proxy stays clamped to its tab strip, and the adapter's own comment states the consequence exactly: "the boundary-exit grammar reads the LIVE proxy rect, and while the proxy is clamped to the tab-strip boundary the intersection ratio never drops — the exit cannot fire." The user cannot drag past the boundary at all; it does not read as disabled, it reads as broken.crossWindowSortGroupbecomessortGroupon the zone (:1256), andmanager.DragCoordinatorkeys its registry by sort group —sortZones.get(sortGroup)?.get(windowId). A null group means the zone never registers, so a popup dragged over the window highlights no drop areas.dockPopOutActionActivederives fromtearOutHandlerson the instance, not from the projection context. So the one path that does not read the context keeps working, and it is the path a host is most likely to test first.That last point is what makes this worth a ticket rather than a consumer footnote: the failure is not just silent, it is masked by a working sibling. A host can pop a pane out, close it, watch it come home, and reasonably conclude the feature is delivered.
This is the #18151 class with an extra turn of the screw — the engine does not merely ask a host to re-derive what it knows, it hands the host a way to drop it and then reports success.
The Architectural Reality
src/dashboard/dock/Workspace.mjs—getDockProjectionOptions()is the sole carrier ofenableDockTearOutandtearOutHandlersinto the context.src/dashboard/dock/projection/LayoutAdapter.mjs:1253-1256— where the three zone flags are derived from it.Workspace#dockPopOutActionActive— derived from the instance bundle, which is why the action survives the loss.The Fix
Preferred: throw at projection when
enableDockTearOutLifecycleistruebut the resolved projection context carries noenableDockTearOut. Same shape, same place, same rationale as the two violations already guarded there — a host that meant to enable tear-out learns immediately, and a host that meant to disable it turns the lifecycle flag off rather than dropping the options.Alternative, if the throw is judged too strict: apply the base wiring outside the overridable hook, so the host's override composes with it rather than replacing it. Cleaner in that a host cannot get it wrong, but it makes the hook's return value no longer the whole context, which is a bigger contract change than the guard.
Either way the host-facing rule wants stating in the hook's JSDoc: an override must spread
super.Acceptance Criteria
enableDockTearOutLifecycle: truewhosegetDockProjectionOptionsoverride omitssuperfails loudly at projection, naming the dropped option.enableDockTearOutprojects silently, as today; the guard must not fire on a host that never wanted tear-out.superprojects unchanged, withallowOverdrag,enableProxyToPopupandsortGroupall reaching the zone.super, and why the pop-out action cannot be used as evidence that it did.Out of Scope
crossWindowSortGrouphaving no engine default. A sort group is a deployment-wide name a host must choose, so requiring it is correct; this ticket is about the options the engine does own being droppable.dockPopOutActionActiveshould derive from the context instead of the instance — arguably it should, so the action tracks the same truth as the gestures, but that is a separate design question and changing it would alter when the action hides.Avoided Traps
super. But an engine that routes its own load-bearing wiring through a host-overridable hook, and then reports success when the host replaces it, has handed out the footgun. The guard is the engine's to add.tearOutHandlerswould pass in exactly the broken case: the bundle is on the instance and intact. The context is the thing that must be inspected.Decision Record impact
none— a projection-time guard consistent with two existing ones in the same method.Related
Handoff Retrieval Hints
query_raw_memories: "tear-out opt-ins lost when a host overrides getDockProjectionOptions without super"Workspace#getDockProjectionOptions,LayoutAdapterzone flags at:1253-1256,DragCoordinator.sortZones,dockPopOutActionActiveOrigin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d