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. The family freezes one ordering contract, stated here once and shared by every leaf: [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated via the tab header's showOnFocus default] → [close — always visible, always last]. Every engine action ships behind its own enableDock<X>Action boolean (the close precedent) with the adapter's reserved-name guard extended to it. Anchors verified at Engine dev@34499f737b.
The Problem
ADR 0029 §2.7's interaction contract specifies a "Collapse control on a pinned/visible pane": a two-step sequence through the reducer — setItemPinned(itemId, false) (when pinned) then setItemAutoHidden(itemId, true) — with each step committing independently and no composite operation. That row was specified as implementation-sufficient, but it has no implementation: #13280 (closed) and PR #13770 landed the other direction — rails project committed-auto-hidden items, the reveal overlay's pin control commits setItemPinned(true) (src/dashboard/dock/interaction/Rail.mjs:423-453, RevealOverlay.mjs:30).
The only non-model caller of setItemAutoHidden in the tree is persistence's RestorePlanner (src/dashboard/dock/persistence/RestorePlanner.mjs:123). A user can bring a pane back from the rail, but no affordance sends one to the rail. The round-trip is half-built.
The Architectural Reality
- Action substrate (shipped): the adapter projects engine-owned actions with a reserved-name guard and host actions preceding them (
src/dashboard/dock/projection/LayoutAdapter.mjs:958-1002); the Workspace owns intent dispatch (Workspace.mjs:1250 onDockHeaderAction) and per-active-item policy by moving hidden on one stable action instance (Workspace.mjs:1164 syncDockCloseAction), never by list replacement — the discipline actionVisibilityChange consumers (tab Overflow) depend on.
- Focus gating (shipped):
toolbar.Base gates actions on showOnFocus through a containsFocus subscription (src/toolbar/Base.mjs:255-301); the tab header's default gates everything the leaf does not exempt.
- Model guards (shipped):
setItemAutoHidden rejects non-boolean payloads, pinnable === false items, and (per §2.7's state table) detached items; setItemPinned(true) clears autoHidden (src/dashboard/dock/model/Operations.mjs:368-408).
- Edge derivation (binding): §2.7 — "the rail an item collapses to is the edge zone that contains it"; center-zone items never rail (fail-safe: main content does not auto-hide). The action is therefore only meaningful for items owned by an edge zone.
The Fix
enableDockPinAction: false on dashboard.dock.Workspace. When on:
- The adapter projects a
pin action into the engine set at its frozen slot; the reserved-name guard covers pin like close.
- The Workspace owns dispatch: for the active item, commit §2.7's two-step collapse sequence through
applyDockZoneOperation — each step independent, the intermediate state (unpinned, visible) benign per the ADR row.
- Visibility policy via the
syncDockCloseAction idiom, recomputed on onDockActiveIndexChange (LayoutAdapter.mjs:418): hidden while the active item is not edge-zone-owned or is pinnable: false — the action never renders where the gesture cannot complete.
- The restore direction stays the shipped overlay pin control; this leaf adds no second path back.
Contract Ledger (consumed cells)
| Cell |
Producer |
Behavior |
Fallback |
Docs |
Evidence |
| action surface |
projection.LayoutAdapter engine-action projection + reserved-name guard; dispatch in Workspace#onDockHeaderAction |
enableDockPinAction: false (default) renders nothing — byte-identical projection; true renders the focus-gated toggle at the frozen slot |
Off by default |
Config jsdoc + guard error text |
close shipping today (LayoutAdapter.mjs:987-1001); #17928 validated the same extension shape |
| collapse dispatch |
NEW Workspace-owned handler committing §2.7's two-step sequence through the existing reducer |
setItemPinned(false) (when pinned) then setItemAutoHidden(true); each commit independent; no composite operation |
Operation errors surface on the result; no partial UI state — the model already fails closed on pinnable:false |
The handler jsdoc cites ADR 0029 §2.7 |
Operations.mjs:368-408 guards exist; RestorePlanner.mjs:123 proves the op path end-to-end |
| visibility policy |
Workspace per-active-item hidden sync (the syncDockCloseAction idiom, Workspace.mjs:1164) driven by onDockActiveIndexChange |
Hidden while the active item is center-zone-owned or pinnable: false; visible otherwise; one stable instance for the node lifetime |
Hidden = safe default |
Config jsdoc carries the reachability rule |
Idiom shipping for close; §2.7 edge-derivation is model-decided, readable from the committed document |
| focus gating |
toolbar.Base showOnFocus default via containsFocus subscription (toolbar/Base.mjs:255-301) |
Gated like every non-close engine action |
Header default |
Already documented on the toolbar |
Shipping |
| rail/reveal side |
Shipped #13280 substrate: rails, reveal overlay, overlay pin control |
Unchanged — this leaf only adds the missing collapse entry |
n/a |
ADR 0029 §2.7 |
Rail.mjs, RevealOverlay.mjs, fleet cycle PR #15474 |
Decision Record impact
aligned-with ADR 0029 §2.7 — implements its specified-but-unbuilt interaction row. No new operations, no schema change, no composite op (the row forbids one).
Acceptance Criteria
Out of Scope
- Hover-reveal defaults, rail drag, overlay behavior — shipped by #13280 per §2.7; untouched.
- A composite pin/auto-hide operation — §2.7 explicitly rejects one.
- The sibling actions (lock, reload, pop-out, maximize) — their own leaves.
Related
Parent: #13158. Siblings: #17928 (maximize, validated), reload / pop-out / lock / overflow-as-action leaves filed alongside this one. Predecessors: #13280, PR #13770, PR #15474. Authority: ADR 0029 §2.7 (learn/agentos/decisions/0029-docking-design.md).
Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock pin header action §2.7 collapse control setItemAutoHidden"
— 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. The family freezes one ordering contract, stated here once and shared by every leaf: [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated via the tab header'sshowOnFocusdefault] → [close — always visible, always last]. Every engine action ships behind its ownenableDock<X>Actionboolean (thecloseprecedent) with the adapter's reserved-name guard extended to it. Anchors verified at Enginedev@34499f737b.The Problem
ADR 0029 §2.7's interaction contract specifies a "Collapse control on a pinned/visible pane": a two-step sequence through the reducer —
setItemPinned(itemId, false)(when pinned) thensetItemAutoHidden(itemId, true)— with each step committing independently and no composite operation. That row was specified as implementation-sufficient, but it has no implementation: #13280 (closed) and PR #13770 landed the other direction — rails project committed-auto-hidden items, the reveal overlay's pin control commitssetItemPinned(true)(src/dashboard/dock/interaction/Rail.mjs:423-453,RevealOverlay.mjs:30).The only non-model caller of
setItemAutoHiddenin the tree is persistence'sRestorePlanner(src/dashboard/dock/persistence/RestorePlanner.mjs:123). A user can bring a pane back from the rail, but no affordance sends one to the rail. The round-trip is half-built.The Architectural Reality
src/dashboard/dock/projection/LayoutAdapter.mjs:958-1002); the Workspace owns intent dispatch (Workspace.mjs:1250 onDockHeaderAction) and per-active-item policy by movinghiddenon one stable action instance (Workspace.mjs:1164 syncDockCloseAction), never by list replacement — the disciplineactionVisibilityChangeconsumers (tab Overflow) depend on.toolbar.Basegates actions onshowOnFocusthrough acontainsFocussubscription (src/toolbar/Base.mjs:255-301); the tab header's default gates everything the leaf does not exempt.setItemAutoHiddenrejects non-boolean payloads,pinnable === falseitems, and (per §2.7's state table) detached items;setItemPinned(true)clearsautoHidden(src/dashboard/dock/model/Operations.mjs:368-408).The Fix
enableDockPinAction: falseondashboard.dock.Workspace. When on:pinaction into the engine set at its frozen slot; the reserved-name guard coverspinlikeclose.applyDockZoneOperation— each step independent, the intermediate state (unpinned, visible) benign per the ADR row.syncDockCloseActionidiom, recomputed ononDockActiveIndexChange(LayoutAdapter.mjs:418): hidden while the active item is not edge-zone-owned or ispinnable: false— the action never renders where the gesture cannot complete.Contract Ledger (consumed cells)
projection.LayoutAdapterengine-action projection + reserved-name guard; dispatch inWorkspace#onDockHeaderActionenableDockPinAction: false(default) renders nothing — byte-identical projection;truerenders the focus-gated toggle at the frozen slotcloseshipping today (LayoutAdapter.mjs:987-1001);#17928validated the same extension shapesetItemPinned(false)(when pinned) thensetItemAutoHidden(true); each commit independent; no composite operationpinnable:falseOperations.mjs:368-408guards exist;RestorePlanner.mjs:123proves the op path end-to-endWorkspaceper-active-itemhiddensync (thesyncDockCloseActionidiom,Workspace.mjs:1164) driven byonDockActiveIndexChangepinnable: false; visible otherwise; one stable instance for the node lifetimeclose; §2.7 edge-derivation is model-decided, readable from the committed documenttoolbar.BaseshowOnFocusdefault viacontainsFocussubscription (toolbar/Base.mjs:255-301)Rail.mjs,RevealOverlay.mjs, fleet cycle PR #15474Decision Record impact
aligned-with ADR 0029 §2.7— implements its specified-but-unbuilt interaction row. No new operations, no schema change, no composite op (the row forbids one).Acceptance Criteria
enableDockPinAction: falseleaves every current consumer's projection byte-identical;truerenders the focus-gatedpintoggle at the frozen engine-set slot, reserved-name guard coveringpin.pinnable: false(witnessed via active-item switch), and visibility moves byhiddenon one stable instance —actionVisibilityChangefires, the action group is never replaced.pinned/autoHiddenfields change when the gesture runs (existing serialization rules).Out of Scope
Related
Parent: #13158. Siblings: #17928 (maximize, validated), reload / pop-out / lock / overflow-as-action leaves filed alongside this one. Predecessors: #13280, PR #13770, PR #15474. Authority: ADR 0029 §2.7 (
learn/agentos/decisions/0029-docking-design.md).Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock pin header action §2.7 collapse control setItemAutoHidden"
— Vega (Fable 5, Claude Code) 🌿