Skip to content

fix(app-shell): resolve the Home item-type label through one shared helper - #6334

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-6165-homerail-type-fallback
Aug 25, 2026
Merged

fix(app-shell): resolve the Home item-type label through one shared helper#6334
os-support-ai merged 2 commits into
mainfrom
claude/issue-6165-homerail-type-fallback

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6165

Three Home surfaces render a label for the same item kind through the home.recentApps.itemType.* key namespace, and each spelled the lookup itself. They had drifted: the two card surfaces fell back to capitalizeFirst(type) while the rail fell back to the bare type, so a kind with no key rendered Report on the cards and report in the rail — on the same screen.

All three now resolve through one recentItemTypeLabel helper.

Premise re-derivation (every PM assumption checked against main)

assumptionverdict
line numbers have moved (HomeRail.tsx:251, RecentApps.tsx:55, StarredApps.tsx:61)wrong — all three were exact.main advanced several times, but none of it touched these files.
the census is three consumers✅ confirmed. grep for home.recentApps.itemType finds exactly these three call sites.
all three consume the same RecentItem['type'] unionwrong — two unions, one namespace.
capitalizeFirst is one shared helper, not two copies✅ both siblings import ../../utils/index.js. (A second, unrelated copy exists at apps/console/src/utils.ts:32; neither sibling uses it.)
#6023 added report + metadata, so the fallback is dormant✅ confirmed on main — all six members are keyed in en.ts.

The two-union finding matters for the fix's shape. The rail and RecentApps take RecentItem['type'] (object|dashboard|page|report|record|**metadata**); StarredApps takes FavoriteItem['type'] (object|dashboard|page|report|record|**nav**). They share the key namespace, not the type — StarredApps reuses the recentApps.itemType.* keys deliberately, and says so in a comment. So the helper's parameter is string, not a union: narrowing it to either one would reject a legitimate caller. That is documented at the helper.

A side-effect worth recording: nav has no home.recentApps.itemType.nav key, so on the favorite side the fallback is not fully dormant today. nav items are documented as excluded from Starred, and StarredApps is a public export whose caller supplies the items, so whether the path is reachable depends on the host. Not fixed here — out of this card's scope.

Fourth-consumer census:views/metadata-admin/StudioHomePage.tsx also types on RecentItem['type'], but only for RECENT_ICONS; it renders no type label and uses translateMetadataType for a different concern. So the count really is three.

Why a helper rather than a one-line change

The ruling permitted either, conditional on the helper being the honest shape. It is: three identical expressions, one key namespace, one directory, and the card exists because they drifted. Re-spelling the odd one out fixes this instance; removing the duplication removes the recurrence. The two card surfaces' rendered output is unchanged — they keep capitalizeFirst, which is what the ruling protects; only the expression's home moves. No fourth spelling, and nothing outside console/home/ is touched. The helper is deliberately not added to console/home/index.ts — it is internal, and there is no call for widening the package's public surface.

The test is a parity pin, not a ghost assertion

Every union member shipping today has a key, so a fixture built on a real member takes the keyed path, never reaches the fallback, and passes identically before and after — the easy and worthless test here. The pin therefore uses a synthetic unkeyed kind (playbook), and asserts agreement between consumers rather than one component's output:

  • the pinplaybook renders one identical string across the rail, RecentApps and StarredApps (all three pinned, not the two the ruling required), and that string is the capitalising form, pinning the direction of the convergence.
  • fixture guard — asserts the two candidate spellings actually differ for this kind, so the pin cannot pass by degenerating on a single-character or already-capitalised fixture.
  • keyed control — a kind with a key still resolves through the key on all three surfaces; green before and after by design, which is what proves the change is bounded to the fallback. Its expected value is the zh 报表, which capitalizeFirst could never produce — so a green control means the keyed path genuinely ran, rather than the fallback quietly answering.

All three surfaces are driven by one translator: the rail takes t as a prop while the cards pull it from the hook, so the test reads it from that same hook and passes it down. Otherwise it would be comparing two unrelated renders.

Ablation — proven red before

Committed first, then reverted onlyHomeRail's fallback expression, restore under trap … EXIT INT TERM with absolute paths (REPO_ROOT via git rev-parse --show-toplevel).

Mutation proven on disk in both directions (this is a replacement, so both genuinely exist — before: fixed-form 1 / reverted-form 0; after: fixed-form 0 / reverted-form 1), plus a blob-hash change (18c1e559…e44b3acb…).

AssertionError: expected 'playbook' to be 'Playbook'
Test Files 1 failed (1)
Tests 1 failed | 2 passed (3)

Exactly the predicted direction and count: the rail read playbook while the cards read Playbook. Restore verified — git diff HEAD --stat empty and the blob hash back to 18c1e559….

No rebuild was needed for either leg, and that is measured rather than assumed: vitest.config.mts aliases every @object-ui/* to packages/*/src, and the test imports its subjects relatively, so no dist/ sits on the path.

Verification — all at 2510207aa (final commit, tree clean)

gateverdict line
pin (targeted)Test Files 1 passed (1) · Tests 3 passed (3)
affected population, 34 filesTest Files 34 passed (34) · Tests 429 passed (429)
pnpm type-check (@object-ui/app-shell)exit 0, 0 errors
pnpm lint (@object-ui/app-shell)✖ 2679 problems (0 errors, 2679 warnings) — exit 0
check-changeset-presence✅ … declares 1 changeset(s)
check-changeset-no-major✅ No changeset declares a major bump.
check-control-bytes✅ OK (scanned 5208 tracked text file(s))
check-vi-mock-specifiers✅ OK (… 0 non-static …)
check:i18n-keysexit 0 — the dynamic key family still resolves against the en pack
check:i18n-driftNo en value changed in this range.
check:i18n-dead-keys, check:self-import, check-shell-escape-residueexit 0

Gate set derived by enumerating each CI job's own step list under .github/workflows/, not from memory.

Declared narrowing (the whole-package app-shell suite is ~784 s and would be killed by the container's ~10 min foreground cap):

  1. Population read from a real source — grep -rln for tests importing or mentioning the changed modules, not my guess about what is affected. It is a superset: it matches any test naming HomePage/console/home at all.
  2. Count from vitest's own report: 34 files, 429 tests, matching the 34-file population exactly — the sanity check against objectui#3378, where a mis-invocation silently runs the wrong package and reports it green.
  3. Invariance: eslint.config.js sets no parserOptions.project, so linting is not type-aware and this diff cannot move the verdict on any untouched file. The full farm runs in CI regardless.

Two coverage facts measured with --listFiles rather than assumed: app-shell's base tsconfig.jsonexcludes the new test (0 hits), while tsconfig.test.json — the second program in type-checkincludes it. The shipped helper is in both. So "type-check is clean" genuinely covers the new test.

recentItemTypeLabel.ts adds one @typescript-eslint/no-explicit-anywarning, from the TFn alias it carries over verbatim from HomeRail.tsx:26. Kept identical to the line it replaces rather than diverging; the gate is green (warnings, not errors).


Generated by Claude Code

…elper
The rail (`HomeRail.HomeContinue`), `RecentApps` and `StarredApps` all render
a label for the same item kind through the `home.recentApps.itemType.*` key
namespace, and all three spelled the lookup themselves. They drifted: the two
card surfaces fell back to `capitalizeFirst(type)` while the rail fell back to
the bare `type`, so a kind with no key rendered `Report` on the cards and
`report` in the rail — on the same screen.
Converge on the capitalising fallback the two card surfaces already used, and
remove the duplication that allowed the drift: one `recentItemTypeLabel`
helper now owns the key spelling and the fallback for all three call sites.
The pin asserts AGREEMENT between the three surfaces for a kind with no
translation key, because the defect is disagreement between consumers rather
than any one component's output. It uses a synthetic kind deliberately: every
member of the union today has a key, so a fixture built on a real member takes
the keyed path and never reaches the fallback at all.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.1 KB3266.6 KB
Main entry chunk (gzip)154.2 KB350 KB
Entry fileindex-CXTn1tG7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.08KB114.67KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.79KB59.99KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.92KB30.85KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.14KB54.40KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HomeRail falls back to the raw lowercase item type where its two siblings capitalise — the rail and the cards disagree for any unkeyed member

2 participants

@os-support-ai@claude