Commit graph

103 commits

Author SHA1 Message Date
Resham Joshi
75d557e87b
feat(app): transient project selector in the desktop top bar (#1680)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Tests / vscode (push) Waiting to run
Windows Menubar CI / check (ubuntu-latest) (push) Waiting to run
Windows Menubar CI / check (windows-latest) (push) Waiting to run
* feat(app): transient project selector in the desktop top bar

Scopes every report to one project path for the current session through
the same narrowing the VS Code workspace scope uses. The saved project
filter is never written; a restart is back on all projects.

* fix(app): let the name pickers ellipsize before the top bar overflows

* fix(app): list only selectable, non-zero projects in the top bar picker

Rows with no session cwd (the Cursor and Grok Bot imports, provider label
fallbacks) report their label as the path, so the picker offered names the
bridge refuses and dash-to-slash guesses like eywa/lab. Keep only absolute
paths, and drop rows whose lifetime cost rounds to $0.00. The old test that
rooted a slashless Codex path is replaced: Codex now records the absolute cwd,
and a slashless path is always the label fallback.

* fix(app): show a toast when a project pick is refused

The pick was dropped silently. Also pin that an encoded -Users-... label
never reaches the picker list.
2026-10-08 00:51:31 +02:00
Resham Joshi
e42f1e5db3
Explain why one session cost what it did (CLI + app session view) (#1645)
* feat(sessions): explain why one session cost what it did

`codeburn sessions --id <id> --why` reads one Claude Code transcript on
demand and returns where the money went: a verdict, findings ranked by
dollars, spend by prompt with helper (sub-agent) cost stacked, and every
prompt's model calls and tool steps with timing, cost parts and step detail.
Money comes from the parser's own dedupe, turn grouping and calculateCost,
so the session total matches its `sessions` row.

The desktop app opens this as a focused session view when a Claude Code
session is clicked. Transcript content is read on demand only: never cached,
never routed through serve's output memo, never synced or sent in telemetry.

* fix(sessions): audit fixes for the session cost view

Recovery after a failed step stays inside its prompt; neutral note when the
view and the row differ (forked/resumed transcripts); starting-context
estimate compares against the cached prefix; local dates with the end date
when the session spans days; idle gaps that end with a helper reporting back
say so; savings are the difference of the shown figures; singular counts;
more secret patterns (sk_live_/sk_test_, aws_secret_access_key, URL
credentials); a note when the prompt count differs from the row's turns.
2026-10-06 11:49:58 +02:00
Resham Joshi
04d0b65327
feat(cursor): show the Cursor sync status and an on/off switch in the apps (#1638)
* feat(cursor): show the cursor.com sync in the apps, with an off switch

status --format menubar-json gains an additive cursorSync block (enabled,
state, lastSuccessAt, and a fixed errorCode/error on failure), read from
the sync sidecar, the config and the Cursor app's login. It never syncs,
never makes a request and never carries the token, the account hash or
server text. It is absent when Cursor is not signed in and has never
synced, and it is attached after the snapshot and per-device payloads.

With the Cursor provider selected, the desktop overview, the macOS menu
bar and the Windows tray show one line under the total: "Synced from
cursor.com 12 min ago", or the error in a muted warning colour. A
"Sync Cursor usage from cursor.com" switch writes config cursorSync:
Settings > Providers > Cursor on the desktop, the Cursor settings pane
on macOS and Windows. Desktop and mac strings are in all six locales;
the tray stays English until its localization lands.

* fix(cursor): guard config writes, show the env override in the switches

The tray's shared config writer now refuses to write over a config.json
that does not parse or is not an object (it used to replace it with an
empty map plus the one key), which also protects the currency, budget
and Claude config directory writers. The desktop writer rejects null
and array configs the same way.

With CODEBURN_CURSOR_SYNC=0 set, the desktop, mac and tray switches show
"Turned off by CODEBURN_CURSOR_SYNC=0" and are disabled. A failed mac
save now says so under the switch. The tray hero note uses the warning
ink and maps errorCode to its own messages.
2026-10-05 17:11:41 +02:00
Resham Joshi
2fc62f7876
0.9.25: correctness, energy, menu bar and installer fixes (#1511)
* 0.9.25: correctness + provider fixes, energy guard, logo, default DMG

CLI/engine fixes: local-model savings count reasoning; ZCode reasoning billed;
codex-credits version-boundary tiering; GLM alias repointed to z.ai discounted rate;
openclaw/pi/qwen timestamp mtime fallback; hermes delta timestamp;
sqlite/warp/hermes busy+permission reclassified as failed (no sealed empty);
codex-cache dev+ino; cursor-cache WAL; antigravity server TTL; quota timeout+
grokbot footer+claude near-expiry; serve RSS guard env+hysteresis; CSV export
subfolder; providers pane fixed window; Warp credit-based cost.
Desktop/menubar: unified flame logo; default DMG; docs signed+notarized.

* menubar: one running instance, self-relaunch on language change

The single-instance guard only retired strictly older copies, so two login
items starting in the same instant both survived, and a copy whose bundle
identity did not survive install was never seen at all. The decision is now
total (newest launch wins, ties break on pid) and peers are matched on bundle
id or executable name. --keep-both opts one launch out.

Language changes no longer have the desktop app quit and reopen the menu bar,
which made macOS attribute the new process to the desktop app and re-ask for
the data-access grant. The menu bar relaunches itself via the existing remote
command key; older menu bars that do not watch the key fall back to the old
path after 5s.

* desktop: export toast shows the saved path; resource buckets on app_close

The export toast showed the folder that was picked, not the dated subfolder
the files land in. The handler already parsed the CLI's saved line; it now
returns that path and the toast uses it.

app_close carries cpu/mem (and serveCpu/serveMem when serve reported) as bucket
labels, sampled on the existing flush interval. serve adds cpuSec and peak rssMb
to its answer frames. Unmeasured figures are omitted, never sent as zero.

* menubar guard never leaves zero instances; installer replaces in place; JSON export into folder

Guard: an unreadable launch date on either side could make two copies retire
each other. Unreadable peers now count only when their pid is lower, and a copy
that cannot read its own date retires lower pids only, so a disagreement keeps
at least one instance.

Installer: an existing bundle in /Applications or ~/Applications is replaced
where it lives instead of a second copy being added. The old bundle is moved
aside and restored on any failure; a cross-volume move falls back to a copy that
is re-verified with codesign. An unwritable location is never escalated: the
install goes to ~/Applications and the leftover is named.

Export: -o pointing at an existing folder writes the JSON inside it. Short
sessions omit the cpu buckets. setLanguage reopens the menu bar if its own
relaunch never comes back. AppRelaunch passes the bundle path as an argument.

* menubar guard orders by kernel start time; installer opens the copy that exists and recovers a killed install

Guard: launch dates could be unreadable and pid order can disagree with start
order after pid reuse, so two copies could still retire each other. Ordering now
uses the kernel-reported process start time, which both sides read identically.
A copy whose start time cannot be read is left out, so the worst case is two
menu bars, never none.

Installer: with the only copy in an unwritable /Applications, a plain run tried
to open a path that did not exist. It now opens the installed copy. A placement
killed mid-way is recovered on the next run, a concurrent install is refused,
and a failed restore names where the previous bundle is. Process matching uses
the exact executable name.

Export: a trailing separator names a folder. Telemetry cpu is measured from
telemetry start.

* installer: a recycled pid can no longer block installs; language change waits for the replacement process

An aside left by an earlier install counted as an active install whenever its
pid was live, so once that pid was reused every install was refused for good.
It now also has to be recent (ctime within 10 minutes), a pid we may not signal
is not ours, the refusal names the file, and an orphan that cannot be cleared is
reported instead of aborting the install.

setLanguage confirmed the relaunch with a matcher the dying process still
satisfied, so the reopen fallback never fired. It now waits for a pid that was
not running before.

* desktop: only the headline refreshes live; act, yield and the optimize scan move to slower tiers

An open window ran act report (a fresh CLI process each time), yield and an
optimize scan on every refresh. The headline poll now passes --no-optimize and
takes the snapshot path; act report refreshes on mount, on manual refresh and
every 10 minutes, yield every 5. The optimize scan runs at most once a day per
query scope, is stored in userData with its time, and is forced fresh by the
Optimize page or a manual refresh. Figures that are not live show their age,
and no stored scan means no savings clause rather than a zero.

On battery the live interval doubles; the chosen cadence stays the base.

* desktop: a stored optimize scan is only valid on the day it was computed; cadence changes no longer refetch

The scan cache is keyed on the argv, which says 'today' or '30days', not a date,
so yesterday's scan was served as today's. It is now reused only on the same
local day. Stored findings in Signals carry their age, a failed scan shows the
error instead of scanning forever, and the cold-boot label uses the time the app
received the payload.

usePolled fetched immediately whenever its interval changed, so every power
transition (and launching on battery) refetched every live hook and could spawn
an extra act report. Fetching and timer arming are now separate effects.
Optimize's yield is on the same 5 minute tier as Overview.

* desktop: re-ask for the optimize scan on the first render of a new local day

An app left open across midnight kept yesterday's stored savings under Today,
because the scan was only requested on mount, scope change or manual refresh.
The local day is now one of the hook's triggers; the previous day's figure is
dropped while the new scan runs.

* serve: a menubar poll is never answered from the status snapshot

Inside its settle window the status snapshot returns the pre-change payload on
purpose. The resident serve process then memoized that answer as clean, so after
a single write followed by quiet the headline stayed on the old figure until the
next write or the five minute memo cap. The snapshot is for one-shot runs; the
resident process already holds the parsed state, so it now skips it.

* tests stop writing the real serve pid file; superseded caches are swept; installer notes reach the desktop card

ServeConnection takes its pid file as a parameter, so the Swift tests no longer
share ~/.cache/codeburn/menubar-serve.pid with a running menu bar, and the
reaper only signals a record that is a serve --stdio command.

Superseded session-cache, codex-results and cursor-results files (100MB+ each)
are removed once untouched for 14 days, at most once a day. daily-cache and the
hermes ledger are never swept: they carry sealed days a version bump adopts.

The menu bar installer's leftover-copy advice is emitted as a marked line under
CODEBURN_PROGRESS and shown on the desktop card. The skipped-refresh NSLog is
debug only. The app's vitest workers turn off Node's built-in web storage.

* menubar: anonymous usage statistics, under the same consent as the desktop and the Windows tray

The Swift menu bar sent nothing, so every codeburn-menubar row was a Windows
tray. It now follows the desktop's telemetry.v1.json when that exists (read
only: same install id, same on/off, onboarded required) and otherwise keeps its
own state with the same region default (off in EU/EEA/UK/CH and when the region
is unknown). Settings gains one Privacy toggle, disabled when the desktop owns
the choice. Events use the tray's names; the daily usage snapshot is the CLI's
block passed through a sanitizer and is left to the desktop when it is the
consent source. Debug builds never send.

A resident CLI that gets disabled for the run now says so in the log.

* menubar statistics: private network session, abandoned batch on opt-out, a desktop No survives the desktop going away

The sender used URLSession.shared, so a server or CDN cookie and the default
User-Agent rode along and would have outlived an opt-out. It now owns an
ephemeral session with no cookies, no cache, a fixed agent string and no
redirects. A batch that is out when the toggle goes off is dropped rather than
re-queued under the fresh id. The desktop's answer is recorded locally, so a
missing or damaged state file never falls back to the region default, and the
standalone id is persisted when minted. The toggle stays usable as a local off
switch under desktop consent. Copy states what this app actually sends.

* vercel gateway spend is shown on its own row and kept out of totals by default

The gateway reports daily aggregates per model, with nothing to match against
the local tools that were pointed at it, so every headline counted that traffic
twice. Totals now leave the gateway slice out when all providers are shown; the
row keeps its full amount and is marked not in total, --provider vercel-gateway
is unchanged, and codeburn gateway-totals include folds it back in. The daily
cache still stores the slice, so the choice applies retroactively. Reports with
no gateway credential are byte-identical.

The gateway's request_count now feeds the call count instead of each daily row
counting as one call. Hermes: a mid-session price change lands on the day it is
observed and sealed days are never rewritten; stated in the code and changelog.

* gateway exclusion applies to every report through the parse, and the detail panels follow the headline

The first pass narrowed only the day totals, so By tool, top models, top
projects, activities, status json projects and any project-filtered table still
carried gateway spend next to a headline that did not, and By tool shares could
pass 100%. The rule now lives in parseAllSessions: with all providers in scope
and the opt-in off, aggregate-only sessions are dropped after the memo, and the
callers that derive cached days opt out so stored slices are unchanged.
day.projects is reduced like models. The gateway stays visible once, as a
labelled not-in-total row with no share, and standalone reports note the
excluded amount on stderr so machine output is unchanged.

* dashboard: the background history index follows the gateway exclusion too

The index keeps the whole corpus so it can fill the daily cache, but its
per-period projection handed that unfiltered list to the project, model and
activity panels, so they regained gateway spend once the index landed.

* ci: a hung lock suite fails in 90s and retries once with a warning; slow Windows tests get measured limits

The cache-lock step failed 6 of the last 60 runs with no output for five minutes
after the vitest banner; passing runs take 12-18s on every Node version. Each
attempt is now capped at 90s and retried once, with a warning whenever that
happens. On windows-latest a single CLI spawn takes 1.4-12s, so the four-spawn
status snapshot test and the c6 parser suite get limits taken from those runs,
each with its reason. Both test jobs print their ten slowest cases.

* desktop: Privacy & data uses the same rows as the rest of Settings

The pane mixed two static claims with two controls in its own layout. It now
uses the about-row pattern every other pane uses: title and hint on the left,
one control on the right. The claims became the subtitle. Rows: anonymous usage
statistics (toggle, link to what is sent), device sharing state with Manage,
local report snapshots with Clear, export with Open export.

* desktop: Privacy & data keeps one bounded column and rows that never squeeze

The card stretched with the window, stranding each control hundreds of pixels
from its text. This pane's heading, subtitle and card now share a 760px column
and one text measure. Rows stack on their own width, because the window can
never be narrow enough for the old viewport rule to fire; row buttons do not
wrap. The what-is-sent link sits on its own line, and the snapshot and export
descriptions fit their measure and say this machine.

* menubar tests clean up their scratch defaults and leave the real Keychain alone

Four test files never removed their UserDefaults suites and the rest removed the
domain but left the plist, so thousands of files piled up in
~/Library/Preferences. A shared helper forgets a suite and deletes its file, and
every test that makes a suite now uses it. cfprefsd still rewrites an empty plist
when the test process exits, so a run leaves about a hundred 42-byte files with
no test data in them. The real-Keychain round trip only runs with
CODEBURN_TEST_REAL_KEYCHAIN=1.

* desktop: the resident parser retires after 15 idle minutes and restarts on demand

The renderer stops polling while the window is hidden, but the serve child stayed
resident with its parsed cache, one to two GB on a large corpus. After 15 minutes
without a request it is stopped the way a settings restart stops it, without
spending the unexpected-death budget, and the next request starts a new resident
through the existing lazy path, never a one-shot. CODEBURN_SERVE_IDLE_MS tunes or
disables it.

* desktop: every settings pane keeps a bounded column, and dark buttons have an edge

Cards stretched with the window, so on a wide display controls sat up to 1300px
from their labels and intro lines ran past 140 characters. All panes now share
one left-aligned column (760px, 980px for projects, aliases and pricing, which
hold wide rows) and one text measure. Rows stack on their own width instead of a
viewport rule the 900px minimum window could never reach, and a long path in
Projects no longer overflows the pane.

In dark mode the secondary button matched or sat below its surface, and was
invisible on the plugin install dialog. It gets a face and a border from two new
tokens; light is unchanged. The onboarding consent label matches Settings.

* desktop: this device shows its fingerprint on its own line

The local name and the fingerprint are two adjacent inline spans, so they read as
one run of text.

* menubar: the resident parser retires after 15 idle minutes and restarts on demand

The menu bar kept its serve child, and the parsed cache with it, for the whole
run, even with the popover closed and nothing changing. After 15 minutes without
a request the child is retired without counting as a death, and the next request
starts a new resident through ensureStarted, never the one-shot fallback, whose
snapshot settle window would make the figure lag a whole tick. The window sits
above the longest gap between real requests, so a child stays warm while work is
happening. CodeBurnServeIdleSeconds tunes or disables it.

* serve: a poll re-checks only what the watcher saw change, and shard writes are coalesced

With a session being written, every poll re-listed about 2,400 directories and
re-fingerprinted about 6,000 transcripts, then rewrote the whole current-month
shard (49MB on a large corpus) and read it back on the next poll. The resident
process now reuses the previous listing and fingerprints for paths its root
watcher proves untouched, and falls back to the full sweep whenever the watcher
is missing, unhealthy, overflowed, armed after the sweep, does not cover the
path, the memo is older than 4.5 minutes, or the process was suspended. Shard
publication is held for up to 90s and flushed when stdin closes; a held write is
dropped rather than written over something another process published. One-shot
runs are unchanged. Active poll cost 0.71 to 0.33 CPU-s at a 30s cadence;
time-to-reflect unchanged at about 1.2s across append, new dir, new file, delete,
rename-over, in-flight, watcher overflow and unhealthy watcher.

Changelog: state the telemetry default as it is in this version.

* a retired parser gets to publish its held shards, and durable providers are never held

Both apps retired the serve child with SIGTERM before its stdin-close flush could
run, so every retire discarded the coalesced window. They now close stdin, give
the child a bounded grace to publish and exit, and signal it only if it is still
running; a settings restart does the same. Measured: flushed 0/5 before, 5/5
after, for both clients.

copilot's cache entries are the only record once Copilot prunes its own files, so
a held window that was dropped (retire, crash, kill -9) before the source was
pruned lost that spend for good. A window that a durable provider dirtied is
published on the poll that parsed it, as before coalescing existed. The RSS guard
publishes a held window before it clears the caches instead of discarding it.

* serve: release the lifetime parse after the fill, and make the collection real

The RSS guard dropped its memos and then called globalThis.gc, which exists only
under --expose-gc; no launcher passes it, so the collection had never run and the
freed pages never went back. On a cold start the resident idled at 2.2GB with
364MB of live heap: clearing every memo moved RSS by 0MB, a collection alone by
700MB.

After the background fill has sealed its summaries the process now publishes any
held shard window, drops the memos it can re-read from disk (the cold start had
loaded the cache with scope 'all', which then served every later poll), and
collects; it collects again a minute later for as long as a pass returns 64MB,
at most four times. Cold idle 2198MB to 700MB, warm 900MB to 781MB. Reports are
leaf-identical cold and warm, time-to-reflect unchanged, a released month scope
reloads in 0.3-0.5s.

* warp: the billed cost reaches reports and survives the cache

The provider derived a dollar cost from Warp's own billing record (provider cents,
charged usage, or credits), but warp was missing from the reported-cost
pass-through, so the cached call kept no cost and every report, cold or warm,
priced the conversation from its tokens. A fixture billed at $1.22 reported
$0.15. A call whose cost came from a billing record now keeps it; a call that
only has the token floor stores none and is still re-priced when prices change.

Warp entries re-parse once (provider parse version), and the daily cache moves to
v34 so days whose Warp source still exists re-derive; call counts do not move, so
nothing is double counted or dropped.

* openclaw and pi: a reported cost survives the cache like every other reported cost

Both providers read the cost their tool recorded for a message, and both were
missing from the reported-cost pass-through, so reports priced every call from
tokens instead. Fixtures reported $0.011 against $0.056 (openclaw) and $0.013
against $0.410 (pi). A call with a reported cost now keeps it; a call with none,
or an explicit zero, stores none and is still re-priced when prices change. omp
had been added to the list in #1150 and pi, the same parser, was never revisited.

The provider list is one named set, the unreachable providerCallToTurn is gone,
and both providers re-parse once; the v34 daily cache re-derives their days with
call counts unchanged.

* test: pin parse count per report and per resident poll
2026-09-21 00:16:21 +02:00
Resham Joshi
ea78d36ccf
Windows menu bar and Capacity Dock as a Plugins card, plus desktop fixes (#1507)
Some checks are pending
Tests / desktop (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
CI / semgrep (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
* feat(windows): configure the menu bar and Capacity Dock as a Plugins card

Windows now configures its tray companion from a card on the Plugins page, the
same place macOS does, instead of two switches in the sidebar corner.

- WindowsCompanionCard mirrors the macOS MenuBarCard: Install/Reinstall,
  Update, Open (show the tray popover), Settings (opens the desktop Menu bar
  settings pane), Quit (stop without uninstalling), Uninstall, plus the
  Capacity Dock toggle, a Running dot, the version, and an info popup.
- MenubarCompanion.status() now reports installed/running/version/outdated/
  canInstall, and new discrete install/open/quit/uninstall actions back the
  card. uninstall only forgets the tray once the CLI removal succeeds, and a
  cancelled elevation prompt is treated as a no-op rather than a failure.
- isNewerVersion sorts a dev build below any release, so a staged release still
  lights Update and a dev build never does.
- codeburn menubar --uninstall reads the product code from the install marker
  or the uninstall registry and runs msiexec /x.
- Remove the bespoke sidebar CompanionSwitches and their dead CSS.
- A dev-only CODEBURN_MENUBAR_RESOURCES override lets the tray install run
  without packaging; the CLI still validates the staged path in full.

* feat(desktop): fold the Capacity Dock settings into the Menu bar pane

One tray rail entry instead of two: MenuBarPane and CapacityDockPane render
together in the Menu bar pane with a clear gap between the two cards, so the
card's Settings button opens both. Drops the now-unreachable 'dock' pane value.

* fix(desktop): stop the Spend cost-flow Sankey from re-rendering forever

The label-fit effect measured the already-truncated text, so a capped label
read as short, un-capped, overflowed the cap, and re-capped without end
(Maximum update depth exceeded). It now estimates each label from its full
name via data-name, and caps only tighten per layout, so the measure, cap and
remeasure cycle is guaranteed to settle instead of oscillating.

* feat(desktop): bundle Inter and JetBrains Mono for one UI on every OS

The app used each platform's system font (SF Pro on macOS, Segoe UI on
Windows), so the two looked different. Bundle Inter for text and JetBrains
Mono for figures and lead the --sans and --mono stacks with them, keeping the
per-locale CJK fallbacks, so Windows and macOS render the same faces.

* desktop: gate tray installed state on support; drop strings and channels the sidebar switches left behind
2026-09-19 18:43:27 -07:00
Resham Joshi
71e3d4d44f
desktop: stop refreshing and animating when the window is hidden (#1496)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
The desktop kept polling and re-parsing on its 60s timer even while minimized or hidden, and the flame wordmark animated a background-clip gradient forever, repainting every frame; together they pegged the app near 60% CPU at rest. The refresh timer now stops when the window is hidden and resumes with an immediate catch-up when it returns, and the wordmark gradient is static. Measured: all CodeBurn processes drop to 0% when hidden and the renderer idle drops from about 8% to 2%. Refresh speed and all numbers are unchanged; only when polls fire changed, never what is fetched. The remaining focused-with-active-agents full reparse is addressed separately by incremental re-parse.
2026-09-19 04:52:08 -07:00
Resham Joshi
43608c2558
desktop and menu bar: six-language localization (#1493)
The desktop app and the macOS menu bar can now run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the choice is shared so the menu bar switches with it, relaunching to pick up the language with a retry so a switch never leaves it down.

Every user-facing string in the desktop renderer goes through a local lookup table, 1,249 keys per language, no framework and no network. The menu bar gains four .lproj to join English and Simplified Chinese, 626 keys each. A completeness gate fails the build if any language is missing a key, in both the desktop catalogs and the menu bar coverage suite. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. English output is byte-identical.

Strings produced by the CLI engine stay English until the CLI is localized, a separate effort. Validated live in all six languages: no missing text, no clipping, correct fonts, numbers unchanged, and the menu bar switching for every language.
2026-09-19 03:25:46 -07:00
Resham Joshi
246cb37c83
desktop: light theme default, refreshed type and spacing, plugin and plan cards (#1485)
Fresh installs now default to the light theme; an explicit theme choice is still honored. Type and spacing move one step up (larger body, titles, sidebar rows and page padding), sidebar shortcut chips move to hover tooltips, and primary text gets full contrast in both themes.

The Plugins page shows the Menu bar and Teams plugins as two cards side by side with embossed background art. The Menu bar card carries the Capacity Dock switch and Settings, Quit and Uninstall as icon buttons with tooltips, an info modal describing the menu bar and the Capacity Dock with the real supported-provider list, and it can install, update, open, quit and uninstall the menu bar app, versioned so an older one shows Update, with a Settings action that opens the menu bar app's own settings. The Teams card shows what is planned in an info modal and a Register for beta testing link.

The Plans page is redesigned as provider cards in a responsive grid, each with its own logo embossed. Connection status is now stable: a single empty or slow poll no longer flips a connected provider to disconnected, a connected provider keeps its quota bars through a transient refresh, and a stuck waiting resolves to an actionable state after twenty seconds. A login-expired error offers Connect, and Gemini's expired login is now a terminal actionable state instead of an endless wait, matching the menu bar.

CLI report, models and menubar-json numbers are identical to the cent before and after; only the Gemini quota error string changes.
2026-09-18 15:09:01 -07:00
Resham Joshi
4caf59dafb
desktop: token breakdown popover behind dollar amounts (#1472)
Hovering or focusing a dollar amount shows the input, output, cache read and cache write tokens behind it, plus calls, in a popover portaled to the body. Amounts with no token counts behind them, such as projected month, percentages, retry tax, savings and the Spend and Plans tables, are left alone rather than shown a fabricated breakdown. periodTotals carries the four counts so the hero keeps its breakdown when the number comes from the generation window, and weekly bars sum them. No typography or layout change.
2026-09-18 00:03:37 -07:00
Resham Joshi
3bf2ab6db3
desktop: stop the Overview side column painting over the models table (#1471)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
The Models this period card had an automatic minimum width equal to its no-wrap table, wider than its column between 1000 and 1190px, so the side column painted over the Calls column and the sessions card. Cards now get min-width 0 and the body grid stacks on its own width with auto-fit columns; the table scrolls inside its card where the width is tight.
2026-09-17 16:57:48 +02:00
Resham Joshi
3e9542d63d
Consistent period totals and incremental refresh (#1467)
Every period headline now comes from one aggregation, so switching period no longer mixes snapshots taken minutes apart and Lifetime can no longer read below the six months inside it.
Days preserved from expired transcripts count toward the totals, and the usage streak is one machine-wide number taken from the hydrated daily cache.
A provider-filtered hero reads the scoped payload instead of the unscoped one.
Anchored menus and popovers render through a portal, so the card anatomy no longer clips them after three rows.
The Spend by-branch picker groups checkouts by git origin, one entry per repository with the checkout count beside it.
On the performance side, serve invalidation is scoped to the day a changed file can hold, -shm writes are ignored, and the optimize detectors keep one range-independent scan record with ordered version events.
Memo budgets are set by retained bytes, and every serve response carries the generation and timestamp it was built from.
A 30D stay with appends drops from 7253ms to 2387ms and peak RSS from 2904MB to 1819MB.
No cache format changed. Payloads stay byte-identical except for periodTotals, streak and liveSessions.
2026-09-17 12:35:05 +02:00
Resham Joshi
d81a86dd02
Desktop: measured polish pass, new frame, card system, motion, onboarding (#1459)
Measured design audit fixes, one type ladder and header, zero clipping at 900 to 1680, light-theme contrast, Lucide icons, shared Escape, plurals and provider names, motion tokens with zero refresh shift, black frame with rounded content card, grouped monochrome sidebar with collapse, two-layer card system on every screen, Overview gauge, sparkline cards and chart grid, wallpaper onboarding, Windows fixes. Every number diffed identical to main on a fixed range; 1,032 app tests passing.
2026-09-16 19:48:33 +02:00
Resham Joshi
16c41df8eb
Compare periods: lead sentence, three tiles, day bars, top movers (#1446)
* Add per-day cost for both sides to the compare-periods report

* Rebuild the Compare periods screen around a lead sentence, day bars and top movers

* Update the Compare periods doc for the new layout

* Extract the duplicated chart tooltip into one ChartTip component

* Make the Compare periods sentence, tiles, day chart and movers table say what they measure

* Cover the compare lead sentence rules and the percent, date and path formats

* Update the Compare periods doc for the new day chart

* Style the Compare periods Swap button as an outlined pill instead of a solid grey block

* Fix the round-2 findings on the Compare periods chart, range label, Swap button and duration

* Fix single-day range label doubling and lone day bar width in Compare periods

rangeLabel now short-circuits from === to like terseRangeLabel already did.
pcmp-bar uses flex 0 1 50% so a lone bar in a slot takes half the slot
instead of the full slot, matching paired bars.

* Keep a lone day bar the same width as a paired one
2026-09-15 23:12:12 +02:00
Resham Joshi
ce49dc46a6
Refresh without layout shift: hairline and status spinner instead of banners (#1445)
* fix(desktop): replace switching banners with a non-shifting hairline and footer refresh mark

* fix(desktop): round 3 review fixes for refresh indicator

- footer live region now announces empty string while idle instead of
  re-announcing the ticking elapsed-time label every second
- reserve width on the footer status box so it does not shift between
  "refreshed Ns ago" and "not refreshed yet"
- guard usePolled's fetcher() call so a synchronous throw still
  decrements the in-flight counter
- __resetPolledMemo now notifies in-flight listeners when it zeroes
  the counter
- restore the "sessions updating" suffix on the cached snapshot's
  calls sub-line
2026-09-15 23:11:34 +02:00
Resham Joshi
c2d514d1e5
Session drawer: plain-language lead, three tiles, folded detail (#1444)
* Fix session drawer tile grid wrapping and edge clipping

* Stop the session drawer tile value from clipping

* Rewrite the session drawer around a plain-language lead, three tiles, and folds

* Reword the pull-requests caption to drop the em dash

* Drop the drawer's shape clause instead of labelling most sessions the same

* Base the drawer's multiple on the rows the list shows, and keep it readable

* Put the selected figure in the drawer's lead and Cost tile

* Swap the drawer's empty Duration tile for Calls and say long spans in days

* Name the drawer's breakdowns, singularize one call, and hide an empty fold

* Cover the drawer's ratio bounds, selected cost, Calls tile, and fold rule

* Separate the drawer fold count, punctuate the cost caption, and soften the smallest ratio

* Shorten the drawer cost caption and name the smallest ratio without an inequality
2026-09-15 22:53:43 +02:00
Resham Joshi
38a3d3b1cf
Fix layout, clipping and copy on Compare periods (#1440)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
* Fix layout, clipping and copy on the Compare periods screen

* Drop dead control padding and restore the compare row hover inset
2026-09-15 21:18:55 +02:00
ozymandiashh
9a383dca30 Add Compare cohorts mode with population disclosure, samples, and volume band
Desktop cohorts mode talks to the new cohort-json format through two
typed bridge channels (facets + report) with argv validation for models,
repeatable project patterns, and categories. The section keeps the
classic mode side by side; the cohorts view shows the population before
any metric (included observations, distinct sessions, excluded mixed-
model turns with their combined cost, unknown-cost count), an Inspect
samples list of the declared population through a minimal navigation
adapter (sampleNavigation.ts), cost median/P90/mean with a compact
histogram whose top bucket is open-ended, one-shot/retry rates over the
declared population, and token volume with input + cache-read labeled as
a context proxy. The volume band recomputes renderer-side over the
report's own observation list (cohortStats.ts mirrors the core
convention, pinned to the same test vector), excludes and counts
observations without a token measure, and never reads missing data as
small. CODEBURN_USER_DATA_DIR opts into an isolated Electron profile so
parallel dev runs get their own userData/serve.pid.
2026-09-12 14:01:29 -07:00
ozymandiashh
9a14f8d4b0 feat(desktop): Compare periods screen with contribution drill-down
New sidebar section 'periods' (Compare periods, shortcut 9) rendering
sections/PeriodCompare.tsx: preset last-7-complete-days vs prior 7 or two
custom calendars with a swap control; range meta shows both spans, the
duration delta and any overlap; totals, normalized per-day and per-100-calls
tables (denominators printed, zero denominators render as em dash); project
and model contribution lenses with New/Gone badges and click-to-inspect
session drill-down joined across A and B; coverage block for unpriced models
and aggregate-only carried history. A/B selection, lens and view persist in
localStorage so returning to the screen keeps the selection.

Navigation adapter until the goal-8 navigation lands: onInspectRange opens the
existing Sessions section scoped to the requested range via the app's
custom-range state; the Overview entry card links here. Renderer styling
follows the existing panel/table conventions.

Includes component tests (fixture report, New/Gone labels, per-100-calls
recomputation with zero-call sides, persistence across remounts, lens
switching) and user documentation at docs/compare-periods.md.
2026-09-12 13:43:24 -07:00
ozymandiashh
28120c313f Add Spend By branch lens (renderer -> IPC -> branch-json report)
New getBranchSpend bridge channel (codeburn:getBranchSpend) spawning
'spend --format branch-json' with the same period/provider/range flags and
validation as the other first-click reports, plus background-priority
prefetch handling. The renderer keeps the typed contract in lib/types.ts.

BranchBreakdown panel: project picker (auto top project, explicit All
projects view labeled project/branch), expandable branch rows with token
components, worktree evidence and per-session contributions, a real session
inspect detail, and the coverage note (known / Unknown / no-branch-data,
identity-based distinct sessions). Reuses ListRow/Panel/Dropdown/stale-safe
usePolled memo keys; selection is derived so a refresh or a vanished project
falls back instead of mixing data.
2026-09-12 13:09:00 -07:00
ozymandiashh
d1dcc82ee5 Wire drill-through into the desktop app: entries, chips, contributions, drawer, history
Every report grows real entries: Overview day bars, expensive sessions
(provider+id matched), model names, task categories, Spend projects,
and PR cards. The Sessions destination renders the selection chips with
per-chip removal and Clear, fetches the contributions report once per
population (widened to cover the selected days so a day outside the top-bar
period still reconciles), computes every chip combination client-side over
the full population, and shows each row's contribution beside its full
cost. Row clicks open the side drawer (Escape closes, focus returns); the
top bar gains Back/Forward (Cmd+[ / Cmd+] on macOS, Alt+arrows elsewhere)
over the in-app history; the position survives a renderer restart. IPC
gains getSessionsContributions with the same argv validation as the plain
report.
2026-09-12 12:22:19 -07:00
iamtoruk
a3f2b0b112 feat(app): search and cost order for the Projects pane
The pane rendered one switch per detected project in whatever order the
report returned them: 2,762 rows on this machine, with nothing to type into
and the costliest buried at an arbitrary depth. Sort by lifetime cost
descending and narrow on a case-insensitive substring of the name or path,
with a "12 of 2,762" count beside the box once it narrows.

A substring pass over a few thousand rows is nothing, so no virtualisation.
The switches and the hide-a-pattern field are untouched, and the hidden
count still reads off the whole list, not the visible slice.
2026-09-12 06:01:23 -07:00
Mathieu Stanowski
6cac9d929c feat(app): add a Projects pane that scopes the desktop app to chosen projects
Screen-sharing or demoing the app exposed every project on the machine,
with no way to narrow it. Settings now has a Projects pane: one switch
per detected project, plus a field for hiding a pattern that matches
nothing detected yet.

The filter lives in ~/.config/codeburn/app-filter.json rather than
renderer storage, because the main process is where the CLI argv is
assembled. No file, or empty lists, means no filter, so the default is
unchanged for everyone who never opens the pane. CODEBURN_APP_FILTER
overrides the path and an empty value disables it, which is what the app
test suite sets so a real filter file cannot reach the assertions.

Details worth knowing:

- The pane's own project list is fetched WITHOUT the filter. Scoping it
  would hide the projects the checklist exists to switch back on.
- Patterns are passed as --project=<value>, never as two argv entries. A
  Claude project directory is encoded as "-Users-me-Web-thing", and a
  leading dash in its own argv slot parses as a flag.
- Combined scope and a project filter cannot both hold. The CLI rejects
  --scope combined next to --project/--exclude because paired devices
  report their usage unfiltered, and dropping the filter instead would
  put the hidden projects back inside the combined total, which is the
  one thing the pane exists to prevent. So the filter wins: Settings
  offers Local only while something is hidden (and says why), the
  renderer derives its effective scope from the filter so even the first
  poll of a filtered session is local, and buildOverviewArgs keeps the
  same rule as a guard for a stale caller.
- A read that fails on an existing file keeps the last known filter
  instead of falling back to the empty one. Unhiding every project on a
  torn write or a typo mid-edit is the one failure mode this pane cannot
  have. Only a missing file means "show everything" again.
- The pane's matcher lives in renderer/lib/projectMatch.ts, and
  projectMatch.test.ts pins it against the CLI's matchesProjectPattern
  over every pattern shape. A switch that disagrees with the argv it
  writes would lie about what is on screen.
- Writes are serialized: every write is computed from the filter on
  screen, so a second click before the first landed would be built from
  the pre-click value and drop it.
- export carries the filter too, since it writes project names into a
  file that gets shared. getPlans stays unfiltered, like combined scope:
  a plan is billed on every project, so scoping it would misreport the
  cycle.
2026-09-12 05:38:52 -07:00
Aditya Vikram Singh
18b3f6f948 fix(desktop): keep settings navigation from shrinking 2026-09-09 21:37:25 +05:30
Aditya Vikram Singh
a8cfce0c9f
Merge pull request #1169 from ksp2000/feature/standardize-model-capability-tiers
feat: standardize model capability tiers
2026-09-08 10:07:27 +05:30
iamtoruk
ff2ba5c94a fix(app): typed IPC patches, the staged-msi flag, and honest consent copy
- Tray preference patches from the window were used with `in` before
  any type check; every patch handler now refuses anything that is not
  a plain object.
- The tray install passes `--staged-msi <path>` to the CLI instead of
  an environment variable.
- A timeout or a missing CLI no longer counts as a declined install,
  so it is retried at the next launch; only a cancelled prompt is
  remembered. After msiexec reports a pending restart the old binary is
  not launched, and the sidebar says a restart finishes the install.
- The telemetry opt-in event fired before the toggle was applied and
  could never be sent; it fires after a successful write.
- The ARM64 installer carries the x64-named tray, since the CLI accepts
  only that asset name; the distribution notes say so plainly.
- The README, the onboarding screen and the privacy settings say that
  the daily report includes the names of the models, tools, skills and
  MCP servers in use, and the renderer's fallback builder reports zero
  turns as zero.
2026-09-03 03:06:13 -07:00
iamtoruk
22218b12ea fix(app): keep the sidebar links on macOS and Linux
The tray switches took the sidebar corner on Windows and the social
links moved to About, but the switches render nothing off Windows, so
Mac and Linux users lost the row and gained nothing. The links are back
there on every platform but Windows.
2026-09-02 22:10:37 -07:00
iamtoruk
94a5fcd0ab feat(app): Menu bar and Capacity Dock panes in Settings
The tray app has a settings window of its own, and nobody should have to find it
to change how the thing in their notification area behaves. Two panes join the
Settings rail: Menu bar and Capacity Dock.

Only the settings the desktop app does not already have are there. Currency, the
period, the scope and the daily budget are shared through the CLI config and
already live in General, and repeating them would be two controls for one value.
So Menu bar carries the tray figure's metric and period, the accent, whether
today's figure gets its own tray icon, the two refresh cadences, the terminal and
launch at login; Capacity Dock carries show, resting provider, size, appearance,
gauge shape and the per-provider switches.

Each pane appears only while its switch in the sidebar corner is on: there is
nothing to configure about a tray app that is not running, and the rail is one of
its windows. A pane whose switch has just gone off cannot stay on screen either,
so the rail falls back to General.

Two things the rail's own rules are worth restating here. The last connected
provider cannot be switched off, because the rail must never have nothing to
show. And a provider stays in the list after its connection drops, so one that
has stopped working can still be taken off.

Every control renders what came back rather than what it sent: the main process
is what decides the value that lands, and it repairs one it would not be able to
read later.
2026-09-02 06:28:24 -07:00
iamtoruk
e54d8d0bfd feat(app): the two switches cannot disagree
The rail is a window of the tray app, and every setting it renders from belongs
to the tray app, so a Sidebar that is on while the menu bar app is off describes
something that cannot exist.

Turning Menu bar off now turns Sidebar off with it, and writes enabled: false
into windows-dock.json before sending --quit, so what the tray app finds the next
time it starts is the state the switches are showing rather than a rail that
comes back on its own. Turning Sidebar on with Menu bar off turns Menu bar on
first, installing and starting the tray app and writing the Run value, and only
then enables the rail; if the tray app cannot be turned on there is nothing for
a rail to live in, and both switches stay off.

In the corner the Sidebar switch is disabled while Menu bar is off, with a title
saying why. That is the primary guard, and the cascade above is what answers a
request that arrives any other way: through the IPC channel, or from a settings
file that already disagrees with itself.
2026-09-02 06:07:24 -07:00
iamtoruk
2bd44113c7 feat(app): two switches where the social glyphs were
The sidebar's bottom corner held About and a row of five brand glyphs. The
glyphs were the least useful thing on that screen: they are visited once, and
About already listed every one of them under Links. So they are only there now,
and the corner carries the two things a Windows user does want at hand: whether
CodeBurn sits in the notification area, and whether the Capacity Dock rail is on
the screen edge.

Both are on by default and neither renders at all until the main process says
this build has a tray app staged, which is why there is no placeholder row and
no greyed-out switch: on macOS, on Linux, and in a dev build with nothing
staged, the corner is exactly what it always was.

The switch is the shared one from Settings and onboarding, at 30x17 rather than
38x22 because a 186px sidebar has no room for the full size beside a label, and
keyed off the theme tokens rather than the white overlays the bare shape uses on
the dark onboarding sheet.
2026-09-02 04:43:07 -07:00
ksp2000
08aabc5e5d feat: standardize model capability tiers 2026-08-29 20:22:01 +05:30
Aditya Vikram Singh
6406296cb1
perf(desktop): show exact totals while details refresh (#1157)
* perf(desktop): warm time horizons before providers

* perf(desktop): avoid idle provider parse storm

* perf(desktop): show last exact headline during refresh

* perf(desktop): paint cached headline without animation

* fix(desktop): keep timeout recovery single-flight

* fix(desktop): keep cached headline stable on handoff

* fix(desktop): keep overview timelines legible

* fix(desktop): keep period switches truthful

* fix(desktop): open heatmap at newest dates

* fix(desktop): label cached data during refresh

* fix(desktop): invalidate progressive cache safely

* fix(desktop): keep progressive views exact across boundaries

* fix(desktop): preserve filter responsiveness

* fix(desktop): bound instant-switch memo
2026-08-28 13:20:33 -07:00
iamtoruk
3936284811 optimize: show the applied-fix verdicts in the TUI and desktop app
Same section, compact: one line per still-applied fix with the verdict
glyph, and the undo command for the ones that measured nothing. The app
reads appliedFixes[] off the optimize JSON, tolerating its absence from
an older CLI.
2026-08-18 03:15:12 -07:00
iamtoruk
7c54cf85c2 optimize: classify findings as fix/nudge/keep and mark measured vs estimated
Every finding now resolves to a class (apply-able fix, habit nudge, or
informational keep) and a basis (measured from provider-counted usage, or
estimated from a schema/heuristic model), both from one table next to the
FindingId union. The class follows the plan layer: an id is 'fix' only when
buildPlan routes it, and an instance drops to 'nudge' when it lacks the
payload or cause its builder needs.

CLI, TUI and the desktop app group findings under Fix now / Habits / FYI
with continuous numbering; the CLI header reports 'N measured · M
estimated' in place of the blanket 'Estimates only.' footer. The JSON
report gains class + basis per finding and summary.measuredSavingsUSD;
existing fields are unchanged. The menubar's top three follow the same
order, since every surface reads the sorted findings list.

Sessions whose cost the provider never reported leave the cost-outliers
peer math; when nothing else is priced the comparison falls back to them
and the finding reports itself as estimated instead of disappearing.
2026-08-18 02:26:57 -07:00
Marc Reynolds
61581edb18
feat(dashboard): add combined-device scope to desktop app and menubar badge
Fixes #795: the desktop Dashboard showed only local-device cost while the
menubar and web GUI aggregated across paired devices.

Desktop app:
- Add a Local/Combined Scope setting (Settings > General), mirroring the
  macOS menubar. Combined passes `--scope combined` to the CLI, forces the
  provider filter to all, and persists to localStorage.
- Overview hero shows the paired-device aggregate (cost/calls/sessions) with
  a "Combined · N devices" caption and per-device breakdown when Combined is
  selected; detailed panels remain local (the combined payload carries totals
  only).

Menubar:
- The badge figure now reflects Combined scope instead of always showing the
  local total: refreshMenubarBadge fetches the combined payload for the badge
  period and the badge renders the cross-device aggregate, falling back to
  local when no combined payload is available.
2026-07-31 15:12:29 -06:00
Resham Joshi
2eb4718122
sessions: correlate cross-provider work with pull requests (#792) 2026-07-21 11:50:11 +02:00
Resham Joshi
45e93129f2
sessions: attribute PR spend per turn instead of per session (#790)
* 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>
2026-07-20 20:44:29 -07:00
Resham Joshi
a9d4471d17
payload: spend by pull request and by branch; desktop Pull requests tab (#788)
Some checks are pending
CI / semgrep (push) Waiting to run
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>
2026-07-20 14:36:53 -07:00
Resham Joshi
4d2bcbac7f
app: Workflow card on Overview and session titles in the Sessions view (#784)
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>
2026-07-20 13:55:33 -07:00
Resham Joshi
dbd93fee51
app,dash: render pre-history days as no data, not a currency zero (#765)
* 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.
2026-07-20 08:03:24 -07:00
iamtoruk
0a76626d21 fix(app): clean build stamp, splash without hash, About checks for updates
- 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.
2026-07-17 14:30:00 -07:00
iamtoruk
8fc9e303b9 feat(app): update-available notifications
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.
2026-07-17 04:58:13 -07:00
iamtoruk
e4bbcaaba7 fix(app): zero background work when hidden; currency applies instantly
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.
2026-07-16 18:41:28 -07:00
iamtoruk
ae18b1fd91 fix: audit batch — prefetch one-shot, turn-anchored day bucketing, warm-path collapse
- 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.
2026-07-16 15:37:03 -07:00
iamtoruk
e9655fa03c fix: hydration completeness markers, cold-only splash, honest switching, prefetch
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.
2026-07-16 14:05:11 -07:00
iamtoruk
300e949951 feat(app): refresh cadence, instant switching, cache coherence, telemetry v1
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).
2026-07-16 11:47:05 -07:00
iamtoruk
d5b18ff3b4 polish(app): compact first-run splash
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.
2026-07-16 10:59:20 -07:00
iamtoruk
55bc53e6be fix(app): cold start survives, first-run shows per-provider progress
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.
2026-07-16 10:51:18 -07:00
iamtoruk
28d294cde5 fix(app): provider logos key on internal ids + monogram fallback
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.
2026-07-16 09:27:46 -07:00
iamtoruk
9517d63ba3 fix(app): loader video floats with real alpha (no background)
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.
2026-07-16 06:22:32 -07:00
iamtoruk
000e39a034 feat(app): binary-flame brand art + animated video loader on the splash
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.
2026-07-16 06:19:12 -07:00