Skip to content

fix(app-shell,console): the chrome's Home affordances follow the declared landing - #7372

Merged
hotlong merged 3 commits into
mainfrom
claude/issue-7256-cloud-console-home-and-logo
Sep 2, 2026
Merged

hotlong merged 3 commits into
mainfrom
claude/issue-7256-cloud-console-home-and-logo

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #7256

What the card measured

cloud's control plane lands a signed-in customer on /_console/apps/cloud_control/page/welcome — the app declared with isDefault: true, whose first navigation item is the Welcome page. The top bar's logo then said /_console/home: the ENVIRONMENT layer's launcher (ADR-0075), where "Build an app" and "Start from a template" act on an environment the control plane does not have, and "Your apps" lists the control plane's own internal management apps ("Cloud", "Account"). One product, two homes, two voices.

Premise re-verified against origin/main (63b4e0e71)

Both producers the dispatch predicted are real, and are the two named below:

  • the landing: apps/console/src/components/RootLandingRedirect.tsx — resolveLandingPath() reads app.isDefault, which cloud's CLOUD_APP declares (packages/app-cloud/src/cloud.app.ts, with nav_home → the welcome page first in navigation);
  • the logo: packages/app-shell/src/layout/AppHeader.tsx:419 — a literal Link to="/home".

Three further affordances carried the same literal: UnifiedSidebar's "Home" nav row and its mobile-sheet Home row, and AppSidebar's app-switcher "Home" entry. All four now read useHomePath().

The posture signal is the declaration, not a hostname or a product name

resolveDeclaredHomePath() reads app.isDefault and nothing else. cloud_control appears nowhere in the shared bundle: re-introducing that name is precisely what retiring PREFERRED_APPS = ['cloud_control'] removed, and hostname sniffing is ruled out by the card. A deployment that declares no landing is unaffected — every Home affordance still resolves to /home.

The trade-off the card asked me to state: what becomes of the generic home

After this change the control-plane chrome offers no route into /home: the logo and all three Home rows resolve to the declared landing. So the card's alternative half — hide "Cloud"/"Account" by role and drop the environment-side action cards — is not owed here; there is no control-plane entry point left to sanitise.

What stays reachable, named rather than quietly omitted:

  • typing /_console/home by hand;
  • the "this app is gone / access denied" recovery redirects in console/AppContent.tsx and console/ConsoleShell.tsx, plus AiChatPage's home button, StudioDesignSurface's close, and AcceptInvitationPage.

Those are error-recovery paths, not Home affordances. Retargeting them moves a /home expectation that ~a dozen existing fixtures pin, with a blast radius nobody has measured — deliberately left out of this card rather than folded in.

Why /'s resolver keeps its own reading of the declaration

The first version of this PR had resolveLandingPath import resolveDeclaredHomePath, so the declaration had exactly one reader. It turned 11 tests red: RootLandingRedirect.route.test.tsx and SetupRoute.test.tsx replace @object-ui/app-shell wholesale with a chrome-free hand-rolled mock, and adding the symbol to that mock would have put the FIXTURE under test instead of the shipped policy.

So the resolver is unchanged, and what forbids drift is a behavioural matrix comparing both shipped answers — apps/console/src/components/landingHomeParity-7256.test.ts, 8 deployment shapes plus two drift canaries (which of several declarations wins; a nameless declaration). That is strictly stronger than a source scan of either file.

Tests

Three new files, 20 + 8 assertions:

  • packages/app-shell/src/utils/__tests__/homePath.test.ts — the policy;
  • packages/app-shell/src/hooks/__tests__/useHomePath.test.tsx — the hook's answer, including the in-flight app list;
  • packages/app-shell/src/__tests__/homeAffordancesFollowDeclaration-7256.test.ts — a source scan pinning that no chrome affordance re-types the literal;
  • apps/console/src/components/landingHomeParity-7256.test.ts — the parity matrix above.

Reverse verification (fix committed first, then mutated): restoring to={homePath} to to="/home" in AppHeader.tsx turned the scan red in the expected direction — Tests 2 failed | 6 passed (8), both failures on the "top-bar brand logo" cases. The mutation was confirmed on disk before the run (injected-literal count 0→1, removed-expression count 1→0, blob hash 62364926 vs HEAD 1ea6f4dc); the restore leg was confirmed by hash equality with the HEAD blob and an empty git diff HEAD.

Gates run at 6147b3c86

  • pnpm --filter @object-ui/app-shell type-check — exit 0 (its script is tsc --noEmit && tsc -p tsconfig.test.json, so the new test files ARE covered)
  • pnpm --filter @object-ui/console type-check — exit 0
  • pnpm --filter @object-ui/app-shell build — exit 0, dist completeness: 1 package(s) complete (902 emitted files verified); @object-ui/console build exit 0
  • vitest, affected surface (app-shell layout / console / utils / hooks / tests + apps/console/src/components): Test Files 199 passed, Tests 2151 passed besides the pre-existing red below
  • pnpm --filter @object-ui/app-shell --filter @object-ui/console lint — exit 0, 0 errors (216 pre-existing warnings)
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:self-import, check:phantom-deps, check:eager-closure, check:side-effects-array, check:esm-specifiers, check:readme-exports, check:entry-guard, check:i18n-keys, check:i18n-drift, check-changeset-no-major — all printed their own OK line

Lint narrowing: run per PACKAGE (eslint . from each package, i.e. eslint's own config-derived population for both packages this diff touches, not my guess at a file list); the repo's flat config enables no type-aware linting (no project / projectService), so this diff cannot move the verdict on any file it does not contain.

Pre-existing reds, not caused by this diff

  • packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx — 7 red. Ablation: reverting all six of my modified source files to 63b4e0e71 (confirmed on disk, homePath reference count 0,0,0) leaves the same 7 failures. Already filed as objectui#7271 (Node 26 localStorage probe).
  • packages/auth/src/__tests__/signOut-client-cache-purge-5198.test.tsx:5777 — 1 red, same family. packages/auth declares no dependency on @object-ui/app-shell and this diff contains no file in that package, so it cannot be reached from here.

Changeset

.changeset/7256-chrome-home-follows-declared-landing.md, patch on @object-ui/app-shell + @object-ui/console.


Generated by Claude Code

…ared landing

The top-bar logo, both sidebar Home rows and the app-switcher Home entry each
hard-coded `/home` — the environment layer's launcher. On cloud's control plane
the post-login landing is the app declared with `isDefault` (`cloud_control` →
its Welcome page), so one click on the logo left the guided welcome for a
screen whose "Build an app" / "Start from a template" cards act on an
environment the control plane does not have, and whose "Your apps" tiles are
the control plane's own internal management apps.

`resolveDeclaredHomePath` is now the one reader of `app.isDefault`; `/`'s
resolver layers its emptiness heuristic on it, and `useHomePath()` gives the
chrome the same answer. No hostname sniff and no product name in the shared
bundle.
`/`'s routing tests replace `@object-ui/app-shell` wholesale with a chrome-free
hand-rolled mock, so importing `resolveDeclaredHomePath` there put the fixture
under test rather than the shipped policy. The resolver keeps its own reading of
the declaration; a matrix comparing both shipped answers is what now forbids
drift.

Adds the changeset.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3166.8 KB 3191.4 KB
Main entry chunk (gzip) 142.7 KB 350 KB
Entry file index-BD77sc1O.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 514.59KB 117.40KB
core (index.js) 5.80KB 2.32KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 178.20KB 49.60KB
fields (index.js) 244.25KB 61.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.00KB 12.97KB
plugin-charts (index.js) 70.02KB 19.44KB
plugin-chatbot (index.js) 194.82KB 46.02KB
plugin-dashboard (index.js) 132.63KB 34.56KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.63KB 63.90KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 166.93KB 40.82KB
plugin-grid (index.js) 209.10KB 56.65KB
plugin-kanban (index.js) 53.21KB 14.66KB
plugin-list (index.js) 113.51KB 27.67KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 29.15KB 8.40KB
plugin-tree (index.js) 8.98KB 3.08KB
plugin-view (index.js) 85.90KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 13:54
@hotlong
hotlong added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit d4c6a86 Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7256-cloud-console-home-and-logo branch September 2, 2026 14:14
os-tesla pushed a commit that referenced this pull request Sep 19, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…tui#7373) (objectstack-ai#10041)

Fixes objectstack-ai#7373

`app.isDefault` declares where a deployment's home is. objectui#7256 (PR
objectstack-ai#7372) moved the console chrome's four Home affordances onto
`useHomePath()`. 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 `/home` occurrence in `packages/app-shell/src` +
`apps/console/src` at `030a675b0`, in every quoting. Re-derive it with

```bash
git grep -n -E "['\"]/home['\"]" 030a675 -- packages/app-shell/src apps/console/src
git grep -l isDefault -- packages/app-shell/src   # lit control: the instrument is live
```

| | at `030a675b0` |
|---|---:|
| non-test occurrences, single-quoted (the card's own recipe) | 13 |
| non-test occurrences, double-quoted | 5 |
| **non-test, both quotings** | **18** (3 of them inside comments ⇒ 15
live) |
| **test-side, both quotings** | **80**, across 34 files |

Two readings worth having before the table:

- ⭐ **The card's re-check recipe cannot see the two most important
sites.** It greps `'/home'` — single quotes — and both
`RedirectWithSplash to="/home"` redirects are double-quoted. One of them
is `AppContent`'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.
- The fixture half is not "about a dozen": it is 80 occurrences over 34
files at this denominator (57 by the dispatch's single-quote count).
Most are route sinks and start URLs, not destination pins — which the
table below is what separates.

## 2. Classification — every non-test occurrence, with its verdict

### MOVED — nine live sites now resolve through `useHomePath()`

| file (at `030a675b0`) | what it is | why it moves |
|---|---|---|
| `console/AppContent.tsx` — the denied screen's Back-to-home button |
recovery affordance on "you don't have access to this app" | the card's
own case; the screen renders above the single `ConsoleLayout` mount, so
this button is the whole way back |
| `console/AppContent.tsx` — the no-app-to-enter bounce
(`RedirectWithSplash`) | recovery redirect | the surface the card names
first; sits below the readiness gate, so the list it resolves against
has settled |
| `console/ConsoleShell.tsx` — `RequireAiSurface`'s `redirectTo` default
| recovery redirect for a runtime with no AI agent | only the DEFAULT
moved; a host passing `redirectTo` still wins. No caller in this repo
passes one |
| `console/ai/AiChatPage.tsx` — `resolveCollapseToDockTarget`'s last
rung | landing when `/ai` was the entry point | triage flagged this
conditional for individual thought: rungs 1 and 2 (remembered origin,
history back) are unchanged; only "nothing behind us" was a literal. It
is now a **required parameter**, so a new call site cannot reintroduce
the literal |
| `console/ai/AiChatPage.tsx` — the no-agent screen's Home button |
recovery affordance | same screen family as `AppContent`'s denied case |
| `views/studio-design/StudioDesignSurface.tsx` — deleted-package
eviction | recovery redirect: the package under the editor is gone and
no sibling is left | destination was a literal; the sibling-package
branch beside it is untouched |
| `views/studio-design/StudioDesignSurface.tsx` — header Home button |
Home affordance inside the Studio frame | named by the card |
| `apps/console/.../StudioRoute.tsx` — `RequireStudioAccess`'s
`redirectTo` default | recovery redirect: no `studio.access` | the "no
access" half of the card, in the console app |
| `apps/console/.../StudioRoute.tsx` — the Studio front door's wordmark
| Home affordance one route from the previous row | ⚠️ **declared
in-place fix, flag it if you disagree.** It is the sibling screen of the
header Home button above: leaving it would put two different homes one
route apart inside one frame, which is the objectui#7256 defect
reproduced by this very change |

A **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

| file | reading |
|---|---|
| `console/organizations/manage/AcceptInvitationPage.tsx` — after
accepting an invitation | The app list in hand here belongs to the
organization the user is **leaving**. `switchOrganization` has just
resolved, `MetadataProvider` drops 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 let `RootLandingRedirect`
resolve 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()`) and
`RootLandingRedirect` resolves 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 on `resolveRootUrl` rather than on the hook, so a later
edit that folds it onto `useHomePath()` goes red there. |

### NOT MOVED — with the reason, per the dispatch's requirement

| file | why it stays |
|---|---|
| `utils/homePath.ts` — `HOME_LAUNCHER_PATH` | it IS the launcher, and
the `??` 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` | not a recovery redirect:
it is `/`'s landing. `/`'s policy is `resolveLandingPath`, 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 here |
| `apps/console/.../RootLandingRedirect.tsx` — `return '/home'` | this
IS rule 3 of the `/` policy — the documented answer when nothing is
declared. Moving it is option C by another route |
| `apps/console/src/App.tsx` — `Route path="/home"` | the route
declaration. Move it and `/home` stops existing |
| `apps/console/src/inbox-arrival-preview.tsx` | a `MemoryRouter
initialEntries` preview fixture — a start URL, not a destination |
| `layout/UnifiedSidebar.tsx` x2, `console/AppContent.tsx` x1 | prose
comments about past defects. (The `AppContent` one 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 `/home` assertions 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.

⚠️ On their own they are not enough, and that is the trap this card
could have fallen into: an unchanged-and-still-green pin passes
identically on the fixed and the unfixed implementation. So every moved
site also gained a **declared-deployment** pin, which is what fails on
the previous code. The one pin whose expected value genuinely changed is
`resolveCollapseToDockTarget`'s, because the function took a new
required argument; its launcher case is kept as a separate assertion so
both answers stay pinned.

`resolveCollapseToDockTarget` aside, no expectation was rewritten.

## 3. Evidence

**Reverse verification** (mutate → prove it landed on disk → run →
restore; `trap` on absolute paths, restoration proved by an empty `git
diff HEAD` and a blob-hash comparison, not by an exit code):

| leg | mutation | result |
|---|---|---|
| A — `AppContent` | `navigate(homePath)` 1 → 0, `navigate('/home')` 0 →
1; `to={homePath}` 1 → 0, `to="/home"` 0 → 1 | **3 failed, 31 passed** —
exactly the two new behavioural pins and the source scan. Every
pre-existing `/home` pin stayed green, which is the point: they cannot
tell the two implementations apart |
| B — `StudioRoute` | both `homePath` reads back to literals (2 literals
on disk, verified by count) | **3 failed, 27 passed** — the gate pin,
the wordmark pin and the scan |

Restored byte-identically both times (`898a638de…` and `363d5b8f6…`
match `HEAD`).

**Suites** (repo-root invocation; all at `ad21d1a59`):

| run | result |
|---|---|
| `vitest run packages/app-shell/src/console/
packages/app-shell/src/views/studio-design/` | 139 files, 887 tests
passed |
| `vitest run packages/app-shell/src/__tests__/ .../hooks/ .../utils/
.../chrome/ .../layout/` | 121 files, 1533 tests passed |
| `pnpm --filter @object-ui/console test` | 101 files, 1161 tests passed
|
| `pnpm --filter @object-ui/app-shell type-check` | exit 0 |
| `pnpm --filter @object-ui/console type-check` | exit 0 |
| `pnpm exec eslint` over the 15 changed files | exit 0 — 0 errors, and
**0 warnings on the lines this change wrote** (the fixtures are typed
with `MetadataContextValue`, not `any`) |
| `node scripts/check-changeset-presence.mjs` | exit 0 — 13 published
source files, 1 changeset declared |
| `node scripts/check-changeset-no-major.mjs` ·
`check-control-bytes.mjs` | exit 0 |

Each exit code was captured before any pipe, from a redirected log.

## 4. One question raised, carried to its own card

1. **`RootRedirect` (`console/ConsoleShell.tsx`).** It is the `/`
element published to consumers that do not mount `apps/console`'s
`RootLandingRedirect`, and it ignores the declaration. Folding it into
`useHomePath()` 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.tsx` ships a user-visible
Chinese `title` attribute on the Studio wordmark, outside i18n, and
`views/studio-design/StudioDesignSurface.tsx` carries 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
the `title` is a behaviour change rather than a mechanical one.
- `apps/console/src/components/StudioRoute.test.tsx`'s
`@object-ui/app-shell` mock carries a comment explaining that the
factory pulls one submodule "instead" of the whole barrel, while the
factory does call `importOriginal()` on the barrel. The comment's own
timing figures therefore describe a shape the code no longer has.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant