fix(app-shell): read the single AI quota pool and draw one ring (objectui#8524) - #10911
Conversation
The cloud usage endpoint now answers { pool, breakdown } (one quota pool plus
an observation-only split). useAiUsage parsed only the retired per-meter
{ meters: { build, dataChat } } answer, so it returned null and the ChatDock
indicator disappeared without any error.
- useAiUsage reads { pool, breakdown? } strictly. An absent breakdown is a
normal reading; the retired { meters } answer parses to null and sets error.
- AiUsageIndicator draws ONE ring from pool. The split is listed as text in
the popover (share of the same pool), only when a member was measured.
- New console.ai.usage.breakdownTitle row in every locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…ectui#8524) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…usage-single-pool
…usage-single-pool
…dy the indicator guard Comment wording only in useAiUsage (the optional breakdown comes from cloud#2133, not ADR-0015), and a behaviour-identical early-return guard in AiUsageIndicator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
…usage-single-pool
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card #8524 (body and all five comments), PR #10911 (body, 15-file list, net diff against ① Derived judgmentsAccept set of
Public surface and behaviour:
Shipped prose —
Locale copy: PR body producer sentences: " Nothing in the diff or its prose is judged WRONG. ② Semver levelThe changeset declares
The changeset's level matches what the diff publishes and matches ③ Boundary flagsThe dev report (
Check-runs on the head: 43 in all, 40 Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #8524
Clause-②: no
What was wrong
After objectstack-ai/cloud#2144 the tenant runtime's
GET /api/v1/ai/usageanswers{ pool, breakdown }: one AI quota pool, plus an observation-only split of that same pool.useAiUsageonly parsed the retired per-meter{ meters: { build, dataChat } }answer, so its parser returnednullandAiUsageIndicatorrendered nothing. No ring, no error.Producer read at cloud
main96eb092,packages/objectos-runtime/src/ai-usage-endpoint-plugin.ts(symbolsAiUsageIndicator,AiUsageBreakdownFractions,AiUsageResponse,buildAiUsageResponse,unknownResponse):poolcarriesplanType,fraction(number or null),unmetered,resetKind,resetsAt,upgrade,topUp. These are the same fields each old meter carried.breakdownis always an object whose members are number or null. Every error path answers 200 with a neutral pool (fraction: null) and both membersnull.build/dataChatsplit read-only statistics that no quota decision uses. cloud#2133 lets the endpoint keepbreakdownbut does not require it, so its absence is a normal reading. The triage comment on the card says the same.What changed
useAiUsagereads{ pool, breakdown? }and only that shape.poolmust carry every declared field with its primitive type.resetKindandplanTypeare checked as strings, so a reset kind this build does not know still renders without a reset line (the objectui#7371 behaviour, still pinned).breakdown, or one withnullmembers, is a normal reading and never the parse-failure branch.{ meters }one included, givesusage: nulland now also setserror. There is no dual-shape read.AiUsageIndicatordraws ONE ring frompool.Build 30%andAsk 20%, each a percentage of the same pool. It is shown only when at least one member is a number, and it is never a second ring.ai-usage-cta.console.ai.usage.breakdownTitle, in every locale pack. The split rows reusemeterBuild/meterAsk..changeset/8524-ai-usage-single-pool.mddeclares a patch for@object-ui/app-shelland@object-ui/i18n.File surface is the claim's: the hook, the component, their two test files, the ten locale packs and one changeset. The hooks barrel is untouched (see Acceptance notes).
Tests
packages/app-shell/src/hooks/__tests__/useAiUsage.test.ts:{ pool, breakdown };breakdownabsent parses to{ pool }with no error;breakdownwith both membersnullparses as a normal reading;{ meters }answer givesusage: nullwitherrorset (the strict reader, pinned);breakdownmember that is not a fraction givesusage: null.packages/app-shell/src/layout/__tests__/AiUsageIndicator.test.tsx:nullgives one ring and no split;breakdownabsent gives one ring and no split;{ pool }answer with nobreakdownrenders the ring, and the{ meters }answer renders nothing once the hook reports its error.Reverse verification (after committing the fix; mutation and restore scripted with a trap): restoring the base versions of
useAiUsage.tsandAiUsageIndicator.tsxunder the new tests givesTest Files 2 failed (2),Tests 20 failed | 4 passed (24). The 4 that stay green are the shape-independent cases: null usage renders nothing, non-2xx, inert without apiBase, and refetch on nudge. The restore was proven by HEAD blob hashes and an emptygit diff HEAD.Local verification
Final reading at
12e1c1d. That head is this branch after mergingorigin/mainthree times; the merges brought no change to the files this PR touches. Each exit code was captured to a file before any pipe. The full app-shell suite ran once, atfd6907c(the fix commit):Test Files 843 passed | 1 skipped (844),Tests 8673 passed | 9 skipped. It was not re-run at the final head, because CI runs it.12e1c1d)node scripts/check-changeset-presence.mjspnpm changeset:check(fixed group + no-major)pnpm check:i18n-keyspnpm check:i18n-driftpnpm check:i18n-dead-keyspnpm check:control-bytespnpm check:new-line-citations(VERDICT 0 new citations)pnpm check:changeset-claimspnpm check:pending-changeset-literalspnpm check:vi-mock-specifiers/vi-mock-inherit/vi-mock-override-shapepnpm check:test-path-rootspnpm check:unreferenced-sources/phantom-deps/self-importturbo run build --filter=@object-ui/app-shell^...(28/28, all cache hits) thenpnpm --filter @object-ui/app-shell run type-check(verify lock)vitest runonuseAiUsage.test.ts+AiUsageIndicator.test.tsx(verify lock): 2 files, 24 tests passedeslint --format jsonon the 14.ts/.tsxfiles in this diff: 0 errors, 2 warnings that were already therepnpm check:eager-locale-cataloguesAlso run at
a5d7c25, after the first merge (which touched the locale packs): thepackages/i18n/suite plus the two touched tests plus the two ChatDock tests gave 76 files and 1234 tests passed, exit 0.tsc -p tsconfig.test.json --listFilesOnlyconfirms the package type-check compiles both touched test files.Lint was narrowed to the diff's
.ts/.tsxfiles. The narrowing is safe becauseeslint.config.jssets noparserOptions/project/projectService/ type-checked preset (zero hits, with a lit control), has noimport/*plugin rules, and none ofeslint-rules/*reads another file. Every rule therefore judges one file, and this diff cannot move a verdict on an untouched file. The count comes from--format json: 14 files, 0 errors, 2 warnings. Both warnings arereact-hooks/set-state-in-effecton effects this diff does not change, and linting the base blobs of those two files shows the same two warnings.NOT MEASURED:
check:eager-locale-catalogues: PREREQUISITE NOT MET, because it needs a built console bundle (apps/console/dist/eager-closure.json). CI builds it.Acceptance notes
titletooltip. The card allows "a tooltip or a detail view", and atitletooltip never shows on touch devices.breakdown, decided by the PM on the dev report (a PM decision, not a maintainer ruling: option A, keep strict): abreakdownthat is present but malformed is rejected like any other unreadable answer.erroris set and the ring hides. "Malformed" means not an object, a member that is neither a number nor null, or a literalbreakdown: null, which the producer never sends. The alternative, dropping a malformed split and keeping the ring, would be exactly the tolerant read the card's routing constraint forbids, and it would hide a future rename on the producer side. Only an absentbreakdown, or one with null members, means "no split".erroron an unreadable answer: the hook now setserrorwhen an answer does not parse. Before, it returnedusage: nullwith no error. No UI readserrortoday.AiMeterUsagekeeps its name and now documents the pool. The newAiUsageBreakdownlives in the hook file. Neither change touchespackages/app-shell/src/hooks/index.ts, which is outside the claimed surface. The package entrysrc/index.tsre-exports none of these types, henceClause-②: no..objectui-sha(the overlay layer inapps/objectos/Dockerfile). So this reader ships with a runtime that already serves{ pool, breakdown }. The one path where it could meet a{ meters }runtime is a hotfix branch that moves the pin without cloud#2144. That path is noted here and deliberately not answered with a dual read.Session:
https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1Generated by Claude Code