The spend badge is a second tray icon carrying the number as its bitmap, which
only the Tauri tray backend provides; Linux runs its own SNI tray and has no
equivalent. The module was compiled there anyway, so every item in it - and the
two tray ids in lib.rs - tripped dead_code under clippy -D warnings on the
ubuntu leg.
- `mod tray_badge` and TRAY_ID / BADGE_TRAY_ID are now cfg(not(linux)), so the
code is absent on Linux rather than present and unused.
- `set_tray_badge` reports the badge as unsupported on Linux instead of
returning a success that never happened.
- The frontend hides the control wherever it is unsupported, behind
TRAY_BADGE_SUPPORTED in lib/platform.ts - one constant, three call sites
(the settings row, the footer menu item, and the effect that would otherwise
invoke the command).
- AppState's `linux_tray` field is dropped: it was written and never read, which
is the same lint one file over. init_tray_linux already owns the handle.
Checked rather than reasoned: `cargo clippy --all-targets -- -D warnings` passes
on macOS, and a scratch copy of the crate with the linux/macos cfg arms swapped
(so a normal macOS clippy selects everything a Linux build would keep, and drops
everything it would drop) also passes. Reinstating the ungated `mod tray_badge`
in that copy reproduces the exact CI failure, so the check is real. It stubs
tray_linux.rs, whose ksni and png deps do not build here - that file is still
only covered by CI's ubuntu leg.
Process spawning (Windows searches the current directory before PATH):
- reg.exe and cmd.exe are now spawned by absolute System32 path via
cli::system_command, which also carries the CREATE_NO_WINDOW flag the three
previous copies each set by hand. The tray badge re-ran `reg query` on every
refresh, so this was the most reliably reachable planted-binary path.
- "Connect Claude" resolves the claude binary itself instead of handing a bare
name to the console shell.
- CLI lookup ignores empty and relative PATH entries. `;;` or a trailing `;`
used to yield PathBuf::from("").join("codeburn.cmd"), a current-directory
lookup, at startup. The filter lives in one place (find_in_dirs) that every
search - codeburn, claude, and the Linux terminal probe - goes through.
- The Linux terminal path re-validates the whole command against the argument
allowlist before joining it into the string `bash -lc` parses; anything that
fails falls through to the argv-only detached spawn.
CLI version gate:
- MIN_CLI_VERSION moves from 0.7.0 to 0.9.9, the first release accepting
`status --format menubar-json --no-optimize` (every quiet refresh passes it)
and emitting all the payload fields the popover reads.
- The gate is probed on mount, before the first fetch. It previously only ran
when a fetch failed with the literal "CLI not found", so an old CLI produced
a payload whose missing history.daily threw and blanked the popover.
- A successful fetch no longer flips an incompatible CLI to compatible, and the
settings panel no longer probes on its own - App owns the verdict, so a
transient probe failure there cannot drop a working app onto the setup screen.
- The payload reads App makes are optional now, so a surprising payload lands on
an empty state rather than a blank window.
Refresh cadence, mirroring mac RefreshCadence.swift: 60s with optimize findings
while the popover is visible, 120s today/all without them while it is hidden,
and an immediate refresh on show when the visible key is stale. Every hide path
in lib.rs now goes through mark_hidden so the debounce stamp and the frontend
signal cannot drift. Previously it was 60s with optimize regardless of
visibility - about 2880 CLI spawns a day.
Claude quota (plan.rs) stops calling the token refresh endpoint. Claude's
refresh token is single-use and rotates, so spending it invalidated the token
Claude Code itself holds and broke the user's login. Parity with
ClaudeCredentialStore.refreshAfter401: re-read Claude's own store for a token it
has already rotated, and report a transient failure when there is not one yet.
Smaller:
- Snapshot writes refuse a symlinked target and are 0600 on unix, mirroring
mac SafeFile; no home directory now means no snapshots rather than a file
dropped in whatever directory the tray was launched from.
- The Windows config lock keeps its file handle open, so the stale sweep can
only ever unlink a lock whose owner is gone. The doc comment now says what the
lock actually buys (the CLI never takes it).
- "updated Xs ago" is only stamped by a fetch of the key on screen.
- External links go through tauri-plugin-opener instead of target=_blank.
- The tray badge font is loaded once into a OnceLock instead of read and parsed
on every render.
- autostart's shared import and constant are cfg-gated so clippy is clean on
every OS.
Adds unit tests for the PATH filter and the version gate.
Brings the Tauri 2.x tray popover from #1022 onto main as windows/, mirroring
mac/. Product name, bundle identifier, and version line up with the macOS
menubar (org.agentseal.codeburn-menubar, 0.9.20); the crate is renamed off
"desktop" so it no longer collides with the Electron app in app/.
Linux (ksni) stays compiled and dev-usable but is documented as experimental:
gnome/ is the shipping Linux surface.
The five src/ CLI commits on that branch are dropped - they re-implement a
daily-bucketing fix main already carries.