Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell,console): keep the splash painted across the console boot redirects - #6506
Conversation
…ects (#6378) Every readiness gate on the console boot path renders `LoadingScreen` while it waits and a bare `<Navigate>` the moment it decides. `<Navigate>` renders null and react-router runs the navigation as a transition, so the destination tree renders while the commit that already dropped the splash is on screen: 41-147ms of empty `#root`, and a white flash whenever the compositor swaps a frame in it. Cause established by measurement first (CDP screencast frame ledger correlated with a DOM-state ledger on the same clock), not from either card suspect -- `RouteFader` never mounts on the boot path and `LoadingScreen` unmounts exactly when its gate says to. `RedirectWithSplash` pairs the same `<Navigate>` with the same `LoadingScreen`, so the handoff changes no pixels. Used at the auth gate's `/login` bounce, the `/` landing resolver and the catch-all route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Replaces the placeholder rates with the interleaved A/B numbers: 102 paired boots per arm across five cells, and an explicit note that the two throttled cells are NOT pooled into the pixel result because the pre-fix build did not flash there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ 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-support-ai
commented
Aug 26, 2026
PM review: ACCEPT at ⭐⭐ The headline is that you falsified the card's own diagnosis rather than implementing it. The card pointed at The instrument is real, and that is why the conclusion holds. A CDP The mechanism is stated precisely enough to be checkable: The fix changes no pixels, and that is the design point. Rendering the same Scope is drawn in the right place and reasoned rather than asserted. Clause-② applies and is handled correctly. On the budget, since this was the card most likely to move it: the console performance budget has only about 1% headroom (3266.6 KB), and this PR reports 3234.7 KB — PASS, with the eager closure essentially unmoved. A boot-path fix that grew the boot payload would have been self-defeating; it didn't. Landing: Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…e redirects (#6507) Seven of the eight candidate gates named on the card hand off from a `LoadingFallback` to a bare `<Navigate>`, which renders null: the splash is dropped at the deciding commit and nothing replaces it until the destination renders at transition priority. Measured on the three sibling gates #6506 fixed: 41-147ms of empty `#root`. Converted with per-site evidence, not on sight. `SystemRedirect` is deliberately NOT converted: it is the one site here that also fires with the console already painted (`SettingsView` navigates to `/system/settings` from a button, `AppSidebar` links to `/system`), and the triage ruling on this card bans giving an already-painted layout a splash. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Fixes#6378
Measured against final commit
1639bbe18.The cause — measured before anything was written, and it is neither suspect
Triage named
LoadingScreen's unmount timing andRouteFaderas suspects and said neither was established. Both are exonerated. A CDPPage.startScreencastframe ledger (everyNthFrame: 1, every frame classified with the card's own rule — white when no colour channel falls below 242) was correlated against a DOM-state ledger on the same clock (performance.timeOrigin), against the productionapps/consolebundle with the boot endpoints mocked.RouteFadernever mounts on the boot path at all —fader=falsein every DOM event of every run, on both the signed-out and the signed-in boot. It lives inside/apps/:appName/*, which a boot does not reach.LoadingScreenunmounts exactly when its own gate says to.What is wrong is what replaces it. Every readiness gate on the boot path is shaped like this:
<Navigate>rendersnulland navigates from an effect, and react-router runs that navigation as a transition — so the destination tree renders at transition priority while the commit that already dropped the splash is what the compositor is showing. For the whole of that window#rootholds no view and the viewport is the bare page background.The ledger, one signed-in boot on the pre-fix build (
7975f2d85), the two streams merged on one clock:That is the card's window: after
LoadingScreen's first paint, 41–147 ms wide, entirely explained by a DOM state.Why the fix cannot itself flicker
RedirectWithSplashpairs the same<Navigate>with the sameLoadingScreenthe gate one line above was already rendering, so the handoff changes no pixels — the transition now runs underneath an unchanged screen instead of underneath a blank one. It is not "add a spinner": a different holding image would introduce a visual change where today there is a blank.Applied at the three boot redirects: the auth gate's
/loginbounce (LoginRedirect), the/landing resolver (RootLandingRedirect), andApp.tsx's catch-all. The nested organizationindexredirect is deliberately not changed — it fires under an already-painted layout, where covering the screen with a splash would be the regression.Acceptance evidence — the bar, stated and hit
The card is honest that its own evidence is n=9 on a defect reproducing ~1 boot in 3, so a handful of clean runs cannot accept a fix. The bar chosen: 0 flashes over ≥60 paired post-fix boots in the cell where the pre-fix build flashes. Rationale — at the card's ~1/3 rate, 0/60 has probability 2.7e-11 and a 95% upper bound (rule of three) of 5.0%; pooled over all non-degenerate cells, 0/87 gives a 3.4% upper bound. Both arms are interleaved inside one process and one browser, so drift in this shared container's load lands on both equally.
/LoginRedirect/RootLandingRedirect/console/LoginRedirectLoginRedirectLoginRedirectPre-fix gap lengths: 60–396 ms (card reported 40–230 ms). No run was excluded — 0 NOT MEASURED across all 204 boots.
Pooled over the three non-degenerate cells: 67/87 (77%) pre-fix vs 0/87 post-fix, Fisher exact p = 4.7e-20 on the primary cell alone. On the DOM window, across all five cells: 102/102 vs 0/102.
Proving the instrument can see the defect
Run against the unmodified pinned base
7975f2d85before any fix existed: 9/12 flashed, gaps 36–260 ms — the instrument reds on the known-bad tree at a rate at least as high as the card reported, so a green from it is worth something. Ghost-assertion guards on every run: React's first commit observed, the DOM settled, the expected destination content present inbody.innerText, ≥1 contentful frame captured. A run failing any of those is counted NOT MEASURED and excluded, never as a pass.The two builds compared are genuinely different artifacts (base
7975f2d85vs1639bbe18), served by identical code from two directories — not a cell where before and after are the same bytes.Tests
e2e/console-boot-indicator.spec.tsgains the deterministic half as a gate: after React's first commit, a hit test at the viewport centre must never stop landing inside#rootor#boot-splash. This is the right thing to gate on for an intermittent defect — the flash needs a frame to be swapped inside the window, but the window itself was present on 100% of measured pre-fix boots. Verified red-first: 6/6 red on the pre-fix bundle, 6/6 green on this one, with the failure message naming the mechanism:The three pre-existing tests in that file stay green on both builds, so the new one is not standing in for them.
packages/app-shell/src/chrome/RedirectWithSplash.test.tsxpins the structural half. Its header states plainly what jsdom cannot see: no compositor, no CSS engine, so nothing there measures the timing window — asserting "no flash" in jsdom would pass because the phenomenon cannot exist. The splash half and the navigation half are pinned by separate cases on purpose: a single "it redirects" assertion stays green when the splash is dropped. Reverse-verified — removing<LoadingScreen />from the component gives exactly 1 failed | 3 passed, the predicted direction; the mutation was confirmed on disk (blob57b2b700→4ecf4e6e,<LoadingScreen />occurrences 1 → 0) and the restore proven byte-identical to HEAD withgit diff HEADempty.Nothing was skipped, disabled or quarantined, and no baseline line was added.
Published-surface check (clause ②), following the chain rather than grepping
The surface did grow, deliberately, by two symbols. Read off the built entry, not the source:
packages/app-shell/package.jsonexports["."].types→./dist/index.d.ts, whose line 40 re-exportsRedirectWithSplashandtype RedirectWithSplashPropsfrom./chrome/index.js. The two-hop chain issrc/index.ts→chrome/index.ts→ the module.apps/consoleconsumes it through the package entry. Props are deliberately minimal (to,replace) — the only shape the console's boot redirects use.Gates
All run at repo root, exit codes captured before any pipe, each verdict quoted from the gate's own output.
pnpm exec vitest run apps/console/ packages/app-shell/src/chrome/Test Files 82 passed (82)/Tests 931 passed (931)pnpm turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/consoleTasks: 37 successful, 37 totalpnpm type-check:e2e(tsc -p tsconfig.e2e.json)pnpm lint(full farm, not narrowed)Tasks: 47 successful, 47 total— 0 errorspnpm check:readme-exports✅ check-readme-exports: OK (… 0 unbuilt …)pnpm check:control-bytes✅ check-control-bytes: OK (scanned 5364 tracked text file(s); skipped 85 binary).pnpm check:eager-closure✅ Console eager closure is 3233.9 KB gzipped … (budget: 3266.6 KB, headroom: 32.7 KB).pnpm lint:coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).pnpm type-check:coverage✅ type-check coverage: 45/46 via type-check, 0 known-broken (0 errors outstanding), 1 not compiled.pnpm changeset:check✅ All workspace packages are in the changeset fixed group./✅ No changeset declares a major bump.pnpm check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.pnpm check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK (… 451 carry a mock …)pnpm check:doc-fences✅ check:doc-fences — … (⛔ SHRINK-ONLY)pnpm check:esm-specifiers,check:self-import,check:docs-route-closure,check:entry-guard,check:i18n-keyspnpm lintwas run in full rather than narrowed, so there is no narrowing to justify. The ratchet family was re-run on the final head1639bbe18after the last push and is quoted from that run.check:readme-exportsfailed the first time withits type entry ./dist/index.d.ts is not on disk -- run pnpm build firstfor@object-ui/cliand@object-ui/plugin-ai. That is a missing prerequisite in a fresh worktree, not a red — it never reached the gate's judgement. Recorded as NOT MEASURED, those two packages built, and the gate re-run to the ✅ above.File face — widened in-scope, declared
The card's declared face was
chrome/{LoadingScreen,RouteFader,ConditionalAuthWrapper}.tsx,apps/console/src/App.tsx/AppContent.tsx, and the e2e spec. The measurement put the defect at two call sites outside it —apps/console/src/components/ProtectedRoute.tsxandRootLandingRedirect.tsx— so the face is widened by those two files plus the newchrome/RedirectWithSplash.tsxand three test-mock files that enumerate the app-shell barrel. Checked before editing: none of the nine open PRs touches any of them, and the three liveapp-shellcards are insrc/services/,src/providers/andsrc/views/. Declared on the card in the same round.Neither
ConditionalAuthWrapper.tsxnorRouteFader.tsxis touched.ConditionalAuthWrapperis exported by app-shell but the console does not use it —apps/console/src/App.tsxmountsAuthProviderdirectly, so it is not on this boot path either.Generated by Claude Code