mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
docs(usage): update plan-usage design doc to match what shipped
Rewrite to the as-built design: header chip (account limits, green/yellow/red) + session-status footer split; fixed /api/status-telemetry endpoint; curl -sk; add-only create injection + settings-toggle reconcile; no CASES_DIR gate; chip robustness (live SSE + init-snapshot replay + localStorage); and the E2E bugs that earlier builds hid. Status: shipped/pushed, not released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,22 @@
|
||||
# Plan Usage Limits Display — Design Plan
|
||||
# Plan Usage Limits Display — Design & As-Built
|
||||
|
||||
> **Status: IMPLEMENTED — disabled by default (2026-06-14, not yet committed/COM'd).** Surface the user's Claude subscription **plan usage limits** (5-hour rolling + 7-day weekly windows: percent used + reset time) in the Codeman web UI via a Codeman-managed `statusLine` callback. Opt-in via App Settings → Display → **Plan Usage Limits** (`showPlanUsageLimits`, default OFF). The `rate_limits` JSON schema below was **empirically confirmed** against Claude Code 2.1.177 on a Claude Max account (2026-06-14); see the Verification appendix to reproduce.
|
||||
> **Status: SHIPPED — deployed to prod + pushed to master, not yet released (2026-06-14).** Opt-in via App Settings → Display → **Plan Usage Limits** (`showPlanUsageLimits`, default OFF). Commits `c82f6c8` (feature) → `4d9d93d` (end-to-end fixes) → `eae225b` (per-user reconcile) → `95fb5fc` (init-snapshot replay). Full suite green (2869), CI green. No changeset/version bump yet.
|
||||
>
|
||||
> **Shipped surface:** pure parser `src/usage-telemetry.ts` (+ `test/usage-telemetry.test.ts`); `applyStatusLineConfig`/`generateStatusLineCommand` in `hooks-config.ts`; `POST /api/status-telemetry` (`status-telemetry-routes.ts`, auth-exempt in `middleware/auth.ts`, schema in `schemas.ts`); SSE `session:statusTelemetry`; create-payload `statusLineTelemetry` gate in `session-routes.ts`; header chip + `applyHeaderVisibilitySettings` toggle + `renderIndexHtml` strip + `_onSessionStatusTelemetry` handler. Verified: typecheck, lint, full test suite (2866 pass), and live (endpoint/settings/render-strip on an isolated instance).
|
||||
> Two surfaces from one `statusLine` callback:
|
||||
> - **Header chip** (top-right) — account-wide **plan limits**: `5h 35% · 7d 38%`, per-window green/yellow/red.
|
||||
> - **In-terminal statusline footer** — the **current session's** status: `Opus 4.8 (1M context) in:562,411 out:1,188 ctx:56%`.
|
||||
>
|
||||
> The `rate_limits` JSON schema below was **empirically confirmed** against Claude Code 2.1.177 on a Claude Max account; see the Verification appendix to reproduce.
|
||||
|
||||
## Problem
|
||||
|
||||
Codeman has no proactive view of how much of the Claude subscription is left. Today it only learns about limits **reactively**: `usage-limit-patterns.ts` regex-scrapes ANSI-stripped terminal output for footer strings like `5-hour limit reached ∙ resets 8pm`, and extracts only the **reset time** — and only *after* Claude has already stalled. There is no "73% of your 5-hour limit used" anywhere.
|
||||
Codeman had no proactive view of how much of the Claude subscription is left. It only learned about limits **reactively**: `usage-limit-patterns.ts` regex-scrapes ANSI-stripped terminal output for footer strings like `5-hour limit reached ∙ resets 8pm`, extracting only the **reset time**, and only *after* Claude has already stalled. There was no "73% of your 5-hour limit used" anywhere.
|
||||
|
||||
We want a live gauge:
|
||||
|
||||
```
|
||||
5-hour limit ▕███▏ 15% resets 8:50am
|
||||
7-day limit ▕███████▏ 34% resets Fri
|
||||
```
|
||||
|
||||
So the operator can see a wall coming, pace overnight Ralph/autonomous runs, and (future) pre-arm auto-resume instead of waiting for the stall.
|
||||
|
||||
## Current state (verified in code)
|
||||
|
||||
| Piece | File | Behavior |
|
||||
|-------|------|----------|
|
||||
| Reactive scraper | `src/usage-limit-patterns.ts` | Pure regex over cleaned PTY output; detects limit footers, parses **reset time only**. Conservative (no reset time → ignored). |
|
||||
| Auto-resume | `src/session-auto-ops.ts` | Arms a timer at reset+2min, sends Esc + `continue`. Persists via `SessionState.autoResumeEnabled/autoResumeAt`. |
|
||||
| SSE events (existing) | `src/web/sse-events.ts` | `session:limitPauseScheduled`, `session:limitResume`, `session:limitResumeCancelled`. |
|
||||
| Settings injection | `src/hooks-config.ts` | Writes `hooks` (+ `env`/`model`) into each case's `.claude/settings.local.json`. **No `statusLine` is ever written** (confirmed via grep). |
|
||||
|
||||
Codeman therefore has **zero access to Claude Code's structured usage data** — it has never configured a statusLine, which is the channel that data flows through.
|
||||
We wanted a live, always-visible gauge so the operator can see a wall coming and pace overnight/autonomous runs — without hijacking the in-terminal statusline, which should keep showing the current session's status.
|
||||
|
||||
## Data source: the statusline `rate_limits` JSON
|
||||
|
||||
Claude Code (**v2.1.80+**; prod box runs **2.1.177**) pipes a JSON blob to a configured `statusLine.command` on stdin after each render. On Pro/Max subscriptions that blob includes `rate_limits`.
|
||||
Claude Code (**v2.1.80+**; prod box runs **2.1.177**) pipes a JSON blob to a configured `statusLine.command` on stdin after each render. On Pro/Max subscriptions that blob includes `rate_limits`. **This is the only channel that exposes plan-limit data** (see rejected alternatives) — so the feature *must* set a statusLine command, which is why the footer is also reconstructed by it (below).
|
||||
|
||||
### Confirmed schema (real captured payload)
|
||||
|
||||
@@ -49,127 +35,126 @@ Claude Code (**v2.1.80+**; prod box runs **2.1.177**) pipes a JSON blob to a con
|
||||
|
||||
**Confirmed facts & gotchas:**
|
||||
|
||||
- **Only two windows exist: `five_hour` and `seven_day`.** There is **no separate Opus-weekly field**, even on a Max/Opus account. Do **not** promise an Opus gauge in the UI.
|
||||
- `rate_limits` is **absent on the first render**, **present after the first API response**. UI must degrade to "usage not yet available."
|
||||
- statusLine fires **only in interactive TUI mode**, never `--print`. Fine — Codeman sessions are interactive TUIs.
|
||||
- **Only two windows exist: `five_hour` and `seven_day`.** There is **no separate Opus-weekly field**, even on a Max/Opus account.
|
||||
- `rate_limits` is **absent on the first render**, **present after the first API response**. UI degrades to "no chip yet."
|
||||
- statusLine fires **only in interactive TUI mode**, never `--print`. Fine — Codeman sessions are interactive TUIs (and so are Codeman-spawned ones in tmux).
|
||||
- **Subscriber-gated.** Absent for API-key / non-subscriber auth.
|
||||
|
||||
### Bonus telemetry in the same payload (free to surface)
|
||||
### Bonus telemetry in the same payload — used for the footer
|
||||
|
||||
The same stdin object also carries (non-sensitive):
|
||||
|
||||
```jsonc
|
||||
"model": { "id": "claude-opus-4-8[1m]", "display_name": "Opus 4.8 (1M context)" },
|
||||
"context_window": { "context_window_size": 1000000, "used_percentage": 2, "remaining_percentage": 98,
|
||||
"current_usage": { "input_tokens": …, "output_tokens": …,
|
||||
"cache_creation_input_tokens": …, "cache_read_input_tokens": … } },
|
||||
"cost": { "total_cost_usd": 0.0415, "total_duration_ms": …, "total_api_duration_ms": …,
|
||||
"total_lines_added": 0, "total_lines_removed": 0 },
|
||||
"effort": { "level": "xhigh" },
|
||||
"fast_mode": false, "exceeds_200k_tokens": false,
|
||||
"session_name": "…", "session_id": "…", "transcript_path": "…", "version": "…"
|
||||
```
|
||||
|
||||
One statusLine callback thus unlocks live **context-window %**, **per-session cost**, **model**, **effort**, and **fast-mode** alongside the plan limits — a meaningful expansion of what the feature can show.
|
||||
The same stdin object also carries `model.display_name`, `context_window.{used_percentage, total_input_tokens, total_output_tokens, …}`, `cost.total_cost_usd`, `effort.level`, etc. The shipped feature uses **model + token totals + context %** to build the in-terminal footer (so the statusline stays useful even though we own it). The endpoint also broadcasts `contextUsedPercentage`/`costUsd`/`modelDisplayName` alongside the limits for future chip tooltips.
|
||||
|
||||
### Alternatives considered & rejected
|
||||
|
||||
| Source | Why not |
|
||||
|--------|---------|
|
||||
| OAuth endpoint `api.anthropic.com/api/oauth/usage` | Undocumented, aggressively rate-limited, needs extracting the **encrypted** OAuth token. Only worth it for *dollar spend* data the statusline lacks. |
|
||||
| OAuth endpoint `api.anthropic.com/api/oauth/usage` | Undocumented, aggressively rate-limited, needs the **encrypted** OAuth token. Only worth it for *dollar spend*. |
|
||||
| `/usage` slash command | Interactive-only, no programmatic output. |
|
||||
| On-disk `~/.claude/` files | No usage state persisted (verified: only `daemon.status.json` = auto-updater supervisor). |
|
||||
| CLI flag (`claude usage` / `--check-usage`) | Does not exist (open upstream feature request). |
|
||||
| `StopFailure` hook | Carries only an `error_type` (`rate_limit`) on *failure* — no live percentages. Could complement, not replace. |
|
||||
| On-disk `~/.claude/` files | No usage state persisted (only `daemon.status.json` = auto-updater supervisor). |
|
||||
| CLI flag (`claude usage` / `--check-usage`) | Does not exist. |
|
||||
| `StopFailure` hook | Carries only an `error_type` on *failure* — no live percentages. |
|
||||
|
||||
## Design
|
||||
|
||||
A Codeman-managed `statusLine.command` that mirrors the existing hook-callback pattern: it POSTs the full stdin payload to a new loopback endpoint and prints whatever the server returns as the visible status text.
|
||||
## As-built architecture
|
||||
|
||||
```
|
||||
Claude TUI (managed case)
|
||||
│ renders statusline (~300ms debounce, after each assistant msg)
|
||||
Claude TUI (any Claude session, incl. linked-case/real-repo sessions)
|
||||
│ renders statusline after each assistant msg (+ /compact, mode change)
|
||||
▼
|
||||
statusLine.command ──reads stdin JSON──▶ curl POST $CODEMAN_API_URL/api/sessions/:id/statusline
|
||||
│ (X-Codeman-Hook-Secret: $(cat $CODEMAN_HOOK_SECRET_FILE))
|
||||
│ ◀── HTTP 200 body = formatted status string ──┘
|
||||
statusLine.command (settings.local.json) ──reads stdin JSON──▶
|
||||
curl -sk POST $CODEMAN_API_URL/api/status-telemetry {sessionId, data}
|
||||
(X-Codeman-Hook-Secret: $(cat $CODEMAN_HOOK_SECRET_FILE))
|
||||
│ ◀── HTTP 200 text/plain = current-SESSION status string ──┘
|
||||
▼
|
||||
printf '%s' "$body" → in-terminal statusline stays useful ("Opus 4.8 · 5h 15% · 7d 34%")
|
||||
printf '%s' "$body" → in-terminal footer: "Opus 4.8 (1M context) in:… out:… ctx:…%"
|
||||
|
||||
server: parse payload → extract rate_limits/context_window/cost/model
|
||||
→ store on Session → broadcast SSE → format & return status string
|
||||
server (status-telemetry-routes.ts):
|
||||
parse rate_limits → (if changed) store last-known + broadcast SSE session:statusTelemetry → header chip
|
||||
parse model/tokens/ctx → return the session-status footer string
|
||||
▼
|
||||
SSE session:statusTelemetry → app.js listener → header/Respawn-tab gauge
|
||||
app.js: _onSessionStatusTelemetry → chip (per-window colors) + localStorage save
|
||||
handleInit → chip from init-snapshot planUsage (fresh-load replay)
|
||||
```
|
||||
|
||||
### 1. The exporter (reuses hook plumbing)
|
||||
### 1. The exporter — `generateStatusLineCommand()` in `hooks-config.ts`
|
||||
|
||||
Mirror `curlCmd()` in `hooks-config.ts`. The managed-session env already carries `$CODEMAN_SESSION_ID`, `$CODEMAN_API_URL`, and `$CODEMAN_HOOK_SECRET_FILE` (set by `tmux-manager.buildEnvExports()`), which the hooks already rely on — the statusLine command inherits the same env. No `jq` dependency: POST the **raw** stdin and let the server parse. The server's HTTP response body is the status string to print, so formatting lives server-side and the in-terminal statusline stays useful even when attached directly (`sc`):
|
||||
Mirrors the hook `curlCmd()`. Reads the stdin JSON, POSTs `{sessionId, data}` to a **fixed** loopback path, and prints the response body back to stdout (print-through, so the footer stays useful). The managed-session env carries `$CODEMAN_SESSION_ID` / `$CODEMAN_API_URL` / `$CODEMAN_HOOK_SECRET_FILE` (from `tmux-manager.buildEnvExports()`).
|
||||
|
||||
```bash
|
||||
# conceptual — generated into settings.local.json statusLine.command
|
||||
PAYLOAD=$(cat 2>/dev/null || echo '{}')
|
||||
BODY=$(printf '%s' "$PAYLOAD" | curl -s -X POST "$CODEMAN_API_URL/api/sessions/$CODEMAN_SESSION_ID/statusline" \
|
||||
INPUT=$(cat 2>/dev/null || echo '{}'); \
|
||||
printf '{"sessionId":"%s","data":%s}' "$CODEMAN_SESSION_ID" "$INPUT" | \
|
||||
curl -sk -X POST "$CODEMAN_API_URL/api/status-telemetry" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "X-Codeman-Hook-Secret: $(cat "$CODEMAN_HOOK_SECRET_FILE" 2>/dev/null)" \
|
||||
--data @- 2>/dev/null || true)
|
||||
printf '%s' "${BODY:-codeman}" # fallback keeps a sane statusline if the server is down
|
||||
--data @- 2>/dev/null || echo codeman
|
||||
```
|
||||
|
||||
### 2. Settings injection (opt-in, Claude-only)
|
||||
⚠️ **`curl -sk`, not `curl -s`.** Prod is loopback **HTTPS with a self-signed cert**; without `-k`, curl returns `000` and the statusline silently shows nothing. `-k` is safe (loopback only). *(The existing hook curls use `-s` without `-k` and have the same latent issue on HTTPS installs — a known, separate follow-up.)*
|
||||
|
||||
Add a `statusLine` block to `generateHooksConfig()` (or a sibling) in `hooks-config.ts`, gated by a new opt-in app setting (e.g. `showPlanUsageLimits`) and only for Claude-mode cases (OpenCode/Codex emit no such JSON — gate exactly like the hooks). Merge into `settings.local.json` touching only the `statusLine` key, same as `writeHooksConfig()` merges `hooks`.
|
||||
### 2. Endpoint — `POST /api/status-telemetry` (`status-telemetry-routes.ts`)
|
||||
|
||||
### 3. New endpoint
|
||||
Fixed path (sessionId in the **body**, not the URL) so the auth exemption is an exact-match like `/api/hook-event` (`middleware/auth.ts`: loopback-only; `X-Codeman-Hook-Secret`-gated while a tunnel runs). Schema `StatusTelemetrySchema` in `schemas.ts` validates the subset; unknown keys are stripped. Pure parsing/formatting in `usage-telemetry.ts`:
|
||||
|
||||
`POST /api/sessions/:id/statusline` in `src/web/routes/session-routes.ts`:
|
||||
- Zod schema in `schemas.ts` validates the subset Codeman cares about (`rate_limits`, `context_window`, `cost`, `model`, `effort`) — all `.optional()`/`.nullish()` since fields come and go (⚠️ recall: `.optional()` rejects `null`; the payload is machine-generated so unlikely, but use `.passthrough()`/`.nullish()` defensively).
|
||||
- Auth: exempt like `/api/hook-event` (localhost-only), gated by `X-Codeman-Hook-Secret` while a tunnel is up (reuse `config/hook-secret.ts`). Dedicated rate-limit bucket — **never** the login bucket (COD-55 lesson).
|
||||
- Returns the formatted status string in the response body.
|
||||
- **Debounce/dedup:** statusline renders ~every 300ms. Server should ignore payloads with no `rate_limits` change and avoid re-broadcasting unchanged telemetry, to keep SSE quiet.
|
||||
- `parseStatusTelemetry(data)` → `{ fiveHour, sevenDay, … }` or `null`. On change (signature dedup; statusline fires often), store last-known (`plan-usage-latest.ts`) and `broadcast('session:statusTelemetry', { sessionId, …telemetry })`.
|
||||
- `parseSessionStatus(data)` + `formatSessionStatusText()` → the **footer** string `Opus 4.8 (1M context) in:562,411 out:1,188 ctx:56%` (returned as `text/plain`). Available from the first render, even before `rate_limits` appears.
|
||||
|
||||
### 4. SSE event
|
||||
### 3. SSE + frontend chip
|
||||
|
||||
Add `session:statusTelemetry` (or `account:rateLimits`) to `src/web/sse-events.ts` **and** the `SSE_EVENTS` mirror in `constants.js` (both must stay in sync), emit via `broadcast()`, handle in `app.js` (`addListener(`).
|
||||
`session:statusTelemetry` registered in `sse-events.ts` + `constants.js`. `app.js`:
|
||||
- `_onSessionStatusTelemetry` → `updatePlanUsageChip(data)` + save to `localStorage['codeman:planUsage']`.
|
||||
- `updatePlanUsageChip` renders two `5h`/`7d` windows; **per-window color by usage** — green `<60%`, yellow `60–84%`, red `≥85%` (`pu-green/pu-yellow/pu-red`); bold labels/values; reset times in the tooltip. `resets_at*1000 → Date`.
|
||||
- Chip element ships hidden (`header-plan-usage--hidden`); `applyHeaderVisibilitySettings()` reveals it client-side when the setting is on (response-viewer pattern — **no `renderIndexHtml` strip**, which kept the "title-only" render contract intact).
|
||||
|
||||
### 5. State
|
||||
### 4. Chip data robustness — three layers
|
||||
|
||||
Store last-seen telemetry on the `Session` (e.g. `_lastStatusTelemetry`), include in `session.toState()`, persist via `persistSessionState()` so a reload re-renders immediately. See *account-global* caveat below for whether to also keep a global singleton.
|
||||
1. **Live:** `session:statusTelemetry` SSE on every distinct render.
|
||||
2. **Fresh load / reconnect:** server stores the latest in `plan-usage-latest.ts`; `getLightState()` includes it as `planUsage`; the per-connection **init snapshot** replays it; `handleInit` paints the chip immediately (authoritative over localStorage). Null until the first telemetry of the process.
|
||||
3. **Offline / cross-restart:** `restorePlanUsageChip()` reads `localStorage` on load (12h freshness guard).
|
||||
|
||||
### 6. Frontend
|
||||
### 5. Injection lifecycle — works for *any* user, never self-destructs
|
||||
|
||||
A compact gauge component (two bars: 5h / 7d, percent + humanized reset). Natural home: the **header** (account-global) or beside the existing token-pause / auto-resume control at the top of the **Respawn tab** (`respawn-ui.js`). Reset time = `resets_at * 1000` → `new Date(...)`; render relative ("resets in 2h14m") with absolute on hover.
|
||||
The setting `showPlanUsageLimits` is **synced** (in `settings.json`, not a per-device `displayKey`).
|
||||
|
||||
- **On toggle** (`PUT /api/settings`, `system-routes.ts`): reconcile the exporter across **all active Claude sessions' working dirs** — inject on enable, remove on disable. Server-side and authoritative, so existing sessions get the footer + feed the chip *immediately*, no new session needed, no dependency on a client's synced localStorage.
|
||||
- **On session create** (`session-routes.ts`): **ADD-ONLY** — inject when `statusLineTelemetry` is true; **never remove**. Sessions in a repo share one `settings.local.json`, so a single create-with-false (e.g. a client whose synced setting hadn't loaded) must not yank the statusLine out from under other live sessions. Removal happens only via the explicit toggle.
|
||||
- `applyStatusLineConfig()` is **`isOurs`-guarded** (matches `/api/status-telemetry`), so a user's own hand-authored statusLine is never touched, and it **updates an out-of-date ours-command** so fixes (e.g. `-k`) propagate. **No `CASES_DIR` gate** — runs for linked cases / real repos (where sessions actually run), mirroring `updateCaseModel`.
|
||||
|
||||
## Codeman-specific considerations
|
||||
|
||||
1. **The limits are account-global, not per-session.** The 5h/7d pools are shared across every session authenticated as the same Claude account. So all sessions report the **same** numbers → prefer a **single global widget** (freshest sample wins) over a per-tab bar showing identical values. Caveat: if different sessions ever use different Claude accounts (rare — one machine usually = one login), keep per-session storage and label the global widget with the account/most-recent source.
|
||||
1. **Account-global limits.** The 5h/7d pools are shared across all sessions on the account → one shared header chip (freshest sample wins), not a per-tab bar.
|
||||
2. **The footer is owned, by necessity.** A statusLine command always replaces Claude's default footer. Since `rate_limits` *only* arrives via statusLine, we reconstruct a useful **session-status** footer (model · tokens · ctx %) from the same payload rather than showing the limits there.
|
||||
3. **`isOurs`-guarded.** Never removes/overwrites a user's own statusLine on disable; only manages the Codeman exporter.
|
||||
4. **Security envelope unchanged.** The exporter runs arbitrary shell every render — same trust model as the hook curls (localhost + `$CODEMAN_HOOK_SECRET_FILE`); reuses the hook-secret gate.
|
||||
5. **Claude-only.** OpenCode/Codex emit no `rate_limits` JSON; injection is gated to `mode === 'claude'`.
|
||||
6. **Future — auto-resume synergy.** Live percentages would let `SessionAutoOps` pre-arm *before* the wall instead of reacting to the stall footer. Not built.
|
||||
|
||||
2. **Settings-override risk.** `settings.local.json.statusLine` **overrides** the user's own `~/.claude/settings.json` statusLine inside managed sessions. The print-through design (§1) mitigates the visible degradation; keeping the feature **opt-in** avoids surprising users who have a custom statusline. (Managed Ralph/autonomous cases are rarely viewed in-terminal anyway — the web UI is the real surface.)
|
||||
## Files shipped
|
||||
|
||||
3. **Subscriber + post-first-response gating.** Degrade gracefully when `rate_limits` is absent (API-key auth, free tier, or before the first response). Keep `usage-limit-patterns.ts` as the **fallback** for older CLIs / non-subscribers.
|
||||
- `src/usage-telemetry.ts` — pure parse/format (`parseStatusTelemetry`, `parseSessionStatus`, `formatSessionStatusText`, `telemetrySignature`) + `test/usage-telemetry.test.ts`.
|
||||
- `src/hooks-config.ts` — `generateStatusLineCommand()` (`curl -sk`), `applyStatusLineConfig()` (add/update/remove, `isOurs`-guarded).
|
||||
- `src/web/routes/status-telemetry-routes.ts` — `POST /api/status-telemetry`.
|
||||
- `src/web/plan-usage-latest.ts` — process-wide last-known store for init replay.
|
||||
- `src/web/schemas.ts` — `StatusTelemetrySchema` + `showPlanUsageLimits` + create-payload `statusLineTelemetry`.
|
||||
- `src/web/middleware/auth.ts` — exemption extended to `/api/status-telemetry`.
|
||||
- `src/web/routes/session-routes.ts` — add-only create-time injection.
|
||||
- `src/web/routes/system-routes.ts` — settings-toggle reconcile.
|
||||
- `src/web/server.ts` — `getLightState().planUsage` (init snapshot).
|
||||
- `src/web/sse-events.ts` + `constants.js` — `session:statusTelemetry`.
|
||||
- Frontend: `app.js` (`_onSessionStatusTelemetry`, `updatePlanUsageChip`, `restorePlanUsageChip`, `handleInit`), `settings-ui.js` (toggle + `applyHeaderVisibilitySettings`), `index.html` (chip + toggle row), `styles.css` (chip + colors), `session-ui.js` (create payload).
|
||||
|
||||
4. **Security stays in the existing envelope.** The exporter runs arbitrary shell every render, but that's the same trust model as the hook curls (localhost + `$CODEMAN_HOOK_SECRET_FILE`). No new exposure; reuse the hook secret and a separate rate-limit bucket.
|
||||
## Bugs E2E testing caught (that unit tests didn't)
|
||||
|
||||
5. **External CLI modes.** OpenCode/Codex render their own TUIs and emit no `rate_limits` JSON. Gate the statusLine injection to Claude mode only (`isExternalCliMode()` guard), exactly as Ralph/BashToolParser are gated.
|
||||
The first "shipped" build passed every test and was broken in practice. End-to-end testing on the real install (the lesson: drive a REAL session, observe the REAL output) surfaced:
|
||||
|
||||
6. **Synergy with auto-resume (future).** Live percentages let `SessionAutoOps` pre-arm *before* the wall (e.g. at 100% projected) rather than only reacting to the stall footer, and let the UI show "limit imminent." Out of scope for v1 but the data makes it trivial later.
|
||||
1. **`CASES_DIR` injection gate** excluded the user's whole workflow — sessions run in linked cases / real repos, not under `~/codeman-cases`. → dropped the gate.
|
||||
2. **`curl -s` → `000`** on the loopback self-signed HTTPS cert; statusline silently empty. → `curl -sk`.
|
||||
3. **Remove-on-create-false + shared `settings.local.json`** let a single stale client yank the statusLine out from under all sessions in a repo. → add-only on create; removal only via the toggle reconcile.
|
||||
4. **Chip blank after reload** (localStorage-only, lost on restart/fresh browser). → server-side last-known in the init snapshot.
|
||||
|
||||
## Implementation surface area (checklist)
|
||||
## Open questions / future
|
||||
|
||||
- [ ] `src/hooks-config.ts` — generate a `statusLine` block (opt-in setting, Claude-only); merge into `settings.local.json`.
|
||||
- [ ] `src/web/routes/session-routes.ts` — `POST /api/sessions/:id/statusline` (auth-exempt + hook-secret + dedicated rate-limit bucket), returns formatted status string.
|
||||
- [ ] `src/web/schemas.ts` — Zod schema for the telemetry subset (`.nullish()`/passthrough).
|
||||
- [ ] `src/web/sse-events.ts` + `src/web/public/constants.js` — new SSE event (keep in sync).
|
||||
- [ ] `SessionState` + `session.toState()` + `persistSessionState()` — persist last telemetry.
|
||||
- [ ] Frontend gauge + `app.js` listener; placement in header or `respawn-ui.js`.
|
||||
- [ ] App Settings toggle `showPlanUsageLimits` (Display section), reload-on-toggle (statusLine is settings-injected at session create / via `writeHooksConfig`).
|
||||
- [ ] Tests: pure formatter (percent + reset humanizer) unit-tested like `usage-limit-patterns.test.ts`; route test via `app.inject()`.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
- **Schema stability.** `rate_limits` is an officially-shipped statusline field but undocumented in exact shape; a future CC version could add windows (e.g. an Opus field) or rename keys. The server parser should be tolerant (ignore unknown windows, render whatever windows exist) rather than hard-coding only `five_hour`/`seven_day`.
|
||||
- **Re-injection timing.** Toggling the setting must (re)write `statusLine` into existing cases or only new sessions — decide whether to push to live cases via `writeHooksConfig`-style update or require a respawn.
|
||||
- **Multi-account.** See consideration #1 — confirm whether any real deployment runs sessions under different Claude logins before committing to a single global widget.
|
||||
- **Schema stability.** `rate_limits` is officially shipped but undocumented in exact shape; the parser is tolerant (renders whatever windows exist, ignores unknown).
|
||||
- **Hook `curl -s` parity.** Hooks share the no-`-k` issue on HTTPS installs — worth fixing the hook curl too (separate change; covered by `cod54` tests).
|
||||
- **Disable cleanliness.** Disabling removes the statusLine from active sessions; a brand-new session created by a *stale* client could re-add it (chip still hidden, footer benign). Fully server-authoritative create-time injection (read the setting server-side instead of the payload flag) would close this — deferred.
|
||||
|
||||
## Verification appendix — how the schema was captured (reproducible)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user