Uh oh!
There was an error while loading. Please reload this page.
feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091
Conversation
… blocks Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183 — the two `PageComponentType` members that are purely metadata-driven. Both are registered in app-shell, following objectui#6757, because they read this package's providers; `@object-ui/components` depends on neither `@object-ui/layout`, `@object-ui/permissions` nor `react-router-dom`. `resolveNavItemLabel` becomes an export of `@object-ui/layout` (additive, no behaviour change) so the authored menu and the sidebar cannot show one nav entry under two names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…packs `check-i18n-call-site-keys` is explicit that an inline `defaultValue` is not a fix — it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517). The three strings the two new page blocks introduce now live under the existing `console.nav` block in `en.ts` and its nine siblings, which is what `all-locales-key-parity` demands. Also drops the recursive `useCallback` in the nav renderer for a hoisted function declaration: an arrow bound to a `const` cannot be called from inside its own initializer without reading the binding before it is declared (`react-hooks/immutability`), and memoising bought nothing — the row list is recomputed every render regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
`check:side-effects-array` caught it: a module that registers at load time and is not named in the array gets dropped by a consumer's bundler, silently. That is this card's own defect reintroduced one layer down — the page would render the placeholder again in any tree-shaken consumer build, with nothing red to say so. Also declares `@object-ui/i18n` on the changeset, since the locale packs are published source this change edits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
os-sam
commented
Aug 31, 2026
Body repair + durable attribution copyRead the PR body back after creating it. One byte-level loss, the known angle-bracket collapse: In the Ablation paragraph the restore command reads Everything else survived intact. Durable copy of the attribution, since a later body edit would demote the footer to its bare form: Two notes for whoever picks this up:
Generated by Claude Code |
✅ 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-sam
commented
Aug 31, 2026
Dispatching seat review ( Two of my dispatch premises were wrong, and the corrections are the useful part(a) I read a list and called it behaviour. My order stated that both members "register as placeholders and draw the scaffold", citing
The cause is the distinction I skipped: (b) I attributed a quote to the wrong card. My Correction ① quoted #6661 as saying "Register renderers for the two types in the block registry the Phase-1 members used". That sentence is #6757's, not #6661's — #6661's implementation note says something different. Thank you for checking the source instead of taking the quote. The direction survived, but it is now measured rather than asserted: ⭐ Two gates caught real defects, and both were fixed rather than suppressed
And the proof is end-to-end, not a green tick:
⭐ The ablation avoided a vacuous greenWith the real The |
Uh oh!
There was an error while loading. Please reload this page.
…rt set so a false "no renderer" cannot pass `exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry` whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package, which has registered page blocks since #6757 and #7091, nor `@object-ui/plugin-detail`, which registers `record:chatter`. Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to 'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a renderer for it. Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and add a derived guard: the leaf list is read from the directory and every registered key must resolve, so a seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is what drifted; deriving it is the point. The package barrel would track registrations automatically too, but costs 6105ms to load against 553ms for the leaves (same harness, same baseline) — this is a pure-logic gate in the cheap `unit` project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
…rt set so a false "no renderer" cannot pass (objectstack-ai#7133) `exclusion-reason-truthfulness.test.ts` asks the runtime `ComponentRegistry` whether a `PALETTE_EXCLUSIONS` reason claiming "no renderer" is true, so its coverage is bounded by its own side-effect import set. That set never grew to include this package, which has registered page blocks since objectstack-ai#6757 and objectstack-ai#7091, nor `@object-ui/plugin-detail`, which registers `record:chatter`. Measured on 44ea62d: setting `PALETTE_EXCLUSIONS['app:launcher']` to 'no renderer ZZMUTZZ' — a string that matches the guard's CLAIMS_NO_RENDERER regex — passed 4/4 while `views/app-launcher-renderer.tsx` registers a renderer for it. Widen the set to the six `views/*-renderer.tsx` leaves and plugin-detail, and add a derived guard: the leaf list is read from the directory and every registered key must resolve, so a seventh renderer leaf that is not imported here reds this file instead of silently shrinking its coverage. A hand-maintained list is what drifted; deriving it is the point. The package barrel would track registrations automatically too, but costs 6105ms to load against 553ms for the leaves (same harness, same baseline) — this is a pure-logic gate in the cheap `unit` project. Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM Co-authored-by: Claude <noreply@anthropic.com>
Closes#6661
Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two
PageComponentTypemembers that are purely metadata-driven. Phase 2 (global:search/global:notifications, #6757) landed first and is the pattern this follows — both new modules sit inapp-shell, register with a bare name plus namespace, and publish noinputs.Before, measured — the two members were not symptomatic in the same way
Measured on base
592acafbewith a probe that imports@object-ui/componentsand nothing else. A positive control (element:text, certainly registered) came backtruein the same run, so the zeroes below are readings rather than a broken census:nav:menuprotocol-placeholdernav:menuComponent Placeholderapp:launcherUnknown component type: app:launcher … (OBJUI-001)nav:menuis in the eagerly-registeredPALETTE_PLACEHOLDER_BLOCKS;app:launcheris only inPROTOCOL_COMPONENTS, which registers when a host opts in viaregisterPlaceholders()— and justapps/consoledoes (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the consoleapp:launcherred-boxed instead. Both texts are now asserted absent, per member.What each member reads, and why that is "no external data-source dependency"
Neither block issues a request or touches an adapter. Both read the metadata the shell already holds:
app:launcher— the app registry:useMetadata().apps, whichMetadataProviderfetches eagerly (appis in itsEAGER_TYPES, i.e.GET /api/v1/meta/appon mount). Same readAppSwitcherand Home make. The openable set comes from the sharedfilterActiveAppspredicate rather than a second copy of theactive/hiddenrule, and the grid itself isHomeAppsStrip— the console's own launcher — so an authored launcher and the Home launcher cannot drift into two looks for one thing.nav:menu— the active app's navigation tree from that same registry:activeArea.navigation ?? app.navigation. Every derived fact comes from@object-ui/layout: hrefs fromresolveHref, labels fromresolveNavItemLabel, the active row fromresolveActiveNavItem, and the item-level guards (visible,requiredPermissions,requiresObject/requiresService) in the orderNavigationItemRendererapplies them, wired to the same console providersAppSidebarwires them to.actionitems dispatch throughuseNavActionDispatch, so framework#4509's "renders but dead-clicks" shape is not reintroduced.Verdicts on the two dispatch corrections
① "register in the block registry the Phase-1 members used" — the quoted sentence is not on the card.#6661's body says something different: "unimplemented types reach the renderer as plain strings and fall to the placeholder branch". The quoted line appears nowhere in the body or its one comment, so it is likely from the parent. The direction of the correction is right regardless, and measured rather than assumed:
@object-ui/components'package.jsondeclares neither@object-ui/layout, nor@object-ui/permissions, norreact-router-dom, and both blocks need the router;app-shelldeclares all three. app-shell it is.②
PALETTE_EXCLUSIONSis not a blocker — confirmed, and NOT touched.app:launchercarriesshell singleton — the app shell renders it, not a page;global:notificationscarries the identical marker and #6757 shipped its renderer anyway.exclusion-reason-truthfulness.test.tsonly judges reasons that claim no renderer, which neither does, so nothing here goes red. One thing for triage, not fixed here: that reason's clause "the app shell renders it, not a page" becomes literally false once a page can render it — the same class #6071 corrected forelement:text_input/element:record_picker(text, never the decision). The dispatch was explicit not to touch this table, so it is untouched and raised instead.The two members are asymmetric, as flagged.
nav:menuis inBLOCK_TYPE_META(Studio offers it, and it now renders for real);app:launcherstays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.Not
NavigationRendereritself — measuredNavigationRendererrenders throughSidebarMenuButton, which callsuseSidebar(), which throws outside the shell's provider (components/src/ui/sidebar.tsx:56-63, read point at:576). A page block must render standalone, so mounting it would trade a dashed box for a crash — and wrapping the block in its ownSidebarProvideris worse: that provider renders amin-h-svhfull-viewport wrapper and registers a window-level Ctrl/Cmd+B handler, so an authored menu would resize the page around itself and fight the real sidebar for the shell's shortcut. This block therefore reuses every pure helper and none of the sidebar chrome.resolveNavItemLabelbecomes an export of@object-ui/layoutfor that reason — additive, no behaviour change — so one nav entry cannot appear under two names on two surfaces of one app.Two narrowings are stated in the module header rather than left implicit: the block renders the sidebar's default area (first with a visible item, via the shared
hasVisibleNavigationItems) and offers no area switcher, and it passes onlycurrentUserId/currentOrgIdin the template context, not app-level context-selector values.Two gate catches worth calling out
check:side-effects-arraywas red, and it is this card's own defect one layer down: a module that registers at load time and is not named insideEffectsgets dropped by a consumer's bundler silently, so the page would render the placeholder again in any tree-shaken build with nothing red to say so. Both modules are now named.check:sdui-registration-pinsconfirms the fix end to end against the built console:launcherpresent in 1 chunk,menuin 6.check:i18n-call-site-keyswas red on three new strings. Its own message rules out the easy fix: an inlinedefaultValuerenders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live underconsole.navinen.tsand its nine sibling packs, andall-locales-key-paritypasses.Verification
All runs below are on the final commit,
48d1beee1.Ablation, per member — the pin test can fail, in the documented shape. Each leg was run from a committed tree; the mutation was confirmed on disk by the anchor count going 1 → 0, the injected marker 1, and the blob hash changing; the restore leg was
git checkout HEAD -- <abs paths>and proven by an emptygit diff HEAD:app:launcherUnknown component typepanel;getConfig(...).namespaceundefinednav:menuComponent Placeholder; namespace readsprotocol-placeholderThat second row is why the suite carries a separate "overwrites the protocol placeholder rather than sitting behind it" case:
ComponentRegistry.get('nav:menu')stays truthy with the real registration gone, so a bare registered/not-registered assertion would have passed on the placeholder.Tests —
packages/app-shell/+packages/i18n/in full: 643 files, 6662 passed, 1 skipped, 0 failed. Final targeted union (app-shell/src/views/**, previews,palette-placeholder-blocks, all ofpackages/layoutandpackages/i18n, the known-schema-types derivation pin, and the console'sregistry-inputs-spec-parity): 103 files, 1490 passed.Type-check —
turbo run type-checkgreen for@object-ui/app-shell(tsc --noEmit && tsc -p tsconfig.test.json) and@object-ui/layout. Coverage checked rather than assumed, via--listFiles: the new test file appears in the test program (1 hit), both renderers in the main program (2 hits).Lint —
turbo run lintforapp-shell/layout/i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursiverenderItemwas auseCallbackwhose arrow called its ownconstbinding (react-hooks/immutability); it is now a hoisted function declaration, which also drops a memo that bought nothing.Gate family, re-derived from the actual diff — all 32
check:*scripts run at48d1beee1; exit codes captured before any pipe. 31 green. The one red ischeck:published-dist, and it is pre-existing and untouched by this diff:@object-ui/fieldsshipsdist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-basedexclude(**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed:search_issuesansweredAPI rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.Also worth recording:
check:eager-closureis green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusingHomeAppsStriphad to fit inside.check:dist-completenesswas red on an incomplete localdistand went green after the repair the gate itself prescribes — local build state, not a code fact.Left to CI: the workflow's own run of the farm, and the console build gates that need a fresh production build.
Generated by Claude Code