LearnNewsExamplesServices
Frontmatter
id19350
titleThe dock Workspace composes its default cross-window Participation
stateClosed
labels
enhancementaiarchitecturedashboard
assigneesneo-fable
createdAtOct 1, 2026, 6:15 PM
updatedAtOct 1, 2026, 9:05 PM
githubUrlhttps://github.com/neomjs/neo/issues/19350
authorneo-fable
commentsCount0
parentIssue15239
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 1, 2026, 9:05 PM

The dock Workspace composes its default cross-window Participation

Closed v13.2.0/archive-v13-2-0-chunk-24 enhancementaiarchitecturedashboard
neo-fable
neo-fable commented on Oct 1, 2026, 6:15 PM

Context

Institution #382 (→ PR neomjs/neo-agent-institution#383) measured the Fleet Manager cockpit showing no drop zones while a tab header is dragged: the cockpit extends Neo.dashboard.dock.Workspace with enableDockTearOutLifecycle: true and never composed the engine's drop-feedback tier — the façade leaves that tier to the consumer by design. Fixing it raised the question the record's author answered as its author (@neo-fable-clio, A2A 2026-10-01 12:40Z; her turn 12:41Z): should a workspace that opts into the tear-out lifecycle compose its default cross-window Participation itself, the way the flag already composes the tear-out machine and the maximize affordance? Yes — as its own engine leaf under #15239, with the §2.3 amendment, not inside #382. This is that leaf; PR neomjs/neo-agent-institution#383's Deltas and #382's Out of Scope name it as the follow-up.

The Problem

Every workspace that participates in cross-window drags composes Neo.dashboard.dock.window.Participation by hand, and each one re-derives the same lifecycle the façade already owns the inputs of:

  • apps/workstation/view/PopupWorkspace.mjs:85-95 syncParticipation() — the minimal composition (dragEmbodiment, hitTest, sortGroup, windowId, workspace, workspaceId, workspaceSet), re-run from afterSetWindowId (:77) and after the set registration (:67), destroyed at :192.
  • apps/workstation/view/VesselWorkspace.mjs:184-195 (a Map + a promise), :312-316 (construct), :529-594 (createCrossWindowParticipation / refreshCrossWindowParticipation), :1737-1738 (destroy) — the main workspace's composition of its Workstation.window.Participation subclass (apps/workstation/window/Participation.mjs: the saved-home drop policy) with the park / conversion seams; the class comment at :306-312 spells out the registration triggers the app re-derives: construction when the Group is already bound, afterSetTopologyGroupId when a first boot's identity arrives later.
  • examples/dashboard/crossWindow/DemoBCrossWindowStage.mjs:163-215 — per projected host, with hand seams (its holder DemoBWorkspace extends Container, not the façade).
  • The cockpit composes none, and its vessels are shells, so the gap there is only the missing default.

The engine class already carries a complete default for every seam when it is given its workspace: defaultCommitLocal (src/dashboard/dock/window/Participation.mjs:436), defaultCommitTransfer (:456), defaultGetDocument (:479), defaultGetForeignDocument (:490), defaultHitTest (:502), defaultPreviewFor (:522), defaultSortGroup (:635 — the workspace's published crossWindowSortGroup; absent, "the target never registers and the workspace stays fully in-window — the unchanged opt-in default"), defaultResolveOwnershipId (:654 — the workspace's Group, read live), and resolveAffordances (:310 — Preview + DropIndicators + DragAffordances composed over workspace.getDockHost() when no controller is supplied). The one thing the engine does not own is when the instance exists.

The Architectural Reality

  • src/dashboard/dock/Workspace.mjs is a façade: "it owns cross-concern ordering and the one mutation boundary, and it owns no concern outright … declinable stateful affordances, the tear-out gesture machine, multi-window lifecycle — belongs to a named owner beside it" (class doc :91-95). It composes two named owners today: the maximize affordance as a declinable collaborator (:616-620 — installed unless the consumer supplied its own instance through plugins) and, under enableDockTearOutLifecycle, the tear-out handlers (:622-657).
  • The lifecycle inputs a participation needs are façade-owned already: windowId (afterSetWindowId :1804, which also resolves the Group), the Group (onTopologyGroupBinding :806, afterSetTopologyGroupId :969 — an empty hook whose doc says "A host whose participants could not register at construction … registers them here"), workspaceSet (injected before construction, :899), the set identity (workspaceKey, :2584), the dock host (getDockHost() :1562), teardown (destroy() :1510).
  • Neo.dashboard.dock.window.DragTarget registers with the coordinator only while sortGroup && windowId != null (src/dashboard/dock/window/DragTarget.mjs:218, :482); the participation registers it at construct with stableTargetId = workspaceId (Participation.mjs:273-305), so a re-sync on a window change is destroy + recreate, never a second slot. Ordinary commits retain the same target (test/playwright/unit/apps/workstation/Workspace.spec.mjs:132-144).
  • ADR 0029 §2.3 "Dock participation: remote drag → preview → operation" (learn/agentos/decisions/0029-docking-design.md:351-353) states what a dock workspace registers and which pipeline its drop rides; it does not say who composes the participation or when it exists. That is the gap the amendment closes.

The Fix

Corrected while building (2026-10-01 16:4xZ, the author): the opt-in is the published crossWindowSortGroup, not the tear-out flag — the Workstation's popup workspaces participate with enableDockTearOutLifecycle off (they return through their close policy), and the engine class already states the sort group as its registry identity. Every "under the tear-out lifecycle" below reads "for a published sort group"; the Institution cockpit publishes none and still composes nothing.

  1. The façade composes the default. For a published crossWindowSortGroup, Neo.dashboard.dock.Workspace owns one Neo.dashboard.dock.window.Participation bound to itself — workspace: this, windowId, workspaceId: this.workspaceKey, workspaceSet, the published crossWindowSortGroup through the engine default — created once the workspace has a window and publishes the sort group — the Group is read live at claim time, never waited for; the set is bound when present and re-bound when it arrives — at the construct tail, in afterSetWindowId (configs applied) and after afterRefreshDockWorkspace; a changed binding re-composes an instance the façade owns (destroy + recreate); destroyed in destroy() before the tear-out handlers retire (sentence corrected 2026-10-01 18:1xZ on @neo-gpt-sophie's second read; the first version named the Group, the set and afterSetTopologyGroupId). A workspace that publishes no crossWindowSortGroup composes nothing — the opt-in default the engine class already states; the tear-out flag plays no part.
  2. Declinable, the maximize way. One config, dockParticipation, following the dockHeaderActionPolicy instance-or-config shape (:1535): the default is true (the engine class, imported on first use); null declines (a consumer keeping its own composition); a subclass config or a consumer-owned instance wins and is never duplicated. One hook, getDockParticipationConfig(), returns the consumer's extra seams (dragEmbodiment, hitTest, the park / conversion seams) and merges under the façade's bindings, the way getDockProjectionOptions() merges under the collaborators' options.
  3. The consumers delete their re-derivations. VesselWorkspace declares dockParticipation: {module: WorkstationParticipation} and moves its seams into getDockParticipationConfig(); createCrossWindowParticipation, refreshCrossWindowParticipation, crossWindowParticipationPromise and the main-workspace handling of crossWindowParticipations go. PopupWorkspace deletes syncParticipation, its afterSetWindowId override and the destroy line; reads of popup.participation move to the façade's member. Demo B and examples/dashboard/crossWindowWitness/Harness.mjs:83 stay (not façade consumers). The Institution cockpit inherits the default and publishes no sort group, so its census stays Participation 0 — the negative control.
  4. ADR 0029 §2.3 amendment under "Dock participation": composition ownership (the façade, under the tear-out lifecycle), the decline rule, the lifecycle triggers, and the unchanged opt-in through crossWindowSortGroup — headed (amendment 2026-10-01, #N) per the record's convention.

Net lines: the façade grows by the collaborator (~40 lines); the Workstation loses ~90. Workspace.mjs sits at 3360 lines; if the reviewer asks for a seam cut, the collaborator composition is the extractable unit, not the façade's bindings.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.dashboard.dock.Workspace#dockParticipation (new config) ADR 0029 §2.3 (amended here); the maximize collaborator precedent Workspace.mjs:616-620; the instance-or-config precedent :1535 true (the engine class, lazily imported) for a published crossWindowSortGroup; null declines; a subclass config or instance supplied by the consumer is kept, never duplicated no sort group published → no participation (today's behavior) Workspace.mjs class doc + the config's JSDoc; ADR 0029 §2.3 red-first unit arms (AC-1)
Neo.dashboard.dock.Workspace#getDockParticipationConfig() (new hook) the getDockProjectionOptions() hook precedent :1603 returns consumer seams merged under the façade's bindings (workspace, windowId, workspaceId, workspaceSet) — a consumer cannot unbind the workspace {} JSDoc AC-1, AC-2
Neo.dashboard.dock.Workspace#dockParticipation lifecycle (src/dashboard/dock/window/ParticipationLifecycle.mjs) the construct tail, afterSetWindowId (once configs are applied), the tail of refreshDockWorkspace after afterRefreshDockWorkspace, destroy() composed once the workspace has a window AND publishes crossWindowSortGroup — the Group is read live at claim time and is not a precondition; the set is bound when present (workspaceSet ?? null) and a later set re-composes; a changed binding (window, set, published identity, sort group) → destroy + recreate of an instance the façade composed (participationOwned); unchanged bindings keep the registered instance; a SUPPLIED instance is adopted as-is and never destroyed by the façade — recompose: true included — and stays its creator's; destroyed (owned) or released (adopted) before tearOutHandlers retire (row corrected 2026-10-01 17:5xZ by the author on @neo-gpt-sophie's review finding: the first version said the Group and the set were waited for) no sort group published → nothing composed, DragTarget never registers (DragTarget.mjs:218) JSDoc + the module header AC-1
Neo.dashboard.dock.Workspace#syncDockParticipation({recompose}) (public seam) the lifecycle module; Workstation.view.VesselWorkspace#initializeVesselResources as the first caller idempotent against the bindings above; recompose: true composes a fresh instance under unchanged bindings for an instance the façade owns (an adopted instance is returned unchanged) — a host whose SEAMS changed after construction says so (the Workstation creates the drag controller and proxy embodiment it lends after construction and calls it once there); returns the settled participationPromise recompose omitted → keep JSDoc AC-1 (the recompose arm + the late-set arm)
Workstation.view.VesselWorkspace / PopupWorkspace this ticket the hand compositions above are deleted; behavior byte-identical (same subclass, same seams, same triggers) — class docs AC-2, AC-3
ADR 0029 §2.3 the record's author (Clio, 12:40Z) amendment paragraph as in The Fix 4 — the record AC-4

Decision Record impact: amends ADR 0029 §2.3.

Acceptance Criteria

  • AC-1 Red-first unit arms in test/playwright/unit/dashboard/DockWorkspaceParticipation.spec.mjs (the façade's own spec, the coordinator stub of its sibling): a tear-out workspace that publishes a crossWindowSortGroup, holds a workspaceSet and receives a windowId composes exactly one participation whose target registered once; dockParticipation: null composes none; a consumer-supplied instance is kept and not duplicated; a window change unregisters and registers once; destroy() unregisters; no published sort group → nothing composed; a published sort group with the lifecycle off still composes.
  • AC-2 apps/workstation keeps its behavior with the deletions: test/playwright/unit/apps/workstation green; WorkstationNativePopupOverPopupNL and WorkstationHumanPopupOverlapNL (cross-window drops into main and popups) green headless at the head (their dev reds, if any, named per arm with owners).
  • AC-3 Negative control: the Institution cockpit (tear-out on, no sort group) composes no participation at the head — stated by reading, pinned by a unit arm on a bare tear-out workspace (AC-1's last case).
  • AC-4 ADR 0029 §2.3 carries the amendment paragraph; learn/ guides that describe the cross-window composition as consumer-owned are swept (grep -rn "Participation" learn/guides learn/benefits) and corrected in the same PR.
  • AC-5 Post-merge, outside this PR: the next engine pin in neomjs/neo-agent-institution keeps FleetCockpitTabDragIndicatorsNL green (the cockpit's drag feedback is DragAffordances-composed and unaffected).

Out of Scope

Demo B's per-host composition and the witness harness (not façade consumers). The in-window affordance tier the cockpit composes by hand (PR neomjs/neo-agent-institution#383 — consumer-owned by the record). Any coordinator change (ADR 0029 §2.3 binding invariant: the coordinator stays dock-blind). The vessel shells of the cockpit becoming dock workspaces.

Avoided Traps

  • Composing the default whether or not a sort group is published: an instance that can never register is a member nobody can observe — the opt-in stays crossWindowSortGroup, exactly as the engine class states it.
  • A plugins entry: Participation extends core.Base, not plugin.Base; forcing it into the plugin list would make getDockCollaboratorOptions() (:1618) read a surface it does not have.
  • Re-registering after every projection: the target's slot is stable by workspaceId and ordinary commits retain it (Workspace.spec.mjs:141); a per-projection re-sync would churn the coordinator's registry for nothing.

Related

#15239 (parent epic — multi-window docking choreography), ADR 0029 §2.3, neomjs/neo-agent-institution#382 / PR #383 (origin of the question), #18651 (closed, Grace — the vessel-close hook lifted from three consumers into the engine: the same consumer-to-façade direction, shipped), apps/workstation/window/Participation.mjs (the subclass that becomes the declared module).

Sweeps

Live latest-open sweep: latest 20 open neomjs/neo issues at 2026-10-01 16:13Z — no equivalent (#19297 … #19134). Exact sweep (Participation, all states): #18233 (CLOSED — the 19 host-wired seams became the engine defaults this leaf relies on), #18459 / #18395 / #18398 (CLOSED — preview, affordance and staging ownership moved to the engine), #18379 (OPEN — the two-root receipt, a different question), #15614 (OPEN — Demo B decomposition, out of scope here). A2A in-flight sweep: latest 30 messages at 16:13Z (all read states, 15:07Z–16:12Z) — no claim on this scope. MC sweep ("cross-window Participation composed by hand in every consumer; registration timing re-derived per workspace; cockpit never composes it"): 6 results, no prior decision against; the nearest prior art is @neo-opus-vega's 2026-09-03 fork "port ~150 LOC into the second consumer, or lift participation into src/ so any dock host inherits it" — the first half of that lift landed as the engine defaults (#18233), this leaf is the second half. KB sweep (ask_knowledge_base, type ticket): no equivalent. Own-assignment sweep: 3 open (#19186, #19101, #15252), none overlapping.

Structure map: N/A — no new file; owning folder src/dashboard/dock/, sibling precedent the maximize collaborator at Workspace.mjs:616.

Origin Session ID: 0b1ea382-7458-4ce2-9f30-469e8a89f8ad Retrieval Hint: "tear-out lifecycle composes default cross-window Participation declinable collaborator dockParticipation"

🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session 0b1ea382-7458-4ce2-9f30-469e8a89f8ad

tobiu referenced in commit 08ff2a5 - "feat(dock): the Workspace composes its default cross-window Participation for a published sort group (#19350) (#19351) on Oct 1, 2026, 9:05 PM
tobiu closed this issue on Oct 1, 2026, 9:05 PM