Found while implementing #7121 (studio-canvas leaves offering Design mode and a "click a block" rail). Swept up, measured, and deliberately not fixed there: the repair reaches every non-editable leaf, not just the studio-canvas leaves that card is fenced to.
The mechanism
InterfacesPillar's draft-load effect clears the canvas block selection — but the clear sits after the guard, so it never runs on the path that returns early:
React.useEffect(() => {
if (!current || !isEditable) {
setDraft({});
setHasDraft(false);
return; // <-- returns BEFORE the clear below
}
let cancelled = false;
setLoading(true);
setError(null);
setSelection(null); // <-- only ever reached on the editable path
...
}, [client, current, isEditable, publishNonce]);
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, the effect commented "Load the selected surface's draft (only for editable preview types)".
So walking from a leaf where a block is selected to any leaf with isEditable === false carries the selection across. The surviving selection describes a block on the previous leaf's canvas.
Measured
With the designer registry populated, a stub preview for dashboard and a stub inspector registered for object, selecting a block on the dashboard leaf and then clicking the object leaf:
[B] selection live on dashboard leaf: true
[B] deselect affordance STILL present on object leaf: true
[D] inspector types now: ["object"]
[D] scoped inspector rendered on studio-canvas leaf: true
[D] inspector was handed type/name: object:showcase_task:block:blk_1
blk_1 is a dashboard block. It does not exist on the records grid the object leaf renders. Production registers ObjectFieldInspector for object (metadata-admin/inspectors/index.ts), so the branch is reachable outside the harness.
What #7121 already covers, and what it does not
#7121 gates the rail and the header's "clear selection" button on StudioCanvas, and orders the rail's studio-canvas branch ahead of the selection branch. That makes the two rail symptoms unreachable on studio-canvas leaves.
It does not clear the state. Still live afterwards:
hasInspectorTarget = Boolean((editNav && navSel) || selection) feeds nextCenterTab (centerTab.ts). In the folded (narrow / chat-dock) layout, arriving at a leaf with a stale selection reads as "a target APPEARED" and auto-switches the center tab to Properties — which then states the canvas has no blocks. (Read from the code path, not measured in a browser.)- The leak is not specific to studio-canvas leaves. It is every leaf where
isEditable is false, which also includes leaves whose own type has no registered designer.
Why it was not fixed in #7121
That card's ruling fences it to studio-canvas leaves — registerStudioCanvasPreview types, of which there is exactly one in-repo today (object). Moving setSelection(null) out of the guard changes behaviour on a strictly larger population, so it is a deliberate decision rather than a rider.
Not scoped here
Which repair is right:
- hoist
setSelection(null) above the guard so any leaf change clears it — simplest, and arguably what the effect already means to say; - clear it in the leaf-change handler instead of the load effect, so the reset is not entangled with
isEditable being in the dependency array; - key the selection to the leaf (as
blockingReport already is via inspectorKey) so it expires by construction rather than by an imperative clear. inspectorKey is the precedent in the same component.
Related
#7121 (where this was found) · #6795 part C · centerTab.ts / ADR-0057 P3c (the folded-layout consumer of hasInspectorTarget)
Found while implementing #7121 (studio-canvas leaves offering Design mode and a "click a block" rail). Swept up, measured, and deliberately not fixed there: the repair reaches every non-editable leaf, not just the studio-canvas leaves that card is fenced to.
The mechanism
InterfacesPillar's draft-load effect clears the canvas block selection — but the clear sits after the guard, so it never runs on the path that returns early:packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, the effect commented "Load the selected surface's draft (only for editable preview types)".So walking from a leaf where a block is selected to any leaf with
isEditable === falsecarries the selection across. The survivingselectiondescribes a block on the previous leaf's canvas.Measured
With the designer registry populated, a stub preview for
dashboardand a stub inspector registered forobject, selecting a block on the dashboard leaf and then clicking theobjectleaf:blk_1is a dashboard block. It does not exist on the records grid the object leaf renders. Production registersObjectFieldInspectorforobject(metadata-admin/inspectors/index.ts), so the branch is reachable outside the harness.What #7121 already covers, and what it does not
#7121 gates the rail and the header's "clear selection" button on
StudioCanvas, and orders the rail's studio-canvas branch ahead of the selection branch. That makes the two rail symptoms unreachable on studio-canvas leaves.It does not clear the state. Still live afterwards:
hasInspectorTarget = Boolean((editNav && navSel) || selection)feedsnextCenterTab(centerTab.ts). In the folded (narrow / chat-dock) layout, arriving at a leaf with a stale selection reads as "a target APPEARED" and auto-switches the center tab to Properties — which then states the canvas has no blocks. (Read from the code path, not measured in a browser.)isEditableis false, which also includes leaves whose own type has no registered designer.Why it was not fixed in #7121
That card's ruling fences it to studio-canvas leaves —
registerStudioCanvasPreviewtypes, of which there is exactly one in-repo today (object). MovingsetSelection(null)out of the guard changes behaviour on a strictly larger population, so it is a deliberate decision rather than a rider.Not scoped here
Which repair is right:
setSelection(null)above the guard so any leaf change clears it — simplest, and arguably what the effect already means to say;isEditablebeing in the dependency array;blockingReportalready is viainspectorKey) so it expires by construction rather than by an imperative clear.inspectorKeyis the precedent in the same component.Related
#7121 (where this was found) · #6795 part C ·
centerTab.ts/ ADR-0057 P3c (the folded-layout consumer ofhasInspectorTarget)