LearnNewsExamplesServices
Frontmatter
id18305
titleMaximize and its FLIP motion are six state fields and a transition lifecycle inside Workspace, and no consumer can decline them
stateClosed
labels
enhancementaiarchitecturedashboard
assigneesneo-fable
createdAtSep 4, 2026, 8:16 PM
updatedAtSep 5, 2026, 12:15 AM
githubUrlhttps://github.com/neomjs/neo/issues/18305
authorneo-opus-grace
commentsCount1
parentIssue18304
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 18328 Maximize measures the workspace root when no dock host is named, so chrome outside the dock area is covered, [x] 18306 Header-action policy is 609 lines of workspace-only state with no vehicle: the largest cluster in Workspace is not misfiled, it is unowned, [x] 18318 Workspace has no stated façade contract, so every extraction has to guess which callbacks the host still owes its collaborators
closedAtSep 5, 2026, 12:15 AM

Maximize and its FLIP motion are six state fields and a transition lifecycle inside Workspace, and no consumer can decline them

Closed v13.2.0/archive-v13-2-0-chunk-19 enhancementaiarchitecturedashboard
neo-opus-grace
neo-opus-grace commented on Sep 4, 2026, 8:16 PM

Sub of #18304 (Workspace decomposition). Graduated from D#18150; the vehicle classification is Option G — a vehicle per responsibility.

The Problem

Workspace.mjs owns maximize as stateful presentation nobody can switch off. At dev@131c782ca8 the cluster is six class-state fields — dockMaximizeMotion (:442), dockMaximizeResizeObserved (:450), dockMaximizeResizeWired (:458), dockMaximizeRestore (:483), dockMaximizeTransition (:508), dockMaximizePlay (:518) — plus maximizedNodeId, a ResizeObserver registered per maximize (:3269–3292), an escape handler (onDockMaximizeEscape :2789), rect-style derivation (:2854), and the FLIP capture/play pair. By member span: 321 code / 198 doc lines, self-contained.

It is the cleanest plugin candidate in the file: it owns its own state, it has a real teardown obligation, and it is optional by nature — a consumer that never maximizes still pays for the observer wiring and cannot decline it without subclassing the host.

The Fix

Extract to src/dashboard/dock/plugin/Maximize.mjs as a Neo.plugin.Base subclass — a new plugin/ folder on existing in-package precedent (src/tab/plugin/Overflow.mjs; five modules in src/plugin/).

component/Base.mjs:1322 already destroys owned plugins, so the teardown obligation is discharged by engine code rather than by a new convention. plugins_ is a per-instance config (component/Base.mjs:216), which is exactly the declinability Neo.mixin.* cannot offer — the falsifier that removed Option A.

Workspace retains only the existing enableDockMaximizeAction install switch; maximize behavior and state move to the plugin without forwarding wrappers. The Ownership test's question 4 still rejects owner.foo() proxies.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
dock/plugin/Maximize.mjs D#18150 Option G; tab/plugin/Overflow.mjs precedent owns the six dockMaximize* fields, the ResizeObserver, escape handling and the FLIP lifecycle plugin absent ⇒ no maximize affordance, zero registered instances class docblock member-span census, 321/198 lines
Workspace maximize surface the Ownership test deleted, not forwarded none — zero-backcompat operator ruling — method + config counts fall in the same PR
enableDockMaximizeAction current config surface selects whether the plugin is instantiated false ⇒ plugin never created config docblock declined-plugin arm below
dockHeaderAction event (broadened) the existing re-emit fired for every action the engine set does not own — host actions AND collaborators' — {action, dockNodeId, tabContainer}; maximize is the plugin's by name a throwing listener propagates like any listener the dispatch docblock DockLayoutAdapter.spec intent arms; DockMaximizePlugin.spec
beforeDockZoneDocumentChange event (new) this leaf synchronous, subscription order, before dockModel changes; {descriptor, document, source} a throwing listener aborts the commit; a collaborator writes its own state and never throws the comment at the fire DockMaximizePlugin.spec "a committed operation is announced to collaborators before it applies"
Workspace#getDockCollaboratorOptions() (new) this leaf every plugin's getDockProjectionOptions() merged in plugins order, spread before the consumer's getDockProjectionOptions() two collaborators claiming one key throw at projection naming both; the consumer hook overrides a collaborator key its docblock the collision arm
awaited plugin.syncDockProjection() (new) this leaf awaited in plugins order inside refreshDockWorkspace, before the FLIP play a rejection is reported (console.warn + dockCollaboratorSyncFailed {component, error, plugin}) and skipped; the refresh settles the loop comment the reported-rejection arm
owner surface read by plugin.Maximize this leaf id, windowId, keys, plugins, dockModel, dockShellIndex, refreshPromise, getDockHost(), on/un/fire, addDomListeners/removeDomListeners, isDestroying/isDestroyed — nothing else — the plugin's class docblock the plugin-only destroy arm exercises the listener pair and plugins

Acceptance Criteria

  • src/dashboard/dock/plugin/Maximize.mjs owns every dockMaximize* field, maximizedNodeId, the per-maximize ResizeObserver, onDockMaximizeEscape, the rect-style derivation and the FLIP capture/play pair.
  • Workspace.mjs retains no maximize behavior or state beyond the enableDockMaximizeAction install switch — no forwarding wrappers. Its method count and config/state count both fall in this PR; the delta is recorded in the PR body.
  • Ownership test, Stage 0 = yes (owns state) → all four questions answered in the PR body, question 4 with a cross-concern call count before and after.
  • Declined-plugin arm: with the plugin not configured, a Workspace instantiates and destroys with zero registered Maximize instances and no ResizeObserver registration — the guard D#18150 assigned to Option E.
  • Teardown arm: destroying the host destroys the plugin via component/Base.mjs:1322; no ResizeObserver and no pending transition survives. Red-first against a deliberately leaked observer.
  • Existing maximize behaviour is unchanged for a consumer that keeps it enabled — the dock example and Workstation both still maximize, restore, and honour escape.

Out of Scope

  • Any other cluster in Workspace — one leaf per concern (#18304).
  • Motion tokens / reduced-motion policy: unchanged, this moves the mechanism not the design.
  • The no-regrowth ratchet (#18278).

Avoided Traps

  • A forwarding shell. Workspace#maximize() delegating to the plugin passes a file-size check and fails the Ownership test by construction — the exact "relocation, not refactoring" defect D#18150 named in its own original criteria.
  • A mixin. Neo.mixin.VdomLifecycle composes unconditionally at component/Abstract.mjs:80; a mixin cannot be declined per instance, which is what this concern requires.
  • Leaving the observer registration behind "because the host owns the element". The observer is maximize state; it moves with the concern or the split is wrong.

Related

#18304 (epic) · D#18150 (source) · #18278 (ratchet) · src/tab/plugin/Overflow.mjs, src/plugin/* (precedent) · #18143 (the red-first case for owner-destroys-collaborator)


Live latest-open sweep: latest 20 open issues at 2026-09-04T18:12Z; no equivalent. A2A claim sweep: no claim on this surface. Own-assignment sweep: 12 open, none on the maximize cluster. Memory Core sweep: semantic recall is serving stale 2025 hits today (@neo-opus-ada, 17:22Z) — recorded as a null, not a clean sweep.

Decision Record impact: none — placement follows existing plugin precedent; ADR 0029 is untouched by this leaf.

Origin Session ID: b983b2a8-18bc-4dd0-a63f-23380dc3a49d