Context
Measured live on 2026-09-04 by the popup-birth arm for D#18224 (apps/workstation at dev@2ee23d7801, Playwright e2e rig, scratch spec test/playwright/e2e/workstation/PopupBirthArm.spec.mjs, untracked). The Feed pane — Workstation.view.FeedPane, a grid.Container with a sparkline column — was popped out into a real popup window through the production tab action. The grid subtree moved as the same instance (20 descendants, 0 lost, consistency 4/4/4, same provider and store), every descendant in the items tree took the popup's windowId — and every sparkline canvas that travelled with the grid went blank.
Receipt: the five pre-existing Neo.component.Sparkline cells kept the opener's windowId and read offscreenRegistered: false for the whole 5 s settle poll; the three cells the grid created after the hop (row pool grown for the larger vessel) carry the popup's windowId and registered at once. Screenshots: before, the Live column draws a sparkline in every row; after, the Live column is empty in every row of the popup.
The Problem
Neo.grid.Row extends component.Base, not a container (src/grid/Row.mjs:39–42). It owns its cell components in this.components (:155–184) and forwards two lifecycle configs by hand: mounted (afterSetMounted, :84–92, added for #9107 so canvases re-register after a VDOM purge) and theme (afterSetTheme, :100–108). It has no afterSetWindowId. grid.Body#afterSetWindowId (src/grid/Body.mjs:671–681) forwards to its rows and stops there. After a cross-window move, grid.Container → grid.View → grid.Body → every row carries the new id, and every pooled cell component keeps the old one. column/Component#cellRenderer (src/grid/column/Component.mjs:159–171) stamps windowId only at creation, which is why the three new cells were right.
For a canvas cell the consequence is total. component/Canvas#afterSetMounted (src/component/Canvas.mjs:50–91) re-transfers the canvas on remount with transferCanvasToWorker({componentId, nodeId, windowId}); with the stale id the request goes to the opener's main thread, where the node no longer exists, so the promise never resolves and offscreenRegistered stays false. Canvas#afterSetWindowId (:102–108) already resets the flag on an id change so the remount can re-register — the hook exists, the cascade never reaches it. Any component cell with window-scoped main-thread state fails the same way.
The Architectural Reality
container.Base#afterSetWindowId (src/container/Base.mjs:319–338) cascades to items, layout and sort zone. Rows are not containers, so they forward by hand, as they already do for mounted and theme.
- #9107 (2026-02) established the pattern for
mounted; theme followed; windowId is the third config the row must relay.
- D#18224 row 8 / Euclid's provider-topology correction: realm registrations are re-armed by the component that owns them through
afterSetWindowId. This defect is the one missing link in that chain for grids.
The Fix
Neo.grid.Row#afterSetWindowId(value, oldValue): call super, then for every key in this.components set components[key].windowId = value when value is set — the exact twin of afterSetMounted. Fold the three loops into one small forwarding helper so a fourth config never needs a fourth copy.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
grid.Row#afterSetWindowId (new) |
container.Base:319 cascade contract; #9107 precedent |
pooled cell components take the row's new windowId |
none needed — cells without window-scoped state ignore it |
Row docblock line 36 (component management) |
popup arm: 8/8 canvases in the popup carry its id and offscreenRegistered: true |
Decision Record impact
none.
Acceptance Criteria
Out of Scope
- The grid view's scroll model resetting on remount (view
scrollTop 126 → 0 and body startIndex 2 → 0 in the same arm) — a separate finding, reported on D#18224.
- The owner-design work on D#18224.
Avoided Traps
- Fixing it at the column (
column/Base#afterSetWindowId only inserts theme files) or in the body: the row owns the pool and already forwards lifecycle configs.
- Re-creating cells on a window change: instance permanence is the point of the hop.
Related
- #9107 (the
mounted twin) · D#18224 (popup-birth arm receipt) · #18276 (same arm, same session) · ADR 0029 §2.6.
Live latest-open sweep: the latest 20 open issues (created-desc, 2026-09-04 14:49Z) — none equivalent (#18286 … #18064). Issue search "grid row windowId cell component canvas window" (all states) — only #15252, unrelated. A2A claim sweep (all read-states, last 60 min): no claim on the grid or canvas surface. Memory Core sweep: #9107's mounted fix (2026-02-11) is the precedent; no windowId twin recorded. Own-assignment sweep: none.
unowned-rationale: a four-line engine fix with a red-first unit; claimable by any seat — my seat stays in peer-role on D#18224, and the consumer surface where it shows (Workstation pop-out) is Vega's.
Origin Session ID: ee061205-8271-4458-b73b-b132c1bec310
Retrieval Hint: query_raw_memories("grid Row afterSetWindowId cell components sparkline canvas blank popup offscreenRegistered")
Context
Measured live on 2026-09-04 by the popup-birth arm for D#18224 (
apps/workstationatdev@2ee23d7801, Playwright e2e rig, scratch spectest/playwright/e2e/workstation/PopupBirthArm.spec.mjs, untracked). The Feed pane —Workstation.view.FeedPane, agrid.Containerwith asparklinecolumn — was popped out into a real popup window through the production tab action. The grid subtree moved as the same instance (20 descendants, 0 lost, consistency 4/4/4, same provider and store), every descendant in the items tree took the popup'swindowId— and every sparkline canvas that travelled with the grid went blank.Receipt: the five pre-existing
Neo.component.Sparklinecells kept the opener'swindowIdand readoffscreenRegistered: falsefor the whole 5 s settle poll; the three cells the grid created after the hop (row pool grown for the larger vessel) carry the popup'swindowIdand registered at once. Screenshots: before, the Live column draws a sparkline in every row; after, the Live column is empty in every row of the popup.The Problem
Neo.grid.Rowextendscomponent.Base, not a container (src/grid/Row.mjs:39–42). It owns its cell components inthis.components(:155–184) and forwards two lifecycle configs by hand:mounted(afterSetMounted,:84–92, added for #9107 so canvases re-register after a VDOM purge) andtheme(afterSetTheme,:100–108). It has noafterSetWindowId.grid.Body#afterSetWindowId(src/grid/Body.mjs:671–681) forwards to its rows and stops there. After a cross-window move,grid.Container→grid.View→grid.Body→ every row carries the new id, and every pooled cell component keeps the old one.column/Component#cellRenderer(src/grid/column/Component.mjs:159–171) stampswindowIdonly at creation, which is why the three new cells were right.For a canvas cell the consequence is total.
component/Canvas#afterSetMounted(src/component/Canvas.mjs:50–91) re-transfers the canvas on remount withtransferCanvasToWorker({componentId, nodeId, windowId}); with the stale id the request goes to the opener's main thread, where the node no longer exists, so the promise never resolves andoffscreenRegisteredstays false.Canvas#afterSetWindowId(:102–108) already resets the flag on an id change so the remount can re-register — the hook exists, the cascade never reaches it. Any component cell with window-scoped main-thread state fails the same way.The Architectural Reality
container.Base#afterSetWindowId(src/container/Base.mjs:319–338) cascades to items, layout and sort zone. Rows are not containers, so they forward by hand, as they already do formountedandtheme.mounted;themefollowed;windowIdis the third config the row must relay.afterSetWindowId. This defect is the one missing link in that chain for grids.The Fix
Neo.grid.Row#afterSetWindowId(value, oldValue): call super, then for every key inthis.componentssetcomponents[key].windowId = valuewhenvalueis set — the exact twin ofafterSetMounted. Fold the three loops into one small forwarding helper so a fourth config never needs a fourth copy.Contract Ledger Matrix
grid.Row#afterSetWindowId(new)container.Base:319cascade contract; #9107 precedentwindowIdoffscreenRegistered: trueDecision Record impact
none.Acceptance Criteria
grid.Rowholding a component cell receiveswindowId = 'w2'; the cell reports'w2'(today it keeps the old value).Neo.component.Canvascell in a moved row resetsoffscreenRegisteredand re-registers with the newwindowIdon remount (spy ontransferCanvasToWorker, assert itswindowIdargument).windowId, is mounted andoffscreenRegistered: true, and the Live column is drawn in the popup.Out of Scope
scrollTop126 → 0 and bodystartIndex2 → 0 in the same arm) — a separate finding, reported on D#18224.Avoided Traps
column/Base#afterSetWindowIdonly inserts theme files) or in the body: the row owns the pool and already forwards lifecycle configs.Related
mountedtwin) · D#18224 (popup-birth arm receipt) · #18276 (same arm, same session) · ADR 0029 §2.6.Live latest-open sweep: the latest 20 open issues (created-desc, 2026-09-04 14:49Z) — none equivalent (#18286 … #18064). Issue search "grid row windowId cell component canvas window" (all states) — only #15252, unrelated. A2A claim sweep (all read-states, last 60 min): no claim on the grid or canvas surface. Memory Core sweep: #9107's
mountedfix (2026-02-11) is the precedent; nowindowIdtwin recorded. Own-assignment sweep: none.unowned-rationale: a four-line engine fix with a red-first unit; claimable by any seat — my seat stays in peer-role on D#18224, and the consumer surface where it shows (Workstation pop-out) is Vega's.
Origin Session ID: ee061205-8271-4458-b73b-b132c1bec310
Retrieval Hint:
query_raw_memories("grid Row afterSetWindowId cell components sparkline canvas blank popup offscreenRegistered")