Context
Operator, 2026-09-26 ~09:3xZ, watching the conversion probe on the capture display: "a popup resolved to a window content as the drag proxy … this is a design fail: it should convert to the tab header. we drag a tab header within the main window. we drag outside and get the popup proxy. now, on entering the popup window, adding a 3rd proxy type would be insane. poor UX." And the rationale: "consistency: it should feel the same like dragging a tab inside a main window. and popups can have a big size and their own dock layout." — a window-sized content proxy covers exactly the dock layout the hand is trying to aim at.
Also observed by the operator: the tab header inside that content proxy renders without the compact styling (taller). The header comparison probe shows a committed popup's dock header identical to main's (33 px toolbar, 32 px button); the tall header is the content proxy's own, rendered outside the dock host's class chain. Retiring the content proxy retires that symptom with it.
The three-phase model, in the operator's words (09:4xZ): "in app => tab header proxy => outside apps popup proxy (this part is good, since a drop keeps the visual state as is). re-entering an app => tab header proxy again." Re-entering MAIN already does this (the boundary re-entry retires the vessel and the in-window tab-header proxy resumes); re-entering a POPUP is what this ticket makes consistent.
The Problem
After the park admits, the target-side embodiment moves the SAME live pane into a DragProxyContainer in the target window (src/dashboard/dock/window/VesselEmbodiment.mjs: "the SAME live pane renders inside one target-window DragProxyContainer"; .workstation-vessel-dragproxy { opacity: .7 }), sized by the engaged record's sourceRect — the vessel's live extent — through DragCoordinator#onDragMove (targetProxyRect takes transitionSourceRect.width / height when the transition owns the frame, the ordinary proxyRect otherwise). That is the third proxy type: the in-window drag shows the tab-header proxy, the void shows the popup, the target shows a window-sized pane.
Design authority
The operator's statement above is the product intent (a UX call, human-owned). ADR 0029 §2.8.6 names the transition ("popup-over-vessel converts to a proxy over the target while the park keeps the real vessel alive") without prescribing the proxy's shape. Decision Record impact: amends ADR 0029 §2.8.6 with one sentence naming the target-side proxy as the tab-header proxy of the in-window drag.
The Architectural Reality
src/manager/DragCoordinator.mjs onDragMove (~:1100-1140): targetProxyRect sizing from transitionSourceRect; the ordinary path sizes from proxyRect (during a window drag the source proxyRect is the future vessel, src/main/addon/DragDrop.mjs ~:575, so the tab-header size must come from the zone's item rect).
src/dashboard/dock/interaction/TabSortZone.mjs resolveRemoteDragTransition: the engaged record's sourceRect ("the exact live source extent for target-proxy embodiment").
src/dashboard/dock/window/VesselEmbodiment.mjs + Participation#defaultStageDragEmbodiment / resolveProxyConfig: the pane move into the target's DragProxyContainer.
apps/workstation/view/VesselWorkspace.mjs vesselProxyEmbodiment: the Workstation's binding.
- The commit path (
commitTransfer / transferItem) adopts the pane from the parked vessel; with a header proxy the pane simply stays in the parked vessel until the drop.
The Fix
- The engaged record carries the dragged tab's header rect (the zone's
itemRects[startIndex] extent) instead of the vessel's extent; the coordinator sizes targetProxyRect from it at localX − offsetX, localY − offsetY like the ordinary proxy.
- The target-side embodiment renders a tab-header proxy (the same
DragProxyComponent shape the in-window drag uses: the button's vdom clone, dock header classes, compact height) in the target window; the live pane stays in the parked vessel; restore retires the header proxy.
- The stand-in placeholder logic that reserves the pane's slot in the vessel is unnecessary for a header proxy — retire what no longer has a reader (Accretion Defense: name the deletion).
- ADR 0029 §2.8.6: one sentence.
Acceptance Criteria
- AC-1 (unit, red-first): the engaged record's
sourceRect is the tab header's extent; the coordinator's targetProxyRect equals the ordinary proxy sizing for a transition-owned frame.
- AC-2 (unit): staging the embodiment creates a header proxy in the target window and leaves the live pane in the vessel; restore removes it; no placeholder stand-in is created.
- AC-3 → transferred to #19278 AC-5 (hand, on the operator's display), delivered with the target-clear park. Annotated 2026-09-26 by the author: under a hand drag the parked vessel covers the header proxy as it covers the zones (the operator's recording, #19186), so this witness cannot be delivered from PR #19254 alone. Was: (headed, one run) the hand-conversion probe shows a tab-header-sized proxy inside the target (bounding box ≈ the source tab button's), the target's drop zones visible around it, drop commits the pane.
- AC-4 (docs): ADR 0029 §2.8.6 carries the sentence;
.workstation-vessel-dragproxy's comment no longer speaks of the live pane.
Contract Ledger
Claimer's section (@neo-opus-ada, from the review of PR #19254), accepted by the author 2026-09-26. Surfaces are anchored at the PR head 5b4a8095f1.
| Target Surface |
Source of Authority |
Behavior |
Fallback / Edge |
Docs |
Evidence |
TabSortZone#resolveRemoteDragTransition(): the engaged record's proxyRect, from getVesselConversionProxyRect() (new) |
this ticket |
{height, offsetX, offsetY, width}: the dragged tab header's measured extent (dragElementRect) and the grab offset inside it |
null before the drag element is measured. The sensor's vesselConversionSourceRect, the park's anchor, is unchanged |
JSDoc |
DockTabSortZone.spec conversion-binding arms |
DragCoordinator#onDragMove → onRemoteDragMove: proxyRect sizing and embodyHeader (new) |
this ticket |
A transition-owned frame: the target proxy is the header's extent at the pointer minus the grab offset, with embodyHeader: true |
A non-finite or non-positive extent or offset cancels the conversion before preview. Every other remote drag keeps the ordinary proxy rect and embodyHeader: false |
inline comment |
DragCoordinator.spec: "conversion resolver receives one live INNER-viewport frame…"; "an engaged record whose header extent or grab offset is not finite cancels the conversion before preview (#19248)" |
DragZone#getDragProxyVdom() (new, public) |
this ticket |
An id-free copy of the configured proxy vdom, else of the drag element, wrapped unless useProxyWrapper is off. createDragProxy renders the same content |
null with neither |
JSDoc |
DragZone.spec: "getDragProxyVdom answers an id-free copy of the drag element, wrapped unless useProxyWrapper is off (#19248)" |
createDockVesselProxyEmbodiment().move({embodyHeader: true}) |
this ticket; ADR 0029 §2.8.6 |
One DragProxyComponent over the source zone's getDragProxyVdom() in the target window. Nothing is staged; the pane stays in the parked vessel. restore and promote retire it exactly once |
No proxy vdom: move() answers false and admits nothing. A refused mount retires that generation and settles whenSettled() false |
JSDoc (factory, move) |
DockVesselEmbodiment.spec, "a converted tab drag enters the target as its tab-header proxy (#19248)", four arms |
snapshot().header (new) |
this ticket |
true for a header proxy. CrossWindowGestureSnapshot reads it in place of ownsPane |
null snapshot without an active proxy |
JSDoc |
WorkstationFiveBeatNL scene 3 (headless Neural Link, not CI-run) |
DragTarget#isDragEmbodimentLive, defaulted by Participation#defaultIsDragEmbodimentLive (new) |
the PR #19254 review, RA-1 |
The drop asks whether the embodiment owner's proxy for the item has settled (snapshot(itemId).settled). A proxy still mounting, or refused, answers false: the drop restores, clears the preview and commits nothing |
Unset, or an owner without snapshot: null, and the drop proceeds as before |
JSDoc |
DockCrossWindowParticipation.spec: "a header proxy that has not rendered commits no drop, refused or still mounting; over a rendered one the same drop commits" |
| Native title-bar drag (the alternate) |
the operator, per PR #19254's Deltas: a real popup stays a popup |
Unchanged: the live pane moves into a DragProxyContainer in the target, and the VesselPlaceholder stays its reader |
— |
ADR 0029 §2.8.6 sentence |
the existing pane-mode DockVesselEmbodiment.spec arms |
Out of Scope
- #19278 (the target-clear park, under #19186) — the blocker for the hand witness; this ticket is the UX shape on top. #19247 is closed: the title-bar-strip re-entry hypothesis was not the mechanism.
- #19186 (the stand-in mask over another popup) may dissolve with the placeholder's retirement — verify, do not assume.
Related
#19241 / PR #19242, #19243 / PR #19244, #19186, #19278 (the park; owns AC-3's witness), PR #19254, ADR 0029 §2.8.6, #15396 (conversion + park origin).
Live latest-open sweep: latest 20 open issues at 2026-09-26T09:37:50Z, no equivalent. A2A sweep (last 8 at 09:35Z): none. Memory sweep: the operator's two statements this session. Own-assignment sweep: none covers it.
handoff: @neo-opus-ada (dock lane, offered a second pair of hands this morning; Opus budget) — 1:1 wake follows.
Origin Session ID: 519ec01f-3aec-4247-a34a-e8741fd4757e
Retrieval Hint: "converted vessel target proxy tab header not window content"
🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session 519ec01f-3aec-4247-a34a-e8741fd4757e
Context
Operator, 2026-09-26 ~09:3xZ, watching the conversion probe on the capture display: "a popup resolved to a window content as the drag proxy … this is a design fail: it should convert to the tab header. we drag a tab header within the main window. we drag outside and get the popup proxy. now, on entering the popup window, adding a 3rd proxy type would be insane. poor UX." And the rationale: "consistency: it should feel the same like dragging a tab inside a main window. and popups can have a big size and their own dock layout." — a window-sized content proxy covers exactly the dock layout the hand is trying to aim at.
Also observed by the operator: the tab header inside that content proxy renders without the compact styling (taller). The header comparison probe shows a committed popup's dock header identical to main's (33 px toolbar, 32 px button); the tall header is the content proxy's own, rendered outside the dock host's class chain. Retiring the content proxy retires that symptom with it.
The three-phase model, in the operator's words (09:4xZ): "in app => tab header proxy => outside apps popup proxy (this part is good, since a drop keeps the visual state as is). re-entering an app => tab header proxy again." Re-entering MAIN already does this (the boundary re-entry retires the vessel and the in-window tab-header proxy resumes); re-entering a POPUP is what this ticket makes consistent.
The Problem
After the park admits, the target-side embodiment moves the SAME live pane into a
DragProxyContainerin the target window (src/dashboard/dock/window/VesselEmbodiment.mjs: "the SAME live pane renders inside one target-window DragProxyContainer";.workstation-vessel-dragproxy { opacity: .7 }), sized by the engaged record'ssourceRect— the vessel's live extent — throughDragCoordinator#onDragMove(targetProxyRecttakestransitionSourceRect.width / heightwhen the transition owns the frame, the ordinaryproxyRectotherwise). That is the third proxy type: the in-window drag shows the tab-header proxy, the void shows the popup, the target shows a window-sized pane.Design authority
The operator's statement above is the product intent (a UX call, human-owned). ADR 0029 §2.8.6 names the transition ("popup-over-vessel converts to a proxy over the target while the park keeps the real vessel alive") without prescribing the proxy's shape. Decision Record impact:
amends ADR 0029 §2.8.6with one sentence naming the target-side proxy as the tab-header proxy of the in-window drag.The Architectural Reality
src/manager/DragCoordinator.mjsonDragMove(~:1100-1140):targetProxyRectsizing fromtransitionSourceRect; the ordinary path sizes fromproxyRect(during a window drag the sourceproxyRectis the future vessel,src/main/addon/DragDrop.mjs~:575, so the tab-header size must come from the zone's item rect).src/dashboard/dock/interaction/TabSortZone.mjsresolveRemoteDragTransition: the engaged record'ssourceRect("the exact live source extent for target-proxy embodiment").src/dashboard/dock/window/VesselEmbodiment.mjs+Participation#defaultStageDragEmbodiment/resolveProxyConfig: the pane move into the target'sDragProxyContainer.apps/workstation/view/VesselWorkspace.mjsvesselProxyEmbodiment: the Workstation's binding.commitTransfer/transferItem) adopts the pane from the parked vessel; with a header proxy the pane simply stays in the parked vessel until the drop.The Fix
itemRects[startIndex]extent) instead of the vessel's extent; the coordinator sizestargetProxyRectfrom it atlocalX − offsetX,localY − offsetYlike the ordinary proxy.DragProxyComponentshape the in-window drag uses: the button's vdom clone, dock header classes, compact height) in the target window; the live pane stays in the parked vessel;restoreretires the header proxy.Acceptance Criteria
sourceRectis the tab header's extent; the coordinator'stargetProxyRectequals the ordinary proxy sizing for a transition-owned frame..workstation-vessel-dragproxy's comment no longer speaks of the live pane.Contract Ledger
Claimer's section (@neo-opus-ada, from the review of PR #19254), accepted by the author 2026-09-26. Surfaces are anchored at the PR head
5b4a8095f1.TabSortZone#resolveRemoteDragTransition(): the engaged record'sproxyRect, fromgetVesselConversionProxyRect()(new){height, offsetX, offsetY, width}: the dragged tab header's measured extent (dragElementRect) and the grab offset inside itnullbefore the drag element is measured. The sensor'svesselConversionSourceRect, the park's anchor, is unchangedDockTabSortZone.specconversion-binding armsDragCoordinator#onDragMove→onRemoteDragMove:proxyRectsizing andembodyHeader(new)embodyHeader: trueembodyHeader: falseDragCoordinator.spec: "conversion resolver receives one live INNER-viewport frame…"; "an engaged record whose header extent or grab offset is not finite cancels the conversion before preview (#19248)"DragZone#getDragProxyVdom()(new, public)useProxyWrapperis off.createDragProxyrenders the same contentnullwith neitherDragZone.spec: "getDragProxyVdom answers an id-free copy of the drag element, wrapped unless useProxyWrapper is off (#19248)"createDockVesselProxyEmbodiment().move({embodyHeader: true})DragProxyComponentover the source zone'sgetDragProxyVdom()in the target window. Nothing is staged; the pane stays in the parked vessel.restoreandpromoteretire it exactly oncemove()answers false and admits nothing. A refused mount retires that generation and settleswhenSettled()falsemove)DockVesselEmbodiment.spec, "a converted tab drag enters the target as its tab-header proxy (#19248)", four armssnapshot().header(new)truefor a header proxy.CrossWindowGestureSnapshotreads it in place ofownsPanenullsnapshot without an active proxyWorkstationFiveBeatNLscene 3 (headless Neural Link, not CI-run)DragTarget#isDragEmbodimentLive, defaulted byParticipation#defaultIsDragEmbodimentLive(new)snapshot(itemId).settled). A proxy still mounting, or refused, answersfalse: the drop restores, clears the preview and commits nothingsnapshot:null, and the drop proceeds as beforeDockCrossWindowParticipation.spec: "a header proxy that has not rendered commits no drop, refused or still mounting; over a rendered one the same drop commits"DragProxyContainerin the target, and theVesselPlaceholderstays its readerDockVesselEmbodiment.specarmsOut of Scope
Related
#19241 / PR #19242, #19243 / PR #19244, #19186, #19278 (the park; owns AC-3's witness), PR #19254, ADR 0029 §2.8.6,
#15396(conversion + park origin).Live latest-open sweep: latest 20 open issues at 2026-09-26T09:37:50Z, no equivalent. A2A sweep (last 8 at 09:35Z): none. Memory sweep: the operator's two statements this session. Own-assignment sweep: none covers it.
handoff: @neo-opus-ada (dock lane, offered a second pair of hands this morning; Opus budget) — 1:1 wake follows.
Origin Session ID: 519ec01f-3aec-4247-a34a-e8741fd4757e Retrieval Hint: "converted vessel target proxy tab header not window content"
🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session 519ec01f-3aec-4247-a34a-e8741fd4757e