#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.
cli_error gains duration, exit, a locally classified reason and the scoped provider. New provider_read_fail (desktop) and update_result (desktop, menubar, tray) events, enums only.
* 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(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
Localizes the Windows tray menu, tooltip, usage row, dock menu and footer into Simplified Chinese, reusing the macOS menubar glossary. Refs #1336, #1646.
* feat: Command Code support
Reads the cmd CLI's session transcripts at
~/.commandcode/projects/<slug>/<id>.jsonl (CODEBURN_COMMANDCODE_DIR moves
the root). Checkpoint and prompt sidecars are skipped.
usage.inputTokens includes cacheReadTokens, so fresh input is
inputTokens - cacheReadTokens. The recorded costUsd is kept per call
(costFromBilling); a call without one is priced from tokens and marked
estimated. Forks copy parent message ids into a new file, so calls dedupe
by message id across sessions.
Desktop: Electron sandbox read path, macOS and Windows change watchers,
Windows tab colour. No quota adapter.
* fix(command-code): dedupe on meta.messageId, not the 8-hex line id
The top-level id is randomUUID().slice(0, 8) and only unique within one
file, so calls from unrelated sessions could collide and vanish.
meta.messageId is a full uuid that survives forks. Fixture ids now use
the real 8-hex shape.
* test(command-code): check sidecar skipping by file name, not a posix path
* 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(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(amp): update stale ledger comment; split AMP_DATA_DIR list in the Windows guard
* fix(amp): watch thread subfolders under each Amp root in the Windows guard
* feat: Amp support - read the local thread mirror (#1572)
Amp (Sourcegraph's coding agent) mirrors every thread it serves as one
JSON document per thread at ~/.local/share/amp/threads/ (AMP_DATA_DIR
honored, comma-separated). Each file is { id, messages,
usageLedger?{events} }; the ledger's events are the per-request record
(tokens.input/output, cache tokens joined from the billed message
through toMessageId -> messages[].messageId), with a per-assistant-
message usage fallback for threads without a ledger and a total-only
record billing as output.
Amp bills a subscription and records no charged dollars, so every call
is priced from the underlying model's token rates and marked
costIsEstimated; the recorded credits field is ignored (no published
per-credit dollar rate). Thread files carry no cwd, so sessions group
under the provider name. Registered eager; parse version
usage-ledger-v1; no artwork yet (mono-initial fallback, like Codebuff).
Surfaces: CLI registry + doctor probe roots, Electron sandbox read
paths + menubar catalog (Amp after Cursor, matching the Swift order),
Windows tray tab color + usage_guard root, GNOME prefs dropdown, macOS
ProviderConnectionCatalog, docs/providers/amp.md, README tool list,
CHANGELOG.
* fix: address Amp provider review feedback
serde's rename_all on an internally tagged enum only renames the
variant tag, not the fields of a struct-like variant, so this field
shipped as claude_profiles. The frontend reads answer.claudeProfiles,
so the Claude profiles settings row and the per-config dock rings
from #1570 never showed.
Adds a field-level rename plus a test pinning the serialized key.
* feat(dock): one Claude ring per config directory, with its own sessions and today (#1523)
Rate limits are per account, so a machine with a work and a personal
~/.claude cannot be served by one Claude ring. When the CLI knows two or
more Claude config directories:
- `codeburn quota --format json` adds a top-level `claudeProfiles` array,
one entry per directory (`providers[]` unchanged). Each entry carries
the directory's own windows, read from its own `<dir>/.credentials.json`
(the macOS Keychain fallback stays reserved for the default ~/.claude),
plus that directory's own today totals from a single Claude-only,
cache-backed parse of today's range, filtered per source id exactly like
the menubar's per-config payloads.
- Live sessions carry `claudeConfigSourceId`, so a per-profile ring claims
only its own sessions. Claude Desktop sessions keep their own bucket id
and belong to no profile.
- The Windows Capacity Dock gains a `claudeProfiles` preference (combined,
default; separate draws one captioned ring per directory). Profile rings
keep addressing the `claude` provider id for the provider set, glyphs,
colors and the connect button; the settings row appears only when the
CLI reports more than one profile.
- Captioned rows are taller by two new metrics (caption line + gap),
mirrored in TS and Rust: the page tells Rust the rail is captioned in
the layout request, and the frame carries the effective row extent the
hit-test uses. Non-captioned rails are pixel-identical to before.
- The desktop app's shared dock-prefs schema parses and sanitizes the new
key, so its pane cannot strip it from windows-dock.json.
Closes#1523
* review: opt-in profile reads, today timeout, missing-dir filter (#1570)
- `quota --claude-profiles` gates the per-directory reads: without it the
payload lists profiles ({id,label,path}) only, so a combined rail and
WSL users pay nothing; the Windows dock passes the flag only while its
claudeProfiles preference is separate, and an unknown option from an
older CLI reads as CliOutdated.
- The default ~/.claude profile reuses the claude provider row's result —
no second request, no second macOS Keychain read.
- claudeTodayBySource races the same timeoutMs and answers an empty map
on loss, so a silent child cannot cost the dock every provider's quota.
- Missing config directories are filtered before the >1 check.
- `captioned` applies only while a Claude ring is in the selected set;
profile rows tolerate a list-mode answer; comments trimmed; merge with
#1560 keeps both keepExpanded and claudeProfiles.
* test(quota): set USERPROFILE for the default-dir check; drop issue refs
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* menubar: dock preference for the hover bubble's surface
* menubar: let the dock's hover bubble keep its own surface
* menubar: setting for the dock's hover bubble surface
* docs: dock hover bubble surface setting
* 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.
* quota: Claude plan label prefers the subscription type
* desktop: Claude plan label prefers the subscription type
* menubar: Claude plan label prefers the subscription type
* mac: Claude plan label prefers the subscription type
The codeburn models default table dropped every row under its $0.01 floor with nothing saying so, hiding usage that prices at $0 for lack of pricing data. The default table keeps its floor and its numbers but prints one line naming what the floor hid: N more models price at $0 (M tok), with codeburn models --unpriced to see them. An explicit --min-cost stays silent; json, csv and markdown are unchanged. The macOS popover, Windows tray and web dashboard render the existing unpricedModels block as one secondary line. Verified: machine formats byte-identical, the hidden count matches --unpriced, totals unchanged.
The test ran 100 rounds a side, both taking the lock, while a reader thread
hammered the same file. Production allows a 2s wait with a 50ms poll, which
suits a UI that writes on a click but starves under that much contention: a
waiter that loses the race after each poll can exceed the budget, and the
acquire then fails by design. That is what turned main red on the merge,
not a lost update.
24 rounds a side keeps the property under test and the harness stops being
the thing that fails. Measured on this machine, ten runs each way: the test
still fails 10 out of 10 with CB_TRAY_XPROC_NOLOCK=1 and passes 10 out of
10 with the lock, so it detects the bug exactly as reliably as before.
The production budget and poll are untouched. If sustained contention ever
becomes a real workload rather than a test, the poll is the thing to
revisit, not the budget.
The rename retry landed, but its test kept the watcher reading in a
tight loop, and on real Windows that lost 8 runs in 12: a reader holding
the file open every instant is a race no bounded retry can win, because
an open handle blocks a rename until it closes. That is not a reader
either app has. The watcher now polls at 1ms, which still samples the
file thousands of times and still catches a lost update, while leaving
the writers room to rename. The locked path passes 15 of 15 on Windows,
and the unlocked path still reports a rolled-back write.
Also restores the lineage case the review asked for: a refresh where the
account id and refresh token are unchanged and only the id_token subject
moves, so the subject comparison is what discards the write. Both copies.
The Windows CI leg failed on the cross-process test with EPERM from
renameSync. It is not a test artifact. Windows refuses a rename over a
file another process holds open rather than waiting, with
ERROR_ACCESS_DENIED or ERROR_SHARING_VIOLATION, and reads of these two
files are deliberately lock-free on both sides. So any tray or app read
that overlaps a writer's replace fails that write, and since this branch
made writers report a failure instead of swallowing it, the outcome is
exactly the lost update the lock exists to prevent, reached by another
route. The test's reader thread only made it visible by reading the way
both apps really do, continuously and without the lock.
The replace is now retried on that error class, ten attempts about 20ms
apart, on both sides: replace_file in settings.rs, used by settings patch
and by the dock's write_state_at, and renameOnto behind writeFileAtomic
in tray-settings.ts. The lock is already held at that point, so the wait
can only ever be on a reader, never on another writer, and a read of one
of these files is microseconds of open file. Once the budget is out the
original error is returned unchanged and the caller cleans up as before.
On POSIX, where a rename over an open file cannot fail this way, it
compiles out in Rust and short-circuits on process.platform in Node.
Reads stay lock-free, which is the right trade and the reason the files
can be read at all without coordination; both read paths now say what it
costs and where that cost is absorbed. The reader thread stays as it is,
and is now the regression test for the retry as well: it is what puts a
replace under a continuous reader.
The retry itself is covered on the Node side by faking the platform and
the rename, since a rename that fails this way cannot be produced on a
Mac: a reader waited out and the write landing, the budget running out
with the old file still whole, and an error a reader cannot cause not
being waited out at all. The Rust half is compiled out off Windows, so
CI is its only proof.
writeDockEnabled still did its own read, mutate and atomic write of
windows-dock.json, outside the lock the rest of the module takes. It is
the Node side of the dock's enabled switch, reached from the desktop
app's Sidebar toggle, so a toggle there could still land between the
tray app's read and its rename and erase whatever it had just stored,
which in practice is the rail's placement. It now merges through
patchTrayFile, which takes the lock for the whole cycle; enabled is
already an accepted dock key, so nothing is dropped on the way.
Stale-lock recovery could hand out two owners at once. Both sides did
"if abandoned, unlink and retry", and nothing stood between the check
and the unlink: contender A could already have removed the stale lock
and created a fresh one of its own, and B's unlink then deleted A's
fresh lock, leaving both of them holding the file. Windows' share mode
hides this most of the time, because a lock a live holder still has open
will not unlink at all, but the same protocol runs on Linux and macOS,
where the unlink always succeeds. Removing an abandoned lock is now
itself arbitrated, by the same exclusive create the lock uses, on a
sibling <lock>.takeover file. Only its winner may unlink, and only after
re-checking staleness under that right, so a rival that reclaimed first
keeps the lock it now holds. A takeover file is dropped on every path
out, never held across the wait, and one left behind by a reclaimer that
died is freed by the same staleness rule as the lock itself.
The cross-process test asserted only each side's final value, so it
noticed a lost update only when the last writes happened to collide: on
a deliberately broken lock it passed about one run in three, which is
weak evidence for the property it exists to prove. Each side now writes
a counter that only rises, and a reader thread watches the file for the
whole run. A separate reader is the point: neither writer can see its
own counter rolled back, because in the unlocked cycle each carries the
other side's value forward itself, so what it reads back is never older
than what it last read. With the lock both counters only ever rise in
the file; without it one falls. It now fails on every unlocked run.
Also: a placement that cannot be saved is logged instead of discarded
silently, since losing the lock under contention is a new way for the
rail to revert at the next launch; and the Codex sameLogin comment now
states outright that a replacement document carrying none of the three
identifying fields reads as the same login and is written over, which is
the deliberate side to err on.
The desktop app and the tray app both read, merge and rename the same
preference files. The atomic rename stopped a torn file but not a lost
update: both read state A, each changed a different field, and the last
rename erased the other's change, so dock placement, enabled state,
provider choice or scale silently reverted.
One cross-process lock now covers the whole read-modify-write cycle,
designed once and implemented in both Rust and Node against the same
protocol: a sibling .lock file taken by exclusive create, carrying the
holder's pid and timestamp, taken over only when abandoned by a dead pid
or a stale mtime, and failing loudly rather than writing behind a live
holder. It follows the CLI's existing cache-refresh lock. All three
sites use it, including the dock's enabled, provider and placement
writes. A test drives real Node and Rust processes at the same file and
fails without the lock.
- A rail dropped while expanded reappeared elsewhere: the drop
normalised against the live rail's travel while layout denormalised
against the resting one, 71 px of drift at four rows on this work
area. The drop now walks back to the resting start the rail grew
from and normalises over the resting travel; a test drops an
expanded rail and asserts it lands exactly where it was released.
- The click-through state was set before the call that applies it and
a failure was discarded, so one failed call left an always-on-top
window hit-testable forever. The state records only a successful
call and the next tick retries; the same fail-open in begin_drag is
fixed.
- Any button held anywhere on the desktop pinned the poll at 62 Hz;
only a press that began on the rail counts now.
- The dock state file the desktop app also writes is replaced through
a temp file and rename.
- The consent notice and the settings hint say that the daily report
includes the names of the models, tools, skills and MCP servers in
use, as the owner decided, and no longer claim more than that.
- The non-Windows random fill read /dev/urandom with fs::read, which
never returns on a character device, and had a borrow error beside it
that failed clippy on the ubuntu leg. It opens the device and reads
exactly sixteen bytes, keeping the fallback; the reader is compiled
for tests on Windows too, and the Linux target was checked from an
extracted copy with clippy against x86_64-unknown-linux-gnu.
- The telemetry commands were synchronous and every tracked event
rewrote the whole queue file. They are async now and the save is
debounced by two seconds, with a save before each flush and on quit.
- The queue is capped by bytes as well as count, drops events older
than seven days on load, backs off exponentially with jitter from
five to thirty minutes while the endpoint fails, and keeps an
in-flight batch in the file until it lands so app_close can no
longer overwrite it.
- One reqwest client for the process, a 0600 queue temp file on Unix,
the hidden window station warning in the release log, and
open_terminal_command limited to a named read-only subcommand.
The tray had no telemetry. It now sends the same envelope as the desktop
app to the same endpoint, with app name codeburn-menubar. Consent comes
from the desktop app's telemetry state file when that app is installed,
so one decision covers both apps and events share one anonymous id;
otherwise the tray asks once, defaulting off for EU, EEA, UK and Swiss
locales and for an unknown locale, and keeps its own toggle under
Settings, General, Privacy. Switching off rotates the id.
Events, all name-only or bucketed: app_open, app_close with session
minutes, popover_open, settings_open with the pane, update_click with
the action, glance_open, dock_enabled and dock_disabled with the edge
and a scale bucket, dock_provider_switch, dock_drag_end with the edge,
and the CLI's usage_snapshot once a day, skipped when the desktop app is
the consent source so nothing is counted twice. The queue persists under
the app's local data directory, capped at two hundred events, and flushes
every five minutes and on quit; a 4xx drops the batch, anything else
keeps it. Debug builds never send.
The Store package is signed at submission and updates itself. The tray
.msi under windows-v* and the desktop setup exe are a developer preview:
unsigned, SmartScreen warns on first run, and they do not update
themselves; the tray app reports a newer version and links to it. Said
in the README, the distribution notes, the tray development notes and
the release body.
Outside a Store package the tray app no longer runs `npm install -g
codeburn@latest --force` and `codeburn menubar --force` on one click.
The MSI is unsigned and its checksum is published in the same release,
so an automated install has nothing to verify; a manual install at
least passes through SmartScreen. The check still runs and reports a
newer version, the badge and About now open the windows-v* release
page, and the manual command is shown as copyable text. Inside a Store
package the checker stands down as before. One-click install returns
once the MSI is signed and the installer verifies the signature.
- The tray resolves codeburn from PATH and the npm locations and never
saw the CLI the desktop app bundles, so a machine with only the
desktop app had a tray with nothing to run. The desktop app now writes
a launcher that runs its bundled CLI through its own executable,
records the path in the tray settings, and the tray's resolver takes
it after PATH and before giving up; a global codeburn still wins. The
first launch also gets CODEBURN_BIN in its environment.
- Four IPC handlers for tray preferences and launch at login had no
platform check and would have touched Windows files or reg.exe on a
Mac; they return neutral values off Windows.
- The install probe ran on every launch and a declined UAC prompt came
back at the next one. A declined or failed staged version is recorded
and not retried until the version changes or the user flips the
switch, and a matching installed version skips the probe.
- Launch at login in the Store build is managed by Windows through the
package's startup task; the switch said otherwise and did nothing.
That build now says so and opens the Startup Apps page.
- Settings files the tray reads were truncated and rewritten in place;
they are written to a temp file and renamed over.
- DISTRIBUTION.md records that the installer's staged tray and the
windows-v* release asset at the same version are separate builds.
- On Linux provider keys were written under the plain umask despite a
comment promising owner-only permissions. The directory is created
0700 and the file 0600, the temp file before the rename too, and the
plain-text fallback is stated where it lives, including in the
settings pane copy.
- A release build has no console, so every eprintln wrote into nothing.
A small logger appends timestamped lines to codeburn.log under the
app's local data directory, rolls at 256 KB, and keeps stderr in
debug builds. All call sites use it.
- Uninstall left the launch-at-login Run value pointing at a deleted
exe, so Windows errored at every login. A WiX fragment removes that
one value on a real uninstall, not on upgrade. User data under the
config directory is kept on purpose and the fragment says why.
- The webview CSP allowed a host nothing in the page contacts.
- The updater's verification site now states its trust boundary: the
hash and the MSI come from the same release, so the check proves
transfer integrity only. Signing is a release decision.
Four dock bugs and one Linux gap:
- A fast off/on toggle destroyed the fresh window. Hiding bumped a
dismiss generation but the page's close request ignored it, so the
retract that finished after the next show tore down the reused window.
The generation now travels through the dismiss handshake and a stale
close is ignored; the page also replays its entrance on a new present.
- The pointer tracker could exit and never restart, and a reused or
rebuilt window got none, leaving a transparent always-on-top window
that swallowed every click. A running flag and a drop guard respawn
it, a panic costs one tick, and the window is created click-through
so no path leaves it hit-testable.
- A layout request during a drag snapped the rail out from under the
pointer. Requests are kept and deferred until the drop settles.
- Pointer polling ran at 62 Hz whenever the dock was on. It now polls
at 62 Hz only while engaged or within 120 px, 25 Hz to 600 px, and
8 Hz beyond, about an 87 percent cut at an idle desktop.
- On Linux the dock could be switched on and produce a window nothing
ever made click-through. It is unavailable there now and reports off.
Error output in these files goes through the tray's log file instead
of a stderr a release build does not have.