You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The client router's layout-marker pairing mispairs when a NESTED layout's INNER <!--/wj:children--> close comment is missing (while the outer close survives). collectChildrenSlots (packages/core/src/router-client.js) walks comments with a LIFO stack: on the surviving OUTER close it pops the still-open INNER frame, so the inner slot is registered as innerOpen -> outerClose, an over-wide span, and the outer open is left unclosed.
On a nested-layout soft nav in that state: buildHaveHeader reports the mispaired inner path (non-empty), the server short-circuits on that innermost have match (packages/server/src/ssr.js ~L581-587) and returns a reduced inner-only fragment, and applySwap's scoped swap uses the over-wide live inner range, wiping any outer-layout trailing chrome (a <footer>) rendered between the dropped inner close and the outer close.
This is a pre-existing pairing-logic limitation. #994 (dropped-close recovery) neither introduces nor worsens it; #994 handles the SOLE or OUTERMOST dropped close and documents this nested case as a known limitation, with the wrapped-${children} idiom (<main>${children}</main>, footer a sibling outside the marker's parent) as the mitigation. This issue tracks a real fix.
Design / approach
The root difficulty: <!--/wj:children--> close comments are ANONYMOUS (they carry no path), so a dropped inner close cannot be path-matched to its open. The LIFO stack is the only pairing signal, and it mispairs on a missing inner close.
Candidate directions:
Emit the path on the CLOSE marker too (<!--/wj:children:/docs-->), so pairing is by path, not stack position. A dropped inner close then leaves the inner open unpaired (recoverable per dogfood: soft-nav to /blog drops the top navbar across browsers (#936 residual) #994) instead of stealing the outer close. This is the cleanest fix but changes the SSR marker format (server + client + every marker test).
Depth-tracked synthesis: track nesting depth and, on an unexpected close, decide by the emitted layout chain which open it belongs to. More complex, no format change.
boundRecoveredEnds cannot help here because both mispaired slot ends are non-null (bounding runs only when exactly one side is orphaned).
Prefer the path-on-close-marker approach if the format change is acceptable; it makes the whole dropped-marker class path-addressable.
Implementation notes (for the implementing agent)
Marker emission: packages/server/src/ssr.jswrapWithChildrenMarker() (~L683) emits <!--wj:children:${segmentPath}--> ... <!--/wj:children-->. Adding a path to the close is here.
Pairing: packages/core/src/router-client.jscollectChildrenSlots() (~L724, the LIFO stack plus the data.trim() === '/wj:children' close match) and the #994 orphan recovery loop after the walk. Update the close regex plus pop logic to path-match.
The server short-circuit that turns a have path into a reduced fragment: packages/server/src/ssr.js ~L581-587 (innermost-first have.has(segmentPath)).
Swap consumers: applySwap / swapMarkerRange / reconcileSiblings in router-client.js (should need no change if the slot ends become correct).
Landmines:
The marker format is depended on by many tests (packages/core/test/routing/**, test/e2e/e2e.test.mjs "layout emits wj:children markers"). A format change touches all of them.
Backwards-compat is a non-issue (WebJs has no released users), so a clean format change is fine.
Invariants: AGENTS.md router invariant "preserve outer-layout DOM identity" (never destructively swap outer chrome); the marker pair is the sole swap-scoping signal.
Tests plus docs: add a nested-layout inner-close-drop browser test under packages/core/test/routing/browser/ (assert the outer footer survives) and a counterfactual; update .agents/skills/webjs/references/client-router-and-streaming.md (the dogfood: soft-nav to /blog drops the top navbar across browsers (#936 residual) #994 dropped-marker note now scopes out this case) once fixed.
Acceptance criteria
A nested-layout soft nav with a dropped INNER close preserves outer-layout trailing chrome (footer) and takes the correct scoped swap
collectChildrenSlots no longer mispairs an inner open with an outer close when the inner close is missing
A browser/e2e test reproduces the footer-sweep and fails when the fix is reverted (counterfactual)
Problem
The client router's layout-marker pairing mispairs when a NESTED layout's INNER
<!--/wj:children-->close comment is missing (while the outer close survives).collectChildrenSlots(packages/core/src/router-client.js) walks comments with a LIFO stack: on the surviving OUTER close it pops the still-open INNER frame, so the inner slot is registered asinnerOpen -> outerClose, an over-wide span, and the outer open is left unclosed.On a nested-layout soft nav in that state:
buildHaveHeaderreports the mispaired inner path (non-empty), the server short-circuits on that innermosthavematch (packages/server/src/ssr.js~L581-587) and returns a reduced inner-only fragment, andapplySwap's scoped swap uses the over-wide live inner range, wiping any outer-layout trailing chrome (a<footer>) rendered between the dropped inner close and the outer close.This is a pre-existing pairing-logic limitation. #994 (dropped-close recovery) neither introduces nor worsens it; #994 handles the SOLE or OUTERMOST dropped close and documents this nested case as a known limitation, with the wrapped-
${children}idiom (<main>${children}</main>, footer a sibling outside the marker's parent) as the mitigation. This issue tracks a real fix.Design / approach
The root difficulty:
<!--/wj:children-->close comments are ANONYMOUS (they carry no path), so a dropped inner close cannot be path-matched to its open. The LIFO stack is the only pairing signal, and it mispairs on a missing inner close.Candidate directions:
<!--/wj:children:/docs-->), so pairing is by path, not stack position. A dropped inner close then leaves the inner open unpaired (recoverable per dogfood: soft-nav to /blog drops the top navbar across browsers (#936 residual) #994) instead of stealing the outer close. This is the cleanest fix but changes the SSR marker format (server + client + every marker test).boundRecoveredEndscannot help here because both mispaired slot ends are non-null (bounding runs only when exactly one side is orphaned).Prefer the path-on-close-marker approach if the format change is acceptable; it makes the whole dropped-marker class path-addressable.
Implementation notes (for the implementing agent)
packages/server/src/ssr.jswrapWithChildrenMarker()(~L683) emits<!--wj:children:${segmentPath}-->...<!--/wj:children-->. Adding a path to the close is here.packages/core/src/router-client.jscollectChildrenSlots()(~L724, the LIFO stack plus thedata.trim() === '/wj:children'close match) and the#994orphan recovery loop after the walk. Update the close regex plus pop logic to path-match.havepath into a reduced fragment:packages/server/src/ssr.js~L581-587 (innermost-firsthave.has(segmentPath)).applySwap/swapMarkerRange/reconcileSiblingsinrouter-client.js(should need no change if the slot ends become correct).packages/core/test/routing/**,test/e2e/e2e.test.mjs"layout emits wj:children markers"). A format change touches all of them.packages/core/test/routing/browser/(assert the outer footer survives) and a counterfactual; update.agents/skills/webjs/references/client-router-and-streaming.md(the dogfood: soft-nav to /blog drops the top navbar across browsers (#936 residual) #994 dropped-marker note now scopes out this case) once fixed.Acceptance criteria
collectChildrenSlotsno longer mispairs an inner open with an outer close when the inner close is missingclient-router-and-streaming.mdis updated to reflect the fix