Skip to content

feat(sidebar): show provider quota usage in the sidebar footer - #4611

Closed
sharull9 wants to merge 1 commit into
pingdotgg:mainfrom
sharull9:feat/sidebar-usage-meter
Closed

feat(sidebar): show provider quota usage in the sidebar footer#4611
sharull9 wants to merge 1 commit into
pingdotgg:mainfrom
sharull9:feat/sidebar-usage-meter

Conversation

@sharull9

@sharull9 sharull9 commented Jul 27, 2026

Copy link
Copy Markdown

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

ServerproviderRateLimits.ts parses each driver's limit payload into one shared shape ({ planType, updatedAt, windows[] }), and the provider snapshot carries it to the client. Covered by providerRateLimits.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.

Claude                      MAX 20X
─────────────────────────────────────
5h                        resets in 2h
▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░   62%
Weekly                    resets in 4d
▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░   18%
─────────────────────────────────────
Updated 3m ago        Open settings ↗

Edge cases

  • rateLimits present but empty means the account has no plan limits (API key, Bedrock, Vertex) — no rows at all, rather than a permanently pending meter.
  • Authenticated but nothing reported yet shows dashed placeholder rows; the tooltip explains limits arrive when a turn ends.
  • aria-label carries the whole card as one sentence, so screen readers lose nothing to the visual layout.

Testing

providerRateLimits.test.ts passes (11/11); @t3tools/web typechecks. 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.rateLimits in contracts) and a sidebar usage meter so Claude and Codex plan quota is visible without opening settings.

Server — New providerRateLimits.ts normalizes driver-native payloads into shared { planType, updatedAt, windows[] }, merges Claude windows by label (single-window rate_limit_event vs full /usage read), and streamRateLimitUpdates republishes snapshots as limits change. Codex reads account/rateLimits/read during status probes and attaches limits on the probe result. Claude adds a PubSub rateLimitEvents side channel (so snapshot enrichment does not compete with ProviderService on the turn event queue), publishes from rate_limit_event and experimental turn-end usage read, and keeps accumulated windows in a Ref across snapshot refreshes.

WebSidebarUsageMeter in the footer shows 5h / Weekly (and live Overage) with color thresholds, tooltips for all windows, staleness, and navigation to provider settings; empty rateLimits.windows means 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

  • Adds ServerProviderRateLimits to the contracts schema and threads it through provider snapshots, covering both Claude (via a streaming rateLimitEvents side channel) and Codex (via account/rateLimits/read at probe time).
  • Introduces providerRateLimits.ts with normalization and merging utilities for provider-native rate-limit payloads, including window labeling, overage logic, and staleness stamping.
  • Renders a new SidebarUsageMeter component in the sidebar footer showing per-provider usage rows (5h, Weekly, Overage) with tooltips and reset timing, clicking through to provider settings.
  • The Claude adapter publishes rate-limit events to a PubSub side channel so consumers can subscribe without interfering with the main runtime event queue.
📊 Macroscope summarized e67d3e2. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

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>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b6cafed-5ee1-4920-9702-290d40d187a7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

import { useAtomValue } from "@effect/atom-react";

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(() =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 6 potential issues.

Fix All in Cursor

❌ 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,
}),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.

appendCustomCodexModels(models, input.customModels ?? []),
),
skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),
...(rateLimits ? { rateLimits } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.

usedPercent: Math.min(100, Math.max(0, window.usedPercent)),
resetsAt: window.resetsAt,
})),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e67d3e2. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@juliusmarminge

Copy link
Copy Markdown
Member

Closing in favor of #2829 (orchestration V2).

#2829 deletes the V1 orchestration layer this PR builds on — apps/server/src/orchestration/**, provider/Layers/*Adapter.ts and provider/Services/** are removed and replaced by apps/server/src/orchestration-v2/**, with the IPC surface renamed to ORCHESTRATION_V2_WS_METHODS. The files this PR touches either no longer exist or are rewritten, so it can't be rebased — it would need reimplementing against the V2 adapters.

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 main, port the change to the V2 equivalent, and reopen (or open a fresh PR). Ping me and I'll prioritise the review.

@sharull9

Copy link
Copy Markdown
Author

Ok will do that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants