* sessions: attribute PR spend per turn instead of per session
The by-PR surfaces attributed a session's full cost to every PR it
referenced, so one orchestration session that touched many PRs rendered
identical full-session rows. Capture per-turn PR references during
transcript parsing and attribute each turn's cost to the PR set active at
that turn (split evenly across a multi-PR merge-sweep turn), carrying the
most recent PR set forward across turns that reference nothing. Turns
before the first reference form an unattributed bucket.
A session whose transcript expired before per-turn capture keeps its
session-level prLinks but has no per-turn refs; it falls back to an even
whole-session split, and any row carrying such a portion is flagged
approximate.
Rows are now summable: the CLI and app footers report the attributed sum
plus the unattributed remainder instead of a distinct-session total.
Bumps the session cache version so surviving transcripts re-parse and
populate the new per-turn field. Daily-cache versioning is untouched.
* sessions: harden PR attribution and add models + category breakdown
Addresses the review findings on the per-turn PR attribution and adds the
model and task-category surfaces.
Correctness:
- Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5
file's expired-source PR entries instead of abandoning them, and the claude
scan preserves and surfaces PR-bearing orphans, so a session whose transcript
was deleted still appears as a legacy even-split instead of vanishing. The
daily cache is untouched.
- Date-range carry: the parser captures the PR set active at the start of the
in-range turn slice and seeds the state machine with it, so a PR referenced
before the window still owns its later, in-range, ref-less turns instead of
the session falling back to a whole-session approx split.
- Calls are split across a multi-PR turn by largest-remainder, keeping per-PR
counts whole (a 1-call, 2-PR turn no longer renders as 2 calls).
- The CLI and app footers reconcile to the rounded row values actually shown.
- Distinct sessions are keyed by project + sessionId, not sessionId alone.
- The app tolerates an older by-reference payload (no attributedCost): it keeps
the old non-summable footer and never renders NaN.
Features:
- Each PR contribution records the models of its calls and the turn's task
category (split by the same share on a multi-PR turn); legacy even-split rows
carry the model union but no category breakdown.
- Payload rows gain models (short names, cost-desc) and categories (label + cost,
cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for
the PRs beyond the sent top 20.
- CLI --by-pr gains a Models column. The desktop table is now full-width with a
Models column and click-to-expand rows showing a per-category cost breakdown
with proportional bars, keyboard accessible, with an "Other (N more PRs)" row
when capped.
Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs
round-trip through the real incremental-append path (continuation + straddle),
v5 adoption of an expired PR session, payload round-trip, and the desktop
expansion/models/old-payload cases.
* sessions: reconcile mixed PR rows, harden v5 adoption, bound models
Round-2 review follow-ups.
- Mixed legacy/live category breakdown: a PR row that combines an expired
legacy contribution (even-split, no turn data) with a live per-turn
contribution now emits an explicit "Legacy estimate (no per-turn detail)"
category carrying the legacy share, so the expansion reconciles to the row
cost instead of silently dropping it. A legacy-only row still shows no
breakdown.
- v5 adoption is now per cached file: one malformed entry is skipped instead
of rejecting the whole v5 cache and dropping every valid expired PR session.
- The desktop "Other (N more PRs)" line moved to the table footer as a muted,
separated summary rather than a sorted row.
- Row expansion resets when the PR set changes (period/provider/data), so a
stale expansion cannot linger on a row that is gone.
- Payload models per row are capped to the top 4 by attributed cost, with a
name-ascending tie-break; categories get the same stable tie-break.
Tests: mixed live+legacy reconciliation, legacy-only omission, model cap and
tie-break, corrupt-plus-valid v5 adoption, and the desktop expansion-reset.
* session-cache: validate optional agentType and failed during v5 adoption; app: reset PR expansion on period switch
A v5 entry that was valid except for a malformed optional field (agentType,
failed) passed per-file validation and flowed downstream as a non-string.
The expansion reset keyed only on the row URL set, so a period switch that
returned the same PRs kept a stale expansion open over changed numbers.
---------
Co-authored-by: reviewer <review@local>
Part 1 (src): expose two add-only, optional aggregations on the menubar
payload's `current`, computed on the unscoped all-provider path from the
surviving-session parse (carried history cannot contribute, as expected).
- current.pullRequests: aggregateByPr rows (top 20 by cost) plus the
multi-link-safe distinctCost/distinctSessions. Rows are by-reference, so
they are never summed.
- current.byBranch: new aggregateByBranch, per-branch spend (top 15 by cost)
that carries each session's last-seen git branch forward across its turns.
The cache stores a turn's branch only when it changes, so the parser now
resolves the branch at reconstruction (before the date slice) and records
SessionSummary.everHadBranch from the full transcript. That lets the report
keep a branch-bearing session's pre-branch spend in an explicit null row
even when the range clipped the anchor turn, while a provider that never
captures a branch still contributes nothing.
Part 2 (app): new "Pull requests" sidebar entry between Sessions and Spend
(shortcuts reflow to number 8 for Plans). It renders the pullRequests rows as
a refined table with the PR label linking out via openExternal, cost,
sessions, calls and an active-date span, plus a footnote stating the distinct
total and the by-reference attribution. A quiet explanatory line shows when no
PR links exist, never a fake table.
Tests: aggregateByBranch unit tests (carry-forward, null bucket, clipped
anchor, by-reference session counts) and the Pull requests component tests
(table, external link, footnote, both empty states). Nav-reflow assertions in
Sidebar and App tests updated.
Co-authored-by: reviewer <review@local>
Two Phase-1 desktop additions over the renderer's existing card grammar.
Overview workflow card: a panel below the daily chart with two stat tiles
(correction rate with count, median time to first edit) plus a top-rework
line and one coaching caption. The caption is derived locally with the CLI's
buildCoachingNotes thresholds (corrections, then file churn, then time to
first edit; first that fires). The card hides entirely when no real signal is
present, never renders zeros, and shows a "N% priced" chip only when pricing
coverage is a number below 1. Payload types gain optional workflow,
topReworkedFiles, and pricingCoverage fields so older payloads render as before.
Sessions view: the captured session title becomes the row headline with the id
demoted to the mono secondary line; untitled rows keep the project as the
headline (unchanged). Title is also searchable and added as an optional
SessionRow field so older CLIs render unchanged.
Co-authored-by: reviewer <review@local>
* fix(app,dash): render pre-history days as no data, not a currency zero
Days before the first recorded day in history.daily were painted as a real
0.00/0 calls in the daily activity heatmap and the daily spend charts. That
zero is unknown, not measured, so those cells and bars now read "No data
recorded" instead of a currency zero. Genuinely idle days within recorded
history stay as real zeros.
- app: heatmap cells, the daily spend bars, and the daily-by-model columns
get a distinct no-data style plus a "No data recorded" hover for days before
the first recorded day.
- dash: the granular line chart drops buckets before the first recorded day so
no flat zero line is drawn before any history exists.
- data-start is derived in the UI layer from history.daily; payload shape is
unchanged.
Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and
root typecheck clean; dash and app renderer vite builds succeed.
* no-data: cap guard, timezone-free dash trim, StackedBars aria
Three hardening fixes from adversarial review of the no-data rendering:
dataStartKey returns null at the payload's 365-entry history cap, where
the oldest retained entry is no longer the true data start; classifying
past it would label real aged-out history as no data on long custom
ranges. Documented that the install-to-first-use gap reading as no data
is literally accurate: nothing was recorded then either.
The dash granular chart now trims leading zero-only buckets by value
instead of comparing bucket timestamps against date keys, so producer
and viewer timezone skew can never drop a real first-day bucket, and an
all-zero series lands in the established empty state.
StackedBars no-data columns expose their state via aria-label, not only
a title on a non-focusable div.
- build stamp ignores the pricing-snapshot files the build regenerates,
so a release checkout no longer shows a false -dirty
- splash shows just the flame, CodeBurn, and the version (dropped the
commit-hash line); the clean build id stays in About only
- About 'Check for updates' uses the in-app checker and reports inline
('You're on the latest' / 'Update available: X · Download') instead
of blindly opening the releases page
392/392.
Checks the GitHub releases for newer desktop-v tags once per launch
and every 24h (15s timeout, no identifying headers, offline = silent
retry). A dismissible banner and a Settings About row surface the
newer version and link the release page; dismissal persists per
version so each release nags at most once. No auto-install: unsigned
builds cannot, by platform constraint; the checker only notifies.
App 364/364 (+24), typechecks + build green.
Energy (field report: ~3x Chrome drain):
- polls skip entirely while the window is hidden/minimized, with one
catch-up refresh on return when data is stale; visible-but-unfocused
keeps polling (second-monitor case)
- all looping animations pause under html.page-hidden (the sidebar
flame flicker was a perpetual compositor drain)
- default cadence 60s; an explicit stored choice is always honored
Currency (field report: set USD, still saw EUR):
- memo-served payloads re-applied their embedded stale currency; config
mutations now purge the renderer memo and force-refresh, and a
switching payload can never overwrite the applied currency
- USD default verified end to end
Measured: hidden window = 0 new CLI spawns over 5 cadences (was: full
polling forever). App 340/340, build + package green.
- prefetch fires exactly once per provider per session; memo sized to
detected providers so base keys never evict (was: 12 full-history
parses every 30s forever from LRU thrash re-triggering the effect)
- day-aggregator buckets whole turns by user-message timestamp,
matching the report/headline path: trend bars and provider breakdown
now reconcile exactly (midnight-straddling turns were split);
DAILY_CACHE_VERSION 13 with an IANA tz guard that rehydrates when
the cache crosses timezones
- overview's redundant second all-provider scan collapsed (~11% faster
warm); numbers parity-guarded
- lock takeover cleans stale hydrating.lock; SIGINT/SIGTERM release it
- scanner skips EPERM/EACCES provider dirs (progress shows skipped)
instead of aborting; onboarding hints Full Disk Access
- rateLimited quota copy render tests; export -f json emits valid
empty JSON when no data
Root 2023 (+4), app 327 (+3), typechecks clean.
P0: an interrupted cold hydration left partial caches that the
emptiness check read as warm — the daily backfill froze (missing older
days) while session parsing healed gradually (drifting totals),
poisoning every surface sharing the cache. Both caches now carry an
explicit complete marker: partial resumes cold under the hydration
lock; unmarked caches self-heal with one re-hydration. Regression test
seeds the exact frozen artifact and asserts bit-for-bit convergence
with a never-interrupted run.
- splash indexing reveals only on genuinely cold scans (explicit cold
flag in the progress protocol), never on warm incremental re-parses
- uncached filter switches clear to skeleton instead of showing the
previous filter's numbers; cached switches paint same-commit;
background prefetch warms every detected provider after idle
- build stamp (git sha + date) in splash and About ends build ambiguity
- payload parity test: identical payloads on identical cache, and
payload totals equal the CLI report path
- Sessions empty state keeps the provider filter row; zero savings
line hidden; TCC usage-description strings + Full Disk Access docs
- warm profile documented: optimize scan ~1.47s dominant (safe
refactor deferred), parse ~1s, aggregation ~30ms
Root 1848, app 320, typechecks clean.
Performance and coherence:
- Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via
RefreshCadenceContext; Manual polls only on demand
- usePolled memoKey LRU: provider/period switches paint the last-good
result instantly with a switching hairline while refreshing quietly
- quota TTL 5min + honest rate-limited copy on 429 backoff
- version-suffixed cache files (session-cache.v5.json, daily-cache.v12,
auto-minted on future bumps); legacy files never written or deleted,
adopted once when versions match: old and new binaries coexist
without clobbering (field-observed menubar-vs-desktop ping-pong)
- advisory hydration lock: concurrent cold starts share one scan
(wait-then-read-warm), stale/dead locks self-heal, never a
correctness gate
Telemetry v1 + onboarding (desktop only, per owner decisions):
- first-launch onboarding (3 feature screens + consent); region-split
default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends
before consent completion or while off; dev builds never send
- anonymous install UUID, rotated on opt-out; day-granularity events,
cost buckets only; whitelisted names; 200-event queue, 5min flush
- Settings > Privacy live toggle replaces the static claim
Zero computed-number changes. App 316/316, root 1805.
Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
The per-provider ingest list rendered a dozen stacked text rows on
machines with many detected providers. Now one status line with live
tabular-numeral counts (Indexing Claude · 240/699), a single strip of
15px provider logos (dim pending, pulse active, settled done), and a
muted one-time-scan note. ~3 lines of vertical footprint, 340px max
width; reduced motion drops the pulse; warm launches never see it.
Root cause of the field-reported 45s timeout + perpetual slowness: on
a cold cache all six sections spawned different CLI subcommands at
once, each running its own full-history parse (~11-31s, ~3GB RSS),
contending past the 45s kill so the rebuilt cache never persisted and
every poll restarted from zero.
- the first overview fetch runs as a warmup: 10-minute timeout,
re-arms until it succeeds, reverts to 45s after; section polls gate
on that first resolution (usePolled gains enabled), so cold
hydration happens exactly once
- warmup streams the CLI's progress protocol; the splash shows 'First
run: indexing your usage history' with a per-provider ingest list
(logo, live counts, check on done), static under reduced motion,
generic fallback without events; warm launches unchanged
- overview spawns pass --no-timeline (desktop never renders it)
Measured on real data: cold 31s once (was: killed at 45s forever),
warm 2.6s. 295/295 app, 1797 root.
Settings Providers pane keyed logos by lowercased display names
('grok build' never matched THEMED_LOGOS 'grok'); rows now come from
providerDetails ids like the rest of the app. 20 official marks added
from the repo's own assets (hermes, openclaw, antigravity, kiro, devin,
cline, roo-code, ...) and unknown providers render a first-letter
monogram badge so no detected provider is ever logoless.
The h264 loader baked a flat dark background that read as a rectangle
against the splash's warm radial tint. Re-encoded from the original as
VP9 WebM with an alpha channel (checker keyed to transparency), so the
burning flame composites directly onto the gradient. Verified over a
contrasting test canvas.
273/273, typecheck + build green.
New owner-approved art: the binary-digit flame. Checkerboard keyed out,
largest-component masking to drop glow-zone checker fringe, clean
synthetic ember glow rebuilt; composited to the Big Sur squircle as
app/build/icon.png and squared with alpha as the in-app FlameMark
raster (sidebar, About, static splash).
The supplied burn animation becomes the splash loader: checker keyed
per-frame and the splash's fixed dark canvas (#171310) baked in (h264
has no alpha), audio stripped, 944KB looping video shown while the
first scan runs. Splash canvas and text colors are now theme-fixed so
video and page never diverge; reduced motion/tests keep the static
mark, verified by test.
Flame/splash/motion suites green in isolation (full suite re-verified
when the in-flight quota batch commits); typecheck + build green.
- app/build/icon.svg + icon.png: hand-retraced vector of the approved
reference (three flat layers: coral #e86c39, amber #f7943c, cream
#fecb8b on charcoal #231613), Big Sur squircle proportions (824/1024,
r185, transparent corners, no glow), flame ~72% optically centered;
8 render-and-compare iterations against the reference; legible at 16px
- FlameMark retraced to the same three paths (flat fills, no gradients);
flicker now animates the cream core only
- AboutModal renders FlameMark instead of the old gradient flame.png;
orphaned 70KB asset removed
253/253, typecheck + build green.
CodeBurn wordmark on the splash now uses --accent (matching the
website/sidebar brand treatment) with the app version (v0.1.0, from
package.json at build time) in muted small text below. App version
bumped 0.0.0 -> 0.1.0.
253/253, build green.
Replaces the rejected hand-traced silhouette with Bootstrap Icons'
fire path (MIT, embedded with license note) — the flame.fill-style
teardrop with the inner tongue — filled with the menubar BurnFlame's
actual Ember gradient from the Swift theme (#f0a070 > #e8774a >
#c9521d > #8b3e13, bottom to top), not the brighter accent ramp.
Flicker hook moves to the whole mark (single compound path).
Also: .splash-word snapped to var(--fs-kpi).
- FlameMark: hand-traced inline SVG of the brand flame (~1.1KB, warm
3-stop gradients, useId-scoped), used at splash and sidebar sizes
- Splash: full-window first-load overlay echoing the menubar's
BurnLoadingOverlay ignite moment; 600ms floor so warm cache never
blinks, 250ms crossfade on first data, instant yield on error or
reduced motion, done-latch so it never reappears
- Sidebar logo replaced with the live FlameMark: 1-2% inner-tongue
flicker, 4.5s period, random phase; static under reduced motion
250/250, build green (+2.4KB JS, +1.8KB CSS).
- one gate (motionEnabled): off under reduced-motion, missing matchMedia,
and vitest; every path checks it first
- mount/filter-change only: count-up and bar grow-in key off the
period|provider|range key, so 30s poll refreshes snap values silently
instead of re-animating
- first-load skeleton shimmer replaces bare scanning text (kept sr-only
for screen readers); slide-in toast host for Settings/export feedback
(validation errors stay inline); CSS hover-lift + press micro-
interactions, all with a reduced-motion escape hatch
- gsap 3.15.0 + @gsap/react 2.1.2 (+74KB raw JS; G4 flame work shares it)
244/244, typecheck + build green.
Every var(--t1/--t2/--t3/--hair/--hair2) and secondary legacy color
(--lav/--amber/--red/--mint/--blue/--purple/--cyan) repointed to the
canonical family (--ink/--mut/--mut2/--line/--line2, --warn/--bad/--ok,
--s-*); all legacy definitions and the plain.css alias shim deleted.
The explicit dark/light theme blocks carried drifted legacy literals
(including a pre-contrast-fix --t3), so this also extends the AA
contrast values to all four theme modes, which previously disagreed
with system-light.
grep-proven zero legacy usages. 235/235, build green.
- Models gains an Audit lens: raw provider token fields vs displayed
totals and cost derivation per model, est badge where pricing is
missing or recomputed cost diverges from attributed
- Settings Pricing pane: list/add/remove price overrides (USD per 1M),
inline confirm on remove, only provided rates sent; rates validated
finite and strictly positive on both sides of the bridge
- four new IPC channels with the established validation pattern
Root 1617/1617, app 235/235, build green.
Replaces the three-rule anomaly card with a Wins/Improvements/Risks
Signals card mirroring the menubar's rule set and thresholds
(FindingsSection.swift): cache >=80% and one-shot >=0.75 and spend-down
>10% and streak >=5d as wins; top-3 optimize findings, low cache <50%,
low one-shot <0.5, retry tax >=25% of spend as improvements; weekday
spike >1.8x, week-over-week >+25%, projected month >1.3x prior month as
risks. Time-comparison rules suppressed under a custom range. Renders
nothing when empty; icons per group, never color-alone. Anomaly test
coverage ported, not deleted.
221/221, build green.
- TopBar picker (shown only when >1 Claude config dir exists) threads
--claude-config-source through getOverview; synthetic 'All Claude
configs' entry returns to the aggregate default
- config id validation accepts the real <kind>:<16hex> id shape while
still rejecting flag-shaped input (leading char anchored)
- both incompatibility directions handled: selecting a config resets a
non-Claude provider filter, and picking a non-Claude provider clears
the config scope, so the CLI rejection is unreachable
- choice persisted (localStorage); active config shown in the scope
line and read-only in Settings/General
Verified live against 2 real config dirs. 205/205, build green.
- Sessions: All + per-provider logo buttons (detected, cost > 0) that
drive the same app-level provider state as the top-bar dropdown, so
the two controls can never disagree; aria-pressed group semantics
- Spend By project: rows expand inline (single-open, aria-expanded,
chevron rotation) to the project's sessions: date, top model, calls,
currency-aware cost; date-only strings parsed at local noon so days
never roll across time zones
194/194, typecheck + build green.
Replace the $-budget model as the primary view: per-provider cards (Claude/Codex)
from getQuota show the real 5-hour/weekly windows (% used, resets-in, severity
colors), the auto-detected subscription tier, connection states, and the credit
footer. Manual budget plans demoted to a secondary section.
typecheck clean; 148/148 tests pass. Needs a full relaunch for live quota.
- extract contiguousDailyWindow + formatChartDate to lib/period (shared)
- Spend "Daily spend by model": fixed contiguous 15-day window (zero-filled)
with an .ov-xax date axis, matching the Overview chart
- remove the horizontal gridlines from both charts (they read like data lines)
typecheck clean; 129/129 tests pass.
By-project and the Activity/Tools/MCP/Subagents breakdown panels cap at 300px
with a slim internal scrollbar, so they stay a consistent compact size instead
of growing unbounded with the data.
typecheck clean; 126/126 tests pass.
Drop the Projects/Activity/Tools/MCP/Subagents lens tabs. Everything shows on
one scrolling page: chart + By-project, Cost-flow Sankey, then the breakdown
sections in a responsive grid. Empty sections are hidden. All reflects the
selected date range.
typecheck clean; 128/128 tests pass.
Drop the FuelRing (and its now-unused plans polling) from the ov-stats3 card;
the stats row is now Month-to-date + Projected month (2 equal columns).
typecheck clean; 128/128 tests pass.
- new Dropdown component (our .pop menu style, keyboard-accessible) replacing
every native <select> (currency, period, plan preset, export, Compare pickers)
- expand currencies to 30 common ISO codes (engine handles any valid currency)
- fix the Settings pane so rail + content anchor top-left and fill the area
typecheck clean; 128/128 tests pass.
- share a shortenProjectPath helper (Sankey + Sessions) so rows show
"projects/eywa" instead of the dash-mangled full path
- fixed-column table layout so values align and stop shifting row-to-row
- TUI-style inline row expansion: click a row to unfold its 8-stat detail in
place (one at a time, chevron + aria-expanded); replaces the navigate-away view
typecheck clean; 128/128 tests pass.
- provider dropdown: filter to providers with actual usage (value > 0) instead
of every known/detected key (Pi, Goose, Warp, etc. no longer show)
- Overview hero: bottom-align the activity heatmap (space-between) so its grid
lines up with the left column's Saved-to-date line
typecheck clean; 128/128 tests pass.
- add a shell.openExternal bridge (main ipc handler + preload + renderer type)
so links open in the default browser (requires a full relaunch)
- wire the sidebar social icons to real URLs and add LinkedIn
- AboutModal: flame + wordmark, version, tagline, the five social links, and a
"Check for updates" link to the GitHub releases page
typecheck clean; 128/128 tests pass.
- add Claude/OpenAI/Cursor/Grok brand SVGs under app/renderer/assets/providers
- ProviderLogo component: brand-orange Claude in both themes; light/dark
monochrome variants for OpenAI/Cursor/Grok swapped per active theme
- show the logo in the provider dropdown (trigger + menu items) and in
Settings > Providers, left of each provider name
typecheck clean; 128/128 tests pass.
- Sankey project labels now show the last 2-3 path segments joined by "/"
(dropping the users/home prefix) instead of the dash-mangled full path
- Models "By task" regrouped CI-style: a per-model group header (series dot +
model + aggregate calls/cost/saved) with indented task sub-rows and aligned
columns, replacing the flat list that repeated the model name on every row
typecheck clean; 128/128 tests pass.
Fable was bucketing into grey "Other" despite being a large chunk of spend.
- add `fable` SeriesKey across modelSeries.ts (label, css-var, class, matcher)
- validated CVD-safe --s-fable token (aqua #1baf7a light / #199e70 dark) in all theme blocks
- StackedBars now uses a fixed CVD-safe series order [opus, fable, haiku, gpt, sonnet, other]
for both legend and stack, so aqua never sits adjacent to the magenta (Sonnet)
typecheck clean; 128/128 tests pass.
- Plans tab: add/remove budget presets via `plan set`/`plan reset`, lists
configured plans, links to the full Plans screen
- Export tab: format + provider + native folder picker (electron dialog)
→ runs `codeburn export`, reports the result
- Devices: real Remove (`devices rm`) + Refresh; visibility is read-only
from `share status`; pairing shows an honest "pair from terminal" note
instead of a dead Approve button; combine row is read-only status
- Apply the persisted theme at app startup (not only when General mounts)
- Settings gets onNavigate for the "Open Plans" link
Only one-shot, non-interactive CLI commands are wired as buttons; the
interactive share daemon and pairing flow stay honestly read-only.
Settings was a dead shell (rail didn't switch, all buttons disabled).
Now the rail navigates and four tabs are wired to the real CLI:
- action bridge: spawnCliAction + runAction (text output, not JSON) with
getAliases/getProxyPaths reads and setCurrency/resetCurrency/addAlias/
removeAlias actions
- General: theme switcher (System/Light/Dark, persisted) + live currency
(reads status, changes via `currency`) + default-period preference
- Providers: real detected-tools list with spend
- Model aliases: full add/remove against real config
- Privacy: honest local-only info
Devices content preserved as-is; Plans/Export are placeholders for Part 2.
A render error in any section previously white-screened the whole app
(no boundary). Wrap the section content in an ErrorBoundary keyed by
section: the error message + component stack render in a card, the
sidebar stays usable, and navigating away recovers. Logs to the console
for diagnosis.
Search (project/model/id), sort (cost/recent/turns/tokens), group-by-
provider toggle, and a summary bar (count · cost · tokens) over the
filtered set. Dependency-free "show more" paging keeps large lists fast;
provider headers are sticky and the list flows in the page scroll.
Richer detail: 8 real stat cards (cost/calls/turns/saved/input/output/
cache read+hit%/cache write), full date range + duration, Esc-to-back,
selection tracked by id so background refreshes keep the detail live.
Centralizes formatCompact/date/duration helpers in lib/format ("184K"
not "0.2M"); Compare now uses the shared helper. Search-aware empty state.
Sessions: provider-grouped list of every session in range (project,
models, turns, cost, tokens) with a lightweight real detail — no
fabricated titles, no unbacked context-window widget.
Compare: two-model picker (defaults to top two by cost) across
Performance, Efficiency, Category head-to-head, Working style, and a
derived Context panel, all mapped straight from `compare --format json`.
Nav reordered to match the design: Overview, Sessions, Spend, Optimize,
Models, Compare, Plans, Settings (⌘1-7).
Re-arrange everything below the two top cards so it reads as a dashboard:
- Insight band grouping the coach pacing line + anomaly callouts.
- Analytics row: Cost-per-outcome | Routing what-if (equal 2-up).
- Height-balanced 2-column body — Left: Models table + Most expensive
sessions; Right: Top activities + Daily spend — so the left column no
longer dead-ends into a big empty gap.
- Consistent gaps, align-items:start, single-column reflow under 900px.
typecheck clean, 88 tests pass.
- Top card: one card, three equal columns split by dividers —
current spend (with Saved-to-date) | daily activity | efficiency
scorecard. Removed the duplicate one-shot/cache ratings column and the
separate efficiency card (EfficiencyScorecard + FuelRing gain a 'bare'
mode).
- Stats: one card, three equal divided columns — Month-to-date |
Projected | Nearest-limit fuel ring.
- Efficiency grade is now a circular brushed-metal medal (one subtle
gradient + inset highlight + depth shadow), tiered A/B-C/D/F, tokenized
for light+dark — premium, not the flat AI-looking pill.
- Tightened spacing/dead space; 1-column reflow under 900px.
typecheck clean, 88 tests pass.