feat(sidebar): show provider quota usage in the sidebar footer - #4611
feat(sidebar): show provider quota usage in the sidebar footer#4611sharull9 wants to merge 1 commit into
Conversation
Claude and Codex both report plan limits on every turn, but nothing surfaced them until you opened provider settings. Parse the windows into a shared shape (providerRateLimits.ts), carry them on the provider snapshot, and render the two windows that answer "what stops me now" and "what stops me this week" as compact meters in the sidebar footer. Hovering a meter opens a detail card: every window the provider reported, each with its own relative reset time, the plan type, and how stale the numbers are. Clicking opens provider settings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🟡 Medium
SidebarUsageMeter renders clickable rows whose openProviderSettings callback calls navigate({ to: \"/settings/providers\" }) without closing the mobile sidebar. On mobile, tapping a usage row navigates to the provider settings page while the sidebar sheet remains open, covering the destination. The adjacent Settings button avoids this by calling setOpenMobile(false) before navigating — SidebarUsageMeter needs the same treatment.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/sidebar/SidebarChrome.tsx around line 1:
`SidebarUsageMeter` renders clickable rows whose `openProviderSettings` callback calls `navigate({ to: \"/settings/providers\" })` without closing the mobile sidebar. On mobile, tapping a usage row navigates to the provider settings page while the sidebar sheet remains open, covering the destination. The adjacent Settings button avoids this by calling `setOpenMobile(false)` before navigating — `SidebarUsageMeter` needs the same treatment.
| }).pipe( | ||
| Effect.provideService(HttpClient.HttpClient, httpClient), | ||
| Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), | ||
| Effect.flatMap(() => |
There was a problem hiding this comment.
🟡 Medium Drivers/ClaudeDriver.ts:204
Rate-limit events are dropped while the version advisory lookup is in flight. streamRateLimitUpdates is started only after enrichProviderSnapshotWithVersionAdvisory completes and the enriched snapshot is published, so any rate_limit_event or end-of-turn /usage payload the adapter emits during that window (up to the HTTP timeout) finds no subscriber on the non-replaying rateLimitEvents PubSub and is permanently lost. This happens on initial startup and every snapshot refresh, so a turn ending in that window leaves rateLimits stale until another provider event arrives. Start the streamRateLimitUpdates subscription before or concurrently with the version enrichment.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Drivers/ClaudeDriver.ts around line 204:
Rate-limit events are dropped while the version advisory lookup is in flight. `streamRateLimitUpdates` is started only after `enrichProviderSnapshotWithVersionAdvisory` completes and the enriched snapshot is published, so any `rate_limit_event` or end-of-turn `/usage` payload the adapter emits during that window (up to the HTTP timeout) finds no subscriber on the non-replaying `rateLimitEvents` `PubSub` and is permanently lost. This happens on initial startup and every snapshot refresh, so a turn ending in that window leaves `rateLimits` stale until another provider event arrives. Start the `streamRateLimitUpdates` subscription before or concurrently with the version enrichment.
| // The footer stays a predictable height: only the windows that answer "what | ||
| // stops me now" and "what stops me this week" get a row. Everything else the | ||
| // provider reports still shows up in the tooltip. | ||
| const ROW_LABELS: ReadonlyArray<string> = ["5h", "Weekly"]; |
There was a problem hiding this comment.
🟡 Medium sidebar/SidebarUsageMeter.tsx:13
toUsageRows only emits rows for windows whose label is in ["5h", "Weekly"], so an authenticated Codex provider whose windows use the fallback labels Primary or Secondary produces zero rows and the usage meter is hidden entirely — even though rateLimits.windows is populated. Consider including the fallback labels (or deriving rows from the windows themselves) so Codex usage still appears.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/sidebar/SidebarUsageMeter.tsx around line 13:
`toUsageRows` only emits rows for windows whose `label` is in `["5h", "Weekly"]`, so an authenticated Codex provider whose windows use the fallback labels `Primary` or `Secondary` produces zero rows and the usage meter is hidden entirely — even though `rateLimits.windows` is populated. Consider including the fallback labels (or deriving rows from the windows themselves) so Codex usage still appears.
| requestAllCodexModels(client), | ||
| // Best-effort: older Codex CLI builds or non-ChatGPT auth modes may not | ||
| // support this request, so a failure here shouldn't fail the whole probe. | ||
| client.request("account/rateLimits/read", undefined).pipe(Effect.option), |
There was a problem hiding this comment.
🟡 Medium Layers/CodexProvider.ts:403
When account/rateLimits/read fails for an authenticated API-key or Bedrock account, Effect.option swallows the error into None and the snapshot omits rateLimits entirely. This makes those accounts indistinguishable from accounts whose quota hasn't been reported yet — quota consumers can't tell that the account has no plan limits and keep showing a pending/unknown state instead of the authoritative empty-windows state. The error is already handled as None before normalizeCodexRateLimitSnapshot ever sees it, so the normalizer's undefined-on-empty-windows path is skipped. Consider distinguishing a transport/unsupported-method failure from a legitimate empty result — for example, catching the specific error types that indicate unsupported auth modes and returning a normalized { windows: [] } snapshot for those, while reserving undefined for the genuinely unknown case.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexProvider.ts around line 403:
When `account/rateLimits/read` fails for an authenticated API-key or Bedrock account, `Effect.option` swallows the error into `None` and the snapshot omits `rateLimits` entirely. This makes those accounts indistinguishable from accounts whose quota hasn't been reported yet — quota consumers can't tell that the account has no plan limits and keep showing a pending/unknown state instead of the authoritative empty-windows state. The error is already handled as `None` before `normalizeCodexRateLimitSnapshot` ever sees it, so the normalizer's `undefined`-on-empty-windows path is skipped. Consider distinguishing a transport/unsupported-method failure from a legitimate empty result — for example, catching the specific error types that indicate unsupported auth modes and returning a normalized `{ windows: [] }` snapshot for those, while reserving `undefined` for the genuinely unknown case.
| }); | ||
| } | ||
|
|
||
| yield* Stream.runForEach(input.events, (payload) => |
There was a problem hiding this comment.
🟡 Medium provider/providerRateLimits.ts:333
streamRateLimitUpdates merges every normalized payload with the existing store using mergeRateLimits, which preserves windows from base that are absent from override. When a full Claude /usage snapshot arrives with fewer windows than before — e.g. Overage drops out after extra usage is disabled or returns to zero — the stale windows are never removed, so the client permanently displays outdated quota rows. Full /usage snapshots are authoritative and should replace the previous state entirely; only single-window turn events need merge semantics.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/providerRateLimits.ts around line 333:
`streamRateLimitUpdates` merges every normalized payload with the existing store using `mergeRateLimits`, which preserves windows from `base` that are absent from `override`. When a full Claude `/usage` snapshot arrives with fewer windows than before — e.g. `Overage` drops out after extra usage is disabled or returns to zero — the stale windows are never removed, so the client permanently displays outdated quota rows. Full `/usage` snapshots are authoritative and should replace the previous state entirely; only single-window turn events need merge semantics.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 6 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| publishSnapshot, | ||
| normalize: normalizeClaudeRateLimitPayload, | ||
| }), | ||
| ), |
There was a problem hiding this comment.
Quota meters cleared on refresh
Medium Severity
During Claude snapshot refreshes, the enrichSnapshot process publishes a snapshot missing rate limit data before streamRateLimitUpdates can restore or subscribe. This causes the UI to briefly show pending meters and leads to missed rate limit PubSub events.
Triggered by learned rule: PubSub subscribe-before-snapshot to avoid change-stream gaps
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| } | ||
|
|
||
| // ponytail: verification logging for the percent scale — see plan Phase 0. | ||
| yield* Effect.logInfo("Claude account usage read.", { rawUsage: usage.rate_limits }); |
There was a problem hiding this comment.
Temporary verification logs left in
Medium Severity
The Effect.logInfo calls marked 'ponytail: verification logging' are dumping raw Claude rate-limit payloads. This debug logging, likely temporary, creates noisy production telemetry due to frequent rate-limit events, potentially exposing account quota details.
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| appendCustomCodexModels(models, input.customModels ?? []), | ||
| ), | ||
| skills: parseCodexSkillsListResponse(skillsResponse, input.cwd), | ||
| ...(rateLimits ? { rateLimits } : {}), |
There was a problem hiding this comment.
Codex pending meters never resolve
Medium Severity
When account/rateLimits/read fails or returns no usable windows, Codex omits rateLimits instead of publishing an empty window list. The sidebar treats authenticated providers with missing rateLimits as "waiting for first report", and Codex never fills that via turn events, so API-key and unsupported-CLI users keep permanent placeholder rows with a turn-end tooltip.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| } | ||
| const hours = Math.floor(minutes / 60); | ||
| return hours < 24 ? `Updated ${hours}h ago` : `Updated ${Math.floor(hours / 24)}d ago`; | ||
| } |
There was a problem hiding this comment.
Raw Date parsing in meter
Low Severity
formatResetsAt, formatResetsIn, and formatUpdatedAgo parse ISO timestamps with raw new Date(...) instead of parseTimestampDate from timestampFormat.ts, which this codebase prefers for ISO handling and invalid-date fallthrough.
Triggered by learned rule: Use parseTimestampDate for ISO string parsing, return null for invalid
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| windows.push({ label: "Overage", usedPercent: overagePercent }); | ||
| } | ||
|
|
||
| return { ...base, windows: sortWindows(windows) }; |
There was a problem hiding this comment.
Empty parse clears quotas
Medium Severity
normalizeClaudeUsageResponse returns { windows: [] } whenever rate_limits is present but no window yields a parseable utilization, not only when limits are unavailable. mergeRateLimits treats any empty override as an authoritative clear, so streamRateLimitUpdates can wipe windows already accumulated from earlier events.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
| usedPercent: Math.min(100, Math.max(0, window.usedPercent)), | ||
| resetsAt: window.resetsAt, | ||
| })), | ||
| ); |
There was a problem hiding this comment.
Stale quotas after logout
Medium Severity
toUsageRows only checks auth.status when rateLimits is missing. Claude’s process-lifetime rateLimitsStore is restored onto every refreshed snapshot with no auth gate, so after logout or auth loss enrichment can republish prior windows and the footer still renders quota rows for an unauthenticated provider.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.
ApprovabilityVerdict: Needs human review 5 blocking correctness issues found. This PR introduces a new feature (quota usage meters in the sidebar) with ~989 additions across server and client. Multiple unresolved review comments identify potential bugs including race conditions, stale state after logout, and debug logging left in production code. You can customize Macroscope's approvability policy. Learn more. |
|
Closing in favor of #2829 (orchestration V2). #2829 deletes the V1 orchestration layer this PR builds on — This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them. Once #2829 merges, please rebase onto |
|
Ok will do that |


Claude and Codex both report plan limits on every turn, but nothing surfaced them until you opened provider settings. This adds a quota meter to the sidebar footer.
What's here
Server —
providerRateLimits.tsparses each driver's limit payload into one shared shape ({ planType, updatedAt, windows[] }), and the provider snapshot carries it to the client. Covered byproviderRateLimits.test.ts(11 tests).Sidebar — the footer shows only the two windows that answer "what stops me now" and "what stops me this week", so its height stays predictable. Overage earns a row only once it's actually live. Bars stay quiet until they matter: primary under 75%, warning at 75%, destructive at 90%.
Tooltip — the detail surface. Every window the provider reported, each with its own relative reset ("resets in 2h" beats a wall-clock time for the question the meter raises), the plan badge, and how stale the numbers are. The hovered window keeps a full-strength bar; the rest dim. Clicking any row opens provider settings.
Edge cases
rateLimitspresent but empty means the account has no plan limits (API key, Bedrock, Vertex) — no rows at all, rather than a permanently pending meter.aria-labelcarries the whole card as one sentence, so screen readers lose nothing to the visual layout.Testing
providerRateLimits.test.tspasses (11/11);@t3tools/webtypechecks. The sidebar itself hasn't been exercised against a live authenticated provider yet.🤖 Generated with Claude Code
Note
Medium Risk
Touches provider snapshot push path and live Claude adapter event wiring; failures are mostly best-effort (Codex probe, experimental Claude usage API) but incorrect normalization could mislead users about quota.
Overview
Adds account rate limits to the provider snapshot (
ServerProvider.rateLimitsin contracts) and a sidebar usage meter so Claude and Codex plan quota is visible without opening settings.Server — New
providerRateLimits.tsnormalizes driver-native payloads into shared{ planType, updatedAt, windows[] }, merges Claude windows by label (single-windowrate_limit_eventvs full/usageread), andstreamRateLimitUpdatesrepublishes snapshots as limits change. Codex readsaccount/rateLimits/readduring status probes and attaches limits on the probe result. Claude adds a PubSubrateLimitEventsside channel (so snapshot enrichment does not compete withProviderServiceon the turn event queue), publishes fromrate_limit_eventand experimental turn-end usage read, and keeps accumulated windows in aRefacross snapshot refreshes.Web —
SidebarUsageMeterin the footer shows 5h / Weekly (and live Overage) with color thresholds, tooltips for all windows, staleness, and navigation to provider settings; emptyrateLimits.windowsmeans no plan limits apply (no placeholder rows).Reviewed by Cursor Bugbot for commit e67d3e2. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Show provider quota usage meters in the sidebar footer
ServerProviderRateLimitsto the contracts schema and threads it through provider snapshots, covering both Claude (via a streamingrateLimitEventsside channel) and Codex (viaaccount/rateLimits/readat probe time).providerRateLimits.tswith normalization and merging utilities for provider-native rate-limit payloads, including window labeling, overage logic, and staleness stamping.SidebarUsageMetercomponent in the sidebar footer showing per-provider usage rows (5h, Weekly, Overage) with tooltips and reset timing, clicking through to provider settings.📊 Macroscope summarized e67d3e2. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.