LearnNewsExamplesServices
Frontmatter
id17546
titleMigrate the workstation host onto Neo.dashboard.DockWorkspace
stateClosed
labels
enhancementairefactoringarchitecture
assigneesneo-fable
createdAtAug 22, 2026, 4:00 PM
updatedAtAug 22, 2026, 9:45 PM
githubUrlhttps://github.com/neomjs/neo/issues/17546
authorneo-fable
commentsCount0
parentIssue17539
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17541 Lift the dock workspace host into Neo.dashboard.DockWorkspace
blocking[]
closedAtAug 22, 2026, 9:45 PM
milestonev13.2

Migrate the workstation host onto Neo.dashboard.DockWorkspace

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

Context

Leaf 2a of epic #17539 — the first flagship host migration, sequenced after leaf 1 (#17541, PR #17545) merges. apps/workstation/view/Workspace.mjs is the richest hand-rolled dock host (5306 lines, 89 methods, 40 engine-seam uses) and the film rig's primary surface, which makes it the class's best falsifier: it exercises every hook Neo.dashboard.DockWorkspace exposes and needs four things the class does not have yet. Self-select; the default owner is @neo-fable at #17545's merge. The tear-out / vessel / cross-window members stay where they are — that half is leaf 2b, whose boundary and first consumer (the fleet cockpit) are recorded on the epic.

The Problem

The workstation carries the same holder core the engine now owns (applyDockZoneOperation / getDockZoneDocument / onDockZoneDocumentChange / projectDockModel / refreshDockWorkspace, Workspace.mjs:582–590, :716–725, :804–821, :961–1038, :2107–2212), plus everything that makes it the hard case:

  • its view-sync's second argument is a per-commit options object ({geometryOnly, operation, preserveItemIds}), not only the descriptor engine surfaces pass — tear-out paths call onDockZoneDocumentChange(document, {preserveItemIds: itemIds}) (:1986, :3348) while the splitter, rails and DockService pass the operation descriptor, from which operation is read;
  • its refresh takes the reconciler's fast paths (geometryOnly for resizeSplit, retainTopology for detachItem / transferNode), passes onProjectionStaged (tab-bar animation suppression) and waitForOverflowProjection, feeds the reconciler's landedInPlace outcome into flip.play({geometryOnly}) (DockProjectionReconciler.mjs:353, :466), awaits the play inside the motion bracket, then sequences an overflow-menu wait, a 300 ms chrome settle, the bars' animation restore, a host update and refreshCrossWindowParticipation (:2107–2212);
  • its resolver returns cached live instances (resolvePane, :2273–2332; getPaneHeaderText, :2255) and never decorates them — the adapter's decorator already returns live instances untouched, so the class's resolveProjectedPane path is compatible;
  • its projection options carry the whole multi-window surface — cross-window sort group, tear-out boundary and opt-in, vessel conversion seams, the drag-affordance layer's cross-zone seams, the tear-out entry/exit/cancel/terminal handlers, workspaceId (:961–1038) — exactly what getDockProjectionOptions() exists for.

The Architectural Reality

  • construct (:409–470): the projection mounts into a dock-host child (reference: 'dock-host', cls incl. neo-dashboard + neo-dashboard-dock-query-host, layout: fit) beside persistent DockPreview / DockDropIndicators siblings; DockDragAffordances is composed with owner: me; DockService, DockPerspectiveStore, TourRunner, WindowPosition resize observation follow. Maps to dockHostReference: 'dock-host', dockShellIndex: 0, flipMarkerPrefix: 'workstation-pane-'.
  • beforeRefreshDockWorkspace already exists in substance: the refresh invalidates crossWindowPreviewGeometries and calls dragAffordances.clear() before the FLIP capture (:2117–2119) — the class calls its hook before the capture too.
  • The class at ad3a866b98 lacks four things this host needs, all additive: per-commit preserveItemIds in the refresh options (merged with getPreservedItemIds()); the reconciler result captured so landedInPlace reaches flip.play; a reconcile-options hook for onProjectionStaged / waitForOverflowProjection / retainTopology; and an awaited post-refresh hook that receives the play promise, so a host that must sequence chrome settle behind the play can await it while fire-and-forget stays the default for every other host.
  • Facades the witnesses depend on survive the parent swap unchanged: test/playwright/unit/apps/workstation/Workspace.spec.mjs stubs timeout, refreshDockWorkspace and onDockZoneDocumentChange on the instance (instance assignment shadows prototype methods); the 18 test/playwright/e2e/workstation/*.spec.mjs witnesses drive workstation-specific methods through the Neural Link (getPaneIdentity, setWorkspaceTheme, executeTearOutStep, readCrossWindowGestureSnapshot, runTourSpec, executeCrossZoneShowcaseStep, getTabChromeIdentity, getShellIdentity, …), none of which move.
  • projectVesselDockModel(workspaceId) (:1850) projects OTHER workspaces' documents for vessel windows; it keeps calling DockLayoutAdapter.project directly.
  • Structural pre-flight: no new file; the class's own spec gains the delta cases.

The Fix

One PR, two halves:

  1. Class deltas (src/dashboard/DockWorkspace.mjs, each with a DockWorkspace.spec.mjs case): (a) refreshDockWorkspace merges refreshOptions.preserveItemIds with getPreservedItemIds(); (b) the reconciler's return value is captured and flip.play receives geometryOnly: result.landedInPlace; (c) getReconcileOptions(document, refreshOptions) hook — extra reconciler options merged after the class's own (onProjectionStaged, waitForOverflowProjection, retainTopology); (d) afterRefreshDockWorkspace({document, refreshOptions, result, played}) — awaited after the play is dispatched; played is the play promise (or null without the addon) so the host decides whether to wait on it; the motion bracket's leave stays class-owned. getRefreshOptions(descriptor, source) needs no change: it receives whatever the committing caller passed — a descriptor from engine surfaces or the host's own options object — and the host's override maps both shapes.
  2. Workspace.mjsextends DockWorkspace; the five holder-core members deleted; configs dockHostReference: 'dock-host', flipMarkerPrefix: 'workstation-pane-'; overrides: getDockProjectionOptions() (the whole option block of today's projectDockModel minus the three class-owned callbacks), getRefreshOptions() (geometryOnly for resizeSplit; retainTopology for detachItem / transferNode; preserveItemIds pass-through), beforeRefreshDockWorkspace() (geometry invalidation + dragAffordances.clear()), getReconcileOptions() (the animation-suppression onProjectionStaged + waitForOverflowProjection), afterRefreshDockWorkspace() (overflow-menu wait → await played → 300 ms settle → bars restored → host update → refreshCrossWindowParticipation); resolvePane / getPaneHeaderText unchanged; additionalThemeFiles keeps its full list including Neo.dashboard.Container. Every tear-out / vessel / cross-window member stays byte-identical.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
DockWorkspace#refreshDockWorkspace refreshOptions.preserveItemIds — EXISTING method, NEW option this ticket; Workspace.mjs:1986 (per-commit parked ids) merged with getPreservedItemIds() absent → the hook alone, as today for every other host class JSDoc new spec case + WorkstationTearOutSourceContinuityNL
flip.play({geometryOnly: landedInPlace}) — EXISTING call, NEW argument DockProjectionReconciler.mjs:353/:466; Workspace.mjs:2172–2178 the reconciler's actual path, never the requested one, reaches the addon no addon → no play (unchanged) class JSDoc WorkstationDockFlipResizeNL + spec case
getReconcileOptions(document, refreshOptions) — NEW hook this ticket; Workspace.mjs:2137–2153 merged after the class's reconciler options default {} JSDoc spec case
afterRefreshDockWorkspace({document, refreshOptions, result, played}) — NEW hook this ticket; Workspace.mjs:2167–2212 awaited after the play is dispatched; played handed over default no-op; fire-and-forget stays the default JSDoc spec case + WorkstationTabOverflowCapNL
Workspace instance facades timeout / refreshDockWorkspace / onDockZoneDocumentChange — EXISTING stubs test/playwright/unit/apps/workstation/Workspace.spec.mjs still intercept via instance assignment N/A that spec green, unmodified
the 18 test/playwright/e2e/workstation/*.spec.mjs witnesses — EXISTING the film arc's executable authority unchanged methods, unchanged behavior a red carries a dev baseline receipt headed run at head

Decision Record impact

aligned-with ADR 0029 — §2.1 as amended by #17541; no further amendment.

Acceptance Criteria

  • apps/workstation/view/Workspace.mjs extends Neo.dashboard.DockWorkspace; applyDockZoneOperation, getDockZoneDocument, onDockZoneDocumentChange, projectDockModel and refreshDockWorkspace are deleted from it; LOC before → after recorded in the PR body.
  • The four class deltas land additively, each with a DockWorkspace.spec.mjs case; test/playwright/unit/dashboard/* and the example's witnesses stay green.
  • getRefreshOptions reproduces today's fast paths — geometryOnly for resizeSplit, retainTopology for detachItem / transferNode, per-commit preserveItemIds — witnessed by WorkstationDockFlipResizeNL and WorkstationTearOutSourceContinuityNL.
  • The post-refresh sequence keeps its order — overflow-menu wait → awaited play → chrome settle → bars restored → host update → cross-window participation refresh — witnessed by WorkstationTabOverflowCapNL, WorkstationCrossZoneCueNL and WorkstationDragAffordancesNL.
  • Workspace.spec.mjs passes unmodified (its instance stubs still intercept).
  • All 18 test/playwright/e2e/workstation/*.spec.mjs files run headed at head, list and counts in Test Evidence; any red carries a detached-worktree origin/dev baseline receipt and is not claimed green. Residual annotation (2026-08-22): [L3-deferred — operator handoff needed] — five pre-existing dev-red cases and one brain-tier-blocked file (receipted identically on unmodified origin/dev 51d6072d79, solo-confirmed) are owned by #17564, the surviving residual owner; this AC is satisfied for everything this branch touches, and the six findings discharge under #17564, not here.
  • WorkstationFiveBeatNL green — the film pipeline's executable authority is unaffected.
  • Zero changes to the tear-out / vessel / cross-window members of Workspace.mjs; zero changes under src/dashboard beyond the four class deltas; additionalThemeFiles still lists Neo.dashboard.Container.

Out of Scope

  • The tear-out / vessel host half — leaf 2b, boundary on the epic, the fleet cockpit its first consumer.
  • Demo A / Demo B (Grace's, after #16322) and the fleet cockpit (Clio's, after #17541 and PR #17542).
  • Any change to projectVesselDockModel or the vessel workspace registry.

Avoided Traps

  • Moving tear-out members "while here." The epic's first rejected shape; the boundary is leaf 2b's.
  • Awaiting the play inside the class for everyone. The example and the cockpit fire-and-forget; the hook receives the promise and the host that needs ordering awaits it.
  • Decorating live instances. The adapter returns them untouched; the class's resolver path already respects that.
  • Passing the REQUESTED fast path to the addon. landedInPlace is what the reconciler did; geometryOnly is only what was admitted — the workstation's own comment (:2172–2177) is the receipt.

Related

Parent #17539 · blocked by #17541 (PR #17545) · #15252 (the film arc whose rig this host is) · #16498 (Emmy's place-cycle gate on this host; coordinate on any refresh-ordering change) · #15178 / #16507 (post-restore continuity on this host).

Live latest-open sweep: checked latest 20 open issues at 2026-08-22T13:58:44Z; no equivalent found. A2A in-flight claim sweep: latest 30 all-state messages at the same timestamp; no overlapping claim (Grace's #16322 claim is the dockdemo relocation).

Origin Session ID: bd272031-6109-449d-8a0c-38230064a8f3

Retrieval Hint: query_raw_memories("workstation Workspace migrate DockWorkspace landedInPlace afterRefreshDockWorkspace getReconcileOptions preserveItemIds")

Mnemosyne (Claude Fable 5, Claude Code) 🪢

tobiu referenced in commit 5c9b8aa - "refactor(workstation): migrate the workspace host onto Neo.dashboard.DockWorkspace (#17546) (#17565) on Aug 22, 2026, 9:45 PM
tobiu closed this issue on Aug 22, 2026, 9:45 PM