Skip to content

fix: show status badges in overview when no quota data is available#489

Open
RaghavShubham wants to merge 4 commits into
robinebers:mainfrom
RaghavShubham:fix/status-badge-hidden-in-overview
Open

fix: show status badges in overview when no quota data is available#489
RaghavShubham wants to merge 4 commits into
robinebers:mainfrom
RaghavShubham:fix/status-badge-hidden-in-overview

Conversation

@RaghavShubham
Copy link
Copy Markdown

@RaghavShubham RaghavShubham commented May 22, 2026

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.tsx scope filter too strict:
overviewLabels is built from skeletonLines with scope === "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, hasStaleData becomes true, but filteredLines stays 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 Error badge → getErrorMessage detected it → data = null, lastUpdatedAt = null → nothing rendered at all.


Before / After

Before

  • Enterprise account: selecting Claude alone in the overview shows a completely blank card — no badge, no text, no indication anything is wrong.
  • Quota data unavailable: same blank behaviour any time the plugin falls back to a status badge (rate-limited, inference-only token, etc.).

After

Scenario Overview card shows
Enterprise account (API returns 403) grey badge: Enterprise — org-level billing
Connected but no recognised quota fields grey badge: Connected — no quota data
Inference-only token / no API call made grey badge: No usage data
Normal Pro/Max account Session + Weekly progress bars (unchanged)

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 like Status). Badges explicitly declared as scope: "detail" in the manifest are intentionally excluded:

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

plugins/claude/plugin.js — Three changes:

  1. Treat HTTP 403 as org-level billing instead of throwing — emit a Status badge and clear cachedUsageData so stale quota lines cannot hide the badge.
  2. Promote orgBillingOnly to module-level cachedUsageOrgBillingOnly (same pattern as cachedUsageData), restored in both the rate-limit and min-interval reuse paths.
  3. Clear cachedUsageOrgBillingOnly = false on a successful 2xx response and in _resetState().

Tests

  • provider-card.test.tsx: regression test for the blank-card scenario; additional test confirming detail-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

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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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

Comment thread plugins/claude/plugin.js Outdated
@validatedev validatedev requested a review from Copilot May 23, 2026 21:57
@validatedev
Copy link
Copy Markdown
Collaborator

@codex review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 ProviderCard scope 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

  • orgBillingOnly is 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.

Comment thread src/components/provider-card.tsx Outdated
Comment on lines 128 to 133
// 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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment thread plugins/claude/plugin.js
Comment on lines +696 to +706
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)) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@zergzorg
Copy link
Copy Markdown
Contributor

zergzorg commented May 24, 2026

I checked the unresolved comments locally. The PR is close, but there are two real edge cases left:

  1. ProviderCard should not show every badge in overview mode. It should show:

    • normal overview-scoped lines, plus
    • unmanifested status badges such as Status: No usage data,
    • but not badges explicitly declared as detail-only in plugin.json.

    Minimal shape:

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))
  )
  1. Claude 403/org-billing needs module-level state, otherwise the badge disappears on the min-interval reuse path. Also, a 403 should clear the cached personal quota data so stale Session/Weekly data from a previous account/success cannot hide the Enterprise badge.

A minimal fix I verified locally:

  • add let cachedUsageOrgBillingOnly = false next to cachedUsageData;
  • when reusing cached data during rate-limit/min-interval skips, also restore orgBillingOnly = cachedUsageOrgBillingOnly;
  • on HTTP 403: set cachedUsageData = null, cachedUsageOrgBillingOnly = true, data = null, orgBillingOnly = true;
  • on successful 2xx usage fetch: set cachedUsageOrgBillingOnly = false;
  • reset it in _resetState().

Regression tests I added locally:

  • overview hides a manifest-declared detail-only badge while still allowing unmanifested status badges;
  • after a successful quota response, a later 403 clears stale quota lines and keeps showing Enterprise — org-level billing during the min-interval reuse window.

Validation on that local patch:

  • bun run test plugins/claude/plugin.test.js src/components/provider-card.test.tsx --run
  • result: 143/143 passed.

@robinebers
Copy link
Copy Markdown
Owner

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.
@RaghavShubham
Copy link
Copy Markdown
Author

@zergzorg Both issues have been addressed in commit 797ed33:

1. provider-card.tsx scope precision — Implemented exactly as you suggested. The skeletonLabels set is built from all manifest labels (any scope). The filter now passes: (a) lines matching overviewLabels, and (b) badge lines whose label is absent from skeletonLabels entirely. Badges explicitly declared as detail in the manifest are excluded. Added regression tests for both paths.

2. cachedUsageOrgBillingOnly module state — Promoted to module scope alongside cachedUsageData. Restored in both the rate-limit and min-interval reuse paths. On 403: cachedUsageData = null, cachedUsageOrgBillingOnly = true, data = null — stale quota lines cannot hide the badge. Cleared to false on 2xx and in _resetState(). Added tests for badge persistence across min-interval reuse and for clearing after a subsequent 2xx.

All 1091 tests pass.

@RaghavShubham
Copy link
Copy Markdown
Author

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude provider not displaying quota remaining

5 participants