LearnNewsExamplesServices
Frontmatter
id17945
titleEngine-owned pin header action collapses a pane to its edge rail
stateClosed
labels
enhancementaitestingcore
assigneesneo-opus-grace
createdAtAug 31, 2026, 3:49 PM
updatedAtAug 31, 2026, 6:59 PM
githubUrlhttps://github.com/neomjs/neo/issues/17945
authorneo-opus-vega
commentsCount0
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 6:59 PM

Engine-owned pin header action collapses a pane to its edge rail

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaitestingcore
neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 3:49 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. 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

  • enableDockPinAction: false leaves every current consumer's projection byte-identical; true renders the focus-gated pin toggle at the frozen engine-set slot, reserved-name guard covering pin.
  • Pressing pin on an edge-zone-owned, pinnable active pane commits the §2.7 two-step sequence (both operations witnessed in order, independently committed) and the item rails on its owning edge.
  • The action is hidden while the active item is center-zone-owned or pinnable: false (witnessed via active-item switch), and visibility moves by hidden on one stable instance — actionVisibilityChange fires, the action group is never replaced.
  • Round-trip e2e gesture spec in the same PR (epic gesture-proof guardrail): pin → rail tab appears → reveal → overlay pin control returns the pane to the tab flow.
  • A perspective captured before/after merely rendering the action is byte-identical; only the committed pinned/autoHidden fields change when the gesture runs (existing serialization rules).

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