feat(usage): plan usage limits header chip via statusLine telemetry

Surface Claude subscription plan usage limits (5-hour rolling + 7-day
weekly: percent used + reset time) in the header, opt-in via App Settings
→ Display → "Plan Usage Limits" (default OFF, no behavior change when off).

A Codeman-managed Claude statusLine exporter forwards the rate_limits JSON
to a new auth-exempt POST /api/status-telemetry (same loopback + hook-secret
gate as /api/hook-event); parsed telemetry broadcasts over SSE
session:statusTelemetry to a header chip (amber >=80%, red >=95%, reset
times on hover). The exporter prints the same summary back as the
in-terminal footer (print-through).

- src/usage-telemetry.ts: pure parser/formatter (epoch-sec -> ms, clamp,
  change signature) + test/usage-telemetry.test.ts
- hooks-config.ts: generateStatusLineCommand + applyStatusLineConfig
  (add/remove; never clobbers a user's own statusLine)
- session-routes.ts: inject gate (Claude-only, Codeman-managed cases),
  driven by create-payload statusLineTelemetry (session-ui.js)
- schemas.ts: StatusTelemetrySchema + showPlanUsageLimits + payload field
- frontend: header chip, applyHeaderVisibilitySettings toggle,
  renderIndexHtml strip, _onSessionStatusTelemetry handler

Schema empirically confirmed against Claude Code 2.1.177 (Claude Max):
only five_hour/seven_day windows exist (no Opus-weekly field); rate_limits
is absent before the first API response and for non-subscriber auth. Design
+ verification method in docs/usage-limits-display-plan.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-06-14 05:00:28 +02:00
co-authored by Claude Opus 4.8
parent 0809f59f0f
commit c82f6c802e
17 changed files with 657 additions and 4 deletions
+30
View File
@@ -157,6 +157,7 @@ const _SSE_HANDLER_MAP = [
[SSE_EVENTS.SESSION_LIMIT_RESUME, '_onSessionLimitResume'],
[SSE_EVENTS.SESSION_LIMIT_RESUME_CANCELLED, '_onSessionLimitResumeCancelled'],
[SSE_EVENTS.SESSION_CLI_INFO, '_onSessionCliInfo'],
[SSE_EVENTS.SESSION_STATUS_TELEMETRY, '_onSessionStatusTelemetry'],
// Scheduled runs
[SSE_EVENTS.SCHEDULED_CREATED, '_onScheduledCreated'],
@@ -1812,6 +1813,35 @@ class CodemanApp {
}
}
// Claude plan usage limits (5-hour + weekly) — account-global, so the latest
// sample from any session drives the shared header chip.
_onSessionStatusTelemetry(data) {
this._latestPlanUsage = data;
this.updatePlanUsageChip(data);
}
updatePlanUsageChip(data) {
const chip = document.getElementById('planUsageChip');
if (!chip || !data) return;
const pct = (w) => (w && typeof w.usedPercentage === 'number' ? Math.round(w.usedPercentage) : null);
const five = pct(data.fiveHour);
const seven = pct(data.sevenDay);
if (five === null && seven === null) return;
const parts = [];
if (five !== null) parts.push(`5h ${five}%`);
if (seven !== null) parts.push(`7d ${seven}%`);
chip.textContent = parts.join(' · ');
// Color hint as a window nears exhaustion.
const max = Math.max(five ?? 0, seven ?? 0);
chip.classList.toggle('header-plan-usage--warn', max >= 80 && max < 95);
chip.classList.toggle('header-plan-usage--crit', max >= 95);
const resetStr = (w) => (w && w.resetAt ? new Date(w.resetAt).toLocaleString() : '—');
chip.title =
`Claude plan usage\n` +
`5-hour: ${five ?? '—'}% (resets ${resetStr(data.fiveHour)})\n` +
`Weekly: ${seven ?? '—'}% (resets ${resetStr(data.sevenDay)})`;
}
// Scheduled runs
_onScheduledCreated(data) {
this.currentRun = data;