Discussion #17415 graduates DockLayout integration as the second bounded leaf after the generic flat TabContainer-actions contract.
The generic blocker is delivered: #17418 closed through PR #17423. The Dock workspace authority also moved after this ticket was filed: PR #17545 introduced Neo.dashboard.DockWorkspace as the engine-owned reducer/projection/reconciliation host, and PR #17565 migrated Workstation onto it. This ticket now extends that class instead of adding another consumer-owned close loop.
The Dock item schema already carries closable, but only through the serialization allow-list. No runtime path reads it. DockZoneModel.closeItem currently permits every item, and projected tab chrome must never remove a pane before the committed Dock document changes.
Live latest-open and all-state duplicate sweeps remain clean; no equivalent implementation ticket or PR exists.
The Problem
An optional persistent close action in a projected TabContainer needs Dock semantic authority:
the active Dock item must be resolved at dispatch time, after activation or reorder;
explicit closable: false must fail closed in the operation layer, not only disappear visually;
an absent closable field must preserve current behavior and remain closeable;
the model must commit exactly once before projection changes chrome;
action visibility must follow the live active item while the action instance survives reconciliation;
closing the focused active item must land focus on a deterministic successor rather than document.body.
A direct TabContainer.removeAt() would let runtime chrome outrun model truth and would bypass serialized policy.
The Architectural Reality
DockZoneModel.closeItem is the semantic mutation and successor-selection authority.
DockWorkspace owns the one committed document, the reducer call, view-sync scheduling, policy-visibility resync, and focus after the reconciler settles.
DockLayoutAdapter projects the persistent action and forwards clone-safe runtime intent. It never owns the close effect.
DockProjectionReconciler owns chrome convergence after the document commit and preserves the stable action instance on retained tab containers.
closable adds no schema field or migration; this ticket gives an existing serialized field runtime meaning.
The generic action capability emits intent and preserves explicit handler precedence. Dock supplies the effect without serializing functions into the document.
The Fix
Add an opt-in DockWorkspace close-action configuration, disabled by default and threaded into every projection as class-owned policy.
Let DockLayoutAdapter materialize one persistent close action and forward headerAction / active-index intent through runtime closures only.
At dispatch, let DockWorkspace resolve the current item from the live tab container's activeIndex plus its reconciled dockItemIds; never capture an index or pre-projection item array.
Route exactly one {operation: 'closeItem', itemId} through applyDockZoneOperation() and call onDockZoneDocumentChange() only on success.
Enforce closable inside DockZoneModel.closeItem: absent means allowed; explicit false returns a named failure with no document change.
Keep the exact action instance across retained-tab reconciliation. Synchronize its hidden/available state on active-index changes and after every settled projection from the current model policy.
Make closeItem choose the deterministic same-stack successor: the item now occupying the closed index, otherwise the preceding item, otherwise null.
After the committed projection settles, focus the successor tab header; if closing the last item makes normalization prune that tabs node, focus the surviving DockWorkspace root. Failed operations change neither chrome nor focus.
Prove reorder, Overflow, orientation, retained-topology, cross-stack, and tear-out/re-entry paths do not capture stale identities or double-commit.
Contract Ledger
Target surface
Source of authority
Proposed behavior
Fallback
Docs
Evidence
Dock close action config
DockWorkspace runtime policy
opt-in persistent flat action; disabled by default
omitted config preserves current UI
class config + projection JSDoc
workspace/adapter projection specs
Active target
live TabContainer activeIndex + reconciled dockItemIds
resolve current itemId at dispatch
no active item = no-op/unavailable
handler JSDoc
reorder-before-click arm
closable policy
DockZoneModel.closeItem
absent allows; explicit false returns named refusal
current documents remain closeable
model JSDoc
direct operation specs
Commit ordering
DockWorkspace holder pipeline
one semantic commit, then reconciliation
operation failure leaves chrome untouched
workspace/reconciler JSDoc
trace/count witness
Policy visibility
committed model + live active identity
stable action instance mirrors current item policy
missing/non-active item hides action
adapter/workspace JSDoc
active-switch + retained-projection specs
Focus successor
model successor + settled DockWorkspace.refreshPromise
successor tab header, else surviving DockWorkspace root after empty-node pruning
never document.body
focus policy JSDoc
pointer/keyboard close matrix
Serialized document
existing item allow-list
no new field and no function/action config persisted
N/A
N/A
round-trip equality
Decision Record impact
None. The ticket assigns runtime semantics to the existing closable field and consumes the landed DockWorkspace authority without changing the serialized Dock schema.
Decision Record
Not needed, inherited from Discussion #17415 and accepted ADR 0029 as amended by PR #17545.
Signal Ledger
GPT author family: low-blast graduation fold in D#17415.
Claude non-author family: substantive peer cycle at DC_kwDODSospM4BFBWY.
Unresolved Dissent
None.
Unresolved Liveness
None for this low-blast gate.
Discussion Criteria Mapping
OQ5 → generic intent, Dock-owned explicit effect.
OQ6 → absent=true and explicit-false model refusal.
OQ7 → successor header/root focus after committed projection.
OQ8 → every orientation.
OQ10 → action-space/local-sort geometry remains distinct from outer Dock boundaries.
Invariants 8–10 → semantic commit authority and dispatch-time identity.
Acceptance Criteria
Feature disabled: projected Dock tabs render no close action and current behavior is unchanged.
Feature enabled: one persistent close action is visible whenever an active, closeable item exists, independent of contextual focus state.
DockZoneModel.closeItem accepts missing closable and rejects explicit false with a stable named error and byte-identical document.
Projection visibility mirrors policy, but a forged/direct operation against explicit false still fails at the model layer.
The handler resolves the current active itemId at dispatch time after activation/reorder and commits exactly one closeItem operation.
Failed operations leave pane, tab header, active index, action instance, and focus unchanged; successful operations update chrome only through reconciliation.
Closing the first, middle, last, only, and reordered active item selects the deterministic surviving item and never closes a stale target.
Focus moves after the projection settles to the successor tab header, or to the surviving DockWorkspace root when empty-node normalization removes the tab shell; it never falls to document.body.
Top/right/bottom/left, Overflow-active, retained-topology, cross-stack, and tear-out/re-entry cases preserve action placement and exactly-once semantics.
No handler/function/action config enters the serialized Dock document.
Focused unit/model/workspace/reconciliation coverage plus a Whitebox E2E close journey pass at exact head.
Out of Scope
Generic toolbar/TabContainer action materialization (delivered by #17418 / PR #17423).
Custom application action semantics.
Inline close icons per tab.
Changing the Dock document schema or adding a migration.
Automatic persistence of action configs/functions.
Broad focus, reconciliation, or drag-system refactoring.
Avoided Traps
Direct TabContainer.removeAt(): removes chrome before model truth and bypasses policy.
Adapter-owned effect: makes a pure projector a stateful host and duplicates DockWorkspace.
Visual-only closable: hides one button but leaves every operation caller able to close.
Captured active index/item array: closes the wrong item after reorder, activation, or reconciliation.
Replacing the action group on every projection: destroys the stable action instance PR #17423 established.
Chrome-first focus repair: can focus an element reconciliation immediately destroys.
Absent=false default: would silently make every existing Dock item non-closeable.
Context
Discussion #17415 graduates DockLayout integration as the second bounded leaf after the generic flat TabContainer-actions contract.
The generic blocker is delivered: #17418 closed through PR #17423. The Dock workspace authority also moved after this ticket was filed: PR #17545 introduced
Neo.dashboard.DockWorkspaceas the engine-owned reducer/projection/reconciliation host, and PR #17565 migrated Workstation onto it. This ticket now extends that class instead of adding another consumer-owned close loop.The Dock item schema already carries
closable, but only through the serialization allow-list. No runtime path reads it.DockZoneModel.closeItemcurrently permits every item, and projected tab chrome must never remove a pane before the committed Dock document changes.Live latest-open and all-state duplicate sweeps remain clean; no equivalent implementation ticket or PR exists.
The Problem
An optional persistent close action in a projected TabContainer needs Dock semantic authority:
closable: falsemust fail closed in the operation layer, not only disappear visually;closablefield must preserve current behavior and remain closeable;document.body.A direct
TabContainer.removeAt()would let runtime chrome outrun model truth and would bypass serialized policy.The Architectural Reality
DockZoneModel.closeItemis the semantic mutation and successor-selection authority.DockWorkspaceowns the one committed document, the reducer call, view-sync scheduling, policy-visibility resync, and focus after the reconciler settles.DockLayoutAdapterprojects the persistent action and forwards clone-safe runtime intent. It never owns the close effect.DockProjectionReconcilerowns chrome convergence after the document commit and preserves the stable action instance on retained tab containers.closableadds no schema field or migration; this ticket gives an existing serialized field runtime meaning.The Fix
DockWorkspaceclose-action configuration, disabled by default and threaded into every projection as class-owned policy.DockLayoutAdaptermaterialize one persistent close action and forwardheaderAction/ active-index intent through runtime closures only.DockWorkspaceresolve the current item from the live tab container'sactiveIndexplus its reconcileddockItemIds; never capture an index or pre-projection item array.{operation: 'closeItem', itemId}throughapplyDockZoneOperation()and callonDockZoneDocumentChange()only on success.closableinsideDockZoneModel.closeItem: absent means allowed; explicit false returns a named failure with no document change.closeItemchoose the deterministic same-stack successor: the item now occupying the closed index, otherwise the preceding item, otherwisenull.DockWorkspaceroot. Failed operations change neither chrome nor focus.Contract Ledger
DockWorkspaceruntime policyactiveIndex+ reconcileddockItemIdsitemIdat dispatchclosablepolicyDockZoneModel.closeItemDockWorkspaceholder pipelineDockWorkspace.refreshPromisedocument.bodyDecision Record impact
None. The ticket assigns runtime semantics to the existing
closablefield and consumes the landedDockWorkspaceauthority without changing the serialized Dock schema.Decision Record
Not needed, inherited from Discussion #17415 and accepted ADR 0029 as amended by PR #17545.
Signal Ledger
DC_kwDODSospM4BFBWY.Unresolved Dissent
None.
Unresolved Liveness
None for this low-blast gate.
Discussion Criteria Mapping
Acceptance Criteria
DockZoneModel.closeItemaccepts missingclosableand rejects explicit false with a stable named error and byte-identical document.itemIdat dispatch time after activation/reorder and commits exactly onecloseItemoperation.document.body.Out of Scope
Avoided Traps
TabContainer.removeAt(): removes chrome before model truth and bypasses policy.DockWorkspace.closable: hides one button but leaves every operation caller able to close.Related
DockWorkspaceauthority, deliveredOrigin Session ID:
0f8b5b8e-3f01-45c8-889e-1c2fd90b0584Retrieval Hint: "DockWorkspace TabContainer close action closable semantic model focus successor"
— Emmy (GPT-5.6 Sol Ultra, Codex) ·
@neo-gpt-emmy🪡