Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion plugins/claude/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,13 @@
: "Live usage rate limited — data may be stale"
lines.push(ctx.line.text({ label: "Note", value: noteText }))
} else if (lines.length === 0) {
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
if (canFetchLiveUsage && data !== null) {
// Successfully connected to the usage API but the response contained no
// recognized quota fields (e.g. Enterprise plans or unsupported plan types).
lines.push(ctx.line.badge({ label: "Status", text: "Connected — no quota data", color: "#a3a3a3" }))
} else {
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
}
}

return { plan: plan, lines: lines }
Expand Down
25 changes: 24 additions & 1 deletion plugins/claude/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,11 @@ describe("claude plugin", () => {
expect(() => plugin.probe(ctx)).toThrow("Usage request failed")
})

it("shows status badge when no usage data and ccusage is unavailable", async () => {
it("shows 'Connected — no quota data' when API returns no recognized fields", async () => {
// The usage API connected successfully but returned no fields that the plugin
// understands (e.g. Enterprise plans or future plan types). The badge must
// say "Connected — no quota data" to distinguish "reachable but unrecognized"
// from "never connected / inference-only token".
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "token" } })
ctx.host.fs.exists = () => true
Expand All @@ -843,7 +847,26 @@ describe("claude plugin", () => {
expect(result.lines.find((l) => l.label === "Last 30 Days")).toBeUndefined()
const statusLine = result.lines.find((l) => l.label === "Status")
expect(statusLine).toBeTruthy()
expect(statusLine.text).toBe("Connected — no quota data")
})

it("shows 'No usage data' for inference-only token with no local ccusage", async () => {
// Inference-only tokens (CLAUDE_CODE_OAUTH_TOKEN env var) skip the live usage API
// entirely. When there is also no local ccusage data the badge should say
// "No usage data" — not "Connected — no quota data" — because no API call was made.
const ctx = makeCtx()
ctx.host.fs.readText = () => JSON.stringify({ claudeAiOauth: { accessToken: "stored-token" } })
ctx.host.fs.exists = () => true
ctx.host.env.get.mockImplementation((name) =>
name === "CLAUDE_CODE_OAUTH_TOKEN" ? "env-inference-token" : null
)
const plugin = await loadPlugin()
const result = plugin.probe(ctx)
const statusLine = result.lines.find((l) => l.label === "Status")
expect(statusLine).toBeTruthy()
expect(statusLine.text).toBe("No usage data")
// The live usage API must not be called for inference-only tokens
expect(ctx.host.http.request).not.toHaveBeenCalled()
})

it("passes resetsAt through as ISO when present", async () => {
Expand Down
23 changes: 23 additions & 0 deletions src/components/provider-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,29 @@ describe("ProviderCard", () => {
expect(document.querySelector('[data-slot="progress-refreshing"]')).toBeNull()
})

it("always shows badge lines in overview scope even when label is not in skeleton", () => {
// Regression test: status badges ("No usage data", "Rate limited") were previously
// filtered out in overview mode because their label ("Status") wasn't listed as an
// overview-scoped line in plugin.json, causing a silently blank card.
render(
<ProviderCard
name="Claude"
displayMode="used"
scopeFilter="overview"
lastUpdatedAt={Date.now() - 60_000}
skeletonLines={[
{ type: "progress", label: "Session", scope: "overview" },
{ type: "progress", label: "Weekly", scope: "overview" },
]}
lines={[
{ type: "badge", label: "Status", text: "No usage data" },
]}
/>
)
expect(screen.getByText("Status")).toBeInTheDocument()
expect(screen.getByText("No usage data")).toBeInTheDocument()
})

it("shows inline warning with stale data on refresh error", () => {
render(
<ProviderCard
Expand Down
4 changes: 3 additions & 1 deletion src/components/provider-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,11 @@ export function ProviderCard({
const filteredSkeletonLines = scopeFilter === "all"
? skeletonLines
: skeletonLines.filter(line => line.scope === "overview")
// 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))

Comment thread
RaghavShubham marked this conversation as resolved.
Outdated
const hasResetCountdown = filteredLines.some(
(line) => line.type === "progress" && Boolean(line.resetsAt)
Expand Down
Loading