Skip to content

fix(app-shell): Home's action centre badges the full unread count, not its capped list - #4348

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4329-home-badge-full-count
Aug 11, 2026
Merged

fix(app-shell): Home's action centre badges the full unread count, not its capped list#4348
yinlianghui merged 1 commit into
mainfrom
claude/issue-4329-home-badge-full-count

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4329. Filed from #4225 / PR #4327, which measured the disagreement and pinned it.

The defect

/home showed two numbers for one question. The bell badges distinct unread topics plus pending approvals over the shared feed's full 20-row window; the action centre two hundred pixels below badged pendingApprovalsCount + notifications.length — and notifications is the list it renders, which useHomeInbox caps at limit (5). With nine unread the bell said 9 and the card said 5, on one page, about one set of rows. The badge was reporting the size of a preview as if it were a total.

Before #4225 the card could not have said anything else: its own read was $top: 5, so nine was not a number it had. Both surfaces now cut from one already-joined feed, so the true count is in hand at Home's call site and the cap is a presentation slice over data the card already holds.

The change

Per the ruling on the issue: the badge shows the full unread count (the same number as the bell), the list stays capped at five. Badge = "how much needs you", list = "the newest few of it".

  • useHomeInbox's public shape grows one additive field, unreadTopicCount — existing fields and their meanings untouched.
  • HomeActionCenter takes it as a required prop, and total becomes pendingApprovalsCount + unreadTopicCount. Required rather than optional-with-a-default for the same reason notificationsStatus is: a call site that cannot say how much is waiting must not be able to badge its own preview length by saying nothing.
  • "You're all caught up" is now gated on that total rather than on the rows on show, so it can no longer contradict the badge above it.

Why the count is the bell's fold and not the list's pre-slice length

The obvious two-line version is "count the list before .slice()". That length is folded by title and drops blank titles, while the bell folds by (topic, title) — so it agrees with the bell on ordinary data and disagrees whenever two topics share a title. Two derivations of one number that agree usually is precisely the shape of #4316. The count is therefore groupNotifications — the bell's own fold — applied to the bell's own rows. One fold, applied twice, cannot drift. packages/app-shell/src/layout/inboxGrouping.ts is only imported; nothing in it (or in sharedUserFeeds.ts, or the bell) is restructured.

Tests

Red-first. #4327's pin that MEASURED the inconsistency (sharedInboxFeed.twoSurfaces.test.tsx, "an unread message with no receipt at all still counts": homeBadge '5' vs bell '9') is flipped into the consistency assertion — both '9', list still exactly 5 rows. A second pin of the same old number lived in HomeActionCenter.unansweredInbox.test.tsx ("lists the QA payload and badges it": badge '5' on nine unread); flipped too, its own claim unchanged.

New #4329 block, all joint (real AppHeader + real HomeActionCenter, one tree, one fake adapter):

case pins
counts unread TOPICS, not the titles the list happens to show two unread topics sharing a title: both badges '2', Home lists 1 row, bell lists 2
adds the same pending-approvals count on both surfaces 4 unread topics + 2 approvals = '6' on both; approvals row still reads "2 pending approvals"
reports the same count past the bell's "9+" display clamp 12 unread: Home '12', bell '9+', list still 5 rows

Controls left untouched and green: the caught-up case (no badge on either surface when nothing is unread and no approvals), the existing "reads the SAME badge number on both surfaces — 4 unread topics", the whole read-state block, and the one-feed-one-read block.

Reverse verification (predictions first, both measured)

  • Restore total = pendingApprovalsCount + notifications.length. Predicted: the two flipped pins plus the topic-fold and clamp cases go red, the approvals case and the entire read-state block stay green. Measured: Tests 4 failed | 44 passed — exactly those four (Expected "9" / Received "5", "2"/"1", "12"/"5", "9"/"5"), nothing else.
  • Count the pre-slice list length instead of the topic fold. Predicted: everything green except the topic-fold case — that case is the only fixture where the two folds disagree, and it is why the count is taken from groupNotifications. Measured: Tests 1 failed | 47 passed, the single failure being Expected "2" / Received "1".

Both taken out with git checkout -- < path > against the commit, never git stash.

Local verification

  • pnpm exec vitest run packages/app-shell/ (repo root) → Test Files 349 passed (349), Tests 3331 passed | 1 skipped (3332)
  • turbo run build --filter=@object-ui/app-shell^...28 successful, 28 total (build closure before type-check)
  • tsc --noEmit and tsc -p tsconfig.typetests.json in packages/app-shell → both exit 0
  • eslint on the changed files → 0 errors (15 pre-existing any/unused-arg warnings in the test file's mock boilerplate, none on changed lines)
  • check-changeset-presence / check-changeset-no-major / check-control-bytes → all green

Scope

useHomeInbox.ts, HomeRail.tsx (HomeActionCenter) and HomePage.tsx's wiring, plus the tests. The bell (AppHeader.tsx / InboxPopover.tsx), sharedUserFeeds.ts and inboxGrouping.ts are read from and not modified. HomeActionCenter and useHomeInbox are not exported from packages/app-shell/src/index.ts, so the required prop is not a public-API change: app-shell's dependents (apps/console, examples/console-starter, examples/byo-backend-console) can only reach HomePage/HomeLayout, whose props are unchanged.

One thing to note, deliberately not changed

Past 9 the bell renders "9+" (its badge is a 20px circle in the top bar, #2765) while Home's card prints the number — "9+" and "12" are two renderings of one count, not two counts. Left as-is and pinned as measured; clamping Home would mean teaching the shared card badge a display rule it has no other use for. Flagged for the maintainer rather than decided here.


Generated by Claude Code

…#4329)

The bell badges distinct unread topics + pending approvals over the shared
feed's 20-row window; Home's action centre badged
`pendingApprovalsCount + notifications.length` — and `notifications` is the
list it renders, capped at 5. Nine unread read as 9 on the bell and 5 on the
card 200px below, one page, one question, two numbers.

`useHomeInbox` grows `unreadTopicCount` (additive) and `HomeActionCenter`
takes it as a required prop. Badge = total waiting, list = the newest few of
it, still capped at 5.

The count is the bell's own fold (`groupNotifications`, by `(topic, title)`)
over the bell's own rows rather than the pre-slice length of Home's list:
that length is title-folded and drops blank titles, so it would agree with
the bell on ordinary data and disagree when two topics share a title — the
"agrees usually" shape of #4316.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 11, 2026 7:16pm

Request Review

@github-actions github-actions Bot added the tests label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 29.6 KB 350 KB
Entry file index-BAUts28q.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.12KB 108.41KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 150.04KB 39.79KB
fields (index.js) 228.37KB 56.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 121.56KB 31.56KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 238.95KB 59.76KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.00KB 49.94KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.10KB 26.74KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.60KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 19:27
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit de62779 Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4329-home-badge-full-count branch August 11, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Home's action centre badges its CAPPED list while the bell badges every unread topic — 5 and 9 for the same question, on one page

2 participants