fix(app-shell): recovery redirects follow the declared landing (objectui#7373) - #10041
Conversation
…tui#7373) `app.isDefault` declares where a deployment's home is. objectui#7256 (PR #7372) moved the console chrome's four Home affordances onto `useHomePath()`; the "you cannot be here" exits still named the environment launcher literally, so a control-plane customer refused an app — or sent off a surface their runtime does not serve — landed among "Build an app" / "Start from a template" cards that act on an environment their deployment does not have, beside "Your apps" tiles that are the control plane's own internal management apps. Measured first, per the ruling on the card: every `/home` occurrence in `packages/app-shell/src` + `apps/console/src` was classified before a line moved, and the classification ships in the pull request body. Nine live occurrences moved, one is held pending a decision, and the rest must not move — including the two this change is most likely to be misread as covering (`HOME_LAUNCHER_PATH`, and `RootRedirect`, which is `/`'s landing and has its own resolver). Retargeted onto the existing policy; no new policy was written: - `AppContent` — the access-denied screen's way back, and the bounce for a viewer with no app to enter; - `RequireAiSurface` — a runtime serving no AI agent. Only the DEFAULT moved: a host passing `redirectTo` still wins; - `AiChatPage` — the no-agent screen's Home, and the collapse-to-dock landing on a cold deep link. `resolveCollapseToDockTarget` takes the home path as a required argument rather than naming one, so a new call site cannot silently reintroduce the literal; - `StudioDesignSurface` — eviction when the package under the editor is deleted, and the header Home button; - `apps/console` — the `/studio` entry gate, and the Studio front door's wordmark beside it (two affordances one route apart may not name two different homes). `AcceptInvitationPage` is deliberately unchanged: it navigates immediately after an organization switch, where the app list in hand still belongs to the organization being left. The reading is recorded at the call site and on the card. Every ordinary environment is unchanged — where nothing is declared, and wherever the list is not yet an answer, the resolved path is `/home`. Each moved site gained a behavioural pin that fails on the previous implementation, and the existing `/home` pins are kept as the undeclared-deployment case rather than re-aimed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ 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
|
…jectui#7373)
The held site from this branch's first round, now ruled: the seat took
option A after verifying the basis on `origin/main` rather than taking
the report's word for it.
`handleAccept` ran `navigate('/home')` in-router, immediately after
`switchOrganization` resolved. That is why it could not simply read
`useHomePath()` like this card's nine other sites: the app list in
memory at that instant belongs to the organization the user is LEAVING.
`switchOrganization` only updates auth state, `MetadataProvider` drops
its whole cache when the active org changes (objectui#4486) and
refetches, and this line runs before any of that can land — so a
declared-landing answer read here would name the PREVIOUS org's app, one
the new organization may not even carry. Worse than the launcher it
would have replaced.
Landing on the console ROOT resolves the declaration AFTER the switch
instead of before it: the full page load re-seats every data scope on
the new organization, and `RootLandingRedirect` then reads that org's
list through `resolveLandingPath`. So the page honours `app.isDefault`
for the organization the user has just joined — which is what this card
asked for — by the only route that can read it.
This is not a new mechanism. `layout/WorkspaceSwitcher.tsx`
(`handleSwitch`) and `console/organizations/OrganizationsPage.tsx`
(`handleSelect`) both do `window.location.href = resolveRootUrl()` after
the same call, and both say why in a comment. Accepting an invitation is
the third site of one transition and was the only one not taking it.
`resolveRootUrl()` rather than a bare `'/'` because a full-page
navigation leaves React Router, so nothing applies the host's
`basename`.
The reading is written at the call site, not left in a thread.
The pin that asserted "lands on /home" is rewritten rather than
loosened: it now captures the full-page navigation, asks the RESOLVED
target (the way a browser resolves it, so the embeddable build's
relative `'./'` stays legal) whether it is the console root, and asserts
the launcher route — still declared in the fixture on purpose — was not
reached. The source scan gains this file keyed on `resolveRootUrl`, not
on the hook, so a later edit that "unifies" this onto `useHomePath()`
fails there instead of passing quietly.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
…e-redirect-declared-landing
✅ 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
|
Fixes #7373
app.isDefaultdeclares where a deployment's home is. objectui#7256 (PR #7372) moved the console chrome's four Home affordances ontouseHomePath(). The error-recovery exits kept naming the environment launcher literally (ADR-0075), so on cloud's control plane a customer refused an app — or bounced off a surface their runtime does not serve — landed among "Build an app" / "Start from a template" cards that act on an environment their deployment does not have, beside "Your apps" tiles that are the control plane's own internal management apps.Ruling B on the card ordered the work: measure first, then change. The classification below is the measurement, and it is the reviewable half of this change.
1. The measurement
Population: every
/homeoccurrence inpackages/app-shell/src+apps/console/srcat030a675b0, in every quoting. Re-derive it with030a675b0Two readings worth having before the table:
'/home'— single quotes — and bothRedirectWithSplash to="/home"redirects are double-quoted. One of them isAppContent's no-app bounce, i.e. the first surface the card names. Anyone re-running that recipe and reading 12 or 13 as "the work" would ship the card's headline case unfixed.2. Classification — every non-test occurrence, with its verdict
MOVED — nine live sites now resolve through
useHomePath()030a675b0)console/AppContent.tsx— the denied screen's Back-to-home buttonConsoleLayoutmount, so this button is the whole way backconsole/AppContent.tsx— the no-app-to-enter bounce (RedirectWithSplash)console/ConsoleShell.tsx—RequireAiSurface'sredirectTodefaultredirectTostill wins. No caller in this repo passes oneconsole/ai/AiChatPage.tsx—resolveCollapseToDockTarget's last rung/aiwas the entry pointconsole/ai/AiChatPage.tsx— the no-agent screen's Home buttonAppContent's denied caseviews/studio-design/StudioDesignSurface.tsx— deleted-package evictionviews/studio-design/StudioDesignSurface.tsx— header Home buttonapps/console/.../StudioRoute.tsx—RequireStudioAccess'sredirectTodefaultstudio.accessapps/console/.../StudioRoute.tsx— the Studio front door's wordmarkA tenth live site,
AcceptInvitationPage, follows the same declaration by a different route — it lands on the console root so the resolution happens after the org switch. See its row below.MOVED BY A DIFFERENT ROUTE — one site follows the declaration without the hook
console/organizations/manage/AcceptInvitationPage.tsx— after accepting an invitationswitchOrganizationhas just resolved,MetadataProviderdrops its cache on an org change (objectui#4486) and refetches, and this line runs before any of that lands — so a declared-landing answer read here names the PREVIOUS org's app, which is worse than the launcher. The two other org-switch paths (layout/WorkspaceSwitcher.tsx,console/organizations/OrganizationsPage.tsx) full-page-navigate to the console ROOT for exactly this reason and letRootLandingRedirectresolve the landing afterwards. This page was the THIRD site of that same transition and the only one not taking its shape, so it now lands on the console ROOT too (window.location.href = resolveRootUrl()) andRootLandingRedirectresolves the declaration for the org just JOINED. Seat ruling A on the fall-back stop (objectui#7373, 2026-09-19); the cache-timing reading is written at the call site, and the source scan keys this file onresolveRootUrlrather than on the hook, so a later edit that folds it ontouseHomePath()goes red there.NOT MOVED — with the reason, per the dispatch's requirement
utils/homePath.ts—HOME_LAUNCHER_PATH??fallback all nine moved sites resolve through. Changing it is option C, excluded by the ruling: it would strip the environment layer of its real launcher (ADR-0075)console/ConsoleShell.tsx—RootRedirect/'s landing./'s policy isresolveLandingPath, which layers a single-visible-app emptiness heuristic (objectui#4048) and refuses to conclude from an unresolved list (objectui#4233). A third reading without those is a design question — raised below, not settled hereapps/console/.../RootLandingRedirect.tsx—return '/home'/policy — the documented answer when nothing is declared. Moving it is option C by another routeapps/console/src/App.tsx—Route path="/home"/homestops existingapps/console/src/inbox-arrival-preview.tsxMemoryRouter initialEntriespreview fixture — a start URL, not a destinationlayout/UnifiedSidebar.tsxx2,console/AppContent.tsxx1AppContentone was reworded because it quoted the line this change rewrote.)Fixture discipline — what happened to the pins, and why each still fails for the right reason
The existing
/homeassertions are kept, not re-aimed. Every one of them renders with an app list that declares nothing (or with no metadata provider at all), and for that population the resolved path is/home— so they now pin the undeclared-deployment case, and they still fail if a site navigates anywhere else.resolveCollapseToDockTarget's, because the function took a new required argument; its launcher case is kept as a separate assertion so both answers stay pinned.resolveCollapseToDockTargetaside, no expectation was rewritten.3. Evidence
Reverse verification (mutate → prove it landed on disk → run → restore;
trapon absolute paths, restoration proved by an emptygit diff HEADand a blob-hash comparison, not by an exit code):AppContentnavigate(homePath)1 → 0,navigate('/home')0 → 1;to={homePath}1 → 0,to="/home"0 → 1/homepin stayed green, which is the point: they cannot tell the two implementations apartStudioRoutehomePathreads back to literals (2 literals on disk, verified by count)Restored byte-identically both times (
898a638de…and363d5b8f6…matchHEAD).Suites (repo-root invocation; all at
ad21d1a59):vitest run packages/app-shell/src/console/ packages/app-shell/src/views/studio-design/vitest run packages/app-shell/src/__tests__/ .../hooks/ .../utils/ .../chrome/ .../layout/pnpm --filter @object-ui/console testpnpm --filter @object-ui/app-shell type-checkpnpm --filter @object-ui/console type-checkpnpm exec eslintover the 15 changed filesMetadataContextValue, notany)node scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjs·check-control-bytes.mjsEach exit code was captured before any pipe, from a redirected log.
4. One question raised, carried to its own card
RootRedirect(console/ConsoleShell.tsx). It is the/element published to consumers that do not mountapps/console'sRootLandingRedirect, and it ignores the declaration. Folding it intouseHomePath()would give/a third answer — one without the emptiness heuristic and without the unresolved-list guard its twin has. Out of this card's subject (it is not a recovery exit), so it stays, documented at the site. Seat ruling A for this card; the follow-up is filed separately as objectui#10042.Acceptance notes
Noted while in these files, not filed and not fixed here:
apps/console/src/components/StudioRoute.tsxships a user-visible Chinesetitleattribute on the Studio wordmark, outside i18n, andviews/studio-design/StudioDesignSurface.tsxcarries Chinese source comments. AGENTS.md commandment #-1 is explicit that user-facing text and comments are English-only. Untouched: out of this card's scope, and thetitleis a behaviour change rather than a mechanical one.apps/console/src/components/StudioRoute.test.tsx's@object-ui/app-shellmock carries a comment explaining that the factory pulls one submodule "instead" of the whole barrel, while the factory does callimportOriginal()on the barrel. The comment's own timing figures therefore describe a shape the code no longer has.Generated by Claude Code
Generated by Claude Code