feat: add Claude Code session usage and account credits - #38
Conversation
…windows
Implements openspec/changes/project-claude-code-session-usage.
- HostUsage / threadUsageSnapshotSchema: add optional Claude.ai plan-window
fields (planFiveHourUsedPercent/ResetsAtUnix, planSevenDayUsedPercent/
ResetsAtUnix); reject a reset without its matching used percent.
- Claude adapter: parse Turn Result usage.result (Session cost, per-model
token totals, latest cache hit rate) and rate_limit_event plan windows;
merge into one latest-still-applicable HostUsage snapshot per Session.
- rate_limit_event parsing reads the real Claude Code wire shape
(rate_limit_info.unifiedWindows.{five_hour,seven_day}, utilization as a
0-1 fraction) rather than the flat 0-100 shape the SDK's .d.ts implies,
confirmed against a live payload; keeps a flat-field fallback for
forward/backward compatibility. Per-model weekly breakdowns and overage
are intentionally ignored.
- Renderer: Usage details popover gains optional 5-hour / 7-day limit rows;
collapsed summary and control visibility unchanged (CH/cost only).
- Host inspection round-trips the new fields without touching
accountCredits; Codex native tokenUsage/updated carrier untouched.
Tests: harness-adapter, shared-contracts, claude-code adapter (native
message, sdk-transport, adapter), renderer-usage-control, protocol-core,
host-runtime, and e2e coverage. Full repo typecheck/lint/vitest pass.
…and refresh
Builds on the earlier Session Usage change with a full account-credits
pipeline for Claude Code, matching (and redesigning) Grok's existing pill.
Renderer (Grok + Claude Code, shared UI):
- Replace the plain dot+percent credits pill with a radial progress ring
(createRendererUsageRing, shared with a future dual-window pill).
- Rewrite the credits popover as stacked tiles: a headline row (big percent,
period label, reset) plus one full-width bar per productUsage entry —
every row gets its own progress bar, not just the sub-rows.
- formatRendererCreditsReset: same-day resets read as a precise time ("4:12
PM today"); every other reset carries date + time together ("Aug 28, 6:00
PM") instead of dropping the time once >24h out — the source data is
minute-precise for both windows, so the display no longer throws that away.
- Both popovers get a raised-card chrome (color-mix over Canvas/CanvasText,
larger radius, real elevation shadow) instead of a flat system panel;
the credits popover adds a tone-tinted glow.
- accountCreditsSnapshotSchema: periodType gains "five_hour"/"seven_day";
accountCreditsProductUsageSchema gains an optional per-entry resetsAt.
- Fix shouldRetryExternalThreadUsage's Grok-only retry-for-credits carve-out
to cover any agent whose Adapter implements credits() (currently Grok and
Claude Code) — Claude Code's credits pill was going permanently blank
after switching threads because Usage alone was enough to stop the retry
loop before credits ever got a chance to populate for agents other than
"grok".
Claude Code Adapter:
- Promote plan-limit tracking from Session-level to Adapter-level
(#latestPlanLimit), since the 5h/7d windows are account-wide, not
per-thread — shared by every concurrent Session (tested).
- projectClaudePlanLimitToCredits: five-hour leads as the primary metric
(periodType "five_hour"), seven-day rides along as a productUsage entry
("7-day window") when both are known; falls back to seven-day alone.
- New on-demand pull: ClaudeTurnTransport.getPlanLimit() calls the SDK's
Query#usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET() control
method (the same channel getContextUsage() already uses) to ask Claude
Code's /usage data for the current plan-window utilization right now,
instead of only replaying whatever the last passive rate_limit_event said.
refreshCredits() now tries each open Session's live Transport in turn
until one answers, routing a fresh answer through the same
#handlePlanLimit path as the push so it updates both the Adapter-wide
credits() cache and that Thread's own Usage snapshot.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eff6296483
ℹ️ 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".
| if (agent === "codex") return false; | ||
| if (usage === null) return true; | ||
| return agent === "grok" && accountCredits === null; | ||
| return externalAgentHasAccountCredits(agent) && accountCredits === null; |
There was a problem hiding this comment.
Stop retrying credits when Claude plan limits are unavailable
For Claude threads using API-key, Bedrock, or Vertex authentication, Usage can be available while accountCredits is permanently absent. This predicate therefore remains true, the scheduler polls forever at its 8-second ceiling, and every inspection invokes refreshCredits(), producing endless experimental control-channel queries per mounted composer. Make the retry conditional on an observed/capable plan source or bound the attempts.
AGENTS.md reference: AGENTS.md:L61-L66
Useful? React with 👍 / 👎.
| if (event.lastRequestUsage) { | ||
| const cacheHitRatePercent = claudeCacheHitRatePercent(event.lastRequestUsage); | ||
| if (cacheHitRatePercent !== undefined) delta.cacheHitRatePercent = cacheHitRatePercent; |
There was a problem hiding this comment.
Clear stale cache-hit data after an incomplete request
When one request publishes a cache-hit percentage and a later request omits any required cache field, lastRequestUsage is absent and this branch leaves cacheHitRatePercent untouched; #mergeAndPublishUsage then carries the previous request's percentage forward even though the UI presents it as the latest request. Clear the old value once the newer result and context fallback cannot provide a complete breakdown.
AGENTS.md reference: AGENTS.md:L61-L66
Useful? React with 👍 / 👎.
| if (typeof utilization !== "number" || !Number.isFinite(utilization) || utilization < 0) { | ||
| return undefined; | ||
| } | ||
| const utilizationPercent = Math.min(100, Math.max(0, Math.round(utilization * 10_000) / 100)); |
There was a problem hiding this comment.
Preserve percent-scaled values in flat rate-limit events
The same parser handles live unifiedWindows fractions and the flat SDK fallback, but it always multiplies utilization by 100. Consequently, a documented flat value such as utilization: 45 is projected as 100% after clamping rather than 45%. Parse the flat 0–100 fallback separately from the live 0–1 window shape.
Useful? React with 👍 / 👎.
Summary
Validation
git diff --check main...HEAD