feat(web): show current-provider usage in the chat box - #8445
feat(web): show current-provider usage in the chat box#8445Aditya190803 wants to merge 17 commits into
Conversation
Show session and weekly quota on each provider in Settings, from status probes and live rate-limit events. Replays the existing feature onto current main so the PR is the real 43-file change instead of a merge-commit dump of unrelated history. Co-authored-by: Cursor <cursoragent@cursor.com>
Match Claude live usage on claudeAgent, keep live patches across status refresh, format reset times with the user's clock setting, honor reduced motion on usage bars, and stop Codex rate-limit probing from swallowing interrupts. EOF Co-authored-by: Cursor <cursoragent@cursor.com>
Move subscription bars off Settings, parse Claude session resets without treating Last 7d stats as weekly quota, and wait for the Cursor composer (including the wide-PTY Run a command placeholder) before sending /usage. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflict in UsagePage.tsx: keep upstream's redesigned page (WorkspacePageContainer + topbar controls) and re-add ProviderQuotaLimitsSection inside it. Co-authored-by: Cursor <cursoragent@cursor.com>
Track which usage windows a live event patched so a periodic probe can no longer overwrite just those bars, and drop the settings-changed override that let an unavailable probe wipe last-good bars. Share the UTC date-only formatter for reset dates and tune the quota badge layout. Co-authored-by: Cursor <cursoragent@cursor.com>
…provider-usage-limits # Conflicts: # apps/server/src/provider/Layers/ProviderRegistry.test.ts
…provider-usage-limits # Conflicts: # apps/server/src/provider/Layers/GrokProvider.ts # apps/web/src/components/usage/UsagePage.tsx # docs/user/usage.md
Keep probes from hanging the rest of status checks, preserve last-good quota bars, and stop empty usage tiles from rendering without an explanation. Co-authored-by: Cursor <cursoragent@cursor.com>
Replay last-exit on a microtask so Terminal Manager can assign session.process first. On Windows, kill the PTY tree with taskkill so cmd.exe-launched CLI children do not leak. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>
Add an opt-in setting that shows session and weekly quota for the thread's current provider in the context-window hover, instead of mixing every provider into the chat UI. Co-authored-by: Cursor <cursoragent@cursor.com>
Empty new threads were showing a quota percent next to send. Keep the meter off until the thread has started a turn. Co-authored-by: Cursor <cursoragent@cursor.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:
Comment |
| if (!date) return null; | ||
| // Cursor's panel is date-only ("Resets 16 Sept"). We store that as UTC | ||
| // midnight; including a clock would invent a local time like 5:30 AM. | ||
| if (/T00:00:00(?:\.000)?Z$/.test(resetsAt)) { |
There was a problem hiding this comment.
🟡 Medium usage/ProviderQuotaLimits.tsx:39
formatUsageResetDate renders every T00:00:00Z reset as a date-only UTC value, so real midnight-UTC instants from providers such as Claude lose their meaningful clock and display the wrong local calendar date for users west of UTC. The date-only classification must come from the provider/source (for example, via an explicit flag), rather than the timestamp shape alone.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/usage/ProviderQuotaLimits.tsx around line 39:
`formatUsageResetDate` renders every `T00:00:00Z` reset as a date-only UTC value, so real midnight-UTC instants from providers such as Claude lose their meaningful clock and display the wrong local calendar date for users west of UTC. The date-only classification must come from the provider/source (for example, via an explicit flag), rather than the timestamp shape alone.
| if (isAuthoritativeUsageUnavailable(probed)) { | ||
| return probed; | ||
| } | ||
| if (published?.available === true && probed?.available !== true) { |
There was a problem hiding this comment.
🟡 Medium provider/providerUsageLimits.ts:188
A successful Grok refresh that returns undefined leaves the previous paid-tier windows visible indefinitely when published.available is true. The published?.available === true && probed?.available !== true branch treats Grok's authenticated Free/None/Unpaid result as a transient probe failure and returns published; distinguish that authoritative no-usage result from a failed probe so the old bars are cleared.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/providerUsageLimits.ts around line 188:
A successful Grok refresh that returns `undefined` leaves the previous paid-tier windows visible indefinitely when `published.available` is `true`. The `published?.available === true && probed?.available !== true` branch treats Grok's authenticated Free/None/Unpaid result as a transient probe failure and returns `published`; distinguish that authoritative no-usage result from a failed probe so the old bars are cleared.
| if (!kind || windows.has(kind)) continue; | ||
|
|
||
| const segmentLines = [line]; | ||
| for (let cursor = index + 1; cursor < lines.length && segmentLines.length < 3; cursor += 1) { |
There was a problem hiding this comment.
🟡 Medium provider/claudeUsageProbe.ts:264
For Claude's session-only output, extractWindowSegments returns a resetsAt converted using the probe host timezone instead of the account's IANA timezone (for example, Asia/Kolkata) when the hosts differ. The loop adds the following What's contributing... line to segmentLines, so segmentLines.join(" ") puts text after (Asia/Kolkata) and breaks the end-anchored IANA_TIMEZONE_PATTERN; stop collecting once the reset line has been included (or otherwise preserve the timezone suffix) before calling parseClaudeUsageWindowSegment.
- for (let cursor = index + 1; cursor < lines.length && segmentLines.length < 3; cursor += 1) {
+ for (let cursor = index + 1; cursor < lines.length && segmentLines.length < 3 && !/\breset(?:s|ting)?(?:\s+(?:at|on|in))?/i.test(segmentLines[segmentLines.length - 1] ?? ""); cursor += 1) {🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/claudeUsageProbe.ts around line 264:
For Claude's session-only output, `extractWindowSegments` returns a `resetsAt` converted using the probe host timezone instead of the account's IANA timezone (for example, `Asia/Kolkata`) when the hosts differ. The loop adds the following `What's contributing...` line to `segmentLines`, so `segmentLines.join(" ")` puts text after `(Asia/Kolkata)` and breaks the end-anchored `IANA_TIMEZONE_PATTERN`; stop collecting once the reset line has been included (or otherwise preserve the timezone suffix) before calling `parseClaudeUsageWindowSegment`.
| On web and desktop, **Settings → General → Show usage in chat** puts the current thread's provider | ||
| limits in the context-window hover next to the chat box. The bars stay hidden until that setting is | ||
| on and the thread has sent its first message, and they only show the provider selected for that | ||
| thread. |
There was a problem hiding this comment.
🟢 Low user/usage.md:15
The documentation directs users to a context-window hover that does not exist for models/providers without a context-window ring, while omitting the ComposerUsageMeter shown next to Send; those users cannot find the provider limits described here. Document the ComposerUsageMeter fallback for that case.
| On web and desktop, **Settings → General → Show usage in chat** puts the current thread's provider | |
| limits in the context-window hover next to the chat box. The bars stay hidden until that setting is | |
| on and the thread has sent its first message, and they only show the provider selected for that | |
| thread. | |
| On web and desktop, **Settings → General → Show usage in chat** puts the current thread's provider | |
| limits in the context-window hover next to the chat box when a context-window ring is available. | |
| For models/providers without a context-window ring, the `ComposerUsageMeter` appears next to | |
| **Send** instead. The bars stay hidden until that setting is on and the thread has sent its first | |
| message, and they only show the provider selected for that thread. |
🤖 Copy this AI Prompt to have your agent fix this:
In file @docs/user/usage.md around lines 15-18:
The documentation directs users to a context-window hover that does not exist for models/providers without a context-window ring, while omitting the `ComposerUsageMeter` shown next to `Send`; those users cannot find the provider limits described here. Document the `ComposerUsageMeter` fallback for that case.
| } | ||
|
|
||
| function shouldShowProviderQuota(provider: ServerProvider): boolean { | ||
| if (provider.driver === "opencode") return false; |
There was a problem hiding this comment.
🟡 Medium usage/UsageRouteScreen.tsx:519
shouldShowProviderQuota hides every provider with driver === "opencode", so mobile users connected through OpenCode Go/Zen never see quota data even when the server snapshot has available opencodeManaged limits. Remove this unconditional exclusion so those providers pass the existing visibility checks.
- if (provider.driver === "opencode") return false;🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/usage/UsageRouteScreen.tsx around line 519:
`shouldShowProviderQuota` hides every provider with `driver === "opencode"`, so mobile users connected through OpenCode Go/Zen never see quota data even when the server snapshot has available `opencodeManaged` limits. Remove this unconditional exclusion so those providers pass the existing visibility checks.
| // Reuse the probe's window resolution so a rolling notification and a full | ||
| // `account/rateLimits/read` produce identical windows, then project back to | ||
| // raw inputs for the shared normalizer. | ||
| const resolved = resolveCodexRateLimitSnapshotUsageLimits({ |
There was a problem hiding this comment.
🟡 Medium provider/runtimeUsageLimits.ts:145
A sparse Codex update containing only a duration-less primary window is resolved as Weekly, so applyUsageLimits upserts it over the existing weekly bar and leaves the session bar stale for older two-window accounts. Passing the partial snapshot directly to resolveCodexRateLimitSnapshotUsageLimits triggers its lone-window fallback; preserve the window's positional identity or merge the update with the existing snapshot before resolving it.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/runtimeUsageLimits.ts around line 145:
A sparse Codex update containing only a duration-less `primary` window is resolved as `Weekly`, so `applyUsageLimits` upserts it over the existing weekly bar and leaves the session bar stale for older two-window accounts. Passing the partial snapshot directly to `resolveCodexRateLimitSnapshotUsageLimits` triggers its lone-window fallback; preserve the window's positional identity or merge the update with the existing snapshot before resolving it.
| source: published.source, | ||
| available: true, | ||
| checkedAt: published.checkedAt, | ||
| windows: mergeUsageLimitWindows(probed.windows, livePatchedWindows), |
There was a problem hiding this comment.
🟡 Medium provider/providerUsageLimits.ts:200
A concurrent runtime update can overwrite the probe's current resetsAt and windowDurationMins with stale values, so the published quota bar shows an obsolete reset time and duration. applyRuntimeUsageLimits has already filled those omitted fields into the live patch, and mergeUsageLimitWindows(probed.windows, livePatchedWindows) gives the patch precedence as if they were freshly reported. Preserve the probe's metadata while applying the live percentage-only update, or retain which fields were actually present in the runtime event.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/providerUsageLimits.ts around line 200:
A concurrent runtime update can overwrite the probe's current `resetsAt` and `windowDurationMins` with stale values, so the published quota bar shows an obsolete reset time and duration. `applyRuntimeUsageLimits` has already filled those omitted fields into the live patch, and `mergeUsageLimitWindows(probed.windows, livePatchedWindows)` gives the patch precedence as if they were freshly reported. Preserve the probe's metadata while applying the live percentage-only update, or retain which fields were actually present in the runtime event.
| // "no usage". A hung `account/rateLimits/read` must not consume the outer | ||
| // 10s probe budget and discard account/models/skills already fetched. | ||
| // Rethrow interrupts so a cancelled probe cannot finish as success. | ||
| const rateLimitsResponse = yield* client.request("account/rateLimits/read", undefined).pipe( |
There was a problem hiding this comment.
🟠 High Layers/CodexProvider.ts:411
A slow but successful provider probe now returns a timeout error when skills/list and paginated model/list consume most of the 10-second deadline: the sequential account/rateLimits/read call can wait another 3 seconds and trigger the outer timeout, discarding the fetched account, models, and skills. Start the optional rate-limit request concurrently with the other requests or bound it by the remaining probe deadline.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexProvider.ts around line 411:
A slow but successful provider probe now returns a timeout error when `skills/list` and paginated `model/list` consume most of the 10-second deadline: the sequential `account/rateLimits/read` call can wait another 3 seconds and trigger the outer timeout, discarding the fetched account, models, and skills. Start the optional rate-limit request concurrently with the other requests or bound it by the remaining probe deadline.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3168a0c74c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const rateLimitsResponse = yield* client.request("account/rateLimits/read", undefined).pipe( | ||
| Effect.timeoutOption(Duration.millis(RATE_LIMITS_PROBE_TIMEOUT_MS)), | ||
| Effect.catchCause((cause) => | ||
| Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.succeed(Option.none()), |
There was a problem hiding this comment.
Keep quota reading outside Codex's 10-second status deadline
When the existing account/models/skills requests consume most of AUTH_PROBE_TIMEOUT_MS, this new sequential request can still be pending when the outer timeout fires. That interrupt is deliberately rethrown here, so checkCodexProviderStatus returns its full error snapshot instead of retaining a healthy Codex status with usage omitted; this occurs for otherwise working accounts whenever account/rateLimits/read cannot finish in the remaining probe budget.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e464240. Configure here.
| ? window.windowDurationMins | ||
| : reported.length > 1 && index === 0 | ||
| ? CODEX_SESSION_WINDOW_DURATION_MINS | ||
| : CODEX_WEEKLY_WINDOW_DURATION_MINS; |
There was a problem hiding this comment.
Sparse Codex updates mislabel session
Medium Severity
Live Codex account.rate-limits.updated payloads are run through resolveCodexRateLimitSnapshotUsageLimits, which treats a lone duration-less primary window as weekly. That heuristic is correct for a full snapshot after Codex dropped the session cap, but runtime notifications are documented as sparse. A primary-only event then upserts the weekly bar (via kind-keyed merge) with session utilization, so the chat meter and Usage page show the wrong window during a turn.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e464240. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a cross-platform feature rather than a small UI tweak: it adds provider-specific subprocess/ACP probes, live snapshot ingestion, shared PTY lifecycle changes, new schema fields, and web/mobile surfaces. These changes alter routine provider refreshes and carry unresolved timeout, quota-merging, timezone, and platform-process risks that require human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


Stacked on #1732. Unique changes vs that branch: remaining session/weekly usage for the current thread's provider next to the chat box.
Review only this stack
Settings (opt-in, default off)
Chat box hover after a turn
Test plan
Note
Medium Risk
Touches provider status checks and PTY subprocess probing on every refresh, but failures degrade to “usage unavailable” rather than blocking auth or chat.
Overview
Adds
usageLimitsto provider status snapshots across Claude, Codex, Cursor, Grok, and OpenCode (Go/Zen), using PTY CLI probes, Codex app-serveraccount/rateLimits/read, OpenCode inventory fields, and Grok ACP auth plus TUI/usage. Snapshots carry structured windows (percent used, reset times, unavailable reasons for API-key/Bedrock/free tiers).Server lifecycle:
makeManagedServerProvidergainsapplyUsageLimitsso liveaccount.rate-limits.updatedevents (viaProviderUsageLimitsIngestion) patch quotas without waiting for the next health refresh, with merge logic so in-flight probes/enrichment do not stomp fresher runtime updates. Claude/Cursor/Grok drivers optionally injectPtyAdapterfrom the registry layer so probes work in full server runtimes.Clients: Mobile Usage gets a Provider limits section with progress bars per environment/provider. Desktop settings test expects
showProviderUsageInComposer: true(feeds the stacked chat-box meter)..claude-work-test/is gitignored to avoid committing test session files.Reviewed by Cursor Bugbot for commit e464240. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add provider usage meter to chat composer and provider usage probes
showProviderUsageInComposersetting and aComposerUsageMetercomponent to display current provider usage in the chat box.ProviderUsageLimitsIngestionLivelayer to apply live usage updates to provider snapshots.killusestaskkill /T, andonExitreplays the last event to late subscribers.NodePtyProcess.killon Windows now spawnstaskkillvianode:child_process.spawn; check fallback behavior inNodePtyAdapter.tsiftaskkillfails.📊 Macroscope summarized 3168a0c. 40 files reviewed, 16 issues evaluated, 8 issues filtered, 8 comments posted
🗂️ Filtered Issues
apps/mobile/src/features/usage/UsageRouteScreen.tsx — 1 comment posted, 2 evaluated, 1 filtered
usageLimits.available: truewith an emptywindowsarray, this branch renders only the provider name and no meter or explanation. Empty windows are a valid decoded outcome when all received windows have invalid percentages, so the mobile Usage screen presents a misleading blank provider row instead of treating usage as unavailable or hiding it. [ Out of scope (post-validation triage) ]apps/server/src/provider/Layers/CursorProvider.ts — 0 comments posted, 1 evaluated, 1 filtered
probeCursorUsageLimitscall exposes an immediate-exit cleanup bug incollectPtyProbeOutput: its replayedonExitcallback can settle the promise before that helper installs its timeout, after which it still installs an uncleared 25-second timer. Thus, when the usage PTY exits immediately (for example, an interactive Cursor launch failure), this successful status check leaves a referenced Node timer behind and can delay server-process shutdown by 25 seconds. [ Out of scope (triage) ]apps/server/src/provider/Layers/GrokProvider.ts — 0 comments posted, 1 evaluated, 1 filtered
probeGrokUsageLimitscall exposes an immediate-exit cleanup bug incollectPtyProbeOutput: its replayedonExitcallback can settle before the helper creates its timeout, but the helper then creates an uncleared 10-second timer. A Grok usage PTY that exits immediately (such as an interactive-launch failure) therefore leaves a referenced timer that can delay server-process shutdown despite the status probe having returned. [ Out of scope (triage) ]apps/server/src/provider/Layers/ProviderUsageLimitsIngestion.ts — 0 comments posted, 1 evaluated, 1 filtered
Effect.forkScopedschedules the entireStream.runForEachat line 62, whileproviderService.streamEventsis backed byStream.fromPubSub, whose subscription is acquired only when that fork begins running. A rate-limit event published in that gap has no subscriber and is dropped, so the first live Claude/Codex usage update can remain stale until a later status refresh. [ Out of scope (post-validation triage) ]apps/server/src/provider/opencodeRuntime.ts — 0 comments posted, 1 evaluated, 1 filtered
providerId in OPENCODE_MANAGED_PROVIDER_DESCRIPTORSalso accepts inheritedObject.prototypenames such asconstructorortoString. A configured/returned provider with one of those IDs is therefore treated as a managed Go/Zen provider, but the returned value is the inherited function rather than a descriptor; downstream usage extraction readsdescriptor.labelasundefinedand can emit a malformed usage window for that unrelated provider. Check own keys (for example,Object.hasOwn) instead. [ Out of scope (post-validation triage) ]apps/web/src/components/usage/ProviderQuotaLimits.tsx — 1 comment posted, 4 evaluated, 3 filtered
isGrokFreeTierrecognizes only"free", but the server classifies"none"and"unpaid"subscription tiers as non-paying tiers too and deliberately omits their quota snapshots. Consequently those authenticated Grok accounts reachproviderQuotaNoticeas the generic"Usage data unavailable"rather than the intended paid-tier-only explanation. [ Out of scope (post-validation triage) ]providerQuotaNoticereports no notice for anavailablesnapshot even when itswindowsarray is empty. This is reachable for a forward-compatible payload: the contract deliberately drops unrecognized window kinds while retainingavailable: true.QuotaEnvironmentGroupthen renders the provider header butProviderUsageBarsreturnsnullfor that empty array, leaving a blank provider card instead of hiding it or explaining that no supported quota windows are available. [ Out of scope (post-validation triage) ]showEnvironmentLabelsis based on every entry inconfigs, rather than on the quota groups that survive filtering. With two connected configurations but quota-capable providers in only one,collectQuotaGroupsreturns one group while assigning it an unnecessary environment heading, contradicting the intended single-environment-with-quota-data presentation. [ Out of scope (post-validation triage) ]