Leaf 1 of epic #17539. The measurement lives there: six host members implemented in all four dock hosts, sixteen in at least three, bodies diffed and byte-near-identical. This leaf delivers the additive engine class, the ADR 0029 §2.1 amendment in the same PR, and the example host migrated as the minimal witness. It changes nothing under apps/.
The Problem
Three hosts carry the same commit-and-reproject loop by hand: onDockZoneDocumentChange (apps/workstation/view/Workspace.mjs:804, apps/agentos/view/fleet/FleetCockpit.mjs:977, examples/dashboard/dock/MainContainer.mjs:474) is the same refreshPromise chain → timeout(0) → isDestroyed guard; refreshDockWorkspace (Workspace.mjs:2107, FleetCockpit.mjs:999, MainContainer.mjs:570) is the same FLIP captureFirst → placeholder lambda → DockProjectionReconciler.reconcileProjection → DockMotionSignal.enter / flip.play / leave bracket. The only deltas are the slots an engine class exposes as hooks. A defect fixed in one copy stays alive in the others, and every adopter starts by copying 800 lines from the example.
The Architectural Reality
src/dashboard/Container.mjs is Neo.dashboard.Container (ntype dashboard) — the legacy sortable-widget dashboard with its own detach-to-window machinery. It is NOT the dock host and stays untouched; the dock host is a plain container.
All four hosts already extend Neo.container.Base (Workspace, FleetCockpit, DemoBWorkspace, DemoAWorkspace) or Neo.container.Viewport (the example) — the migration is a parent swap, and spec stubs by method name keep working through inheritance.
The Neural Link holder contract is duck-typed in src/ai/client/DockService.mjs:47–80 (resolveHolder: dockZoneDocument field, getDockZoneDocument(), or applyDockZoneOperation()); the class must satisfy it with zero DockService changes.
Projection input: DockLayoutAdapter.project(model, options) (src/dashboard/DockLayoutAdapter.mjs:365) with the options the example assembles at MainContainer.mjs:753–764 (applyDockZoneOperation, onDockCrossZoneDrop, onDockZoneDocumentChange, resolveComponentRef, resolveRevealComponentRef, tabInsertDescriptor).
Reconciliation: DockProjectionReconciler.reconcileProjection({host, nextConfig, placeholders, preserveItemIds, resolveItem, shellIndex}) (MainContainer.mjs:598–616 with shellIndex: 1 behind its toolbar; FleetCockpit.mjs:1022–1051 adds preserveItemIds for parked panes and the same shellIndex: 1).
Theme scope: the example must declare additionalThemeFiles: ['Neo.dashboard.Container'] to load the dock motion tokens (MainContainer.mjs:126). The class declares that itself; the token ROOT is Grace's lane (#17241, #17538) and is not decided here.
learn/agentos/DockZoneModel.md §Split/Tab Adapter Boundary already states the hook boundary: "app-local code owns only its pane resolver, animation, and app-specific menu readiness."
Structural pre-flight, Stage 1 fast path: sibling lift — DockRail.mjs and DockRevealOverlay.mjs extend Container in the same directory.
The Fix
src/dashboard/DockWorkspace.mjs — class DockWorkspace extends Container (Neo.container.Base), className Neo.dashboard.DockWorkspace, ntype dock-workspace, additionalThemeFiles: ['Neo.dashboard.Container']. Owns: dockModel (the committed document, plain member — the single writer is onDockZoneDocumentChange), refreshPromise, dockPreviewProducer (created in construct, destroyed in destroy). Members lifted verbatim from the example with the host-specific slots turned into hooks: getDockZoneDocument(), applyDockZoneOperation(descriptor), onDockZoneDocumentChange(document, descriptor=null, source=null) (deferred atomic chain + one-use addTab correlation via getTabInsertProjectionDescriptor), projectDockModel(tabInsertDescriptor, itemResolver, document), refreshDockWorkspace(tabInsertDescriptor, document, refreshOptions) (FLIP capture → beforeRefreshDockWorkspace(document, refreshOptions) → placeholders → reconciler with preserveItemIds: this.getPreservedItemIds() and shellIndex: this.dockShellIndex → FLIP play inside the motion bracket), onDockCrossZoneDrop(data), destroy(). Hooks with documented defaults: resolvePane(itemId, item) (default: a titled placeholder component — the model contract's recoverable-placeholder policy), resolveRevealPane(itemId, item) (default: resolvePane), getPreservedItemIds() ([]), beforeRefreshDockWorkspace(document, refreshOptions) (no-op), getDockProjectionOptions() ({}, for e.g. autoHideRevealOnHover or a drag-affordance layer's seams), getRefreshOptions(descriptor, source) ({}; the reconciler's geometryOnly / retainTopology fast paths). Configs: flipMarkerPrefix (default 'dock-flip-item-'), dockShellIndex (default 0 — the projection as the host's first child; a host whose chrome precedes the shell sets it, as the example and the cockpit do with 1), dockProjectionConfig (default null; the {flex: 1} the example and the cockpit both wrote as nextConfig.flex = 1 after projecting), dockHostReference (default null — the projection mounts into the workspace itself; a host with persistent overlay siblings names its dock-host child). Anchor & Echo JSDoc on every member.
ADR 0029 — §2.1 "reducer-container pattern (landed, normative)" becomes: the class is the normative host, examples/dashboard/dock/MainContainer.mjs is its minimal consumer, hand-rolled hosts migrate under epic #17539; one §5 leaf-table row. Same PR (the #17507 precedent).
examples/dashboard/dock/MainContainer.mjs — extends the class; its hand-rolled holder / projection / refresh / cross-zone members are deleted; the perspective toolbar stays example-local through beforeRefreshDockWorkspace; dockShellIndex: 1, dockProjectionConfig: {flex: 1}. Whether the example root keeps Viewport and composes a dock-workspace child, or extends the class directly, is the implementer's call under one constraint: observable behavior, the theming DOM scope, and every e2e witness that targets the example stay unchanged.
test/playwright/unit/dashboard/DockWorkspace.spec.mjs — pattern: the Demo-A workspace contract spec (holder contract, reducer purity, full seam loop, teardown).
learn/guides/uibuildingblocks/DockLayouts.md — §Adopting step 1 teaches extends DockWorkspace; the "adoption surface is deliberately small" sentence is replaced by the measured statement. The series (#17540) does the rest.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback / Edge Case
Docs
Evidence
Neo.dashboard.DockWorkspace — NEW class, src/dashboard/DockWorkspace.mjs
dockModel === null → empty projection, holder reads return null; a reducer error leaves document and chrome untouched; a rejected refresh stays observable on its own commit's refreshPromise snapshot and never suppresses a later transaction (scheduling chains off the settled tail); a non-null dockHostReference resolving to no live host rejects the refresh loudly — the document advanced, silence would freeze stale chrome (ledger rows added 2026-08-22, review round 1 RA-1/RA-3)
class JSDoc + ADR §2.1 + guide step 1
DockWorkspace.spec.mjs + the example's e2e witnesses
this ticket; the four hosts' census (peer pass by Clio on #17539: the example and the cockpit pass shellIndex: 1 and set flex: 1, Workstation and Demo B pass neither)
FLIP marker prefix; reconciler shell index; config merged onto every projected shell; the child the projection mounts into
the marker prefix reproduces the example's landed value; the example sets dockShellIndex: 1 and dockProjectionConfig: {flex: 1} explicitly because its toolbar precedes the shell — a consumer that omits them gets the projection as the host's first child, unmerged
amends ADR 0029 (§2.1, §5) — carried in this leaf's PR.
Acceptance Criteria
src/dashboard/DockWorkspace.mjs exists as specified: Neo.dashboard.DockWorkspace extends Neo.container.Base, ntype dock-workspace, owning dockModel, refreshPromise, dockPreviewProducer, the holder trio, projectDockModel, refreshDockWorkspace, onDockCrossZoneDrop, getTabInsertProjectionDescriptor, destroy, the six hooks with documented defaults, and the four configs; every member carries Anchor & Echo JSDoc.
DockService.resolveHolder resolves a DockWorkspace instance with zero DockService changes (spec case).
Rapid successive commits stage exactly one projection transaction at a time while dockModel advances synchronously; a destroyed instance drops its pending refresh without throwing (spec cases).
applyDockZoneOperation never mutates dockModel; a rejected descriptor leaves the document byte-identical and the chrome untouched (spec case).
A cross-zone drop commits exactly one operation through previewToOperation; a rejected preview commits nothing (spec cases).
The example extends the class; its hand-rolled holder / projection / refresh / cross-zone members are gone; LOC before → after recorded in the PR body; the perspective toolbar still syncs on every refresh.
Every test/playwright/e2e/dashboard/*.spec.mjs witness that targets examples/dashboard/dock passes at head, headed, list recorded in Test Evidence; test/playwright/unit/dashboard/* green. Residual (2026-08-22): two DockOperationsNL cases (:65 moveItem, :82 splitNode) fail identically on unmodified origin/dev — diagnosed on #17555 as spec-fixture drift against the grown example boot document (delta and independent read agree; the expectation is stale) and owned there; every other targeted witness green at head.
ADR 0029 §2.1 amended and a §5 row added; lint-adr-seam-table green.
DockLayouts.md §Adopting step 1 teaches extends DockWorkspace and the "deliberately small" sentence is replaced by the measured statement; lint-guides and lint-tree-json green.
Zero diff under apps/workstation and apps/agentos; DockZoneModel, DockLayoutAdapter, DockProjectionReconciler and the neo.harness.* strings untouched.
Theme loading for the example unchanged (DockStandaloneThemingNL green); no new token root introduced.
Out of Scope
Migrating workstation / dockdemo / FleetCockpit — own leaves under #17539 (the cockpit leaf is Clio's, filed when this leaf merges and after PR #17542).
The tear-out/vessel host half — the second engine leaf; its boundary and its first consumer (the cockpit) are recorded on the epic.
Token root and paint (#17241, #17522, #17538); #17419's close action (fits the class later).
A reactive dockModel_ config with automatic re-projection — rejected: it changes the single-mutation-path semantics every host and DockService rely on.
Avoided Traps
Extending Neo.dashboard.Container. Wrong lineage: the legacy widget dashboard carries its own detach machinery the dock host must not inherit.
Hiding the shell index or the host reference behind getters. Explicit configs; grep-able.
A shell-index default that silently regresses the first consumer. The default is the host's first child; a host whose chrome precedes the shell declares the index — the example does, and the cockpit will.
Bundling a flagship migration "while we are here" — the epic's first rejected shape.
Touching adapter / reconciler signatures or the wire vocabulary — none of them need to move.
Related
Parent #17539 · #17540 (guide series; its adoption part is blocked by this leaf) · #17419 · #17241 · #16322 · #15614 · #13247 · peer passes on the epic: Grace, Clio.
Live latest-open sweep: checked latest 20 open issues at 2026-08-22T12:58:37Z (this filing pass); no equivalent found. A2A in-flight claim sweep: latest 30 all-state messages at the same timestamp; no overlapping claim found.
Context
Leaf 1 of epic #17539. The measurement lives there: six host members implemented in all four dock hosts, sixteen in at least three, bodies diffed and byte-near-identical. This leaf delivers the additive engine class, the ADR 0029 §2.1 amendment in the same PR, and the example host migrated as the minimal witness. It changes nothing under
apps/.The Problem
Three hosts carry the same commit-and-reproject loop by hand:
onDockZoneDocumentChange(apps/workstation/view/Workspace.mjs:804,apps/agentos/view/fleet/FleetCockpit.mjs:977,examples/dashboard/dock/MainContainer.mjs:474) is the samerefreshPromisechain →timeout(0)→isDestroyedguard;refreshDockWorkspace(Workspace.mjs:2107,FleetCockpit.mjs:999,MainContainer.mjs:570) is the same FLIPcaptureFirst→ placeholder lambda →DockProjectionReconciler.reconcileProjection→DockMotionSignal.enter/flip.play/leavebracket. The only deltas are the slots an engine class exposes as hooks. A defect fixed in one copy stays alive in the others, and every adopter starts by copying 800 lines from the example.The Architectural Reality
src/dashboard/Container.mjsisNeo.dashboard.Container(ntypedashboard) — the legacy sortable-widget dashboard with its own detach-to-window machinery. It is NOT the dock host and stays untouched; the dock host is a plain container.Neo.container.Base(Workspace,FleetCockpit,DemoBWorkspace,DemoAWorkspace) orNeo.container.Viewport(the example) — the migration is a parent swap, and spec stubs by method name keep working through inheritance.src/ai/client/DockService.mjs:47–80(resolveHolder:dockZoneDocumentfield,getDockZoneDocument(), orapplyDockZoneOperation()); the class must satisfy it with zero DockService changes.DockLayoutAdapter.project(model, options)(src/dashboard/DockLayoutAdapter.mjs:365) with the options the example assembles atMainContainer.mjs:753–764(applyDockZoneOperation,onDockCrossZoneDrop,onDockZoneDocumentChange,resolveComponentRef,resolveRevealComponentRef,tabInsertDescriptor).DockProjectionReconciler.reconcileProjection({host, nextConfig, placeholders, preserveItemIds, resolveItem, shellIndex})(MainContainer.mjs:598–616withshellIndex: 1behind its toolbar;FleetCockpit.mjs:1022–1051addspreserveItemIdsfor parked panes and the sameshellIndex: 1).Neo.main.addon.DockFlip.captureFirst / play({hostId, markerPrefix})bracketed byDockMotionSignal.enter / leave(MainContainer.mjs:578–626).DockPreviewProducer.produce→previewToOperation→ commit (MainContainer.mjs:778–813).additionalThemeFiles: ['Neo.dashboard.Container']to load the dock motion tokens (MainContainer.mjs:126). The class declares that itself; the token ROOT is Grace's lane (#17241,#17538) and is not decided here.learn/agentos/DockZoneModel.md§Split/Tab Adapter Boundary already states the hook boundary: "app-local code owns only its pane resolver, animation, and app-specific menu readiness."DockRail.mjsandDockRevealOverlay.mjsextendContainerin the same directory.The Fix
src/dashboard/DockWorkspace.mjs—class DockWorkspace extends Container(Neo.container.Base), classNameNeo.dashboard.DockWorkspace, ntypedock-workspace,additionalThemeFiles: ['Neo.dashboard.Container']. Owns:dockModel(the committed document, plain member — the single writer isonDockZoneDocumentChange),refreshPromise,dockPreviewProducer(created inconstruct, destroyed indestroy). Members lifted verbatim from the example with the host-specific slots turned into hooks:getDockZoneDocument(),applyDockZoneOperation(descriptor),onDockZoneDocumentChange(document, descriptor=null, source=null)(deferred atomic chain + one-useaddTabcorrelation viagetTabInsertProjectionDescriptor),projectDockModel(tabInsertDescriptor, itemResolver, document),refreshDockWorkspace(tabInsertDescriptor, document, refreshOptions)(FLIP capture →beforeRefreshDockWorkspace(document, refreshOptions)→ placeholders → reconciler withpreserveItemIds: this.getPreservedItemIds()andshellIndex: this.dockShellIndex→ FLIP play inside the motion bracket),onDockCrossZoneDrop(data),destroy(). Hooks with documented defaults:resolvePane(itemId, item)(default: a titled placeholder component — the model contract's recoverable-placeholder policy),resolveRevealPane(itemId, item)(default:resolvePane),getPreservedItemIds()([]),beforeRefreshDockWorkspace(document, refreshOptions)(no-op),getDockProjectionOptions()({}, for e.g.autoHideRevealOnHoveror a drag-affordance layer's seams),getRefreshOptions(descriptor, source)({}; the reconciler'sgeometryOnly/retainTopologyfast paths). Configs:flipMarkerPrefix(default'dock-flip-item-'),dockShellIndex(default0— the projection as the host's first child; a host whose chrome precedes the shell sets it, as the example and the cockpit do with1),dockProjectionConfig(defaultnull; the{flex: 1}the example and the cockpit both wrote asnextConfig.flex = 1after projecting),dockHostReference(defaultnull— the projection mounts into the workspace itself; a host with persistent overlay siblings names its dock-host child). Anchor & Echo JSDoc on every member.examples/dashboard/dock/MainContainer.mjsis its minimal consumer, hand-rolled hosts migrate under epic#17539; one §5 leaf-table row. Same PR (the#17507precedent).examples/dashboard/dock/MainContainer.mjs— extends the class; its hand-rolled holder / projection / refresh / cross-zone members are deleted; the perspective toolbar stays example-local throughbeforeRefreshDockWorkspace;dockShellIndex: 1,dockProjectionConfig: {flex: 1}. Whether the example root keepsViewportand composes adock-workspacechild, or extends the class directly, is the implementer's call under one constraint: observable behavior, the theming DOM scope, and every e2e witness that targets the example stay unchanged.test/playwright/unit/dashboard/DockWorkspace.spec.mjs— pattern: the Demo-A workspace contract spec (holder contract, reducer purity, full seam loop, teardown).learn/guides/uibuildingblocks/DockLayouts.md— §Adopting step 1 teachesextends DockWorkspace; the "adoption surface is deliberately small" sentence is replaced by the measured statement. The series (#17540) does the rest.Contract Ledger Matrix
Neo.dashboard.DockWorkspace— NEW class,src/dashboard/DockWorkspace.mjsdockModel === null→ empty projection, holder reads returnnull; a reducer error leaves document and chrome untouched; a rejected refresh stays observable on its own commit'srefreshPromisesnapshot and never suppresses a later transaction (scheduling chains off the settled tail); a non-nulldockHostReferenceresolving to no live host rejects the refresh loudly — the document advanced, silence would freeze stale chrome (ledger rows added 2026-08-22, review round 1 RA-1/RA-3)DockWorkspace.spec.mjs+ the example's e2e witnessesgetDockZoneDocument()/applyDockZoneOperation()/onDockZoneDocumentChange()— EXISTING duck-type,src/ai/client/DockService.mjs:47–80DockService.resolveHolderDockLayoutAdapter.project(model, options)— EXISTING,DockLayoutAdapter.mjs:365projectDockModel+getDockProjectionOptions()DockProjectionReconciler.reconcileProjection({…})— EXISTINGresolvePane/resolveRevealPane/getPreservedItemIds/beforeRefreshDockWorkspace/getDockProjectionOptions/getRefreshOptions— NEW template methodsresolvePane/[]/ no-op /{}/{}flipMarkerPrefix('dock-flip-item-'),dockShellIndex(0),dockProjectionConfig(null),dockHostReference(null) — NEWshellIndex: 1and setflex: 1, Workstation and Demo B pass neither)dockShellIndex: 1anddockProjectionConfig: {flex: 1}explicitly because its toolbar precedes the shell — a consumer that omits them gets the projection as the host's first child, unmergedDockMotionNL+ the new spec's consumer caselint-adr-seam-table+ reviewexamples/dashboard/dock/MainContainer.mjs— EXISTING#13247verification surfacelearn/guides/uibuildingblocks/DockLayouts.md§Adopting — EXISTING#17514lint-guides,lint-tree-jsonDecision Record impact
amends ADR 0029(§2.1, §5) — carried in this leaf's PR.Acceptance Criteria
src/dashboard/DockWorkspace.mjsexists as specified:Neo.dashboard.DockWorkspace extends Neo.container.Base, ntypedock-workspace, owningdockModel,refreshPromise,dockPreviewProducer, the holder trio,projectDockModel,refreshDockWorkspace,onDockCrossZoneDrop,getTabInsertProjectionDescriptor,destroy, the six hooks with documented defaults, and the four configs; every member carries Anchor & Echo JSDoc.DockService.resolveHolderresolves aDockWorkspaceinstance with zero DockService changes (spec case).dockModeladvances synchronously; a destroyed instance drops its pending refresh without throwing (spec cases).applyDockZoneOperationnever mutatesdockModel; a rejected descriptor leaves the document byte-identical and the chrome untouched (spec case).previewToOperation; arejectedpreview commits nothing (spec cases).test/playwright/e2e/dashboard/*.spec.mjswitness that targetsexamples/dashboard/dockpasses at head, headed, list recorded in Test Evidence;test/playwright/unit/dashboard/*green. Residual (2026-08-22): twoDockOperationsNLcases (:65moveItem,:82splitNode) fail identically on unmodifiedorigin/dev— diagnosed on #17555 as spec-fixture drift against the grown example boot document (delta and independent read agree; the expectation is stale) and owned there; every other targeted witness green at head.lint-adr-seam-tablegreen.DockLayouts.md§Adopting step 1 teachesextends DockWorkspaceand the "deliberately small" sentence is replaced by the measured statement;lint-guidesandlint-tree-jsongreen.apps/workstationandapps/agentos;DockZoneModel,DockLayoutAdapter,DockProjectionReconcilerand theneo.harness.*strings untouched.DockStandaloneThemingNLgreen); no new token root introduced.Out of Scope
#17241,#17522,#17538);#17419's close action (fits the class later).dockModel_config with automatic re-projection — rejected: it changes the single-mutation-path semantics every host and DockService rely on.Avoided Traps
Neo.dashboard.Container. Wrong lineage: the legacy widget dashboard carries its own detach machinery the dock host must not inherit.Related
Parent #17539 · #17540 (guide series; its adoption part is blocked by this leaf) ·
#17419·#17241·#16322·#15614·#13247· peer passes on the epic: Grace, Clio.Live latest-open sweep: checked latest 20 open issues at 2026-08-22T12:58:37Z (this filing pass); no equivalent found. A2A in-flight claim sweep: latest 30 all-state messages at the same timestamp; no overlapping claim found.
Origin Session ID: bd272031-6109-449d-8a0c-38230064a8f3
Retrieval Hint:
query_raw_memories("DockWorkspace base class holder contract projectDockModel refreshDockWorkspace hooks resolvePane")Mnemosyne (Claude Fable 5, Claude Code) 🪢