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>
The update Download button opened the GitHub release page; it now
downloads the right artifact for the running platform directly: arm64
or x64 dmg on macOS (preload newly exposes process.arch), the Setup exe
on Windows. Linux keeps the release page since it ships three formats
and the user picks. Unknown platforms and preloads without arch fall
back to the page. URL mapping is unit-tested per platform.
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.
- 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.
Root causes (verified live): the Claude keychain branch was never
enabled (allowClaudeKeychain never passed), security -w returns
hex-encoded JSON the parser rejected, and Codex had no keychain
discovery at all.
- shared readKeychainPassword: Apple-signed /usr/bin/security, 90s
window so the user can answer the macOS dialog, hex decode,
notFound vs accessDenied classification; secrets never logged
- Claude: keychain fallback active (gated to explicit Refresh so
launch never ambushes with a dialog); connection retained across
keychain-less background polls
- Codex sources in order: the menubar's own cached OAuth (read-only,
401 = one re-read, never rotate), legacy ~/.codex/auth.json
(writable), com.openai.codex plaintext if usable; Safe Storage
never decrypted
- new accessDenied state (amber, 'locked') + ConnectAffordance on
Plans cards and Settings rows: login command, keychain-Allow note,
force Refresh
Live verification: Claude connected with its real tier and windows;
Codex correctly accessDenied until the keychain dialog is allowed.
273/273 (26 files), 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.
The owner supplied the final flame render; the hand-retrace is retired.
Checkerboard keyed out to real alpha, composited to the Big Sur squircle
(824/1024, r185, charcoal #231613) as app/build/icon.png (the
electron-builder master, verified legible at 16px), and the flame-only
alpha PNG becomes the in-app mark: FlameMark now renders the raster so
splash, sidebar, About and dock are pixel-identical. Flicker applies to
the whole mark (raster has no separable core). Stale icon.svg removed.
Flame suites green in isolation; full suite re-verified before the
in-flight quota batch commits.
- 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.
- 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.
- Provider filter: Models-this-period sources current.topModels (history
days carry empty topModels per provider); StackedBars falls back to a
cost-proportional single segment like the Swift menubar
- Chart windows align to the CLI (week -7, 30days -30, all = 6 months)
and zero-fill a contiguous calendar window; history.daily is sparse and
both sides key local YYYY-MM-DD, so lookups are safe (verified on real
CLI output)
- Custom range threads into Overview panels (chart, models table) and
suppresses MTD/projected and vs-last-week comparisons; Compare states
that custom dates fall back to the period
- usePolled clears errors per attempt and sections show a StaleBanner
instead of silently rendering last-good data after a failed refresh
- Cache-hit denominator matches the CLI; hero savings relabeled 'Saved by
applied fixes' + new 'Saved via local models' line; formatCompact
everywhere; Optimize 'Fixes' count matches the rendered list
App suite 163/163 (was 147), root 1613/1613.
- 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.
Drop the 11px stacked-segment cap so the bars fill their columns like the
Overview .chart; render only the last 15 days.
typecheck clean; 126/126 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.
- ProviderLogo: add Copilot/Gemini/Qwen/OpenCode marks; reuse the Cursor logo
for Cursor Agent; single-color logos (Claude, Gemini) via a SINGLE_LOGOS map
- remove the redundant model-series dot from Overview "Most expensive sessions"
(the row already names the model in its sub text)
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.
The StackedBars chart lived in a fixed 96px band, so when the grid
stretched its card to match the taller "By project" panel the bars
floated at the top over a large empty gap. Make the panel/body flex so
the chart fills the card height (min 120px) with bars bottom-anchored;
round stacked-segment corners only at the top/bottom of each column.
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.
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).
Per feedback, the hero (number + KPIs) and the git-style daily-activity
heatmap now share a single card, split left/right with a divider.
ActivityHeatmap gains a 'bare' mode (renders head+grid without its own
card wrapper). Efficiency scorecard + fuel ring move to a row below.
- Period seg now uses the 5 real CLI periods (Today/7D/30D/Month/6M);
6M maps to 'all' (matches the menubar).
- Provider control is a real dropdown listing only DETECTED providers
(accumulated from current.providers so it never shrinks) + All.
- New RangeCalendar: month grid with click-and-drag range selection
(+ two-click fallback, future-date disabling) behind a calendar button;
commits a custom {from,to} threaded to Overview/Spend/Models/Optimize
via new --from/--to CLI args (Plans/Settings stay period-only).
- typecheck clean, 87 vitest pass.
Flame logo + orange CodeBurn wordmark, an outline icon on every nav item
(active icon in accent), and an About + GitHub/Discord/X/YouTube social
footer replacing the price status line (per the approved design). Bundles
the flame asset. typecheck + 82 tests green.
Install an app menu without the CmdOrCtrl+R reload accelerator so ⌘R reaches the renderer for
in-place refresh (keeps Edit/Window roles + dev DevTools). Model labels are now family-level
("Opus"/"Sonnet"/"Haiku"/"GPT / Codex") and the Sankey shows the real model id instead of the
wireframe's sample version names. Optimize retains last-good yield across 30s revalidation (no
flicker to "—"). Removed hints for unimplemented ⌘K/⌘E/esc; fixed stale TopBar comment.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Addresses T3 review: full test coverage for all lenses + empty/error states, segment
color mapping, Sankey ribbon properties, and lib/period.ts across all windows; Sankey
ribbon widths now truly proportional (removed the 28px cap) and labels are pretty/basename
+ truncated so real ids don't clip; legend reflects present models; honest "top N" project count.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
Period-sliced StackedBars (history.daily[].topModels within the selected window via
a new shared lib/period.ts), the By-project list, and a Sankey rendered dynamically
from getSpendFlow (per-model hue ribbons). Lens tabs; series palette shared with the
legend. Adds a neutral .s-other class for the rollup segment.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).