Context
Found in manual testing of apps/workstation on dev (2026-10-22). Tear one tab out into a popup, move a second tab into that popup, then drag the tab headers inside it:
- No docking. A drag inside the popup only ever resorts the strip. No dock drop zones appear, so the popup's own layout cannot be docked into.
- No tear-out. Dragging a tab out of the popup often does not tear out. The tab-header proxy is clamped to the popup's boundary. This does not happen every time.
@tobiu's screen recording shows both.
Measurements: the live session, read-only over the Neural Link
Session 1d9d302d… (a fresh load), main window 59069496…, popup d100e2b4… (320×269, tabs "Metrics ⠿" and "Audit"). Nothing was mutated.
| sort zone |
window |
allowOverdrag |
enableProxyToPopup |
dockGroupNodeId |
neo-dock-tab-sortzone-1 … -6 (all main strips) |
main |
true |
true |
null |
neo-dock-tab-sortzone-7 (the popup's strip) |
popup |
false |
false |
workstation-vessel-tabs:metrics |
The popup workspace neo-dock-workspace-5 is a Workstation.view.PopupWorkspace with enableDockTearOutLifecycle: false. Its getDockProjectionOptions() returns only {workspaceId, crossWindowSortGroup, enableStackDrag: true}.
The Problem
The popup's strip is projected without the drag contract the main window's strips get:
LayoutAdapter arms allowOverdrag and enableProxyToPopup only for context.enableDockTearOut === true. Without them the proxy is clamped to its boundary and the tear-out exit can never fire, as the adapter's own comment says.
Neo.dashboard.dock.Workspace#getDockProjectionOptions() returns {} unless enableDockTearOutLifecycle is on. PopupWorkspace keeps that off, because the root workspace owns the vessel lifecycle. Its override spreads super correctly, but super has nothing to give.
VesselWorkspace, the main window, also wires onDockCrossZoneDragMove / onDockCrossZoneDragCancel to its dragAffordances, plus dockTearOutBoundaryContainerId and enableVesselConversion. PopupWorkspace publishes none of them, so a drag inside the popup paints no drop zones.
Why "not always", inferred and not traced: the popup still publishes crossWindowSortGroup, so a drag that ends over another Workstation window can be carried there by the cross-window coordinator. Over empty desktop the clamped proxy has no way out.
The engine guard from #18197 / #18199, which refuses a context that dropped the tear-out opt-ins, does not fire here. It only checks workspaces whose tear-out lifecycle is on.
The Architectural Reality
apps/workstation/view/PopupWorkspace.mjs: getDockProjectionOptions() (identity only), getDockParticipationConfig() (the root's proxy embodiment, and a hit-test that admits drops only while populated).
apps/workstation/view/VesselWorkspace.mjs: getDockProjectionOptions(), the full contract the popup lacks.
src/dashboard/dock/Workspace.mjs getDockProjectionOptions(), and src/dashboard/dock/projection/LayoutAdapter.mjs (the sort-zone flags).
- Design authority: the product intent recorded in #19248. Dragging inside a popup should "feel the same like dragging a tab inside a main window", because "popups can have a big size and their own dock layout". Its three-phase model: a tab-header proxy in an app, a popup outside apps, a tab-header proxy again on re-entering an app.
The Fix
Give the popup's strips the main window's drag contract, delegated to the root that owns the vessel lifecycle:
- the tear-out opt-in and gesture callbacks from
rootWorkspace.tearOutHandlers.getProjectionCallbacks();
- the popup's own
dockTearOutBoundaryContainerId: me.id;
enableVesselConversion;
- drop-zone affordance callbacks (
onDockCrossZoneDragMove / …Cancel) for the popup's dock host.
The whole-stack drag (enableStackDrag, the ⠿ handle) and the populated-only hit-test stay as they are.
Trap. A tear-out from a popup is a second window.open under the same held gesture only if the drag already rode a vessel. Use the same admission path main uses, which already handles the one-popup-per-activation constraint (#19533), rather than opening windows from the popup directly.
Acceptance Criteria
Out of Scope
- Vessel sizing for a tear-out from a popup.
- #19989, the stale source header after a transfer into a popup. Found in the same session family.
Related
Live latest-open sweep: checked latest 20 open issues at 2026-10-22T10:17:30Z; no equivalent found.
Keyword sweep: "popup tab drag tear-out", "PopupWorkspace tear out", "popup vessel drop zones", "allowOverdrag popup"; closed adjacent only (#18197/#18199, #19448, #19278, #19243, #18409).
A2A claim sweep: no overlapping lane-claim.
MC sweep: "dragging a tab inside a popup vessel only resorts…", 6 results; the 2026-09-03 WebStudio case (an override dropped the opt-ins) is the same mechanism in another consumer, and there is no prior decision for popups.
Own-assignment sweep: none overlapping.
Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff
Retrieval Hint: "popup vessel tab drag only resorts, no tear-out, allowOverdrag false, PopupWorkspace getDockProjectionOptions"
Context
Found in manual testing of
apps/workstationondev(2026-10-22). Tear one tab out into a popup, move a second tab into that popup, then drag the tab headers inside it:@tobiu's screen recording shows both.
Measurements: the live session, read-only over the Neural Link
Session
1d9d302d…(a fresh load), main window59069496…, popupd100e2b4…(320×269, tabs "Metrics ⠿" and "Audit"). Nothing was mutated.allowOverdragenableProxyToPopupdockGroupNodeIdneo-dock-tab-sortzone-1…-6(all main strips)truetruenullneo-dock-tab-sortzone-7(the popup's strip)falsefalseworkstation-vessel-tabs:metricsThe popup workspace
neo-dock-workspace-5is aWorkstation.view.PopupWorkspacewithenableDockTearOutLifecycle: false. ItsgetDockProjectionOptions()returns only{workspaceId, crossWindowSortGroup, enableStackDrag: true}.The Problem
The popup's strip is projected without the drag contract the main window's strips get:
LayoutAdapterarmsallowOverdragandenableProxyToPopuponly forcontext.enableDockTearOut === true. Without them the proxy is clamped to its boundary and the tear-out exit can never fire, as the adapter's own comment says.Neo.dashboard.dock.Workspace#getDockProjectionOptions()returns{}unlessenableDockTearOutLifecycleis on.PopupWorkspacekeeps that off, because the root workspace owns the vessel lifecycle. Its override spreadssupercorrectly, butsuperhas nothing to give.VesselWorkspace, the main window, also wiresonDockCrossZoneDragMove/onDockCrossZoneDragCancelto itsdragAffordances, plusdockTearOutBoundaryContainerIdandenableVesselConversion.PopupWorkspacepublishes none of them, so a drag inside the popup paints no drop zones.Why "not always", inferred and not traced: the popup still publishes
crossWindowSortGroup, so a drag that ends over another Workstation window can be carried there by the cross-window coordinator. Over empty desktop the clamped proxy has no way out.The engine guard from #18197 / #18199, which refuses a context that dropped the tear-out opt-ins, does not fire here. It only checks workspaces whose tear-out lifecycle is on.
The Architectural Reality
apps/workstation/view/PopupWorkspace.mjs:getDockProjectionOptions()(identity only),getDockParticipationConfig()(the root's proxy embodiment, and a hit-test that admits drops only while populated).apps/workstation/view/VesselWorkspace.mjs:getDockProjectionOptions(), the full contract the popup lacks.src/dashboard/dock/Workspace.mjsgetDockProjectionOptions(), andsrc/dashboard/dock/projection/LayoutAdapter.mjs(the sort-zone flags).The Fix
Give the popup's strips the main window's drag contract, delegated to the root that owns the vessel lifecycle:
rootWorkspace.tearOutHandlers.getProjectionCallbacks();dockTearOutBoundaryContainerId: me.id;enableVesselConversion;onDockCrossZoneDragMove/…Cancel) for the popup's dock host.The whole-stack drag (
enableStackDrag, the ⠿ handle) and the populated-only hit-test stay as they are.Trap. A tear-out from a popup is a second
window.openunder the same held gesture only if the drag already rode a vessel. Use the same admission path main uses, which already handles the one-popup-per-activation constraint (#19533), rather than opening windows from the popup directly.Acceptance Criteria
allowOverdrag: trueandenableProxyToPopup: true, the same as main's.test/playwright/e2e/workstation/WorkstationPopupTabDragNL.spec.mjs, uses the existing tear-out helpers. It tears out a tab, moves a second tab into that popup, then asserts (a) a drop preview appears for a drag inside the popup and (b) a drag out of the popup admits a new vessel. It must be red ondev(resort-only, clamped) and green with the fix, and it joins the hosted Workstation NL tier (#19655).Out of Scope
Related
Live latest-open sweep: checked latest 20 open issues at 2026-10-22T10:17:30Z; no equivalent found. Keyword sweep: "popup tab drag tear-out", "PopupWorkspace tear out", "popup vessel drop zones", "allowOverdrag popup"; closed adjacent only (#18197/#18199, #19448, #19278, #19243, #18409). A2A claim sweep: no overlapping lane-claim. MC sweep: "dragging a tab inside a popup vessel only resorts…", 6 results; the 2026-09-03 WebStudio case (an override dropped the opt-ins) is the same mechanism in another consumer, and there is no prior decision for popups. Own-assignment sweep: none overlapping.
Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff Retrieval Hint: "popup vessel tab drag only resorts, no tear-out, allowOverdrag false, PopupWorkspace getDockProjectionOptions"