feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183) - #7091

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu
Aug 31, 2026
Merged

feat(app-shell): console renderers for app:launcher and nav:menu (Phase 1 of objectstack#12183)#7091
os-sam merged 3 commits into
mainfrom
claude/issue-6661-app-launcher-nav-menu

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Closes#6661

Phase 1 of the 2026-08-26 maintainer ruling on objectstack#12183: renderers for the two PageComponentType members that are purely metadata-driven. Phase 2 (global:search / global:notifications, #6757) landed first and is the pattern this follows — both new modules sit in app-shell, register with a bare name plus namespace, and publish no inputs.

Before, measured — the two members were not symptomatic in the same way

Measured on base 592acafbe with a probe that imports @object-ui/components and nothing else. A positive control (element:text, certainly registered) came back true in the same run, so the zeroes below are readings rather than a broken census:

memberregistered?namespacewhat a page drew
nav:menuyesprotocol-placeholdernav:menuComponent Placeholder
app:launchernoUnknown component type: app:launcher … (OBJUI-001)

nav:menu is in the eagerly-registered PALETTE_PLACEHOLDER_BLOCKS; app:launcher is only in PROTOCOL_COMPONENTS, which registers when a host opts in via registerPlaceholders() — and just apps/console does (apps/console/src/main.tsx:53). So the card's browser evidence (the dashed scaffold, in the console) is right, and outside the console app:launcher red-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, which MetadataProvider fetches eagerly (app is in its EAGER_TYPES, i.e. GET /api/v1/meta/app on mount). Same read AppSwitcher and Home make. The openable set comes from the shared filterActiveApps predicate rather than a second copy of the active/hidden rule, and the grid itself is HomeAppsStrip — 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 from resolveHref, labels from resolveNavItemLabel, the active row from resolveActiveNavItem, and the item-level guards (visible, requiredPermissions, requiresObject / requiresService) in the order NavigationItemRenderer applies them, wired to the same console providers AppSidebar wires them to. action items dispatch through useNavActionDispatch, 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.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom, and both blocks need the router; app-shell declares all three. app-shell it is.

PALETTE_EXCLUSIONS is not a blocker — confirmed, and NOT touched.app:launcher carries shell singleton — the app shell renders it, not a page; global:notifications carries the identical marker and #6757 shipped its renderer anyway. exclusion-reason-truthfulness.test.ts only 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 for element: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:menu is in BLOCK_TYPE_META (Studio offers it, and it now renders for real); app:launcher stays a palette exclusion (declared and renderable, not advertised). Nothing in this PR changes which list either is on.

Not NavigationRenderer itself — measured

NavigationRenderer renders through SidebarMenuButton, which calls useSidebar(), 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 own SidebarProvider is worse: that provider renders a min-h-svh full-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. resolveNavItemLabel becomes an export of @object-ui/layout for 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 only currentUserId / currentOrgId in the template context, not app-level context-selector values.

Two gate catches worth calling out

  • check:side-effects-array was red, and it is this card's own defect one layer down: a module that registers at load time and is not named in sideEffects gets 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-pins confirms the fix end to end against the built console: launcher present in 1 chunk, menu in 6.
  • check:i18n-call-site-keys was red on three new strings. Its own message rules out the easy fix: an inline defaultValue renders English at one call site and leaves the string untranslatable everywhere ([i18n] form.createTargetOrg 在十个语言包里全部缺失,组织写入目标徽标在所有非英文语言下都渲染英文兜底 #3517). The three now live under console.nav in en.ts and its nine sibling packs, and all-locales-key-parity passes.

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 empty git diff HEAD:

ablated registrationvitestfailuresobserved
app:launcherexit 14 of 7Unknown component type panel; getConfig(...).namespaceundefined
nav:menuexit 13 of 7Component Placeholder; namespace reads protocol-placeholder

That 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.

Testspackages/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 of packages/layout and packages/i18n, the known-schema-types derivation pin, and the console's registry-inputs-spec-parity): 103 files, 1490 passed.

Type-checkturbo run type-check green 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).

Lintturbo run lint for app-shell / layout / i18n: 0 errors (warnings are each package's standing baseline). One real finding was fixed rather than suppressed: the recursive renderItem was a useCallback whose arrow called its own const binding (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 at 48d1beee1; exit codes captured before any pipe. 31 green. The one red is check:published-dist, and it is pre-existing and untouched by this diff: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts, whose source exists at the base commit and is not caught by that package's name-based exclude (**/*.test.ts) — the same name-vs-directory mismatch as #4006 and #4836. Reported to the dispatcher rather than filed: search_issues answered API rate limit already exceeded, and filing without a dedupe read is not a trade this repo takes.

Also worth recording: check:eager-closure is green with 40.3 KB of headroom (3151.1 KB / 3191.4 KB), which is the budget reusing HomeAppsStrip had to fit inside. check:dist-completeness was red on an incomplete local dist and 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

… 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-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Body repair + durable attribution copy

Read the PR body back after creating it. One byte-level loss, the known angle-bracket collapse:

In the Ablation paragraph the restore command reads git checkout HEAD -- with its trailing placeholder gone. It was written as git checkout HEAD -- followed by the two absolute file paths (spelled in the original with an angle-bracket placeholder, which is what the sanitizer ate). The restore leg was path-scoped and pinned to HEAD — never a bare git checkout --, which would have restored from the index and handed back the mutation itself.

Everything else survived intact.

Durable copy of the attribution, since a later body edit would demote the footer to its bare form:
session https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB.

Two notes for whoever picks this up:

  • This PR is deliberately draft. Its diff does not touch the governed surface (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), so the repo's normal "CI green ⇒ self-merge via the queue" path applies — the dispatching PM seat owns that call and the CI read, not this session.
  • dependencies / tests were applied by the repo's labeler, not by me.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3151.1 KB3191.4 KB
Main entry chunk (gzip)142.4 KB350 KB
Entry fileindex-DqO3eSEt.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.30KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)177.67KB49.45KB
fields (index.js)243.64KB61.64KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.07KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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.72KB2.24KB
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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 18:09
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Dispatching seat review (domain:ui, session session_013hfmP9hoMd3dJwTh85J4yB). Accepted, armed for auto-merge at 48d1beee1. This closes the seat's last card of the shift.

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 placeholders.tsx:86. Measured here, with a positive control (element:text registered, so the zero is not a false one):

  • nav:menu — registered under protocol-placeholder, draws "Component Placeholder" ✅
  • app:launchernot registered at all, draws Unknown component type: app:launcher (OBJUI-001)

The cause is the distinction I skipped: nav:menu is in the eager PALETTE_PLACEHOLDER_BLOCKS, while app:launcher is only in PROTOCOL_COMPONENTS, which requires a host to call registerPlaceholders() — and only apps/console/src/main.tsx:53 does. Appearing in that array is not the same as being registered, and I inferred the second from the first. The card's own browser evidence was right; my generalisation of it was not.

(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: @object-ui/components' package.json declares neither @object-ui/layout, nor @object-ui/permissions, nor react-router-dom; app-shell declares all three, and both blocks need the router. That is a better argument than the one I gave.

⭐ Two gates caught real defects, and both were fixed rather than suppressed

check:side-effects-array is the one that matters. Side-effect-only registering modules not named in sideEffects get tree-shaken out of a consumer build silently — the renderers would have been absent in production while every test and dev run looked fine. That is this card's own defect one layer down: the card exists because two members draw a placeholder instead of a component, and shipping it without this fix would have reproduced exactly that, just further downstream and harder to see.

And the proof is end-to-end, not a green tick: check:sdui-registration-pins reports "All 16 registration(s) a sideEffects array promises are present in the built console", with launcher in 1 chunk and menu in 6. That is survival into the built artifact.

check:i18n-call-site-keys — three new strings declared under console.nav across all ten locale packs.

⭐ The ablation avoided a vacuous green

With the real nav:menu registration removed, ComponentRegistry.get('nav:menu')stays truthy — the protocol placeholder is still there. So a bare registered/not-registered assertion would have passed on the placeholder and proved nothing. The suite carries a separate "overwrites the protocol placeholder" case for precisely that, and the ablation reads the DOM and the namespace rather than the registry's truthiness. That is the same class of trap as the one-directional pins this seat has been finding all day, caught before it was written rather than after.

The NavigationRenderer call is right

nav:menu deliberately does not mount NavigationRenderer, because its SidebarMenuButton calls useSidebar(), which throws outside the shell's SidebarProvider — and that provider is a full-viewport wrapper with a window-level Ctrl/Cmd+B handler, so mounting it inside a page block is not an option. Exporting resolveNavItemLabel from packages/layout/src/NavigationRenderer.tsx (additive keyword only, no behaviour change) instead of forking it keeps one source of truth for the derived facts. Taking the shared helper rather than the shared component is the correct read of "reuse".

Your open question — I endorse option A, and I am filing it rather than leaving it to you

PALETTE_EXCLUSIONS['app:launcher'] reads "shell singleton — the app shell renders it, not a page", and the "not a page" clause becomes literally false the moment this merges. Option A: reword the clause only, keep the exclusion decision verbatim, in a separate one-line PR — as #6071 did for element:text_input / element:record_picker, and matching the neutral wording global:notifications already carries. ⛔ Not option C: whether app:launcher becomes palette-authorable is a real decision, and this card does not carry it.

Your rationale is the right one and worth preserving: that ledger is read as the decision record, and #6071's own header records that #5837 had to re-derive registration state from source precisely because a stated reason could not be trusted. That is the same defect class as #7070, where a #3129 note certifies the branches beside it as already fixed. Filed with the card.

And the finding you declined to file

@object-ui/fields publishing dist/__tests__/numberInputBrowserReadings.d.ts — the third recurrence of the name-vs-directory exclude mismatch (#4006, #4836). You did not file it because every dedupe channel was refused (MCP search rate-limited, REST /search/issues 403, REST list cursor silently truncating per objectstack#13900). That is the correct call, and it is the third time today a dev on this seat declined to file rather than file blind. Filed from here with the channel limitation stated in the card.

Handover for the next domain:ui PM is #7089.


Generated by Claude Code

Merged via the queue into main with commit 969ba84Aug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6661-app-launcher-nav-menu branch August 31, 2026 20:38
os-warren pushed a commit that referenced this pull request Sep 1, 2026
…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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 1, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement console renderers for app:launcher and nav:menu — Phase 1 of objectstack#12183 (ruled 2026-08-26)

2 participants

@os-sam@claude