fix: show status badges in overview when no quota data is available#489
fix: show status badges in overview when no quota data is available#489RaghavShubham wants to merge 4 commits into
Conversation
When a plugin returns only a "Status" badge (e.g. "No usage data" or "Rate limited") and the card is rendered in overview scope, the badge was silently filtered out because its label was not listed as an overview-scoped line in plugin.json. This left the card visibly blank after the first successful probe set `lastUpdatedAt`, making `hasStaleData` true while `filteredLines` remained empty. Fix the scope filter in ProviderCard to always pass badge-type lines through regardless of scope — they are status indicators, not metric lines, and should always be visible. Also improve the Claude plugin's fallback message when the usage API responds successfully but returns no recognized quota fields (e.g. Enterprise plans or future plan types): show "Connected — no quota data" instead of the generic "No usage data" so users can distinguish a working connection from an authentication or network failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The personal usage endpoint (/api/oauth/usage) returns 403 for Enterprise accounts because their billing is tracked at the organisation level, not per-user. Previously the plugin treated any 403 as a token-expired error, which caused the JS probe to throw, the Rust runtime to emit an Error badge, and the state management to set data=null. On the first load this left the provider card completely blank because hasStaleData was false and no PluginError was prominently surfaced. Fix: intercept 403 from the usage endpoint before the generic isAuthStatus check and treat it as "no personal quota data" instead. The probe no longer throws, returns a "Status: Enterprise — org-level billing" badge, and the card renders meaningful content on the first load. A three-way fallback is now emitted when lines is empty: • 403 response → "Enterprise — org-level billing" • 200 but no recognized quota fields → "Connected — no quota data" • Inference-only / no API call → "No usage data" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
@codex review |
There was a problem hiding this comment.
Pull request overview
This PR fixes an overview rendering bug where providers that only emit a status badge (e.g. “No usage data”, “Rate limited”) could appear blank in overview/menu-bar scope, and it refines the Claude plugin’s fallback status messaging when quota data can’t be derived.
Changes:
- Update
ProviderCardscope filtering so status-only output isn’t silently dropped in overview scope. - Improve Claude plugin behavior/messages for “connected but no quota fields”, and for usage API 403 (Enterprise org-billing).
- Add/adjust regression tests covering the overview badge scenario and Claude fallback paths.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/components/provider-card.tsx | Adjusts runtime line filtering logic in overview scope to avoid blank cards. |
| src/components/provider-card.test.tsx | Adds a regression test ensuring a status badge renders in overview scope. |
| plugins/claude/plugin.js | Adds clearer fallback badges for “no quota fields” and Enterprise (403) scenarios. |
| plugins/claude/plugin.test.js | Updates/adds tests to validate the new Claude status badge behaviors and API-call skipping. |
Comments suppressed due to low confidence (1)
plugins/claude/plugin.js:639
orgBillingOnlyis a per-probe local flag. If the usage request is skipped on subsequent probes (min-interval throttle or existing rate-limit window), the code won’t remember that the last real response was 403 and may fall back to the wrong status badge. Consider deriving this state from the cached value (or caching a sentinel) so the badge remains accurate across skipped polls.
let lines = []
let rateLimited = false
let retryAfterSeconds = null
let orgBillingOnly = false // true when the API returned 403 (Enterprise org-level billing)
if (canFetchLiveUsage) {
if (nowMs < rateLimitedUntilMs) {
// Still within a rate-limit window from a previous probe call — skip the
// API request entirely and surface the remaining wait time to the user.
rateLimited = true
retryAfterSeconds = Math.ceil((rateLimitedUntilMs - nowMs) / 1000)
data = cachedUsageData
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Badge lines are status indicators (e.g. "No usage data", "Rate limited") and must | ||
| // always be shown regardless of scope so the overview card is never silently blank. | ||
| const filteredLines = scopeFilter === "all" | ||
| ? lines | ||
| : lines.filter(line => overviewLabels.has(line.label)) | ||
| : lines.filter(line => line.type === "badge" || overviewLabels.has(line.label)) | ||
|
|
There was a problem hiding this comment.
Fixed in commit 797ed33. The overview scope filter now builds a skeletonLabels set from all labels in the manifest (any scope). Lines whose label is in overviewLabels are shown normally. Badge lines whose label is absent from skeletonLabels entirely are shown (unmanifested runtime indicators like Status). Badge lines whose label is in skeletonLabels as detail-only are hidden - preventing detail-only badges from leaking into overview. Added a regression test in provider-card.test.tsx confirming this behaviour.
| if (resp.status === 403) { | ||
| // A 403 from the usage endpoint means the account type does not have access | ||
| // to personal quota data. Enterprise accounts are billed at the organisation | ||
| // level and consistently return 403 here. Treat it as "no personal quota data" | ||
| // so the card shows a helpful badge rather than the error state, which leaves | ||
| // it blank on first load. A 401 (truly expired token) falls through to the | ||
| // isAuthStatus handler below. | ||
| ctx.host.log.info("usage API returned 403 — organisation-level billing; no personal quota data") | ||
| orgBillingOnly = true | ||
| data = cachedUsageData // keep previous cache if any, otherwise null | ||
| } else if (ctx.util.isAuthStatus(resp.status)) { |
There was a problem hiding this comment.
Fixed in commit 797ed33. On HTTP 403 the handler now explicitly sets cachedUsageData = null and data = null before setting orgBillingOnly = true, so stale Session/Weekly lines from a previous successful fetch cannot survive and hide the Enterprise badge. cachedUsageOrgBillingOnly is also promoted to module scope and restored in both the rate-limit and min-interval reuse paths, so the badge persists across throttled probes. Added tests for badge persistence across min-interval reuse and for clearing after a later successful 2xx response.
|
I checked the unresolved comments locally. The PR is close, but there are two real edge cases left:
const skeletonLabels = new Set(skeletonLines.map(line => line.label))
const filteredLines = scopeFilter === "all"
? lines
: lines.filter(line =>
overviewLabels.has(line.label) ||
(line.type === "badge" && !skeletonLabels.has(line.label))
)
A minimal fix I verified locally:
Regression tests I added locally:
Validation on that local patch:
|
|
before/after screenshots would help thank you! |
…persistence Two issues raised by reviewers: 1. provider-card.tsx (zergzorg): The first fix let *all* badge-typed lines through the overview scope filter, including badges that are explicitly declared as detail-only in plugin.json. Tighten the rule: only pass through badge lines whose label is *absent* from skeletonLines entirely (i.e. runtime status indicators like "Status", "Rate limited"). Badges declared in the manifest at any scope follow the normal label-match path. Add a regression test confirming detail-manifest badges stay hidden in overview. 2. plugin.js (zergzorg): orgBillingOnly was a function-local variable, so on the second probe() call — when the min-interval guard skips the real API fetch — it reset to false and the Enterprise card fell back to "No usage data". Promote to module-level cachedUsageOrgBillingOnly (alongside cachedUsageData) and restore it in both the rate-limit and min-interval reuse paths. Clear it back to false on a successful 2xx response and in _resetState(). Add tests for badge persistence across min-interval reuse and for clearing after a subsequent successful response. All 1091 tests pass.
|
@zergzorg Both issues have been addressed in commit 1. provider-card.tsx scope precision — Implemented exactly as you suggested. The 2. cachedUsageOrgBillingOnly module state — Promoted to module scope alongside All 1091 tests pass. |
|
@robinebers Added a Before / After section to the PR description with a table showing what each scenario renders before and after this fix. The short version: Before: selecting Claude alone in overview shows a completely blank card — no badge, no text — because (a) Enterprise accounts get a 403 which was treated as a thrown error leaving nothing to render, and (b) even when a status badge was produced, the scope filter silently dropped it. After: the card always shows a meaningful badge — Enterprise — org-level billing, Connected — no quota data, or No usage data depending on the situation — and normal Pro/Max accounts continue showing the Session + Weekly progress bars unchanged. Unfortunately the Tauri app cannot be run headlessly so actual screenshots are not possible to attach here, but the Before/After table in the description captures the full behaviour difference. Happy to guide you through taking screenshots locally if that would help! |
Problem
When a plugin returns only a status badge — "No usage data" or "Enterprise — org-level billing" — and the card is rendered in overview/menu-bar scope, the badge was silently dropped, leaving the card visibly blank.
Root cause — two separate bugs
Bug 1 —
provider-card.tsxscope filter too strict:overviewLabelsis built fromskeletonLineswithscope === "overview". For Claude that is only"Session"and"Weekly". The fallback badge has label"Status"— not in the set — so it was filtered out.After the first probe sets
lastUpdatedAt,hasStaleDatabecomestrue, butfilteredLinesstays empty → blank card.Bug 2 — Claude plugin throws on Enterprise accounts:
Anthropic personal usage API returns HTTP 403 for Enterprise (org-level billing) accounts. The plugin treated that as a generic auth failure → threw → Rust emitted an
Errorbadge →getErrorMessagedetected it →data = null, lastUpdatedAt = null→ nothing rendered at all.Before / After
Before
After
The badge persists correctly on subsequent probes even when the API call is skipped by the min-interval throttle — fixes the case where the Enterprise badge would flip to No usage data on the second probe.
Fix
src/components/provider-card.tsx— Tighten the overview scope filter so only unmanifested badge lines pass through (runtime status indicators likeStatus). Badges explicitly declared asscope: "detail"in the manifest are intentionally excluded:plugins/claude/plugin.js— Three changes:Statusbadge and clearcachedUsageDataso stale quota lines cannot hide the badge.orgBillingOnlyto module-levelcachedUsageOrgBillingOnly(same pattern ascachedUsageData), restored in both the rate-limit and min-interval reuse paths.cachedUsageOrgBillingOnly = falseon a successful 2xx response and in_resetState().Tests
provider-card.test.tsx: regression test for the blank-card scenario; additional test confirmingdetail-manifest badges do not leak through in overview.plugin.test.js: 403 → Enterprise badge; badge persistence across min-interval reuse; clearing after a later successful 2xx; 403 after token refresh → Enterprise badge (not "Token expired").All 1091 tests pass.
Closes #440