feat(web): show Codex plan usage in header

This commit is contained in:
Jack Stuart
2026-08-26 18:43:52 +08:00
parent a51563ce1f
commit b00ab3ceea
16 changed files with 444 additions and 34 deletions
+44 -2
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Pure parsing + formatting of Claude Code statusline telemetry.
* @fileoverview Pure parsing + formatting of Claude and Codex plan telemetry.
*
* Claude Code (v2.1.80+) pipes a JSON blob to a configured `statusLine.command`
* on each render. On Pro/Max subscriptions that blob carries a `rate_limits`
@@ -15,7 +15,10 @@
* Only those two windows exist (no Opus-weekly field). `rate_limits` is absent
* before the first API response and for non-subscriber auth — both yield null.
*
* All functions are pure for testability. See `test/usage-telemetry.test.ts`.
* The Codex parser consumes the read-only `account/rateLimits/read` app-server
* response and selects only the main `codex` bucket, excluding model-specific
* buckets. All functions are pure for testability. See
* `test/usage-telemetry.test.ts` and `test/codex-plan-usage.test.ts`.
*
* @module usage-telemetry
*/
@@ -51,6 +54,22 @@ export interface RawStatuslinePayload {
model?: { display_name?: string };
}
interface RawCodexRateLimitWindow {
usedPercent?: unknown;
windowDurationMins?: unknown;
resetsAt?: unknown;
}
interface RawCodexRateLimitSnapshot {
primary?: RawCodexRateLimitWindow | null;
secondary?: RawCodexRateLimitWindow | null;
}
interface RawCodexRateLimitsResponse {
rateLimits?: RawCodexRateLimitSnapshot | null;
rateLimitsByLimitId?: Record<string, RawCodexRateLimitSnapshot | null> | null;
}
function clampPct(n: number): number {
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(100, n));
@@ -88,6 +107,29 @@ export function parseStatusTelemetry(data: RawStatuslinePayload | undefined): St
return t;
}
/** Normalize the main Codex app-server bucket into the chip's two known windows. */
export function parseCodexRateLimitsResponse(value: unknown): StatusTelemetry | null {
if (!value || typeof value !== 'object') return null;
const response = value as RawCodexRateLimitsResponse;
const snapshot = response.rateLimitsByLimitId?.codex ?? response.rateLimits;
if (!snapshot || typeof snapshot !== 'object') return null;
const telemetry: StatusTelemetry = {};
for (const window of [snapshot.primary, snapshot.secondary]) {
if (!window || typeof window.usedPercent !== 'number' || !Number.isFinite(window.usedPercent)) continue;
if (window.windowDurationMins !== 300 && window.windowDurationMins !== 10_080) continue;
const resetsAt =
typeof window.resetsAt === 'number' && Number.isFinite(window.resetsAt) && window.resetsAt > 0
? Math.round(window.resetsAt * 1000)
: 0;
const normalized = { usedPercentage: clampPct(window.usedPercent), resetAt: resetsAt };
if (window.windowDurationMins === 300) telemetry.fiveHour = normalized;
if (window.windowDurationMins === 10_080) telemetry.sevenDay = normalized;
}
return telemetry.fiveHour || telemetry.sevenDay ? telemetry : null;
}
/**
* Current-session status for the in-terminal statusline footer. This is the
* "status of the current session" the user sees in Claude's footer — distinct