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.
* 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.
* 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.
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(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
* 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.
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.
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 onboarding screen, the privacy settings and the README each said the
daily report includes the names of the models, tools, skills and MCP
servers in use, and then in the next breath that it never includes
anything identifying. A server named after an employer is identifying,
so the second half was not true. All three now use the wording the tray
notice already had, which names what is withheld rather than claiming
more than the product does: never your prompts, your code, or your
project and file names.
- 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.
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.
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.
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.
Part 1: Add --json output flags to three CLI commands for machine-readable output
- codeburn plugin list --json: array of plugin objects with status and capabilities
- codeburn plugin info <name> --json: manifest with dir and onDiskSections
- codeburn sync auto status --json: status object with configured, accepted, killed, receipts
Part 2: Implement Electron bridge with read and mutation handlers
- Read handlers: pluginList, pluginInfo, syncAutoStatus (return parsed JSON)
- Mutation handlers: pluginAdd, pluginRemove, pluginVerify, syncAutoEnable, syncAutoDisable
- All handlers follow existing Envelope pattern with proper error handling
- Special case: syncAutoEnable captures disclosure text when accept=false
Part 3: Renderer UI components (minimal implementation D1-D3 shell)
- Add Plugins section to nav rail at bottom (quiet entry, plugin icon)
- Implement PluginsSection component with list rendering for loaded/rejected plugins
- Update Section type and routing to include plugins navigation
- Add test coverage for Plugins panel row states
- Implemented as dormant Team tab registry placeholder for future plugin sections
Tests: All CLI tests (3839 passed), app tests (613 passed), E2E gates all passing
- CLI: tsc clean, vitest passes with 26 pre-existing jsdom errors
- App: typecheck clean, npm test green, npm run build succeeds
Ports the menubar's Kimi flow to the Electron quota module: read-only
~/.kimi-code/credentials (KIMI_CODE_HOME honored), api.kimi.com usages
snapshot, terminal expired-login state that never refreshes the CLI's
short-lived token, 5-hour/weekly window mapping with over-limit clamp,
membership plan label, and the standard per-provider Settings toggle.
Extends the existing borrowed-credential quota module (claude/codex) with
three providers, per-provider settings toggles, and a provider display map:
- gemini: Code Assist loadCodeAssist/retrieveUserQuota via the Gemini CLI's
on-disk OAuth creds; optional env-gated token refresh; retired-tier
responses degrade to terminalFailure with migration guidance
- copilot: copilot_internal/user with editor-plugin headers; token read-only
from hosts.json/apps.json with one re-read on 401; marked as an internal
API that may drift, failures degrade honestly
- antigravity: loopback-only Connect-RPC probe of the local language server
(ps/lsof discovery via execFile, csrf token for the app variant, agy CLI
accepted tokenless); TLS relaxation scoped to 127.0.0.1; no OAuth fallback
- settings: per-provider quota switches (default on) persisted renderer-side,
honored in the main-process service so disabled fetchers never run; IPC
disabled-list filtered against the provider allowlist
- sanitizeError extended for Google (ya29.) and GitHub (gh*_) token shapes
Kimi audited, not duplicated: the macOS menubar already fetches Kimi quota
live (KimiSubscriptionService.swift); the electron app not surfacing it is a
pre-existing gap, unchanged here.
F1 - `codeburn serve` lost an in-flight request's response when stdin closed
mid-flight: runStdioServe returned before its queue drained, and the explicit
process.exit(0) then hit runCaptured's monkeypatched exit, throwing ExitSignal
and exiting 1 with the frame never written. The finally now awaits the queue.
F2 - the SIGTERM rationale was false. armSignalCleanup unlinks the refresh lock
and re-raises; it publishes no partial cache, and a lock left by SIGKILL already
self-heals through the stale-pid takeover. SIGTERM-first is kept for the real
(smaller) benefit - a clean lock release instead of a takeover - and every
comment plus the CHANGELOG now says only that.
F3 - the cold gate had no exit. overviewWarmed only flips on success, so an
install that can never hydrate sat behind an indexing splash forever with no
error and no route to the CLI recovery. The cold claim now expires with the cold
window itself.
F4 - the real CLI does not heartbeat the way the demo did: a cold parse's
inter-provider cache save measured 31.6s of total silence, which a 45s window
survives only until the machine is 1.5x slower. Under CODEBURN_PROGRESS a
running parse now emits a keepalive every 10s regardless of phase, so silence
genuinely means stopped. Consumers that do not know the event ignore it.
F5 - the orphan-reap identity check matched any `cli.js` running `serve`. The
pidfile now records the exact argv and `ps -ww` must match it exactly.
F6 - bump() re-armed the watchdog after settle, leaving a timer finish() never
clears when a killed child's buffered output landed.
F7 - kill paths dropped the child from activeChildren before SIGTERM and the
SIGKILL backstop was unref'd, so a quit inside the 5s grace orphaned a child
that ignores SIGTERM. It now stays registered until it actually dies.
N8 - the silence test wrote its only byte at t~0, so it passed without the
re-arm. The byte now lands mid-window and the kill is asserted from it.
N9 - documented why mutations keep a plain total cap.
The desktop app capped every read at 45s of TOTAL runtime, so a slow machine's
warm `optimize` (52.5s on 0.9.20, 39.6s on main) was SIGKILLed mid-parse and the
panel painted a red "timed out" that a 60s poll then reproduced forever.
- cli.ts: the read timeout is now a no-output watchdog. The window restarts on
every byte the child writes (serve: every frame carrying that request's id),
so only a genuinely silent child times out. A 15-minute absolute ceiling still
reaps a livelocked child. Every read spawn sets CODEBURN_PROGRESS=1 so a long
parse heartbeats through it; progress lines are stripped from any surfaced
stderr so they can never become the error message.
- cli.ts: a timed-out child gets SIGTERM first and SIGKILL only after a 5s
grace, on both the spawn and the serve branch. That lets the CLI's signal
cleanup publish its partial parse and release the cross-process refresh lock
instead of leaving both stale.
- main.ts: the 10-minute cold floor now covers EVERY read while the first
hydration is still running, not just the overview. Sections start polling the
moment `ready` flips (which an overview error also does), and a 45s section
spawn queued behind the cold parse was killed on arrival.
- main.ts/renderer: a timeout raised while the hydration is still running is
flagged `cold`, and the renderer keeps its indexing splash (and keeps the
sections gated) instead of latching ready and painting CliErrorPanel. Only
timeouts are flagged; a permission or nonzero failure still surfaces.
- cli.ts/main.ts: the resident serve child's pid is recorded under userData and
a serve orphaned by a previous crash is reaped at launch, after `ps` confirms
the pid still belongs to a codeburn serve.
- src/main.ts: `codeburn serve` exits explicitly once its stdin closes, so a
handle outliving the transport cannot keep the child alive as an orphan.
- Punchcard tooltips flipped below the cursor on the top rows (the
overflow container clips anything above its own edge) and clamp
horizontally near the strip edges. Applied to both the app and dash
copies of the component.
- The menubar Workflow strip is removed (view, strip model, tests):
the popover is the compact surface and the workflow metrics live in
the desktop app, dash and TUI. The payload keeps emitting the block
(add-only contract; the other surfaces read it).