LearnNewsExamplesServices
Frontmatter
id19990
titleTab drags inside a popup vessel only resort: no dock zones, no tear-out
stateClosed
labels
bugaidashboard
assigneesneo-opus-grace
createdAtOct 22, 2026, 12:18 PM
updatedAtOct 22, 2026, 6:35 PM
githubUrlhttps://github.com/neomjs/neo/issues/19990
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 22, 2026, 6:35 PM

Tab drags inside a popup vessel only resort: no dock zones, no tear-out

Closed Backlog/active-chunk-10 bugaidashboard
neo-opus-ada
neo-opus-ada commented on Oct 22, 2026, 12:18 PM

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:

  1. 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.
  2. 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

  • A popup's tab-strip zones carry allowOverdrag: true and enableProxyToPopup: true, the same as main's.
  • Dragging a popup tab within the popup shows the dock's drop previews over the popup's dock host, and dropping commits the same operations a main-window drop would.
  • Dragging a popup tab out of the popup over empty desktop tears it out into a new popup every time, instead of clamping to the popup's boundary.
  • The whole-stack drag via the ⠿ handle behaves as today.
  • E2E: a new whitebox Neural Link spec, 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 on dev (resort-only, clamped) and green with the fix, and it joins the hosted Workstation NL tier (#19655).

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"

tobiu
tobiu Oct 22, 2026, 12:44 PM

https://github.com/user-attachments/assets/71e90e0a-5c12-4d08-8a34-6d212a388970

  • 2026-10-22T11:04:04Z @neo-opus-ada unassigned from @neo-opus-ada
  • 2026-10-22T13:04:33Z @neo-opus-grace assigned to @neo-opus-grace
  • 2026-10-22T14:16:14Z @neo-opus-ada cross-referenced by PR #20003
  • 2026-10-22T14:45:12Z @neo-opus-grace cross-referenced by #20004
  • 2026-10-22T14:45:35Z @neo-opus-grace cross-referenced by #20006
  • 2026-10-22T15:30:25Z @neo-opus-grace cross-referenced by PR #20010
tobiu closed this issue on Oct 22, 2026, 6:35 PM
tobiu referenced in commit 4350ad7 - "feat(workstation): a popup's tab strips get the main window's drag contract (#19990) (#20010)" on Oct 22, 2026, 6:35 PM