Repository navigation
fix(app-shell): Recently Accessed stores identity and labels each item at render; an unchanged list writes nothing (objectui#11678) - #11713
Conversation
…r; an unchanged list is not rewritten (objectui#11678)
An object, dashboard, page or report entry in "Recently Accessed" now
stores its type and name only. The route tracker no longer mints a label
with titleize ("Showcase Ops Dashboard" for a dashboard labelled
"Delivery Operations"); every surface that renders an entry (Home cards,
Home rail, sidebar Recent, Studio home, the command palette) asks
useRecentItemLabel, which reuses the resolver the console already has for
that kind, in the current language.
The provider decides writes outside state updaters and refuses to write a
list equal to the one it holds: re-adding the entry already at the head is
a no-op (no fresher visitedAt), so revisiting the most recent item no
longer PATCHes sys_user_preference. Lists stored in the old shape are read
by identity (the name is in the id) and their minted label is dropped.
Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
… narrow contentOf by switch (objectui#11678) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…amily stays checked (objectui#11678) check:i18n-keys reads the home.recentApps.itemType.* members off the `type` field of `interface RecentItem`; RecentItem is now a union of two entry shapes, which that reader cannot parse. The kinds are declared once as RecentItemType, both shapes take their arms from it, and the family entry points at it. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Contract reviewServed-tier: Inputs: card #11678 (body, claim 6013942564, os-dev-report 6015145317, claim amendment 6015203262, ACCEPT 6015212592), PR #11713 (body, 21-file list, net diff against ① Derived judgmentsPublic surface of
② Semver level
③ Boundary flagsDev
Check-runs on the head at this read: 30 success, 3 skipped, 0 failure, 9 in progress — Spec Main Shape Gate, Type Check, Test (shard 1/8) through Test (shard 7/8). Their convergence is the seat's landing check. Implemented-by: VERDICT: PASS Generated by Claude Code |
CI red on
|
…bjectui#11678) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…raphs (objectui#11678) The useRecentItems shim re-exported useRecentItemLabel, so every module that only wanted the list (RecordDetailView among them) loaded MetadataProvider and metadataClientFactory's module-load createAuthenticatedFetch(); five RecordDetailView suites whose @object-ui/auth mock closes over a top-level spy then hit it in its TDZ. The shim is hooks and types again and the entry exports useRecentItemLabel from its own module. HomeContinue takes the resolver as a prop from HomePage, as it takes t, so HomeRail.tsx (which also holds the action center and activity card) does not load the cache either; a pin on that /home rail joins the label-at-render suite. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
|
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
|
Contract reviewServed-tier: Inputs: card #11678 (body; claim 6013942564; os-dev-report 6015145317; claim amendment 6015203262; ACCEPT 6015212592; round-1 os-dev-report 6016245224; ACCEPT 6016299959), PR #11713 (body, 22-file list, net diff against ① Derived judgmentsPublic surface of The round's change (CI red on
The whole net diff (round one's judgments, re-read at this head):
② Semver level
③ Boundary flagsRound-1
Round-one
Check-runs on the head at this read: 43, all completed — 40 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot: skipped by design on a PR), 0 failure, 0 in progress. The five shards red at Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11678
Clause-②: yes (narrowing)
"Recently Accessed" stored a label the route tracker made from the route's machine name at visit time (
titleize: the Delivery Operations dashboard was listed as "Showcase Ops Dashboard"), froze it in the visit's language, and saved the list again on every visit to the item already at its head. This PR makes a recent entry store identity, labels it when it is rendered, and makes "no change, no write" a property of the store.What changed
Identity, not text.
useTrackRouteAsRecentrecords an object, dashboard, page or report as{ id, type, name, href };titleizeis gone.RecentItemis nowRecentNamedItem | RecentTextItem: the four named kinds carrynameand nolabel;recordandmetadataentries keep alabel(a record's title, which is data and has no metadata label to resolve; a Studio metadata item's name). The kinds are declared once asRecentItemType.Labelled at render, by the resolvers the console already has. New
useRecentItemLabel()holds no labelling rule of its own: an object or a dashboard asksuseNavTargetLabel(what the sidebar shows for an unlabelled nav entry opening the same target); a page or a report reads its cached metadata label throughuseObjectLabel'spageLabel/reportLabel, using the same two helpersuseNavTargetLabeluses (metadataText,findByName, now exported from that module). No label anywhere: the machine name, which is the navigation renderer's own backstop. Every surface that rendered the storedlabelnow asks it: the /home rail (HomeContinue, whichHomePagehands the resolver aslabelOf, the way it handst), theRecentAppscards, the sidebar's Recent group, Studio home, and the command palette's recent records.useRecentItemLabelis exported from the entry from its own module; theuseRecentItemsshim stays hooks and types only.No change, no write, at the store.
RecentItemsProviderdecides writes from a ref outside any state updater (the old code wrote inside asetStateupdater, which React may run more than once), andcommitrefuses a list equal to the one it holds: no state change, no localStorage write, noadapter.save(), so nosys_user_preferencePATCH.addRecentItemis a no-op when the head entry already has the same content, so a revisit no longer stamps a freshervisitedAt(the head'svisitedAtis when it became the most recent entry). A head whose content changed, such as a record title that resolved after the first add, is still written.Lists already stored. Legacy entries carry their identity in
id(TYPE:NAME, the only id the tracker ever wrote for the four named kinds), so the reader takes the name from there and drops the minted label; it is not used as a fallback. Reading a legacy list writes nothing back; the next real change writes the identity shape.Gate entry repointed.
check:i18n-keysread thehome.recentApps.itemType.*members off thetypefield ofinterface RecentItem, which is now a union it cannot parse; its family entry inscripts/check-i18n-call-site-keys.mjsnow names theRecentItemTypeunion (114 member keys checked exactly, the same six recent kinds as before).Measured before building (the order's Zone 2)
da35453: the tracker minted the label withtitleizeat visit time, the provider persisted it,RecentAppsrendered the stored string. Probe reading of a dashboard visit's saved row at the base:"label":"Showcase Ops Dashboard"; at this head:"type":"dashboard","name":"showcase_ops_dashboard"and no label.RecentItemsProvider, real tracker, realcreateObjectStackUserStateAdapterover a counting data source; not committed) countedupdatecalls, base vs head:appNameresolving after the pathnameStrictMode is not the source. Every re-report of the identity already at the head wrote again at the base, and the store now refuses all of them. NOT MEASURED: which navigation produced the second PATCH in the triage's live session — this container has no backend to run (the sibling objectstack checkout is neither installed nor built), so no browser reading was taken.
id, so no fallback to the minted text is needed (pinned).packages/app-shell/dist/context/RecentItemsProvider.d.ts):RecentItemwent from one interface with a requiredlabelto the union above;addRecentItemtakesRecentItemInput. Six new entry exports:useRecentItemLabel,RecentItemLabelResolver,RecentItemInput,RecentItemType,RecentNamedItem,RecentTextItem. Measured arm: yes (narrowing) — the entry widens, andaddRecentItemrefuses the label-carrying shape it took for the four named kinds. Reverse check against the built.d.ts: a probe compiled with tsc refuses alabelon a dashboard input (TS2353), refuses the old input with noname(TS2353), and refusesitem.labelon an unnarrowed entry (TS2339); the identity-shape and record controls compile.Files beyond the claim's surface
The claim named
RecentAppsas the renderer; /home actually rendersHomeContinue, andRecentAppsis rendered by no page today. Four more render sites read the storedlabeland had to move with the shape, or they would have rendered blank:console/home/HomeRail.tsx,layout/UnifiedSidebar.tsx,views/metadata-admin/StudioHomePage.tsx,chrome/CommandPalette.tsx. Also:hooks/useRecentItemLabel.ts(new),hooks/useNavTargetLabel.ts(two helpers exported),index.ts(exports),console/home/HomePage.tsx(passes the resolver to the rail; patch round 1), the package README andcontent/docs/guide/user-state-persistence.md(AGENTS.md rule 2, docs-driven),scripts/check-i18n-call-site-keys.mjs(the family entry above), and two existing test fixtures whose entries were label-carrying (HomeItemTypeLabel.parity.test.tsx,StudioHomePage.displayLocale-10232.test.tsx). No serial-constraint check was run on these by this dev; that read is the seat's.Behaviour to know
Patch round 1: the import graph (CI run 37455366245 on
ec51ed9)Five
RecordDetailView.*suites failed withReferenceError: Cannot access 'authFetchSpy' before initialization. TheuseRecentItemsshim re-exporteduseRecentItemLabel, which reachesMetadataProviderand through itmetadataClientFactory, whose module load callscreateAuthenticatedFetch(); soRecordDetailView, which only wants the list, newly loaded that call before those suites'@object-ui/authmock spy was initialised. Reproduced locally atec51ed9: 5 files failed, the sameReferenceErrorfive times.useRecentItemLabelandRecentItemLabelResolverfrom./hooks/useRecentItemLabel.js.HomeContinuetakes the resolver as alabelOfprop fromHomePage, soHomeRail.tsx, which also holds the action center and the activity card, does not load the metadata cache either.HomePagealready loaded it onmain.packages/app-shell/src(type-only imports and dynamicimport()excluded),main(abd374b) against the merged head:RecordDetailView,ObjectView, the five suites andHomeRail.tsxreachedMetadataProviderbefore the fix and do not after it. What reaches it newly is only the resolver itself, the two surfaces that render with it (RecentApps,StudioHomePage, the latter already loadingmetadataClientFactoryonmain), Studio's lazily imported barrel, and their own tests. Controls:UnifiedSidebarandHomePagereach it in every tree;RecordDetailViewdoes not onmainand did before the fix, which is the CI reading.main(abd374b) is merged in with a merge commit; no rebase, no force-push.Tests
At head
0c8198b, through the shared verify lock:ec51ed9(5 files failed,ReferenceError×5), green at0c8198b(5 files, 24 tests passed).pnpm exec vitest run packages/app-shell/: exit 0, 1031 files passed and 1 skipped (1032), 10165 tests passed and 9 skipped. It covers every file that referencesauthFetchSpyor mocks@object-ui/auththere (216) and round one's 34-file union.RecentApps.labelAtRender-11678.test.tsx(the three measured pages in en, then zh-CN over the same stored entries, on both the /home rail wired asHomePagewires it and theRecentAppscards — served dashboard label, client bundle page label, and an untranslated page showing its own label; a legacy list shows current labels, never the minted text; an unloaded type shows the machine name);useTrackRouteAsRecent.test.tsx"revisiting the most recent item makes no write", list route then view route saved once, and a control that does write;RecentItemsProvider.test.tsxhead re-add writes nothing, clearing an empty list writes nothing, a changed head is written, and the legacy read from localStorage and from the adapter.ec51ed9; the store and resolver code they mutate did not change this round), each with the fix committed first, the mutation proven on disk and restored to the HEAD blob with an emptygit diff HEAD(ablation-replace.mjs): removing the head no-op turned exactly the three head-revisit pins red; removing the store's equality guard turned exactly the empty-clear pin red (the head pins stay green, held by the head no-op, as predicted); short-circuiting the page label to the machine name turned both label pins red.pnpm --filter @object-ui/app-shell type-check(src andtsconfig.test.json, which includes every test file),build(dist completeness verified) andlint(0 errors): exit 0 each. The built entry exportsuseRecentItemLabelfrom./hooks/useRecentItemLabel.js, and the shim's declaration no longer names it. Reverse type check against the rebuilt.d.ts: the same three refusals (TS2353, TS2353, TS2339), controls compile.ec51ed9; the script is unchanged since): 8 files, 493 tests passed.Gates (not under the lock, head
0c8198b)check-i18n-call-site-keys0 ·check-readme-exports0 ·check-doc-snippet-types0 (after building its--build-filterclosure) ·check-changeset-presence0 ·check-changeset-no-major0 ·check-changeset-overwrite0 ·check-changeset-claims0 (report-only; it names.changeset/11659-console-record-ux.md, whoseRecoveryPasswordReminderparagraph namesHomePage.tsx— still true against this diff, which leaves the reminder's import and its two render sites untouched) ·check-pending-changeset-literals0 ·check-new-cross-file-line-citations0 (0 new) ·check-control-bytes0 ·check-doc-links0 ·check-doc-component-types0 ·check-doc-fence-languages0 ·check-doc-example-ids0 ·check-vi-mock-specifiers0 ·check-vi-mock-inherit0 ·check-unreferenced-sources0 ·check-test-path-roots0 ·check-spec-symbol-derivation0 ·check-package-self-import0 ·check-phantom-dependencies0. Repo-wide lint is CI's.@object-ui/consoletype-check not run: no console source names a recent-items symbol.Acceptance notes
load()resolves is overwritten by hydration (probe: 0 writes, entry gone, at the base and at this head alike). Measured only in jsdom, not at a console door; carrier: none.useNavTargetLabel(and so this list and the sidebar) runs a served dashboard label through the client bundle, while the dashboard page header draws the served label as given (objectui#11295). They differ only when a dashboard has both a published label edit and a bundle key. Read-only inference, not measured; carrier: none.RecentItemsProviderstill keys its debounced flusher effect on auseMemoidentity (AGENTS.md rule 10); pre-existing and not changed here.Generated by Claude Code