Skip to content

feat(app-shell): render global:search and global:notifications instead of the placeholder - #6838

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6757-global-search-notifications-renderers
Aug 30, 2026
Merged

feat(app-shell): render global:search and global:notifications instead of the placeholder#6838
os-sam merged 1 commit into
mainfrom
claude/issue-6757-global-search-notifications-renderers

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6757

Two spec PageComponentType members had no renderer, so a page that authored either drew the literal "Component Placeholder" scaffold. The 2026-08-26 maintainer ruling on objectstack#12183 kept both declared once the readiness read in objectstack#13117 evidenced both data sources shipped, which left the renderer as the remaining half.

What this adds — two mount points, no new data layer

  • global:search mounts useRecordSearch from @object-ui/react — the same hook the Command-K palette (chrome/CommandPalette.tsx) and the full-page results view (views/SearchResultsPage.tsx) already use. It prefers the adapter's searchAll (GET /api/v1/search, cross-object hits with title/snippet/record) and inherits that hook's per-object fanout fallback for adapters without it. Scope is the metadata provider's searchable object set, which is the hook's documented default when no objectNames whitelist is passed; a page block has no app-nav context of its own the way the palette and the results page do.
  • global:notifications mounts InboxPopover — the bell ADR-0012 / ADR-0030 define ("the bell reads sys_inbox_message") — over the shared inbox feed.

The one refactor, and why it is not optional

The header bell's inbox wiring (rows, badge addends, and the three mark-read paths) moves out of AppHeader into a new hooks/useInboxBell.ts that both bells mount. Copying it into the new renderer would have re-opened the two defects sharedUserFeeds closed — #4225 (two owners of one read issuing it twice per page) and #4316 (two derivations of read-state disagreeing, so the bell showed zero unread while Home listed five of the same rows as waiting). With one hook there is no second read and no second optimistic overlay, so a bell in the header and a bell an author declared on a page have no representable state in which they disagree about a row. AppHeader loses 130 lines and gains 24; nothing else about it changes.

Contract shape

Both registrations publish noinputs. ComponentPropsMap declares an empty shape for each member ("declares no props at all" is the recorded intent, and it was re-derived here against the installed @objectstack/spec@17.2.0 rather than taken from the card), so declaring even a className input would advertise an authoring key the contract rejects by name — the forward direction of apps/console/src/__tests__/registry-inputs-spec-parity.test.ts is a vice on exactly that move. Both use skipFallback: true, so neither claims the bare search / notifications registry key.

No spec-side change. This also does not change the Studio page palette: PALETTE_EXCLUSIONS still records global:notifications as a shell singleton, which is a palette decision about authoring ergonomics and independent of whether a declared type renders. The eager palette placeholder for global:search in components/renderers/placeholders.tsx stays too — it is the fallback for a host that embeds @object-ui/components without app-shell, and because these modules import that package its registration always runs first and this one overwrites it. The search ablation below measures that ordering rather than assuming it.

Verification — all at 6e602ca4e

Run from the repo root, per AGENTS.md; the file that actually executed is named in every line below.

Targeted suite, five cases, green:

pnpm exec vitest run --reporter=verbose packages/app-shell/src/views/__tests__/global-page-blocks.render.test.tsx
ok |dom| packages/app-shell/src/views/__tests__/global-page-blocks.render.test.tsx > registers both members under the `global:` namespace, not the bare names
ok |dom| ... > publishes NO `inputs` for either — both spec shapes are empty
ok |dom| ... > global:search > renders a working search box and puts a `searchAll` hit on the page
ok |dom| ... > global:notifications > renders the bell and badges the unread topics from the inbox feed
ok |dom| ... > global:notifications > opens the inbox and lists the rows the feed returned
Test Files 1 passed (1)
Tests 5 passed (5)

The assertions are content, not absence — a page declaring each member is rendered through SchemaRenderer over a fake adapter, and each case asserts DOM that had to travel the block's data path: the search box plus the searchAll hit "Wayne Enterprises" linking to /apps/crm/crm_account/record/a1, and the bell plus the badge "2" folded from two unread sys_inbox_message rows joined against their receipts, plus both row titles once the popover opens. "Not the placeholder" is kept only as a second, weaker line, because an empty render is also not the placeholder.

Ablation, per member. Each leg mutated one registration call on disk, proved the mutation landed by anchor counts and a git hash-object blob comparison, ran the suite, restored with git checkout HEAD -- ... against an absolute path, and proved the restore by blob equality plus an empty git diff HEAD. No rebuild leg: this repo's vitest aliases every workspace package to its src, and the suite imports the renderers by relative path, so nothing resolves through dist/.

ablatedanchors before / afterresult
global:searchorig 1 to 0, injected 0 to 1; blob bb0037dd to 8747ef021 failed, 4 passed — the search case fails on the placeholder DOM (dashed border, FileQuestion icon), which is the exact scaffold the card reported
global:notificationsorig 1 to 0, injected 0 to 1; blob e960379b to de3309623 failed, 2 passed — both notification cases plus the namespace-registration case

Restored state is byte-identical in both legs (bb0037dd... and e960379b... back to their HEAD blobs, git diff HEAD empty), and the suite is 5/5 green again on the restored tree.

The asymmetry is worth stating rather than smoothing over: ablating global:search leaves the namespace-registration case green, because the eager palette placeholder still answers ComponentRegistry.get('global:search') truthily. That is precisely why the render assertion is the load-bearing one for that member and the registry probe cannot stand in for it.

Other checks, all on the same tree:

  • pnpm exec vitest run packages/app-shell/ — Test Files 575 passed (575), Tests 5635 passed, 1 skipped. No existing test moved: the AppHeader refactor is behaviour-preserving, and the three suites most exposed to it (AppHeader.inboxVariant, sharedInboxFeed.twoSurfaces, sharedInboxFeed.transient404) pass unedited.
  • pnpm exec vitest run over apps/console/src/__tests__/registry-inputs-spec-parity.test.ts, public-contract.test.ts, preview-samples-registry-resolvable.test.ts, record-block-record-reach.test.tsx, packages/components/src/__tests__/palette-placeholder-blocks.test.tsx, packages/cli/src/utils, packages/app-shell/src/views/metadata-admin/previews — Test Files 49 passed (49), Tests 657 passed. The parity pin needed no edit: its registry graph imports @object-ui/components and the console's register-plugins, not app-shell, so neither member enters its derived sets.
  • pnpm --filter @object-ui/app-shell type-checktsc --noEmit && tsc -p tsconfig.test.json, exit 0. Confirmed with --listFiles that it covers all four new or changed files including the test, so this is a measurement and not an empty one.
  • pnpm run check:side-effects-array — this one caught a real defect: both new modules register at load time and were missing from app-shell's sideEffects, which a bundler would have dropped silently. Added, and the gate now reads "names exactly the 12 module(s) that register at load time".
  • Green: check:control-bytes, check:phantom-deps, check:self-import, check:i18n-keys, check:i18n-drift, check:vi-mock-specifiers, check:element-data-source-declaration, check:doc-types, lint:coverage, type-check:coverage, check-changeset-presence, check-changeset-no-major, check-changeset-fixed.
  • node scripts/regenerate-known-schema-types.mjs — "OK ... matches the registry derivation", no diff. Both spellings were already listed via the placeholder vocabulary, verified rather than assumed.
  • Lint: eslint . for the whole app-shell package, 1020 files, 0 errors, 2795 warnings (file count and counts read from --format json, population from eslint's own config rather than a hand-picked file list). The new files contribute six no-explicit-any warnings of the same class the sibling record-approvals-renderer.tsx already carries. Repo-wide pnpm lint is left to CI, and that narrowing is sound here because this config enables no type-aware linting (no parserOptions.project, no projectService), so an edit inside app-shell cannot move a verdict in an untouched file of any other package.

Not measured locally, and reported as such rather than as passes: check:eager-closure and check:sdui-registration-pins both exit 2 with "no console build to weigh" and describe themselves as a broken gauge in that state. Both need apps/console/dist, i.e. a full console build, which CI performs. The specific hazard the second guards — a wrong sideEffects array dropping registrations at bundle time — is the one check:side-effects-array now covers green. check:readme-exports exits 1 for the same class of reason ("its type entry ./dist/index.d.ts is not on disk -- run pnpm build first"), across READMEs in five packages this branch does not touch.

Files

  • packages/app-shell/src/views/global-search-renderer.tsx (new)
  • packages/app-shell/src/views/global-notifications-renderer.tsx (new)
  • packages/app-shell/src/hooks/useInboxBell.ts (new — extracted from AppHeader)
  • packages/app-shell/src/views/__tests__/global-page-blocks.render.test.tsx (new)
  • packages/app-shell/src/layout/AppHeader.tsx, packages/app-shell/src/index.ts, packages/app-shell/package.json
  • .changeset/6757-global-search-notifications-renderers.md

nav:menu and app:launcher (objectui#6661, Phase 1) are not addressed here — out of scope for this card, and that card had not landed when this was written, so the "block registry the Phase-1 members used" does not yet exist in the tree; these two follow the established ComponentRegistry.register(bare-name, renderer, { namespace, skipFallback }) plus barrel side-effect-import pattern that record:approvals and record:attachments use in this same directory.

Generated by Claude Code


Generated by Claude Code

…stead of the placeholder
Both are first-class `PageComponentType` members that the 2026-08-26 maintainer
ruling on objectstack#12183 kept declared once objectstack#13117 evidenced both
data sources shipped. Nothing registered a renderer, so a page that authored
either drew the literal "Component Placeholder" scaffold.
Neither block adds a data layer:
- `global:search` mounts `useRecordSearch` (the hook the command palette and the
full-page search results already use), which prefers the adapter's `searchAll`
(`GET /api/v1/search`) and falls back to the per-object fanout.
- `global:notifications` mounts `InboxPopover` over the shared inbox feed —
the bell ADR-0012/ADR-0030 defines.
The header bell's inbox wiring moves out of `AppHeader` into `useInboxBell`, so
both bells cut from ONE read with ONE optimistic read overlay (#4225 / #4316).
Both registrations publish no `inputs` (`ComponentPropsMap` declares an empty
shape for each) and use `skipFallback: true` so neither claims a bare key.
Fixes#6757
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3175.1 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-C9ty_dzu.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)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
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)511.50KB116.32KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)240.93KB60.76KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.46KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.03KB32.64KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.57KB54.55KB
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)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.87KB21.12KB
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)76.75KB25.49KB
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 30, 2026 03:36
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit f99932aAug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6757-global-search-notifications-renderers branch August 30, 2026 04:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants

@os-sales@os-sam@claude