LearnNewsExamplesServices
Frontmatter
id17947
titleEngine-owned pop-out header action enters the tear-out lifecycle
stateClosed
labels
enhancementaitestingarchitecturecore
assigneesneo-opus-grace
createdAtAug 31, 2026, 3:50 PM
updatedAtSep 1, 2026, 1:07 AM
githubUrlhttps://github.com/neomjs/neo/issues/17947
authorneo-opus-vega
commentsCount3
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17928 Dock layouts have no maximize — a first-class pane maximize for tabs nodes
blocking[]
closedAtSep 1, 2026, 1:07 AM

Engine-owned pop-out header action enters the tear-out lifecycle

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaitestingarchitecturecore
neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 3:50 PM

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

  • Default off: byte-identical projection. On without enableDockTearOutLifecycle: still byte-identical (double gate witnessed). Both on: focus-gated pop-out renders at the frozen slot.
  • Pressing pop-out detaches the active item into a vessel whose initial geometry is the pane's measured rect; the committed document reflects detachItem, with the item's autoHidden / pinned residency PRESERVED per #17969 option B (witnessed on a railed fixture: the flag survives the round trip and the item comes home railed).
  • Closing the vessel reintegrates through the existing onVesselRetired path — no click-specific reintegration branch exists (asserted structurally: one terminal, one retire path).
  • Vessel-acquisition failure leaves the document uncommitted and the pane untouched (fail-safe ordering witnessed).
  • Gesture proof composes across the shared seam (epic guardrail): a component gesture witness clicks the REAL projected button and asserts the pair-dispatch + commit; a structural arm asserts the action enters the SAME onDockTearOutExit/onDockTearOutTerminal handlers 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

  • 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) 🌿