LearnNewsExamplesServices
Frontmatter
id17541
titleLift the dock workspace host into Neo.dashboard.DockWorkspace
stateClosed
labels
enhancementairefactoringarchitecture
assigneesneo-fable
createdAtAug 22, 2026, 3:03 PM
updatedAtAug 22, 2026, 7:04 PM
githubUrlhttps://github.com/neomjs/neo/issues/17541
authorneo-fable
commentsCount1
parentIssue17539
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 17546 Migrate the workstation host onto Neo.dashboard.DockWorkspace, [ ] 17540 Dock Layouts becomes a guide series: adopt, mechanics, features, UI
closedAtAug 22, 2026, 6:58 PM
milestonev13.2

Lift the dock workspace host into Neo.dashboard.DockWorkspace

Closed Backlog/active-chunk-18 enhancementairefactoringarchitecture
neo-fable
neo-fable commented on Aug 22, 2026, 3:03 PM

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 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.reconcileProjectionDockMotionSignal.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).
  • Motion: Neo.main.addon.DockFlip.captureFirst / play({hostId, markerPrefix}) bracketed by DockMotionSignal.enter / leave (MainContainer.mjs:578–626).
  • Cross-zone drop: DockPreviewProducer.producepreviewToOperation → commit (MainContainer.mjs:778–813).
  • 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

  1. src/dashboard/DockWorkspace.mjsclass 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.
  2. 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).
  3. 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.
  4. test/playwright/unit/dashboard/DockWorkspace.spec.mjs — pattern: the Demo-A workspace contract spec (holder contract, reducer purity, full seam loop, teardown).
  5. 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 this ticket + ADR 0029 §2.1 (amended here) engine host: holder contract + projection + reconcile + FLIP/motion + cross-zone drop + hooks 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
holder contract getDockZoneDocument() / applyDockZoneOperation() / onDockZoneDocumentChange() — EXISTING duck-type, src/ai/client/DockService.mjs:47–80 DockService.resolveHolder unchanged semantics; the class resolves with zero DockService change as today: non-holder component → typed error DockService JSDoc unchanged holder case in the new spec
DockLayoutAdapter.project(model, options) — EXISTING, DockLayoutAdapter.mjs:365 adapter consumed unchanged; options assembled by projectDockModel + getDockProjectionOptions() N/A adapter spec unchanged
DockProjectionReconciler.reconcileProjection({…}) — EXISTING reconciler consumed unchanged N/A reconciler spec unchanged
hooks resolvePane / resolveRevealPane / getPreservedItemIds / beforeRefreshDockWorkspace / getDockProjectionOptions / getRefreshOptions — NEW template methods this ticket; model contract §Stale Component References defaults: titled placeholder / same as resolvePane / [] / no-op / {} / {} a throwing resolver fails the projection loudly, never drops an item silently JSDoc new spec cases
configs flipMarkerPrefix ('dock-flip-item-'), dockShellIndex (0), dockProjectionConfig (null), dockHostReference (null) — NEW 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 JSDoc DockMotionNL + the new spec's consumer case
ADR 0029 §2.1 + §5 — EXISTING document ADR 0005 lifecycle pattern → class same PR lint-adr-seam-table + review
examples/dashboard/dock/MainContainer.mjs — EXISTING #13247 verification surface migrated; toolbar example-local file JSDoc its e2e witnesses
learn/guides/uibuildingblocks/DockLayouts.md §Adopting — EXISTING #17514 step 1 teaches the class; sentence corrected the guide lint-guides, lint-tree-json

Decision Record impact

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.

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