feat(web): add <TurnMeter> composable to unify token/cost display #1182
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/agent-hooks!1182
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1162"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #1162.
Replaces the three bespoke token/cost widgets —
<TokenMeter>(compact + full variants), the inline transcriptTurnMeterpill, and<RunHeaderMeters>'sCostMeter— with one<TurnMeter size="sm|md|lg">composable so list rows, transcript bubbles, and run headers all share field order and color-tier rules.size="sm"— inline↑in ↓out [$cost]chip. Replaces<TokenMeter variant="compact">in task-list rows and the board side panel.size="md"— pill with hover/focus tooltip exposing the cache breakdown. Replaces the inlineTurnMeterpreviously local to<Transcript>.size="lg"— multi-field row withcache_read/cache_creationchips. Wrapped by<RunHeaderMeters>underneath the context-window bar + compaction ticks (those remain — they're context-window state, not turn-level).Cost API
costUsd={number}→$0.043/$1.27/ …costUsd={null}→?rate-table-miss indicator (parity with the prior<CostMeter>cost_unknown_modelchip).costUsd={undefined}→ hide the chip entirely.Adoption
<TurnMeter size="sm">(task.cost_unknown_model ? null : task.cost_usdmapping).<TurnMeter size="md">.<RunHeaderMeters>delegates the token row + cost chip to<TurnMeter size="lg">; keeps the context-window bar + compaction ticks above it.<TokenMeter variant="full">calls intask-detail.tsxdropped — the row is now inside<RunHeaderMeters>.apps/web/src/components/token-meter.tsxremoved entirely.Quality
just qapasses (typecheck + Biome + 3359 server tests).turn-meter.test.tsxcovers all three sizes, thenull/undefined/numbercost branches, the active-zero render branch, and the color-tier escalation.<TaskList>,<TaskDetail>,<Transcript>, and<BoardCard>still pass.Test plan
just qa— typecheck + lint + format + server testsbun x vitest run src/components/turn-meter.test.tsx— 13/13 passbun x vitest run src/components/{task-list,task-detail,planner/transcript,board/board-card}.test.tsx— 119/119 pass🤖 Generated with Claude Code
Replaces the three bespoke token/cost widgets (`<TokenMeter>` compact + full, the inline transcript pill, and `<RunHeaderMeters>`'s cost chip) with one `<TurnMeter size="sm |md|lg">` so list rows, transcript bubbles, and run headers share field order and color tiers. - `size="sm"` — inline `↑in ↓out [$cost]` chip; replaces compact TokenMeter in task-list rows and the board side panel. - `size="md"` — pill with hover/focus tooltip carrying the cache breakdown; replaces the inline `TurnMeter` previously local to `<Transcript>`. - `size="lg"` — multi-field row with cache_read / cache_creation chips; wrapped by `<RunHeaderMeters>` underneath the context-window bar + compaction ticks (those are context-window state, not turn state, and stay where they are). Cost API: `costUsd={number}` → `$0.043`; `costUsd={null}` → `?` indicator (rate-table miss; parity with the prior `<CostMeter>`); `undefined` hides the chip. `<TokenMeter>` is removed entirely — every call site now reaches `<TurnMeter>` directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>Review:
feat(web): add <TurnMeter> composable to unify token/cost displayCI ✅ — all 13 new tests pass,
just qareported green, existing suites (119 tests) pass.All ACs from #1162 are met. The spec called for a temporary
<TokenMeter>wrapper as an interim step, but the PR skips straight to full migration and deletion — that's strictly better; no objection.What's good
sm | md | lgsizing decision boundary is well-documented at the top of the file and each size maps cleanly to its call site.costUsd: number | null | undefinedtri-state — models the three semantically distinct states (known cost / rate-table miss / no cost data) correctly, with all three paths tested.activeprop — cleanly preserves the pre-#952 zero-render guard without duplicating the logic across callers.DEFAULT_SOFT_BUDGET_USDfromrun-header-meters.tsxkeeps the old import path working without a separate shim.getByTestId(...).not.toBeInTheDocument()) is the right idiom for vitest-browser-react.Non-blocking suggestions
1.
tokenColorClassandcostColorClassare identical — merge themBoth return
text-error / text-warning / text-text-dimat the same thresholds. Either collapse into onecolorClass(ratio)or at least make one call the other. As written, a future threshold change must be applied in two places.2. Replace
Math.random()tooltip ID withuseId()Math.random()generates a new ID on every render, which breaks thearia-describedbyassociation during re-renders (the button's attribute points at a stale ID). React'suseId()is stable, SSR-safe, and zero cost:This was a pre-existing issue in
transcript.tsx— good opportunity to fix it in the rewrite.3.
MeterPilltooltip shows "Cache created: 0 tok / Cache read: 0 tok" when both are zeroThe
lgsize correctly gates these on> 0; themdtooltip unconditionally renders them. On turns with no cache activity the tooltip body is two useless zero-lines. The old transcriptTurnMeterhad the same flaw — worth fixing here since this is a ground-up rewrite:(If both are 0 and the tooltip is empty, you can hide the tooltip entirely or drop the button's
aria-describedby.)4.
activeprop behaviour formd/lgis untestedactive=truewith zero tokens +size="md"renders a pill showing0+0 tok— probably fine, but there's no test for it. Worth a single case if someone ever wiresactiveto a transcript bubble during the queued phase.5. Minor:
$?vs icon inmdunknown statesm/lgrenderCircleDollarSignicon +?viaCostChip. Themdpill renders plain$?text (no icon). Intentional for the dense pill format, but thecost-meter-unknowntestid is shared between both shapes, which could confuse future snapshot tests. A comment in the code noting the intentional difference would help.Items 1–3 are worth a follow-up issue or a quick fixup commit; they don't block merge. APPROVED.