Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): scope the Interfaces block selection to its leaf - #7145
Conversation
`InterfacesPillar`'s only clear of `selection` sat inside the draft-load effect, after its `if (!current || !isEditable) … return` guard, so it never ran on the early-return path. `isEditable = !!Preview && !StudioCanvas` is a conjunction, so that is two populations of leaf — a studio-canvas leaf, and a leaf whose own type has no registered designer — and walking to either from a leaf with a block selected carried the selection across. Key the selection to the leaf it was made on and read it back through that key, the same shape `blockingReport` already uses against `inspectorKey`. It expires in the same render as the leaf change rather than one committed render later, and leaves no imperative clear for a future guard to strand. objectui#7121's rail / Design-mode gating is untouched; its discriminator stays `StudioCanvas`, never `isEditable`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-warren
commented
Sep 1, 2026
✅ Reviewed and armedAll 30 check runs read in one call at The reason this card went well: my suggested route was tested, not obeyedI put Candidate 1 (hoist
One frame decided it. Direction predicted before running. The hoist looked equivalent by reading and was not, and nothing but building both would have shown that. Equally good: the two green arms in the red baseline were named and explained, not left as survivors. Three of my briefing assumptions were falsified, and I verified the load-bearing one myselfA2.2(b) — sub-population (b) is EMPTY, not merely small. I briefed "leaves whose type has no registered designer" as a second population. A2.4 — falsified softly, and handled the right way. No test asserts the selection survives, but The And the consequence matters for scope: post-objectui#7121 the folded center tab is the only surviving observable of this leak on population (a), because the rail and clear-button symptoms are already gated off there. A fix validated on the rail symptoms alone would have measured nothing. Recorded: an observation deliberately not filedYou raised this for my call rather than filing it — I agree with not filing it. An unmeasured "maybe" in the queue costs a future triage pass more than it saves, and this seat has spent real time on cards whose disqualifier only appeared on a full read. Recording it here instead so it is not lost — that is the right home for a traced-but-unmeasured observation. Ledger
Landing will be verified by content with a live control once Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7137
The Interfaces pillar's block
selectionoutlived a leaf change on every leafwhere
isEditable === false, because the only clear sat after the draft-loadeffect's early return:
isEditable = !!Preview && !StudioCanvasis a conjunction, so that early returncovers two disjoint populations, and the surviving selection then described a
block on the previous leaf's canvas.
Premise re-derived on the current head
Verified in a fresh worktree at
origin/main=0d4c7890d— the commit thatlanded #7121, i.e. after the card was filed. The guard's shape and the stranded
clear are both still exactly as the card quotes them. Premise holds.
Populations, measured
object,studio-canvas-preview.tsx:97)The pillar's leaves come from
resolveSurface, which can produce exactly fivetypes (
page/object/dashboard/report/action), andregisterBuiltinPreviews()registers a preview for all five. So population (b)is empty when
register-builtinshas run. It is reachable only in a hostthat registers a partial set — which is the state
StudioDesignSurface.designerRegistryPartial.test.tsxalready treats as asupported product fact, and which this PR's pin constructs.
On population (a) the rail and header symptoms are already unreachable, because
#7121 gates both on
StudioCanvas. What that card did not gate ishasInspectorTarget, which feedsnextCenterTab— so the folded center tab isthe one surviving observable there, and it is measured below.
The repair, chosen by measurement
The selection is now stamped with the leaf it was made on and read back through
that key — the same shape
blockingReportalready uses againstinspectorKeyin this component.
The card offered three candidates. Candidate 1, hoisting
setSelection(null)above the guard, was implemented and measured rather than reasoned about: it
closes the center-tab symptom, but the clear runs in an effect, i.e. one
committed render after the leaf change, so the foreign-block inspector still
mounts. Against the same pin:
[ 'page:home_page:block:blk_1', ...(2) ]— 3 stale renders[ 'page:home_page:block:blk_1' ]— 1 stale render,1 failed | 4 passed (5)[]—5 passed (5)That single surviving render is what decided it. Keying also leaves no
imperative clear for a future guard to strand, which is how this defect arose.
Within a leaf nothing changes: the Design/Run round trip keeps its selection
(#5800's stated contract), and a same-leaf reload via
publishNoncestillclears it — both pinned.
Both symptoms are closed, and the card's NOT MEASURED sub-claim is now measured
The card flagged the
centerTabsymptom as "read from the code path, notmeasured in a browser." It is measured here, in the folded layout with
foldInspector, reading the active tab from the DOM:Properties, so the reading below is of a live auto-switch, not a static strip;
expected 'Properties' to be 'Canvas'— the stale target suppressesthe return-to-Canvas edge;
One refinement to the card's wording: the mechanism is not an auto-switch to
Properties on arrival.
hasInspectorTargetnever changes across the leaf walk,so
nextCenterTabsees no edge at all and the author is stranded on theProperties tab they were already on. Same defect, same repair, more precise
cause.
Not done here
StudioCanvas. It was never substituted withisEditable; its live fenceStudioDesignSurface.designerRegistryPartial.test.tsxis green.AutomationsPillarin the same file was checked and does not share thedefect — its guard is
if (!current) return, with noisEditableterm, soits clear runs on every leaf change.
InterfacesPillaris a module export thatpackages/app-shell/src/index.tsdoes not re-export, and no exported typemoved.
Verification
All runs below are at
db22d18b0, on a clean tree.pnpm exec vitest run .../StudioDesignSurface.selectionLeafScope.test.tsx—new pin. Before:
3 failed | 2 passed (5). After:5 passed (5).The 2 that passed before are named and explained in the file: one is the
over-fire fence (green on both sides by design), the other is the
studio-canvas leaf, green because finding(app-shell): the Interfaces pillar offers Design mode and a "click a block" rail on leaves that have no block canvas — measured with a POPULATED registry #7121 already blocks that path — the
finding that made the center-tab test the real measurement for population (a).
pnpm exec vitest run packages/app-shell/src/views/studio-design/—43 passed (43)files,230 passed (230)tests.pnpm exec vitest run packages/app-shell/src/views/metadata-admin/—221 passed (221)files,2303 passed | 1 skipped (2304).pnpm --filter '@object-ui/app-shell' type-check— exit 0(
tsc --noEmit && tsc -p tsconfig.test.json). Confirmed to actually cover thenew file:
--listFileslists it, alongside a known-covered sibling as control.pnpm --filter '@object-ui/app-shell' lint— exit 0,0 errors(2853 pre-existing warnings package-wide, none in the changed files).
check:control-bytes,check:vi-mock-specifiers,check:vi-mock-inherit,check:i18n-keys,check:shell-escape-residue— all exit 0.scripts/check-changeset-no-major.mjs—No changeset declares a major bump.The ablation restore was verified by state, not by the trap firing:
git diff HEAD,git diff --cachedandgit status --shortall empty, and theworktree blob back to HEAD's
f4c98092438320bba6469a3e886d21b2779cc2d8.Repo-wide
pnpm lintand the full gate farm are left to CI, which runs themonce regardless.
Generated by Claude Code