Context
The operator's requirement for dock layouts is "reload the multi-window app with the saved layout ⇒ getting the same multi-window setup." #18252 / PR #18253 made the restore path reachable at all — the seams it needs had no implementer. This is the next measured thing between that path and the sentence: a restored layout brings the tabs back, but not the tab you were on.
Promotion of a defect-note (ticket-create §1e), on the operator-escalation trigger: the active tab is part of "the same setup".
Measured at dev@274b63beac
Real capture, real reconciler, a real workspace (the arm added by PR #18253, test/playwright/unit/dashboard/DockTopologySeams.spec.mjs):
| hop |
nodes['main-tabs'].activeItemId |
| capture |
strategy — kept, the record is complete |
| live composition moves on |
swarm |
after TopologyReconciler.reconcile() |
swarm — the live value survives |
Tab membership and order restore correctly in the same run. Only the active item does not.
The chain, verified end to end
model/TopologyReconciler.mjs:606 delegates each slot to RestorePlanner.restoreToward(live, captured).
persistence/RestorePlanner.mjs builds its plan entirely from TopologyDiff.diffDockDocuments(current, captured), emitting addTab, moveItem, resizeSplit, setItemAutoHidden.
model/TopologyDiff.mjs:105 returns exactly six categories:
{moves: [], adds: [], removes: [], resizes: [], tabReorders: [], autoHideFlips: [], unchanged: [], errors: []}There is no active-item category. The planner cannot emit what the diff never reports, so the string activeItemId appears nowhere in TopologyDiff.mjs, RestorePlanner.mjs or TopologyReconciler.mjs.
It is unaddressed rather than declined — no comment anywhere argues that the active tab is runtime-only state, and the capture format keeps it, which is the opposite argument.
The verb already exists
model/Operations.mjs:253 implements setActiveItem(document, {tabsNodeId, itemId}), registered in the operation table at :46 and classed topology at :87. Nothing new is needed in the model — this is a diff category and one plan step.
Proposed change
TopologyDiff gains an active-item category for tabs nodes whose activeItemId differs between current and captured, in the same shape as the existing ones ({nodeId, from, to}).
RestorePlanner emits setActiveItem for each, ordered after the adds/moves/reorders — the active item must be a member of the node when it is set, so a step that runs before its own item arrives would be rejected by the executor.
Acceptance Criteria
Non-goals
- The single-window (
captureScope: 'window') path, unless it shares the planner — measure, do not assume.
- Focus. Which tab is ACTIVE is document state; which element has DOM focus is not, and restoring focus is a separate question nobody has asked for.
- Any change to the capture format. It already carries the value.
Decision Record impact
aligned-with ADR 0029. No new concept: an existing model verb reaches an existing plan.
Related
#18252 / PR #18253 (made the restore path reachable; this gap is behind it and survives it) · #18151 (epic) · #17838 (closed — the ticket that added setActiveItem to the model)
Sweep before filing
Live latest-20 open at 2026-09-04T02:3xZ — no equivalent. Own-assignment sweep over restore OR perspective OR planner — #18252 (the seams, distinct), #17844, none this. A2A claim sweep over the last 14 messages: only @neo-opus-vega's #18241 is an active claim, no overlap. Closed-queue RestorePlanner OR capturePerspective checked while filing #18252 — #14652, #16315, #16316, none covers the active item.
Authored by Grace (Claude Opus 5, Claude Code). Session 118d8435-8d23-4745-a28a-8d22da918aac.
Context
The operator's requirement for dock layouts is "reload the multi-window app with the saved layout ⇒ getting the same multi-window setup." #18252 / PR #18253 made the restore path reachable at all — the seams it needs had no implementer. This is the next measured thing between that path and the sentence: a restored layout brings the tabs back, but not the tab you were on.
Promotion of a defect-note (ticket-create §1e), on the operator-escalation trigger: the active tab is part of "the same setup".
Measured at
dev@274b63beacReal capture, real reconciler, a real workspace (the arm added by PR #18253,
test/playwright/unit/dashboard/DockTopologySeams.spec.mjs):nodes['main-tabs'].activeItemIdstrategy— kept, the record is completeswarmTopologyReconciler.reconcile()swarm— the live value survivesTab membership and order restore correctly in the same run. Only the active item does not.
The chain, verified end to end
model/TopologyReconciler.mjs:606delegates each slot toRestorePlanner.restoreToward(live, captured).persistence/RestorePlanner.mjsbuilds its plan entirely fromTopologyDiff.diffDockDocuments(current, captured), emittingaddTab,moveItem,resizeSplit,setItemAutoHidden.model/TopologyDiff.mjs:105returns exactly six categories:{moves: [], adds: [], removes: [], resizes: [], tabReorders: [], autoHideFlips: [], unchanged: [], errors: []}There is no active-item category. The planner cannot emit what the diff never reports, so the string
activeItemIdappears nowhere inTopologyDiff.mjs,RestorePlanner.mjsorTopologyReconciler.mjs.It is unaddressed rather than declined — no comment anywhere argues that the active tab is runtime-only state, and the capture format keeps it, which is the opposite argument.
The verb already exists
model/Operations.mjs:253implementssetActiveItem(document, {tabsNodeId, itemId}), registered in the operation table at:46and classedtopologyat:87. Nothing new is needed in the model — this is a diff category and one plan step.Proposed change
TopologyDiffgains an active-item category for tabs nodes whoseactiveItemIddiffers between current and captured, in the same shape as the existing ones ({nodeId, from, to}).RestorePlanneremitssetActiveItemfor each, ordered after the adds/moves/reorders — the active item must be a member of the node when it is set, so a step that runs before its own item arrives would be rejected by the executor.Acceptance Criteria
activeItemIdfor every tabs node. Red-first: the arm inDockTopologySeams.spec.mjscurrently pins the live value surviving, and that pin flips to the captured value.setActiveItemruns after the item it names has arrived in the node. An arm covers the case where the active item is one the restore had toaddTabormoveItemfirst.surplus, or the node no longer exists) does not emit a step and does not fail the restore. Restore never destroys, and it must not start throwing here either.Non-goals
captureScope: 'window') path, unless it shares the planner — measure, do not assume.Decision Record impact
aligned-with ADR 0029. No new concept: an existing model verb reaches an existing plan.Related
#18252 / PR #18253 (made the restore path reachable; this gap is behind it and survives it) · #18151 (epic) · #17838 (closed — the ticket that added
setActiveItemto the model)Sweep before filing
Live latest-20 open at 2026-09-04T02:3xZ — no equivalent. Own-assignment sweep over
restore OR perspective OR planner— #18252 (the seams, distinct), #17844, none this. A2A claim sweep over the last 14 messages: only @neo-opus-vega's #18241 is an active claim, no overlap. Closed-queueRestorePlanner OR capturePerspectivechecked while filing #18252 — #14652, #16315, #16316, none covers the active item.Authored by Grace (Claude Opus 5, Claude Code). Session 118d8435-8d23-4745-a28a-8d22da918aac.