Context
Found while covering #19516's swap for tab-root shells (Euclid's review packet). The fixture is dock-preview-geometry: a dock whose root is a tabs node, with no app stylesheet. Commits go through the real reducer and refresh loop via its applyOperationJson trigger (added on #19516). Measured on dev 3ab4a3f158 and on #19516's head e93602e0ed:
| Commit |
Observed on dev |
Nest: splitNode puts aside beside the tabs root root |
The root tab container (Main) moves into the new split with visibility: hidden. Its 637 px half stays blank and Main never repaints. On #19516 the swap's retiring class also gives it display: none. |
Un-nest: split s [t1, t2] collapses to t1 (moveItem aside → t1) |
The old split stays the host's shell: visible, 1280 px, its leftover tabs node hidden. The re-rooted t1 (Main, Aside) is a second host child at x=1280. The workspace is blank. Identical on #19516. |
Both states persist until another commit re-projects.
The Problem
Reconciler#reconcileProjection decides which shell is outgoing after moveRetainedTabChrome:
nextShell = nextShell.isDestroyed ? host.items[shellIndex] : nextShell;
retainedRoot = oldShell === nextShell;
That rule was written for a tabs root that stays the root. Its comment reads: "the retained tab is both old and new shell; never retire it as source chrome." A retained tabs node that crosses the root boundary breaks it both ways:
- Nest. The old shell is itself the retained tab, and
moveRetainedTabChrome moves it into the staged split. Nothing is outgoing, yet the swap hides it. The cleanup's host.remove(oldShell) is then a no-op, because the shell is no longer the host's child.
- Un-nest. The staged root is the retained tab's placeholder.
moveRetainedTabChrome destroys it and inserts the tab at the placeholder's index, shellIndex + 1. host.items[shellIndex] is still the old split, so retainedRoot reads true. No swap happens, the old split never retires, and the re-rooted tab sits beside it.
The Architectural Reality
Neo.dashboard.dock.projection.Reconciler#reconcileProjection: phase 3 runs moveRetainedTabChrome, then the visibility swap; phase 4 removes the outgoing shell.
#settleFailedProjection derives survivor and casualty from oldShell / nextShell / swapped, so it inherits the misidentification on a rejected flight.
#moveRetainedTabChrome(plans) re-parents each retained tab into its plan's placeholder slot. When the staged root is a placeholder, plan.placeholder === nextShell names the retained tab that becomes the root.
- Design authority: the transaction's invariant as stated in
settleFailedProjection's @summary: "one shell, at shellIndex, visible." Also the retained-root comment quoted above: a retained tab is never retired. Documents admit any root: LayoutAdapter.projectNode projects edge-zone, split and tabs, and Operations.splitNode replaces the root when its target has no parent.
The Fix
In phase 3, after moveRetainedTabChrome:
- The next shell is the retained tab whose placeholder was the staged root (the
plans entry with placeholder === nextShell), not host.items[shellIndex].
- The outgoing shell is the old shell only while it is still the host's child. A root tab re-homed into the staged shell is not outgoing: nothing is hidden, retired or removed.
settleFailedProjection receives that outgoing shell, null when re-homed. With no outgoing shell, the staged shell, which holds the re-homed tab, survives a rejected flight.
Acceptance Criteria
Out of Scope
- The one-frame blank at a commit (#19514 / #19516).
- The Workstation's default document. Its root is an edge zone, so these commits do not cross its root.
Related
- #19516: the swap's out-of-layout class. This defect predates it; that PR changes only the CSS outcome of the nest case.
#16152 (closed, not planned): a transient drain while retained tabs reparent, a different symptom.
Live latest-open sweep: checked latest 20 open issues at 2026-10-09T17:42Z; no equivalent found.
A2A claim sweep: last 30 messages, no claim on dock root nesting.
MC sweep: "dock tab root split pane hidden after commit retained tab container root boundary Reconciler nextShell retainedRoot", 6 results, no prior decision (one records "any root" as the dock-zone design). KB ticket sweep: no equivalent.
Own-assignment sweep: 6 open, #19514 adjacent (above), none overlapping.
Origin Session ID: fcc49158-63cf-4317-a407-741c5e5e3d8c
Retrieval Hint: "tab root nest un-nest Reconciler retainedRoot moveRetainedTabChrome hidden shell"
Context
Found while covering #19516's swap for tab-root shells (Euclid's review packet). The fixture is
dock-preview-geometry: a dock whose root is a tabs node, with no app stylesheet. Commits go through the real reducer and refresh loop via itsapplyOperationJsontrigger (added on #19516). Measured ondev3ab4a3f158 and on #19516's heade93602e0ed:devsplitNodeputsasidebeside the tabs rootrootvisibility: hidden. Its 637 px half stays blank and Main never repaints. On #19516 the swap's retiring class also gives itdisplay: none.s [t1, t2]collapses tot1(moveItem aside → t1)t1(Main, Aside) is a second host child at x=1280. The workspace is blank. Identical on #19516.Both states persist until another commit re-projects.
The Problem
Reconciler#reconcileProjectiondecides which shell is outgoing aftermoveRetainedTabChrome:nextShell = nextShell.isDestroyed ? host.items[shellIndex] : nextShell; retainedRoot = oldShell === nextShell; // !retainedRoot: oldShell hidden, nextShell shown; later host.remove(oldShell)That rule was written for a tabs root that stays the root. Its comment reads: "the retained tab is both old and new shell; never retire it as source chrome." A retained tabs node that crosses the root boundary breaks it both ways:
moveRetainedTabChromemoves it into the staged split. Nothing is outgoing, yet the swap hides it. The cleanup'shost.remove(oldShell)is then a no-op, because the shell is no longer the host's child.moveRetainedTabChromedestroys it and inserts the tab at the placeholder's index,shellIndex + 1.host.items[shellIndex]is still the old split, soretainedRootreads true. No swap happens, the old split never retires, and the re-rooted tab sits beside it.The Architectural Reality
Neo.dashboard.dock.projection.Reconciler#reconcileProjection: phase 3 runsmoveRetainedTabChrome, then the visibility swap; phase 4 removes the outgoing shell.#settleFailedProjectionderives survivor and casualty fromoldShell/nextShell/swapped, so it inherits the misidentification on a rejected flight.#moveRetainedTabChrome(plans)re-parents each retained tab into its plan'splaceholderslot. When the staged root is a placeholder,plan.placeholder === nextShellnames the retained tab that becomes the root.settleFailedProjection's @summary: "one shell, atshellIndex, visible." Also the retained-root comment quoted above: a retained tab is never retired. Documents admit any root:LayoutAdapter.projectNodeprojectsedge-zone,splitandtabs, andOperations.splitNodereplaces the root when its target has no parent.The Fix
In phase 3, after
moveRetainedTabChrome:plansentry withplaceholder === nextShell), nothost.items[shellIndex].settleFailedProjectionreceives that outgoing shell, null when re-homed. With no outgoing shell, the staged shell, which holds the re-homed tab, survives a rejected flight.Acceptance Criteria
dev.shellIndex: the re-rooted tab container, visible, at x=0. The old split is destroyed. Red ondev.DockProjectionReconciler.specpins pass: the retained root tab and the rejected-flight phases.Out of Scope
Related
#16152(closed, not planned): a transient drain while retained tabs reparent, a different symptom.Live latest-open sweep: checked latest 20 open issues at 2026-10-09T17:42Z; no equivalent found. A2A claim sweep: last 30 messages, no claim on dock root nesting. MC sweep: "dock tab root split pane hidden after commit retained tab container root boundary Reconciler nextShell retainedRoot", 6 results, no prior decision (one records "any root" as the dock-zone design). KB ticket sweep: no equivalent. Own-assignment sweep: 6 open, #19514 adjacent (above), none overlapping.
Origin Session ID: fcc49158-63cf-4317-a407-741c5e5e3d8c Retrieval Hint: "tab root nest un-nest Reconciler retainedRoot moveRetainedTabChrome hidden shell"