Context
Part of the pane-header parity family under epic #13158: engine-owned default header actions on dock tabs nodes, measured against a native Qt-class reference UX for the desktop-to-web migration audience. #17928 (maximize) is the intake-validated sibling. Family ordering contract (shared, frozen): [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated] → [close — always visible, always last]; each engine action behind its own enableDock<X>Action boolean with the reserved-name guard extended. Anchors verified at Engine dev@34499f737b.
The Problem
Tearing a pane out into its own window is reachable today by the drag gesture (ADR 0029 §2.8's hysteresis + outcome machine) and by the keyboard command twin — but not from the pane header. Qt-class pane headers additionally offer a one-click float/undock control — reachable without a precision drag, keyboard-discoverable, and the affordance migration users actually press.
The engine anticipated exactly this consumer and never built it: Workspace#getPreservedItemIds' jsdoc names "a click-detached pane" as its example of a consumer-held pane (src/dashboard/dock/Workspace.mjs:1337-1344), yet the pane header offers no button. The drag terminal's own commit sequence — the Workspace-owned tearOutHandlers pair — is the one shipped, spec-covered path into a vessel; only the header-action SURFACE that enters it from a click is missing.
The Architectural Reality
- Lifecycle (shipped):
enableDockTearOutLifecycle (Workspace.mjs:130) composes tearOutHandlers via createDockTearOutHandlers (Workspace.mjs:271); vessel lifecycle + admission per §2.8.3; reintegration via onVesselRetired (Workspace.mjs:417,647).
- Model (shipped):
detachItem (src/dashboard/dock/model/Operations.mjs:319) — "same-instance semantic return after physical disconnect, and exact-once teardown" (Workspace.mjs:39). Per the #17969 option-B ruling, detach PRESERVES autoHidden (and pinned) as the item's docked residency; the detached/railed mutual exclusion is a projection invariant (a detached item is in no tabs node, so the node-tree rail walk can never rail it), not a commit-time field mutation.
- Action substrate (shipped): engine-action projection + reserved-name guard (
LayoutAdapter.mjs:958-1002), Workspace dispatch (Workspace.mjs:1250), per-active-item hidden policy (Workspace.mjs:1164), showOnFocus gating (toolbar/Base.mjs:255-301).
- Geometry idiom (validated): measure-then-apply via
DomAccess.getBoundingClientRect — the #17928-vetted pattern for workspace-relative geometry.
The Fix
enableDockPopOutAction: false on dashboard.dock.Workspace. When on and enableDockTearOutLifecycle is armed:
- The adapter projects a
pop-out action at the frozen engine-set slot (reserved-name guard extended; focus-gated).
- Dispatch: measure the active pane's current global rect (initial vessel geometry — the pane appears to lift in place), then drive the existing tear-out terminal path programmatically: vessel acquisition + the
detachItem commit through the same handlers the drag terminal already uses.
- While the lifecycle is not armed the action is not projected at all — an action whose gesture cannot complete must not render (state-reachable honesty; same rule the pin leaf applies).
The entry shape (delivered): the producer is the Workspace-owned tear-out pair — tearOutHandlers.onDockTearOutExit({itemId, proxyRect, sortZone: null}) admission-first and fail-closed (a falsy resolution refuses the vessel and the pane stays untouched, nothing committed), then tearOutHandlers.onDockTearOutTerminal({itemId}) carrying the exactly-once detachItem commit. This is LITERALLY the sequence the pointer gesture's terminal calls — not a second sequence that resembles the drag path, so admission, the exactly-once commit, the throwing-reducer refusal route and vessel retirement are inherited rather than restated and cannot drift out of agreement with the gesture. The only delta is geometry: the click path passes the measured pane rect where a discrete entry has no live proxy. (The intake-era sketch routed through the keyboard command twin; implementation found the pair is the real shared seam — the twin is a sibling CONSUMER of it, not the producer.) No parallel lifecycle — one vessel pipeline, one detach commit path.
Contract Ledger (consumed cells)
| Cell |
Producer |
Behavior |
Fallback |
Docs |
Evidence |
| action surface |
projection.LayoutAdapter + Workspace#onDockHeaderAction (the close precedent) |
Default off; on = focus-gated pop-out at the frozen slot; projected only while the tear-out lifecycle is armed |
Absent action |
Config jsdoc names the double gate |
close + #17928 extension shape |
| lifecycle entry |
the Workspace-owned onDockTearOutExit → onDockTearOutTerminal pair — the drag terminal's own sequence, entered from a click |
Vessel admission + detachItem commit IDENTICAL to the drag terminal (same handlers, not a lookalike); autoHidden rides the round trip unchanged per #17969 option B |
Refused admission = no commit, pane untouched (the pair's own fail-closed ordering) |
Handler jsdoc cites §2.8 |
Drag path shipping + spec-covered; getPreservedItemIds jsdoc names the click-detached consumer |
| initial geometry |
Measured pane rect via DomAccess.getBoundingClientRect on the pane root |
Vessel opens at the pane's current global rect |
Unresolvable rect → lifecycle default placement, never a blocked commit |
Method jsdoc |
#17928 measured-rect precedent; DockFlip's measure idiom |
| reintegration |
onVesselRetired (Workspace.mjs:417,647) |
Unchanged — click-detached vessels retire exactly like drag-detached ones |
Existing path |
ADR 0029 §2.8.3 |
Shipping |
| visibility policy |
Per-active-item hidden sync (syncDockCloseAction idiom) |
Hidden for items the model would reject (e.g. a locked item once the lock leaf lands) |
Hidden = safe default |
Config jsdoc |
Idiom shipping |
Decision Record impact
aligned-with ADR 0029 §2.8 — a new entry point into the existing gesture outcome machine and vessel lifecycle; no new lifecycle, no new operations.
Acceptance Criteria
Out of Scope
- Whole-node pop-out (
moveNode/transferNode territory) — the action targets the active item, the close grain.
- New vessel types, OS-window reintegration changes (#13028 lane), cross-window drag semantics.
Related
Parent: #13158. Siblings: #17928 and the pin / reload / lock / overflow-as-action leaves filed alongside. Authority: ADR 0029 §2.8 (learn/agentos/decisions/0029-docking-design.md). Substrate: src/dashboard/dock/window/ tear-out composition.
Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock pop-out header action click-detached tear-out terminal detachItem"
— Vega (Fable 5, Claude Code) 🌿
Context
Part of the pane-header parity family under epic #13158: engine-owned default header actions on dock tabs nodes, measured against a native Qt-class reference UX for the desktop-to-web migration audience.
#17928(maximize) is the intake-validated sibling. Family ordering contract (shared, frozen): [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated] → [close — always visible, always last]; each engine action behind its ownenableDock<X>Actionboolean with the reserved-name guard extended. Anchors verified at Enginedev@34499f737b.The Problem
Tearing a pane out into its own window is reachable today by the drag gesture (ADR 0029 §2.8's hysteresis + outcome machine) and by the keyboard command twin — but not from the pane header. Qt-class pane headers additionally offer a one-click float/undock control — reachable without a precision drag, keyboard-discoverable, and the affordance migration users actually press.
The engine anticipated exactly this consumer and never built it:
Workspace#getPreservedItemIds' jsdoc names "a click-detached pane" as its example of a consumer-held pane (src/dashboard/dock/Workspace.mjs:1337-1344), yet the pane header offers no button. The drag terminal's own commit sequence — the Workspace-ownedtearOutHandlerspair — is the one shipped, spec-covered path into a vessel; only the header-action SURFACE that enters it from a click is missing.The Architectural Reality
enableDockTearOutLifecycle(Workspace.mjs:130) composestearOutHandlersviacreateDockTearOutHandlers(Workspace.mjs:271); vessel lifecycle + admission per §2.8.3; reintegration viaonVesselRetired(Workspace.mjs:417,647).detachItem(src/dashboard/dock/model/Operations.mjs:319) — "same-instance semantic return after physical disconnect, and exact-once teardown" (Workspace.mjs:39). Per the #17969 option-B ruling, detach PRESERVESautoHidden(andpinned) as the item's docked residency; the detached/railed mutual exclusion is a projection invariant (a detached item is in no tabs node, so the node-tree rail walk can never rail it), not a commit-time field mutation.LayoutAdapter.mjs:958-1002), Workspace dispatch (Workspace.mjs:1250), per-active-itemhiddenpolicy (Workspace.mjs:1164),showOnFocusgating (toolbar/Base.mjs:255-301).DomAccess.getBoundingClientRect— the#17928-vetted pattern for workspace-relative geometry.The Fix
enableDockPopOutAction: falseondashboard.dock.Workspace. When on andenableDockTearOutLifecycleis armed:pop-outaction at the frozen engine-set slot (reserved-name guard extended; focus-gated).detachItemcommit through the same handlers the drag terminal already uses.The entry shape (delivered): the producer is the Workspace-owned tear-out pair —
tearOutHandlers.onDockTearOutExit({itemId, proxyRect, sortZone: null})admission-first and fail-closed (a falsy resolution refuses the vessel and the pane stays untouched, nothing committed), thentearOutHandlers.onDockTearOutTerminal({itemId})carrying the exactly-oncedetachItemcommit. This is LITERALLY the sequence the pointer gesture's terminal calls — not a second sequence that resembles the drag path, so admission, the exactly-once commit, the throwing-reducer refusal route and vessel retirement are inherited rather than restated and cannot drift out of agreement with the gesture. The only delta is geometry: the click path passes the measured pane rect where a discrete entry has no live proxy. (The intake-era sketch routed through the keyboard command twin; implementation found the pair is the real shared seam — the twin is a sibling CONSUMER of it, not the producer.) No parallel lifecycle — one vessel pipeline, one detach commit path.Contract Ledger (consumed cells)
projection.LayoutAdapter+Workspace#onDockHeaderAction(thecloseprecedent)pop-outat the frozen slot; projected only while the tear-out lifecycle is armedclose+#17928extension shapeonDockTearOutExit→onDockTearOutTerminalpair — the drag terminal's own sequence, entered from a clickdetachItemcommit IDENTICAL to the drag terminal (same handlers, not a lookalike);autoHiddenrides the round trip unchanged per #17969 option BgetPreservedItemIdsjsdoc names the click-detached consumerDomAccess.getBoundingClientRecton the pane root#17928measured-rect precedent; DockFlip's measure idiomonVesselRetired(Workspace.mjs:417,647)hiddensync (syncDockCloseActionidiom)Decision Record impact
aligned-with ADR 0029 §2.8— a new entry point into the existing gesture outcome machine and vessel lifecycle; no new lifecycle, no new operations.Acceptance Criteria
enableDockTearOutLifecycle: still byte-identical (double gate witnessed). Both on: focus-gatedpop-outrenders at the frozen slot.detachItem, with the item'sautoHidden/pinnedresidency PRESERVED per #17969 option B (witnessed on a railed fixture: the flag survives the round trip and the item comes home railed).onVesselRetiredpath — no click-specific reintegration branch exists (asserted structurally: one terminal, one retire path).onDockTearOutExit/onDockTearOutTerminalhandlers the drag terminal calls (no lookalike sequence); the multi-window vessel reality stays covered by the existing tear-out e2e family that owns that shared path. An action-specific e2e is not required BECAUSE the same-pair assertion is what makes the inheritance witnessed rather than asserted.Out of Scope
moveNode/transferNodeterritory) — the action targets the active item, theclosegrain.Related
Parent: #13158. Siblings: #17928 and the pin / reload / lock / overflow-as-action leaves filed alongside. Authority: ADR 0029 §2.8 (
learn/agentos/decisions/0029-docking-design.md). Substrate:src/dashboard/dock/window/tear-out composition.Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock pop-out header action click-detached tear-out terminal detachItem"
— Vega (Fable 5, Claude Code) 🌿