Skip to content

Usage: cross-machine token and cost dashboard - #134

Merged
badcuban merged 16 commits into
mainfrom
feature/usage-page
Aug 10, 2026
Merged

Usage: cross-machine token and cost dashboard#134
badcuban merged 16 commits into
mainfrom
feature/usage-page

Conversation

@badcuban

Copy link
Copy Markdown
Collaborator

Adds a Usage surface: how many tokens (and what they would cost at API list prices) Claude Code and Codex have burned, across every machine the app knows about — covering all activity in the provider homes, not just Threadlines sessions.

How it works

  • The server scans the provider CLIs' own transcripts (~/.claude/projects, ~/.codex/sessions) with pure line parsers: Claude's per-content-block usage repeats dedupe by message/request pair; Codex forked rollouts (a first-class Threadlines feature) suppress their re-stamped parent-history prefix so fork copies never double count. Records memoise per file by (size, mtime) in an atomic cache; a cold 90-day scan of real data ran ~8s, warm reads 60–160ms.
  • Costs are API list-price equivalents, never billed spend (labeled as such on-page): provider-reported when transcripts carry it, else priced from LiteLLM's public table (24h disk cache, stale-tolerant offline), with the quality split reported. Unpriceable records are counted as unpriced rather than guessed.
  • The client fans usage.summary out to every connected environment and merges in shared pure code, with fingerprint dedupe so two servers scanning one directory can't double count. Machines that can't be reached stay visible as "Not reporting" — totals never silently omit a computer.

The page

/usage: two-column hero (API-equivalent cost as the page's one display figure with an "if billed at full API rates" footnote, per-provider share bars), a smooth overlapping-area daily chart with a cost/tokens toggle (hand-rolled SVG, monotone-cubic so curves can't overshoot the data), a five-stat band with context sublines, a models table with provider icons, and the machines/freshness section. Claude and Codex get the app's first brand hues (terracotta / strong neutral, both themes).

One 90-day scan feeds all windows: 7/30/90 (default 30) are derived client-side by narrowing daily buckets, so switching is instant — verified zero network activity while flipping windows.

Entry points: a live sidebar-footer meter (Usage · 74M today, plain Usage until data), a usage row in settings → providers sharing the same cache, and a command palette action.

Verification

  • Unit: parsers against real-format fixtures (incl. Claude's new cache_creation nesting and real Codex fork timings), scan cache, aggregation windows, pricing normalisation (with a determinism fix over the reference implementation), merge/dedupe, window derivation. Server 71, shared 20, plus RPC surface via server tests.
  • vp fmt / vp lint / vp run typecheck clean; full web unit and browser suites green.
  • Live-driven end-to-end in a sandboxed stack against real transcripts: all 11 scenarios passed (meter, hero, default 30d, instant switching with no usage.summary refetch, chart toggle and peak/axis spot-checks, stat band, model filtering, machines freshness, settings + palette entry points, zero console errors), with light/dark visual review at 1440px and 380px.

Server-side scan of the provider CLIs' own session transcripts
(~/.claude/projects, ~/.codex/sessions), so usage covers turns driven
from any tool sharing the provider home, not just Threadlines. Pure
line parsers handle Claude's per-content-block usage repeats (dedupe by
message/request pair) and Codex forked rollouts, whose copied parent
history would otherwise double count — a first-class concern here since
forking is a native feature. Parsed records are memoised per file by
(size, mtime) in an atomic on-disk cache; a usage.summary RPC returns
timezone-aware daily buckets for a bounded window.

Costs are API list-price equivalents priced from LiteLLM's public rate
table (24h disk cache, stale fallback offline), never billed spend;
records price as provider-reported, model-priced, or unpriced, and the
summary carries the quality split. packages/shared gains pure
usageMerge/usageFormat modules that combine per-environment summaries
with fingerprint-based duplicate-source detection, ready for the
phase-2 page.
/usage renders the merged cross-machine picture: totals strip, a flat
stacked-bar daily chart (empty days kept - gaps are information), a
per-model table, and a machines section that names every environment
the app knows about, including offline ones as "Not reporting" and
stale scans as "Out of date". Every cost is labeled API list-price
equivalent; subscription plans bill separately.

Entry points: a live sidebar-footer meter with today's compact token
total (falls back to a plain "Usage" label without data), a one-line
usage row in settings > providers, and a command palette action. The
meter owns its own query so refetches re-render one line of chrome,
sharing a key family with the page so neither double-fetches. Servers
predating usage.summary read as unreachable rather than erroring.
The page now leads with the figures: a two-column hero with the
API-equivalent cost as the one display-size element (asterisked to its
"if billed at full API rates" footnote) over per-provider share bars,
beside a smooth overlapping-area daily chart with a cost/tokens toggle.
Claude and Codex get the app's first brand hues (--provider-claude
terracotta, --provider-codex neutral, both themes). Below: a five-stat
band with context sublines (per active day, share of observed input,
cache writes, reasoning, savings multiple) and the models table with
provider icons, zero-token placeholder rows filtered out.

Window switching is now instant: one 90-day scan per environment set,
and 7/30/90 (default 30) are derived client-side by narrowing buckets
and re-merging, so the selector never refetches or shows loading. The
settings row shares the same cache. The sidebar meter says what it is:
Usage, then today's compact figure.
The header wraps as a whole row instead of breaking the date range
across three lines, model rows drop the share column below sm so the
model name keeps its room, and stat sublines wrap instead of clipping.
Desktop keeps the single-row band and the four-column table.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadlines Ready Ready Preview Aug 10, 2026 2:38pm

Request Review

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 10, 2026
The toggle used to switch only the chart while the headline stayed in
dollars. Now the hero follows it: the big figure, its label, and the
provider rows all show the selected measure, with the share bars sized by
token share in tokens mode. The cost asterisk moves to a superscript and
keeps its footnote only in cost mode, where the caveat means something.

The breakdown table gains a Model|Day toggle: day rows list each active
day newest first with tokens, cost, and share of the window's cost.
The native title tooltip made the reader wait and gave one line of text.
Hovering the plot now draws a hairline through the hovered day and pins a
card beside it: the day, one row per charted provider with its figure in
the chart's mode, and the total. The card flips sides past the midline so
it never leaves the plot, and everything it shows is precomputed on the
column, so hover work is a lookup.
The plain text row read as page furniture and never said the figures
covered 30 days. It is now a clickable card in the same family as the
provider entries: a window-stamped label over the token and cost figures,
with the view-usage affordance on the right.
The settings sidebar replaced it with nothing, so the one screen where
quota bars live lost the live token count. Same component, same position:
bottom-left means "today's usage" on both surfaces.
The tile's mixed-brightness line made the dim words drop their bottom
anti-aliased pixel and read a hair higher than the numbers; the whole
line is now one color and the caps label above carries the hierarchy.
The chart legend swaps its swatch dots for the provider icons, whose
built-in brand fills already match the series hues.
The advanced section was a native details element that React also controlled: the browser toggled open instantly, and any render committed before the onToggle state update re-applied the stale open and snapped it shut - stuck closed, since the revert also fired onToggle(false). Summary clicks now prevent the native toggle and flip React state, the only driver. Fixes the CI-only failure on the verified-usage settings test.
@badcuban
badcuban merged commit 9cf9670 into main Aug 10, 2026
15 checks passed
@badcuban
badcuban deleted the feature/usage-page branch August 10, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant