#1657 put the ~ mark on estimated rows, but the headline total stayed bare on
the desktop hero, the mac hero, the Windows hero and tray text, and GNOME.
Each now applies the same 1% rule to the provider split's estimated portion
(the desktop helper moves from the VS Code summary into lib/format). Combined
device totals carry no estimated split and stay unmarked.
For a bundled CLI the not-found panel now tells desktop users to reopen or reinstall the app instead of installing from npm. Strings in all six locales.
* 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: key the daily project split per real path, not per label
A project label can span several paths: every Claude session started from the
home folder shares -Users-<name>, while its cwd puts it in a different project.
The day rollup keyed by label kept the first path it saw, so a rooted
--project headline took the whole label bucket on some days and missed it on
others. Key per (label, path), read old label-keyed days unchanged, bump the
daily cache to v68.
* 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.
* feat: group projects by git repository
Clones and worktrees that share an origin remote are one project in every
project list (report JSON, By Project, the top bar picker, the terminal
dashboard), with the checkouts listed under the row. A --project path inside
a checkout scopes to the whole repository; --exact-project keeps the old
single-folder match. The origin is learned while the folder exists, kept in
git-origins.json and on each day's project entry (daily cache v69), so a
deleted scratch clone stays in its repository. The report JSON project list
now comes from the durable day set, so a row costs what selecting it reports.
* fix: a picked project row selects that row exactly
The top bar pick sends =<path>: a checkout in a repository still selects the
whole repository, and a plain folder selects itself without the folders under
it, which are rows of their own. So the picker row and the headline it opens
are the same set. Typed --project paths keep their prefix match. Days adopted
from an older cache learn the origin of every checkout that still exists, and
lookups outside a parse are saved on exit.
* fix: key the status snapshot on --exact-project
* feat: one Temporary folders row for scratch folders outside a repository
Every project path under a temp root (os.tmpdir(), /tmp, /var/folders and
their /private spellings) that is not a checkout of a known repository folds
into one row, in the report JSON, By Project, the terminal dashboard, the top
bar picker and the Settings Projects pane. Its pattern @temp selects or hides
all of them. The Projects pane leaves out rows that round to $0.00 unless a
saved pattern names them, and a saved pattern naming any checkout of a
repository row now names the whole row in the renderer too.
* feat: group a deleted sibling folder with its repository by folder name
A deleted folder with no recorded origin, named <checkout>-<suffix> or
<checkout>_<suffix> next to exactly one repository's checkout, joins that
repository row. The checkout entry carries matchedByFolderName and the Spend
drill-down labels it. Existing folders and names two repositories could claim
are left alone.
* feat: the longest checkout name wins a folder-name match
A deleted folder that fits several sibling checkouts joins the one with the
longest name; it is skipped only when the longest fits belong to two
repositories. The report project type declares matchedByFolderName.
* fix(claude): attribute each call to the project of its own cwd
* test: fold the temp roots the tests set, compare checkout ids as identities (Windows)
* test: compare folder-name checkout ids as identities (Windows)
* fix(claude): keep subfolder cwds in their project, count and show a split session once
* fix: an exclude path hides that checkout only, repository rows count a session once
* feat: mark folder-name matches in the CLI report and the project picker
* fix(claude): a deleted subfolder stays its own project
* 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: key the daily project split per real path, not per label
A project label can span several paths: every Claude session started from the
home folder shares -Users-<name>, while its cwd puts it in a different project.
The day rollup keyed by label kept the first path it saw, so a rooted
--project headline took the whole label bucket on some days and missed it on
others. Key per (label, path), read old label-keyed days unchanged, bump the
daily cache to v68.
* 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.
* feat: group projects by git repository
Clones and worktrees that share an origin remote are one project in every
project list (report JSON, By Project, the top bar picker, the terminal
dashboard), with the checkouts listed under the row. A --project path inside
a checkout scopes to the whole repository; --exact-project keeps the old
single-folder match. The origin is learned while the folder exists, kept in
git-origins.json and on each day's project entry (daily cache v69), so a
deleted scratch clone stays in its repository. The report JSON project list
now comes from the durable day set, so a row costs what selecting it reports.
* fix: a picked project row selects that row exactly
The top bar pick sends =<path>: a checkout in a repository still selects the
whole repository, and a plain folder selects itself without the folders under
it, which are rows of their own. So the picker row and the headline it opens
are the same set. Typed --project paths keep their prefix match. Days adopted
from an older cache learn the origin of every checkout that still exists, and
lookups outside a parse are saved on exit.
* fix: key the status snapshot on --exact-project
* feat: one Temporary folders row for scratch folders outside a repository
Every project path under a temp root (os.tmpdir(), /tmp, /var/folders and
their /private spellings) that is not a checkout of a known repository folds
into one row, in the report JSON, By Project, the terminal dashboard, the top
bar picker and the Settings Projects pane. Its pattern @temp selects or hides
all of them. The Projects pane leaves out rows that round to $0.00 unless a
saved pattern names them, and a saved pattern naming any checkout of a
repository row now names the whole row in the renderer too.
* fix(claude): attribute each call to the project of its own cwd
* test: fold the temp roots the tests set, compare checkout ids as identities (Windows)
* fix(claude): keep subfolder cwds in their project, count and show a split session once
* fix: an exclude path hides that checkout only, repository rows count a session once
* fix(claude): a deleted subfolder stays its own project
* feat(sessions): fold subagent sessions into their parent row
The default sessions list (table, JSON, TUI, contributions, desktop) now shows
one row per parent session with its provider-recorded subagents summed in and
listed under subagents. Grouping reuses the work-unit resolver, so only rows
move; totals are unchanged. --by-work-unit keeps the flat rows.
* test: reconcile the session diagnosis with the folded sessions row
* fix(sessions): find and open a parent row by its subagents, count subagents apart from top-level rows
* feat(desktop): one-click updates through electron-updater on the update-feeds release
* feat(tray): signed one-click updates through tauri-plugin-updater; feed publishing and release docs
* ci(tray): build without the updater key until it is configured
* chore(windows): set the tray updater public key
* ci(windows): skip updater signing in the release build smoke
* ci(windows): build without updater artifacts when no signing key is available
* ci(windows): turn off updater artifacts through TAURI_CONFIG, not CLI flags pwsh mangles
* ci(windows): run tauri build under bash so --no-sign reaches the CLI (pwsh drops the -- separator)
* 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.
* feat: Google AI plan presets and the Antigravity tier label (#1614)
Plan presets google-ai-pro, google-ai-ultra-5x and google-ai-ultra-20x
at Google's US monthly prices, scoped to Antigravity usage.
The Antigravity quota readers (CLI, desktop, menu bar) now also ask
GetUserStatus when the summary answers and label the windows with
userTier.name. planStatus.planInfo is not used: it says Pro on a free
tier account.
* docs: Antigravity probe comments and README preset list
* feat(quota): Command Code quota and ClinePass from the Cline sign-in
Command Code: reads the CLI's ~/.commandcode/auth.json key and the billing
credits and subscription endpoints. 5-hour and weekly windows as used of
cap, remaining credits as a note, plan from planId. Live in the Mac dock
from the local CLI, in codeburn quota and the Windows dock.
ClinePass: without a saved or env key, falls back to the Cline CLI's
providers.json session (accessToken, then apiKey, then auth.apiKey, with
Cline's path overrides). Read-only; an expired session stops before the
network and says to run cline. The Mac catalog adds the local CLI source
so Automatic connects without a key.
* fix(quota): keep retrying after an expired Cline sign-in
Running cline refreshes the session file CodeBurn only reads, so an
expired or rejected Cline session is now a retryable failure that keeps
the last quota and shows the guidance, on the Mac and in the CLI. A
rejected API key and a missing credential stay terminal.
* test(quota): build expected Cline paths with path.join for Windows
* refactor(app): move the bridge handlers out of main.ts
createBridgeHandlers and the argv builders now live in
app/electron/bridge-handlers.ts with no electron import, so a host other
than Electron can run the same channel to argv mapping. main.ts keeps its
default deps and re-exports, so the desktop app and its tests are unchanged.
Two additive hooks for that host: scopeProjectFilter narrows the saved
project filter before it becomes argv, and CODEBURN_NODE_BIN runs the
bundled CLI with a named Node instead of Electron-as-Node.
* feat(app): let the renderer run inside an editor
The bridge can now say it is the VS Code host. When it does, the chord
shortcuts and their hints step aside for the editor's own, Settings hands
theme, language, default period and refresh to the editor's settings,
the top bar shows a this workspace / all projects switch, and the editor
can send a section, a period or a refresh to an open dashboard.
formatResetTime moves to lib/format.ts so the status bar and the summary
use the Plans wording. New strings are in all six locales. Nothing
changes in the desktop app.
* feat(app): build the renderer for VS Code webviews
app/vite.vscode.config.ts builds two webview entries into
vscode/dist/webview: the whole dashboard and the activity bar summary.
renderer/vscode/bridge.ts implements window.codeburn over postMessage,
forwarding only the channels in channels.ts; telemetry, update checks and
the desktop companions are answered in the webview. localStorage is
prefixed per scope so one workspace's cached views never paint in
another's, and the theme follows the editor's light, dark and high
contrast kinds through its --vscode-* tokens.
* feat(vscode): CodeBurn extension for VS Code and its forks
A status bar item with today's spend (~ when estimated) and a tooltip
with the week, the month, this workspace, the top model and plan quotas;
an activity bar summary; the desktop dashboard in an editor tab, scoped
to the open workspace; eight commands and eight settings.
The bundled CLI runs as the desktop's resident serve child through the
shared client. Current VS Code, Cursor and Antigravity ship Node 24, so
it runs on the editor's own Node; an older editor gets a system Node
22.13+ or a degraded run that says what it cannot read. A workspace
folder maps to a rooted --project the way the CLI keys sessions,
worktrees folding into their main repository.
npm run package:vscode builds the .vsix. Unit tests cover the bridge
host, status bar formatting, workspace mapping, settings and runtime
choice; test:smoke drives a real VS Code against a fixture home.
* ci, docs: build, test and package the VS Code extension
A vscode job typechecks, unit tests, packages the .vsix, runs the smoke
test in VS Code under xvfb and uploads the .vsix. docs/vscode.md covers
the architecture, the measured editor runtimes and how to test.
* fix(vscode): keep the currency setting out of workspace settings
Changing codeburn.currency runs the CLI's own currency command, which
every CodeBurn app shares, so a repository's .vscode/settings.json must
not be able to set it: the setting is application scoped now.
The smoke test also checks that the dashboard's renderer booted inside
the webview and called the host, not only that its tab opened.
* feat(vscode): version the extension 1.0.0, apart from the CLI
The extension carried the CLI's 0.9.25. It now has its own version,
1.0.0. The host passes it to the webview, and the dashboard's nav
footer, About dialog and Settings show it there instead of the
renderer's package version; Settings adds "Bundled CLI 0.9.25" beneath
it. The desktop app is unchanged. Nothing in the build or packaging
writes the extension's version from the CLI.
* fix(vscode): run the smoke test in Cursor, keep the demo shim out
Cursor 3 opens its Agents window on a fresh profile, which starts no
extension host, so the smoke run against Cursor.app hung; it now passes
--classic for Cursor. The webview build no longer copies
renderer/public/demo-shim.js, a browser demo harness, into the .vsix.
* feat(vscode): a quiet star line in the summary, and a Star on GitHub command
From the third day of use, the activity bar summary ends with the
desktop's "Enjoying CodeBurn? Star us on GitHub" line, in the same six
languages, with a close button. Clicking it or closing it hides it for
good (kept in the extension's globalState). It is never a notification.
The new "CodeBurn: Star on GitHub" command opens the repository page.
No tracking parameters, no telemetry.
* docs(vscode): marketplace screenshots in the README
Five screenshots of the extension on real data (Overview in dark and
light, the why it cost view, the status bar hover and the activity bar
summary), 1600 px wide, each with a one-line caption. They live in
vscode/media/screenshots, stay out of the .vsix, and the package script
points vsce at the raw GitHub URLs on main.
* fix(vscode): review fixes for settings trust, serve lifecycle and the tooltip
- codeburn.provider and codeburn.quotaProviders are application scoped,
like currency, so a repository's settings cannot turn quota requests
back on, even in Restricted Mode.
- deactivate removes the serve pid file once the child is stopped, and
changing codeburn.nodePath restarts the serve child on the new Node.
- codeburn.currency is applied at startup when the CLI's currency
differs, not only when the setting changes.
- The dashboard's Settings no longer shows per-plan quota switches in the
editor, where the editor setting rewrites them on every boot; it points
to the editor settings instead.
- Workspace and model names are escaped in the status bar tooltip.
- The README lists the dashboard actions that reach the network.
- The staged CLI drops its dependencies' tests and TypeScript sources
(466 files, 9.9 MB to 9.2 MB).
* Mark estimated costs with the same ~ in every view
The ~ marker and legend that report already used now appear wherever a
dollar figure is mostly a guess: the models and sessions CLI output, the
desktop Models, Overview, Sessions and Settings provider rows, the macOS
menu bar model rows and provider tabs, and the Windows tray.
One rule decides the marker everywhere: the estimated portion is at least
1% of the figure. report, overview and the MCP tables switch to it from
"any estimated cent", so a metered Codex row holding a few text-estimated
turns stays unmarked.
JSON is additive: estimatedCostUSD and isEstimated on models rows,
estimatedCost and isEstimated on sessions rows, estimatedCostUSD on
menubar providerDetails rows. Totals are unchanged.
* Skip the ~ marker on figures that print as zero
A sub-cent row that is fully estimated printed as "~$0.00": nothing visible
to qualify. The shared helper now also requires the amount, as the surface
prints it in the active currency, to show a non-zero digit. CLI, desktop,
macOS and Windows helpers all apply it.
* 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.
* 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.
* feat(quota): Devin plan quota from the Devin CLI's local cache
Reads the newest ~/.cache/devin/cli/user_status.*.bin and walks only the
plan status message of its protobuf payload: plan name, daily and weekly
remaining percent and their resets. No network call and no API key; each
reading says how old the cache is. Any out-of-range field fails the read
instead of showing a number, and a window whose reset has passed since
the fetch is dropped.
CLI reader feeds codeburn quota and the Windows dock; the Mac Capacity
Dock gets the same decoder, and Devin moves from browser session to
local CLI, live.
* docs(windows): count twelve quota readers
* fix(app): list Devin among menu bar quota providers
* fix(pricing): carry long-context tier rates through bundling and price them where billing evidence exists
LiteLLM publishes *_above_<n>k_tokens rates for 418 models - OpenAI's
gpt-5.4/5.5/5.6 family above 272k, Anthropic above 200k, and 128k/256k/
512k tiers elsewhere - but the bundler flattened every entry to the
fixed five-slot tuple and calculateCost had no threshold dimension, so
a request past its threshold priced at the base rate forever (#1076).
The tuple gains an optional sixth slot {threshold, input, output,
cacheWrite, cacheRead}, the threshold parsed from the key suffix
because LiteLLM ships no numeric field (272k means 272,000; #1075
verified assuming 128k fabricates +64% spend). Old bundles parse
unchanged; CACHE_SCHEMA_VERSION moves to 5 (4 is taken on main by the
implicit cache-write billing) so a stale on-disk live-pricing cache
cannot reintroduce the old behavior; the live-fetch parser picks the
same tier up; service-tier _priority/_flex variants and the 1-hour
cache-write combination are not context thresholds and stay out.
A tier applies when the request's prompt tokens (input + cached input,
the existing grok-4.6 definition) reach the threshold, each published
rate replacing its base and omitted slots keeping the base.
Application is gated on billing evidence via TIERED_PRICING_PROVIDERS:
codex today, where the maintainer's corpus measurement shows the
surcharge passing through; a real Copilot session on gpt-5.6-terra
with ~6M-token prompts billed at the BASE rate, so Copilot keeps base
pricing until such evidence exists - applying it there would fabricate
spend, the exact class #1075 warned about. The audit report recomputes
through the same tier swap so recomputedTotalUSD keeps tracking
attributedCostUSD.
* fix(pricing): read tier rates only from the chosen threshold; e2e codex tier test
Repair pass from review: both tierOf implementations now collect rates
per threshold and read only from the largest one, so a hypothetical
two-tier entry can never mix the smaller tier's rates under the bigger
threshold; the bundler validates tier values exactly like the live
parser (finite, non-negative). A codex end-to-end test drives a real
rollout past gpt-5.5's 272k prompt threshold and asserts the tiered
arithmetic explicitly (a typo'd provider string in codex.ts would
leave the call at base rates and fail it), alongside the no-provider
base-rate control that keeps the real Copilot billing pin intact.
Comment fixes: the priceOverride wording, the grok pin's actual
resolution branch, and the threading note on TIERED_PRICING_PROVIDERS
for whoever adds the next provider.
* fix(pricing): recompute the audit tier per call and re-derive warm daily caches
Both review blockers:
- audit-report no longer feeds a bucket's SUMMED tokens into the
per-call 272k threshold (a \,979 phantom gap on the reviewer's
corpus, any tiered codex model clearing the threshold on the sum
alone). The recompute accumulates per call, through the same tier
swap calculateCost applies, with each call's own prompt total; a new
fixture mixes one 300k call with 200 small ones whose sum blows past
the threshold and pins that only the big call tiers. Web search
stays on the base rates - it never participates in a tier.
- DAILY_CACHE_VERSION 33 -> 34 with MIN_SUPPORTED_VERSION raised to
match, the cfdaa685 convention: a 33 file written under tierless
pricing is below the floor, is not adopted as the finalized base,
and its days re-derive off the warm session cache (\ of codex
spend on the reviewer's corpus lives only in the daily cache). The
migration tests pin the new floor contract: no version-diff repairs
merge into a below-floor file, and the v30 adoption test tracks
CURRENT.
Nits from review: the kimi precision tightening is reverted to (8, 5),
and the tieredCostsFor comment is rewrapped.
Measured on this machine's corpus, tier-only (branch vs the refresh
branch, same data): +\.43 lifetime codex - this corpus runs far
more sessions past 272k than the reviewer's 8-of-51,801 (+\.012964);
both numbers are real, the corpora differ.
* fix(pricing): preserve base alias selection when bundling context tiers
* fix(pricing): address the 28 Sep review - floor stays at 28, tier bills the implicit cache-write rate
Restore main's two carry-forward tests: with MIN_SUPPORTED_VERSION kept
at 28 (per the review's correction, warm caches re-derive identically
under 41/28, so raising the floor is unnecessary) a v33 file sits in
the adopt-with-repairs window again and main's expectations hold; the
tier's warm-cache coverage stays in daily-cache-version-rederivation.
The tier-omits-slot test bills an implicit cache-write at the input
rate of the costs object in effect (#1544), so the tiered expectation
is 1_000 * cacheWriteCostPerToken('gpt-5.5', tieredCosts).
Note on the Codex estimate path: an estimated prompt can cross a
long-context threshold and tier itself; measured corpora hold no
estimated calls that cross.
* fix(pricing): pin claude-opus-4 primary and keep carried fallback rows hygienic in the refreshed snapshot
The requested regeneration surfaced two upstream moves beside the tier
work. LiteLLM dropped `claude-opus-4`; a refresh carries dropped ids
into the fallback tier, but the Cursor-style alias `claude-4-opus`
resolves against primary rows, so the bare id fell to the snowflake
gateway row and under-priced by 3x. Pinned at Anthropic's list price
in MANUAL_ENTRIES so it stays primary. LiteLLM also repriced
grok-4.6's base input 2e-6 -> 1.25e-6 (tier rates unchanged); the
below-threshold literal follows. Both changes reproduce identically
with main's unchanged bundler on the same live responses.
Today's drops also exposed a gap in the #1483 carry-forward: primary
files legitimately hold @pin, date-suffixed and free-on-both rows, and
carrying those verbatim imports exactly what
tests/pricing-fallback-data.test.ts keeps out of the fallback file
(main's own next refresh fails those tests today). The carry now skips
@pin/date-suffixed keys (no query can arrive in those forms) and
free-on-both rows (an unpriced model falls to expected-free handling);
vendor-prefixed keys stay carriable since queries match them verbatim.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix: open menu bar Settings centered on the active screen
The window was positioned at a 520x380 placeholder while SwiftUI's own
minimum is 880x620, so the grown window kept its top-left corner and ran
off the bottom-right edge. Size the window from the view's minimum, place
it with a tested pure function, and honour a saved frame only while it
still fits on one of today's screens.
* fix: say where the telemetry toggle is managed when the desktop app owns it
A desktop telemetry.v1.json with enabled:false locks the menu bar toggle
off, so clicking it does nothing. Name the desktop app's Privacy & data
pane in the caption, and pin the four desktop/veto combinations plus the
standalone case in tests.
* fix(desktop): export the provider id, and never swallow a rejected setter
The export pane built its provider list from `current.providers`, which is
keyed on the lowercased display name. Five of the seventeen providers on a
real corpus ("cursor agent", "grok build", "kimi code", "hermes agent",
"grok bot") are not valid ids, so vProvider rejected them as bad-args.
`exportNow` had a try/finally and no catch, so the rejection produced no
toast and no file: the button did nothing. Use detectedProviders, which
already resolves ids from providerDetails, and toast on rejection.
The telemetry consent toggle swallowed its rejection the same way.
* fix(desktop): warm only the selected period, and only while visible
The post-paint sweep warmed all six standard periods x seven reports plus
the provider overviews: up to 48 serve-routed queries, ~18 CPU-seconds a
minute for five minutes and 2.79 GB peak RSS on a real corpus, and it ran
with the window hidden. It now warms the selected period's reports (its
own overview is already under that memo key from the visible poll) and
the other providers' headlines for that same period, holding before every
request while a user fetch is in flight or the window is hidden. Other
periods take the ordinary on-demand path.
Also corrects two comments that claimed background priority lets a click
jump ahead: spawnCli takes the serve fast path before priority is read,
and the resident child answers FIFO.
* fix(desktop): answer the click, report the write, tell the truth about the Claude login
Nine bridge setters in Settings attached only .then(), so a rejected
envelope produced no toast and the control read as if it had ignored the
click. One shared helper toasts the rejection, per pane fallback wording.
Telemetry save() swallowed write errors while setEnabled returned the
in-memory status, so the UI could show ON while the file the macOS menu
bar app reads still said OFF. The setters now carry persisted:boolean and
the renderer toasts when it is false; the write goes through the existing
atomic writeFileAtomic (temp file, rename) so a crash cannot tear it.
The Claude card called a logged-in Claude Code 2.x user "disconnected"
forever, because the credential lives only in the keychain and the first
poll never reads it. A new darwin-only keychainUnchecked state says we
have not looked yet and offers the forced read, keeping the login
instructions for the genuinely-logged-out case. ConnectAffordance's
toggle is renamed to what it does: it discloses instructions.
* fix: retry tax is the cost of retried edit turns, not a multiple of spend
* stop the change guard watching a Warp path it can never stat
* apply a menu bar language change in place instead of relaunching
* keep honoring WARP_DB_PATH in the change guard
* restart a menu bar too old to watch AppleLanguages
* one rebuild per language change, and no global bundle in tests
* fix(desktop): a forced quota refresh must not join the poll it just aborted
Check now / Refresh / ⌘R invalidated the quota service and then handed the
caller the background fetch it had just aborted, so every provider failed
its generation check and resolved disconnected: the click repainted every
card as logged out without ever reading the keychain, and Claude showed
"Not connected. Log in with the Claude CLI." A call now joins an in-flight
run only when that run is at least as privileged, so a keychain-allowed
call starts its own; a second forced click still joins the first, which
keeps the single macOS prompt. A superseded run hands its caller the run
that replaced it instead of its own abort artefacts.
retainOnFailure also holds accessDenied through a keychainUnchecked poll,
so the "Allow in keychain" guidance is not replaced by "Check now" one
cycle after the user denied the prompt.
* fix(desktop): stop the sweep churning, and never spend a query on an abandoned one
The prefetch effect depended on the detected-provider entries, whose costs
move on every overview poll, so it was torn down and restarted each cadence
tick: a warm slower than the interval was discarded and re-issued forever.
It now depends on the provider ids alone, and a finished warm is kept for
its own period/provider key even if the effect was torn down meanwhile —
only a config-generation change makes a result wrong.
Both loops re-test cancellation and the memo after the hold, which has no
bound, so an abandoned sweep issues nothing and a section the user visited
meanwhile is not parsed again. The hidden half of the hold waits on
visibilitychange instead of waking the renderer every two seconds for a
window nobody is looking at.
* fix(desktop): never offer a provider id the CLI will reject, and keep the keychain note
detectedProviders applied the round-trip filter only to the legacy
providers map; a providerDetails id with a space reached the picker and the
export as a --provider value the main process rejects.
The Plans settings row showed the "not read your login yet" line without
the note that macOS may ask once for keychain access, which the Plans card
does show.
* 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
* 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
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.
Adds a live quota gauge for the z.ai ZCode coding plan on all three surfaces: the CLI plan view, the desktop Plans page, and the mac menu bar. It reads the provider's own remaining-allowance endpoint, so it is quota-only and touches no cost or spend paths. One decoder is shared between Z.ai and ZCode, and ZCode is hidden when a Z.ai login already covers the same endpoint. The fetch is bounded with graceful disconnected, waiting and error states, and no socket is opened without a login. The logo shows on every surface, and ZCode is listed in the desktop supported-provider list to match the menu bar catalog.
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.
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.
The period model list the Overview table reads is no longer cut at 20 rows, so
every model with usage in the period shows up instead of only the costliest
twenty - the rows that dropped out were exactly the $0 local and free models.
formatTokens gains a billions rung, so a codeburn models totals row reads 29.9B
instead of 29868.7M; the menubar row formatter gains the same rung, and compare
now reuses formatTokens instead of its own K/M copy.
The desktop Models tables stop dimming the calls column on unpriced rows. In the
shipped app those rows do not reach the table yet - codeburn models defaults to
minCost 0.01 and getModels passes no override - so the change takes effect once
that filter is addressed, tracked separately.
Also re-lands the three #1315 commits that never reached main: per-model token
counts beside per-model cost in the Overview and the macOS menubar Models rows.
Status snapshot revision goes to 9 so a warm snapshot cannot keep serving the
truncated list. Fixes#1318.
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.
Adds xAI's Grok Bot desktop app as a provider: per-bot sessions with estimated usage, and the weekly allowance read through the Cursor IDE login on the CLI, desktop Plans screen and menubar. No row unless the app is installed. Two independent review rounds on real data.
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.
* 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
* 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
Chips were keyed by the string they display, which truncates a session
id to 12 characters and a project path to its last segment. Two sessions
whose ids share a prefix, or two projects with the same basename on the
same branch, produced one React key for two chips: React warned about
the duplicate and could reconcile or remove the wrong one.
filterChipKey returns the composite identity each dimension is already
normalized and removed by (provider + session id, project + branch), so
chips that read alike stay distinct to React while the display label is
left as short as it was.
Rebased on main, which since gained #1307, #1316 and #1319.
- Delete app/renderer/lib/sampleNavigation.ts. It was a placeholder for a
registry that never landed: #1316 ships drill-through as an onInvestigate
callback prop plus NavState. The sample rows now call it, threaded from
App.tsx through Compare, and open the owning session for real.
- Give a CohortObservation the provider/project/session triple the sessions
report keys rows by (inferSessionProvider, session.project || project.project),
so a sample resolves to exactly one session row. Without it a cohort sample
could not build sessionRowKey at all.
- Carry projectArgs() on both cohort spawns, per #1307. The saved project
filter now scopes the facet query and the report; --project-id still narrows
within it. Extended the scopes-every-fetch test to cover both.
- Allow --from, --to, --category and --project-id on compare in SERVE_OPTIONS.
Verified over stdio: before, every cohort request but the bare facet query
came back "command not served" and fell back to a spawn.
- Catch the invalid-date throw on compare like report does, instead of an
unhandled UsageQueryError stack trace.
- Drop the unrelated CODEBURN_USER_DATA_DIR hunk in bootstrap().
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.
Two bridge channels follow the existing envelope + argv-validation pattern:
codeburn:getPeriodCompare spawns compare-periods --format json with two
REQUIRED local YYYY-MM-DD range pairs (validated per range), and
codeburn:getPeriodCompareSessions spawns the sessions drill-down for one
project/model contribution key. Contribution keys ride the --key VALUE
position, so a dash-leading slug (Claude sanitizes /work/pricing to
-work-pricing) passes vContributionKey where vToken would wrongly reject it.
Preload and the renderer CodeburnBridge types mirror the two calls.
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.
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.
InvestigationFilters is a normalized, serializable selection (days,
providers, projects, models, categories, PRs, project-scoped branches,
provider-scoped sessions) with OR within a dimension and AND across them.
Contribution math walks each row's segments and multiplies per-dimension
shares, so sums reconcile every dimension's aggregate exactly; rows without
segments are excluded and disclosed. NavState + the history stack back the
in-app Back/Forward (filters, sort, page depth, open drawer) and the
restart snapshot. The drawer shows metadata, full totals beside the
in-selection figures, and the per-model/category/branch/day/PR breakdowns
with an Escape-and-focus-return contract.
The desktop CI job runs `npm ci` inside app/ only, so an app-side test
that imports src/parser.ts cannot resolve the CLI's own dependencies: the
typecheck failed on chalk and strip-ansi before the suite even ran. A
local run hides it, because the repo root's node_modules sits one
directory up and resolves fine from there.
The test needs both halves by definition, so it moves to the suite that
has both, and projectMatch.ts names its new home. Nothing under app/
reaches into src/ any more: `tsc --listFiles` on the desktop project now
resolves no file outside app/.
getUnfilteredProjects passed the period on screen, and the CLI builds
that list from the days the period covers. A project filter has no
period: it scopes every screen and every horizon at once. So an exclude
pattern for a project with no activity in the current period rendered as
"matches nothing detected" next to a Remove button, while the footer read
"Nothing is hidden." Clicking Remove there unhid the project on every
screen, from a control that had just called the pattern dead.
The list is lifetime now, and the period argument is gone from the
channel rather than left for a caller to get wrong. `all` is capped at
six months, so lifetime is the only horizon that can answer for the whole
filter.
The pane's memo key goes through reportMemoKey like every other pane, and
the fetch is off the poll interval: it is the heaviest read in the app, a
toggle cannot change an unfiltered list, and the only thing that can is a
project appearing, which the next open picks up.
A pattern typed into the Projects pane never passes through a shell, so
"~/work/app" reached the CLI as a literal tilde and matched nothing. It
is expanded in the main process now, on the way into app-filter.json and
on the way back out: the renderer has no home directory of its own, and a
tilde it cannot resolve would make its switches disagree with the argv
this process writes.
The file can also be hand-edited, so the expansion runs on read as well
as on write. projectMatch.test.ts covers the expanded shapes against the
CLI's matchesProjectPattern, which is what keeps the switch and the argv
on the same answer.
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.
- 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.