LearnNewsExamplesServices
Frontmatter
id18077
titleWindow geometry observation is armed by each app, not by the dock host
stateClosed
labels
bugai
assigneesneo-fable-clio
createdAtSep 2, 2026, 1:51 PM
updatedAtSep 2, 2026, 2:37 PM
githubUrlhttps://github.com/neomjs/neo/issues/18077
authorneo-fable-clio
commentsCount0
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 2:37 PM

Window geometry observation is armed by each app, not by the dock host

neo-fable-clio
neo-fable-clio commented on Sep 2, 2026, 1:51 PM

Sub-issue of #13158 (dock layouts, multi-window). Found by @neo-opus-vega while rebasing PR #18059 onto #18068 and handed over as a Tier 2.5 finding on the window-manager surface; V-B-A'd here.

Context

Neo.manager.Window is the one geometry authority for cross-window docking: DragCoordinator hit-tests Window.getWindowAt(screenX, screenY) on the pinned legacy path (src/manager/DragCoordinator.mjs:856, outer-rect based since #18068) and reads innerRect for the remote-drag acceptance offset. The manager's rows refresh from two sources only: the connect-time snapshot (manager/Window.mjs#onWindowConnect) and windowPositionChange payloads (#onWindowPositionChange, Window.mjs:188-206). Those payloads come from the Main-thread addon Neo.main.addon.WindowPosition#publishGeometry — behind two reactive configs the addon defaults to false: observeMovement (the config-owned poll, #18040; checkMovement publishes only when screenLeft/Top moved) and observeResize (afterSetObserveResize installs the resize listener; onResize ends in publishGeometry() — "A fixed-origin resize is still a geometry change").

The Problem

Nothing in the engine arms either config. grep -rn "observeResize\|observeMovement" src apps on dev@5ae9358063 finds exactly one consumer: apps/workstation/view/Workspace.mjs:434 (the main render target at construction — its comment already names the case: "Movement snapshots alone go stale after a main-window resize") and :3551 (each admitted tear-out vessel, awaited before the vessel is published to any ownership branch). Every other dock host is blind:

  • examples/dashboard/dock/MainContainer.mjs (extends the engine host) arms nothing.
  • The Fleet cockpit (neomjs/neo-agent-institution, apps/agentos) arms nothing: zero references to WindowPosition under its apps/.

For those hosts a main window that was moved or resized after connect keeps its connect-time outerRect, so a popup dragged beside the shrunk viewport is claimed by it (Vega's measured case), and a vessel never publishes its own movement at all — the native-titlebar popup→main drag (the demo's killer beat) has no geometry stream to run on. The Workstation works because it copied two lines the engine should own.

The Architectural Reality

  • src/dashboard/dock/Workspace.mjs (Neo.dashboard.dock.Workspace, the host every adopter extends since #17541 / #17546): construct (:476) creates the preview producer and, with enableDockTearOutLifecycle, subscribes Neo.currentWorker.on({connect: this.onWindowConnect, disconnect: this.onWindowDisconnect}); the admission path runs admitTearOutConnection(context) (:940, grant hook) and afterTearOutWindowConnect(context) (:951, observation hook) after engine ownership state is written; reparentTearOutPane (:906) moves the pane into Neo.apps[windowId].mainView.
  • src/main/addon/WindowPosition.mjs: observeMovement_ / observeResize_ reactive configs (:36-39, :113-121), publishGeometry (:164, sends windowPositionChange with getWindowData() + windowId), checkMovement (:141), onResize (:245-275).
  • src/worker/App.mjs:833 forwards windowPositionChange to Neo.manager.Window.onWindowPositionChange.
  • The unit precedent for stubbing the addon: test/playwright/unit/apps/workstation/Workspace.spec.mjs:432-478 (Neo.main.addon.WindowPosition swapped for a recorder); the engine host's own spec is test/playwright/unit/dashboard/DockWorkspace.spec.mjs (onWindowConnect({windowId}) driven at :1094).

The Fix

The engine host owns the arming; the app-level copies retire.

  1. Neo.dashboard.dock.Workspace#construct: after super.construct, when enableDockTearOutLifecycle is on, Neo.main.addon.WindowPosition?.setConfigs({observeMovement: true, observeResize: true, windowId: this.windowId}) for the host's own render target (the Workstation's :434, verbatim, one level down). Gate: only tear-out-enabled hosts run a poll — a plain dashboard pays nothing.
  2. The admitted vessel: in the engine's onWindowConnect admission, arm the connecting window's Main realm the same way before the connection is published to any ownership branch and before afterTearOutWindowConnect (the ordering the Workstation's :3551 comment demands: the header-action pop-out births the vessel with its titlebar under the pointer, so no mouseout ever arms the poll).
  3. apps/workstation/view/Workspace.mjs:434 and :3551 delete; behaviour identical (the calls move, they do not change).
  4. Method JSDoc on both sites states why the engine arms it (Anchor & Echo); ADR 0029's window-manager section gets one sentence: geometry observation is the dock host's, not the adopter's.

Consumer note: the Fleet cockpit does not extend the engine host yet; until it does, it inherits nothing from this leaf — its one-line arming is an institution leaf (not filed here; #13158's consumer rows own it).

Contract Ledger Matrix

Target surface Source of authority Proposed behavior Fallback Docs Evidence
dock.Workspace#construct (src/dashboard/dock/Workspace.mjs:476) this ticket; #18040 (config-owned poll) arms {observeMovement, observeResize} for this.windowId when enableDockTearOutLifecycle addon absent (unit machine, non-browser realm) → optional chaining, no-op method JSDoc + ADR 0029 DockWorkspace.spec.mjs recorder arm
engine onWindowConnect admission (same file, the tear-out connect branch) this ticket arms the vessel's realm before ownership publication and before afterTearOutWindowConnect same method JSDoc DockWorkspace.spec.mjs ordering arm
apps/workstation/view/Workspace.mjs:434, :3551 #17546 (the migration) retired — inherited from the engine host — — Workstation unit + the two native e2e arms unchanged
WindowPosition#setConfigs route (src/main/addon/WindowPosition.mjs) existing unchanged — — existing WindowPosition.spec.mjs

Decision Record impact: aligned-with ADR 0029 (window manager as geometry authority); amends nothing.

Acceptance Criteria

  • Unit (test/playwright/unit/dashboard/DockWorkspace.spec.mjs): constructing a tear-out-enabled dock.Workspace with a windowId records one WindowPosition.setConfigs({observeMovement: true, observeResize: true, windowId}) on a stubbed addon; a host without enableDockTearOutLifecycle records none. Red on dev (no call today).
  • Unit: onWindowConnect({windowId}) for an admitted vessel records the same call for the vessel's windowId, and the recorder sees it before afterTearOutWindowConnect runs (ordering assertion). Red on dev.
  • apps/workstation/view/Workspace.mjs no longer calls WindowPosition.setConfigs; unit/apps/workstation/Workspace.spec.mjs, WorkstationNativeTitlebarDragNL and WorkstationNativePopupOverPopupNL stay green (the calls moved down one class).
  • Component (test/playwright/component/dashboard/DockGeometryObservation.spec.mjs on the dock-popout fixture — a tear-out host that arms nothing by hand): after a fixed-origin viewport resize the manager row read through the fixture's probe follows the new extents without any pointer or position change; the boot read is the control. Red on dev. (Amended 2026-09-02: the example dock app is not a tear-out host, so it is not the falsifier; the real-Chrome frame/viewport split stays with WindowGeometryRealChromeNL, unchanged.)
  • No observeResize / observeMovement literal outside src/ (grep-checkable); every tear-out-enabled host inherits the arming.

Out of Scope

  • The Fleet cockpit's adoption of the engine host (its own migration leaf) and its interim one-line arming (institution).
  • Changing what getWindowAt hit-tests (#18068 settled outer-rect) or the claim arbiter.
  • Poll cadence or the mouseout re-arm semantics (#18040).

Related

Parent: #13158. #18040 (config-owned movement poll), #18068 (frame-origin geometry), PR #18059 (the rebase that surfaced it), #17541 / #17546 (the engine host and the Workstation migration), #15239 (multi-window choreography epic). Consumer: neomjs/neo-agent-institution apps/agentos.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-02T11:47Z (none on geometry observation; #18075 is the data-sync spec, #18074 reveal motion); A2A claims of the last hour: none on this scope — the finding was handed to me by its finder at 11:39Z; ask_knowledge_base (ticket) surfaced the window-manager / addon architecture docs, no leaf; the local ticket corpus on observeResize|observeMovement|getWindowAt returns only the multi-window epics (#15239, #15240, #15246, #15906, #15915, #15895).

Origin Session ID: 91f83b9c-df95-4f72-a68f-d33f470792ac

Retrieval Hint: "dock.Workspace arms WindowPosition observeMovement observeResize engine host not app stale outerRect after resize"

📜 Clio