Commit graph

84 commits

Author SHA1 Message Date
Resham Joshi
b876a311e2
feat: telemetry for CLI error causes, provider read failures and update results (#1702)
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.
2026-10-09 12:47:49 +02:00
Resham Joshi
fce75d5420
feat(app,windows): one-click updates for the desktop app and the Windows tray (#1685)
* 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)
2026-10-08 05:06:55 +02:00
Matt Van Horn
ec2822a147
feat(windows): localize the Windows tray into Simplified Chinese (#1608)
Localizes the Windows tray menu, tooltip, usage row, dock menu and footer into Simplified Chinese, reusing the macOS menubar glossary. Refs #1336, #1646.
2026-10-07 20:10:56 +02:00
dependabot[bot]
81e2eb1947
chore(deps): bump rustls from 0.23.38 to 0.23.45 in /windows/src-tauri (#1659)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.38 to 0.23.45.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.38...v/0.23.45)

---
updated-dependencies:
- dependency-name: rustls
  dependency-version: 0.23.45
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-07 03:01:24 +02:00
Resham Joshi
063e42796d
feat(providers): add Command Code (#1658)
* 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
2026-10-07 00:18:50 +02:00
Resham Joshi
04d0b65327
feat(cursor): show the Cursor sync status and an on/off switch in the apps (#1638)
* feat(cursor): show the cursor.com sync in the apps, with an off switch

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

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

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

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

With CODEBURN_CURSOR_SYNC=0 set, the desktop, mac and tray switches show
"Turned off by CODEBURN_CURSOR_SYNC=0" and are disabled. A failed mac
save now says so under the switch. The tray hero note uses the warning
ink and maps errorCode to its own messages.
2026-10-05 17:11:41 +02:00
Resham Joshi
c17e7b05aa
fix(amp): Windows guard reads the AMP_DATA_DIR list and watches threads/ (#1604)
* 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
2026-10-02 15:59:08 +02:00
ozymandiashh
157347aa99
feat: Amp support - read the local thread mirror (#1572) (#1578)
* 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
2026-10-02 10:36:05 +02:00
Swizzy
d3929746ae
fix(dock): serialize claudeProfiles in camelCase on DockQuota::Ready (#1594)
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.
2026-10-02 10:28:54 +02:00
ozymandiashh
59ed66b977
feat(dock): one Claude ring per config directory, with its own sessions and today (#1570)
* 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>
2026-09-28 19:06:53 +02:00
Resham Joshi
ab73bd33b4
fix(windows): read the Claude quota credential from running WSL distros (#1061) (#1554) 2026-09-25 01:35:41 +02:00
Resham Joshi
445d7ad944
fix(windows): find the CLI under a relocated VOLTA_HOME (#1532) (#1534) 2026-09-23 16:30:54 +02:00
Resham Joshi
d71c62de2e
chore: release 0.9.25 (#1518) 2026-09-21 18:33:28 +02:00
Swizzy
42710db3c2
Claude plan label: read subscriptionType so a Team seat is not shown as Max 5x (#1492)
* 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
2026-09-19 11:25:17 -07:00
Resham Joshi
bdf7ecb3e3
chore(windows): Tauri 2.11 on both the npm and Rust side (#1428)
* chore(deps): bump the minor-and-patch group across 1 directory with 8 updates

Bumps the minor-and-patch group with 8 updates in the /windows directory:

| Package | From | To |
| --- | --- | --- |
| [@tauri-apps/api](https://github.com/tauri-apps/tauri) | `2.10.1` | `2.11.1` |
| [@tauri-apps/plugin-opener](https://github.com/tauri-apps/plugins-workspace) | `2.5.3` | `2.5.5` |
| [@tauri-apps/plugin-shell](https://github.com/tauri-apps/plugins-workspace) | `2.3.5` | `2.3.6` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.5` | `19.3.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.14` | `19.3.0` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.5` | `19.3.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.3.0` |
| [@tauri-apps/cli](https://github.com/tauri-apps/tauri) | `2.10.1` | `2.11.4` |



Updates `@tauri-apps/api` from 2.10.1 to 2.11.1
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/api-v2.10.1...@tauri-apps/api-v2.11.1)

Updates `@tauri-apps/plugin-opener` from 2.5.3 to 2.5.5
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/http-v2.5.3...http-v2.5.5)

Updates `@tauri-apps/plugin-shell` from 2.3.5 to 2.3.6
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/nfc-v2.3.5...nfc-v2.3.6)

Updates `react` from 19.2.5 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

Updates `@types/react` from 19.2.14 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.5 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

Updates `@types/react-dom` from 19.2.3 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@tauri-apps/cli` from 2.10.1 to 2.11.4
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v2.10.1...@tauri-apps/cli-v2.11.4)

Updates `@types/react` from 19.2.14 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@tauri-apps/api"
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@tauri-apps/cli"
  dependency-version: 2.11.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@tauri-apps/plugin-opener"
  dependency-version: 2.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@tauri-apps/plugin-shell"
  dependency-version: 2.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(windows): tauri 2.11 on both the npm and Rust side

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 17:54:03 +03:00
dependabot[bot]
ec34a9bea5
chore(deps): bump serde_with from 3.18.0 to 3.23.0 in /windows/src-tauri (#1417)
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.18.0 to 3.23.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.18.0...v3.23.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.23.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 16:48:25 +03:00
dependabot[bot]
73f7107afe
chore(deps): bump quinn-proto in /windows/src-tauri (#1416)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.14 to 0.11.18.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.14...quinn-proto-0.11.18)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 16:47:49 +03:00
dependabot[bot]
5cda22200a
chore(deps): bump rustls-webpki in /windows/src-tauri (#1407)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.12 to 0.103.15.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.12...v/0.103.15)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.15
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 15:57:30 +03:00
dependabot[bot]
bf2005fa47
chore(deps): bump the minor-and-patch group (#1388)
Bumps the minor-and-patch group in /windows/src-tauri with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [tauri-plugin-opener](https://github.com/tauri-apps/plugins-workspace) | `2.5.3` | `2.5.5` |
| [tauri-plugin-shell](https://github.com/tauri-apps/plugins-workspace) | `2.3.5` | `2.3.6` |
| [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.149` | `1.0.151` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.52.1` | `1.53.1` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.104` |
| [ksni](https://github.com/iovxw/ksni) | `0.3.4` | `0.3.6` |
| [tauri-build](https://github.com/tauri-apps/tauri) | `2.5.6` | `2.6.3` |


Updates `tauri-plugin-opener` from 2.5.3 to 2.5.5
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/http-v2.5.3...http-v2.5.5)

Updates `tauri-plugin-shell` from 2.3.5 to 2.3.6
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/nfc-v2.3.5...nfc-v2.3.6)

Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

Updates `serde_json` from 1.0.149 to 1.0.151
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.151)

Updates `tokio` from 1.52.1 to 1.53.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.53.1)

Updates `anyhow` from 1.0.102 to 1.0.104
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.104)

Updates `ksni` from 0.3.4 to 0.3.6
- [Changelog](https://github.com/iovxw/ksni/blob/master/CHANGELOG.md)
- [Commits](https://github.com/iovxw/ksni/compare/v0.3.4...v0.3.6)

Updates `tauri-build` from 2.5.6 to 2.6.3
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v2.5.6...tauri-build-v2.6.3)

---
updated-dependencies:
- dependency-name: tauri-plugin-opener
  dependency-version: 2.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: tauri-plugin-shell
  dependency-version: 2.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: tokio
  dependency-version: 1.53.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: ksni
  dependency-version: 0.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: tauri-build
  dependency-version: 2.6.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 14:56:23 +03:00
iamtoruk
a0e0b9f866 chore: release 0.9.24 2026-09-04 02:05:03 -07:00
iamtoruk
a2885c263b test(windows): stop the cross-process test starving itself on a loaded runner
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.
2026-09-03 12:52:25 -07:00
iamtoruk
5f09d9735f fix(windows): make the cross-process lock test poll, not spin, so it passes on Windows
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.
2026-09-03 11:28:01 -07:00
iamtoruk
96bf1984ff fix(windows): wait out a reader when replacing a preference file
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.
2026-09-03 11:10:33 -07:00
iamtoruk
a1b4b22f51 fix(windows): close three holes in the cross-process preference lock
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.
2026-09-03 10:28:05 -07:00
iamtoruk
615c94241b fix(windows): serialize tray and dock preference writes across processes
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.
2026-09-03 07:59:42 -07:00
iamtoruk
d7f24d6cba fix(windows): the dock lands where it was dropped and never fails open
- 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.
2026-09-03 03:06:12 -07:00
iamtoruk
c79a25715c fix(windows): tray telemetry off the UI thread, bounded, and Linux-clean
- 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.
2026-09-03 03:06:12 -07:00
iamtoruk
91706484d6 feat(windows): consent-gated telemetry from the tray app and the Capacity Dock
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.
2026-09-03 01:18:23 -07:00
iamtoruk
00403c9152 feat(windows): report updates and point at GitHub instead of installing them
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.
2026-09-02 23:09:36 -07:00
iamtoruk
5b15718058 fix(app): the tray companion works with only the desktop app installed
- 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.
2026-09-02 22:10:37 -07:00
iamtoruk
e95ea95769 fix(windows): private key files, a real log, and a clean uninstall
- 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.
2026-09-02 15:16:06 -07:00
iamtoruk
79b2d7731b fix(windows): the dock survives a fast toggle and never blocks clicks
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.
2026-09-02 15:16:06 -07:00
iamtoruk
0560d5a5bd fix(windows): clear the clippy errors that fail the tray CI on both platforms
The DPAPI calls took `&mut input` for a blob they only read, the dock's
payload command has one argument more than clippy's default limit, and
the Linux leg compiles the Windows-only dock, session and tray-status
code without calling it, which tripped 25 dead-code lints under
`-D warnings`. Dead-code lints now apply on Windows only, where every
function is reachable.
2026-09-02 14:28:08 -07:00
iamtoruk
9e2f140c91 fix(windows): a second launch no longer hangs the tray app or its callers
The single-instance plugin hands a second launch over with a synchronous
SendMessage, so that launch sits inside it until the running app's callback
returns. When the message landed while the first launch was still in setup,
the callback showed the dock from inside a nested message pump, waited on an
event loop that was not running yet, and never returned; every launch after
it, the desktop app's --quit included, blocked behind the same message and
piled up as processes with no windows.

The callback now only records the request and carries it out from the event
loop. The desktop companion also waits, briefly, for a quit it has just sent
to finish before starting the tray app again, because a launch that arrives
while the old process is still going hands its request to that process and
exits with it.
2026-09-02 10:01:57 -07:00
iamtoruk
1b33c9c474 feat(windows): cache the Capacity Dock's glance data in Rust
The dock's hover bubble is about to draw the sessions running right now
and today's totals, both of which live in the menubar payload. The dock
is a window with no payload of its own, and the popover's fetch_payload
already runs the CLI for the same answer, so a second spawn on the dock's
own timer would double the cost of every refresh for one bubble nobody
may be looking at.

So the slice the bubble needs is kept in AppState as each payload goes
past: the liveSessions block passed through untouched, and today's
totals from the one request key whose current block really is today's
(today, all providers, local scope, no day pick). A week's numbers under
a heading that says Today would be a lie, which is why the key is
checked rather than the period alone.

Only a glance that actually moved is broadcast on codeburn://glance, so
a poll that found the same sessions costs the dock no render, and a
payload that carries no liveSessions block never blanks one already
held: absence there means the CLI is older than the block, not that
nothing is running. dock_glance answers with the snapshot, or None when
nothing has been fetched yet, which is the dock's cue to ask once itself.
2026-09-02 08:16:48 -07:00
iamtoruk
b1316717ac feat(windows): the Capacity Dock rail emerges from its edge
Switching the dock on made a rail appear where there had been nothing, and
switching it off made one vanish. Neither reads as a thing arriving or leaving,
because neither has any direction: the window is created, painted, destroyed.

Now the rail comes out of the edge it is docked to. It starts tucked its whole
cross width behind that edge, so nothing of it shows, and the transform is
released on the frame after the first layout, which is the first moment there is
an edge and a size to be tucked behind. A floating rail has no edge to hide
behind, so it fades and scales from 0.92 instead. Both use dockAttach, the same
280 ms curve the drag glide already settles on.

Leaving is the same in reverse, and it needs a handshake because Rust owns the
window: hide asks the page to retract, the bubble goes first, the rail slides
back over dockDetach, and the page then calls dock_close. Rust keeps a 400 ms
timer so a page that never answers costs a fifth of a second rather than a rail
that will not go away, and a generation counter means switching the dock off and
straight back on cannot have the old timer take the new window.

Nothing here moves a window. It is a transform on the rail alone, so every
window metric stays the whole pixel it was, and prefers-reduced-motion skips
both plays: the rail is simply placed, and the dismiss closes without waiting.
2026-09-02 07:56:12 -07:00
iamtoruk
eb6f5de6f0 fix(windows): --quit reaches a running app again
The early return that answered --quit before the Tauri builder ran also skipped
the thing that delivers it. Handing a second launch's argv to the instance that
is already running is the single-instance plugin's job, and the plugin only gets
to do it if the builder runs, so returning first meant the running app never
heard the request: the desktop app's Menu bar switch dropped the Run value and
sent --quit, and the tray app carried on.

It is answered in setup now, where a launch that reached that point is by
definition the only instance: with nothing running it is the one being asked to
go, and it exits without showing anything. With something running the plugin
delivers the argv and this process never reaches setup at all.

Found by driving the switch against the installed app rather than by a test: the
argv parsing this rests on was unit-tested and correct, and the delivery it
depends on was not something a unit test could see.
2026-09-02 07:55:25 -07:00
iamtoruk
076dc1d960 fix(windows): reinstalling the same version replaces the files
Installing the .msi over an install of the same version left the old files
exactly where they were, which makes `codeburn menubar --force` mean nothing:
the one thing force is for is repairing an install that is already there.

It is a WiX rule, and Tauri does expose it, just not under the name it goes by.
Its main.wxs carries two MajorUpgrade elements and picks between them on
allowDowngrades, which defaults to true. That branch is
`AllowDowngrades="yes"` with no AllowSameVersionUpgrades; the other one is
`DowngradeErrorMessage=... AllowSameVersionUpgrades="yes"`. So asking for no
downgrades is what turns same-version upgrades on, and no custom template is
needed.

Refusing a downgrade at the installer is also the rule this codebase already
follows a layer up: both the CLI's bundled install and the desktop app leave a
newer menubar alone rather than replacing it. Now the .msi says the same thing
by itself.
2026-09-02 06:08:46 -07:00
iamtoruk
2fb76dc4cf feat(windows): pin the WiX upgrade code the msi has always carried
The upgrade code is what tells Windows Installer that two .msi files are the
same product, and it is about to matter: the desktop app will ship this build
and install it over whatever a manual `codeburn menubar` already put there.
With one upgrade code that is an in-place upgrade; with two it is a second copy
of the tray app beside the first.

Tauri derives the code from a v5 UUID over "<productName>.exe.app.x64" when the
config does not name one, so the value written here is the one every released
.msi already carries (`tauri inspect wix-upgrade-code` reports the override and
the derived default as the same GUID). Nothing about existing installs changes.
What changes is that renaming the product can no longer move it by accident.
2026-09-02 04:30:20 -07:00
iamtoruk
527340b686 feat(windows): the update checker stands down inside a Store package
The desktop app's Store build ships this tray app inside its own MSIX package,
and a package updates as one thing. An .msi install underneath it would be
refused at worst and undone by the next Store update at best, so the check does
not run there at all: no request to GitHub, no `codeburn --version` spawn, no
cache file on disk.

GetCurrentPackageFullName is the documented ask, which is why the length is
requested with no buffer to put it in: outside a package it answers
APPMODEL_ERROR_NO_PACKAGE, inside one it complains about the buffer.

storeManaged travels with the status so the surfaces go quiet rather than
inventing a silence of their own: the header badge hides, About says where
updates come from instead of offering two buttons that can do nothing, and the
tray menu does not carry Check for Updates at all. The CLI banner needed no
change, since it reads cliUpdateAvailable, which a store-managed status leaves
false.
2026-09-02 04:29:52 -07:00
iamtoruk
c818772b08 feat(windows): a second launch can quit the app or reload its settings
An app whose only surface is a tray icon has no handle anyone else can take
hold of. The single-instance plugin already hands the running process the argv
of whatever tried to start beside it, so that is the control channel: --quit
exits, --reload-settings re-reads what is on disk, and anything else is still
the plain relaunch that shows the popover.

The desktop app that is about to bundle this one needs both. Its Menu bar
switch stops the tray process; its Sidebar switch writes enabled into
windows-dock.json and then asks a running app to notice, which without this
would wait for the next launch.

--quit at an app that is not running would otherwise start one and leave it
there, so it is answered before anything is built rather than in the plugin
callback that a first launch never reaches.
2026-09-02 04:25:40 -07:00
iamtoruk
790c2dd603 fix(windows): the tray shows the logo alone unless the number icon is asked for
The number beside the logo is a second tray icon, the only way the notification
area can carry text, and two icons read as clutter. It is now opt-in: a fresh
install shows the logo, the figure stays in the tooltip and at the top of the
tray menu, and the settings toggle brings the number icon back. Anyone who
already chose either way keeps that choice. Startup no longer restores a badge
from the status cache unless the setting asks for one.
2026-09-02 03:27:43 -07:00
iamtoruk
85ce84a8bb feat(windows): the tray says when a paired device did not report
Under combined scope the tray figure is the cross-device total, so a peer
that failed to answer makes it smaller than what these machines actually
spent. The mac marks that beside its title with a dimmed
"reachable/total" and says "N of M devices reporting" in the tooltip.

The tooltip and the menu's usage row carry the counts here, in the mac's
words. The badge cannot: it is a 16 px bitmap with room for about four
glyphs, and squeezing "2/3" into it costs the figure its legibility. It
is drawn dimmed instead, half way to the panel behind it, so a reduced
total still reads as incomplete rather than as a glitch, and the words
are one hover away.
2026-09-02 01:39:10 -07:00
iamtoruk
929bca8019 fix(windows): the rail keeps its edge while the size changes
Dragging the Capacity Dock size slider tore the rail off the screen edge
and left it floating there for as long as the drag lasted. Three things
were wrong.

The window moves from Rust and the page paints inside it, and a size
change moved the window without telling the page. The page found out one
event and one round trip later, so in between it drew a rail sized and
offset for the window it used to have: on a right-docked rail that is a
pill hanging a bubble's width off its own edge. The new size and the
frame it belongs to now travel together in one event, and the page
applies both in a single render.

The rail is also pinned to the edge it is docked to, right or bottom,
rather than positioned from the window's left and top. A docked rail's
outer edge is the window's own edge, because the window is sized from the
rail, so that pin holds through any resize the window is given
underneath it.

And the writes are serialized. `set_dock_prefs` is async, so the several
writes a dragged slider starts were several tasks that could finish in
any order and leave the rail at a size the slider was no longer showing.
One at a time now, with a patch that is still waiting merged into the one
ahead of it, which also costs the drag fewer round trips.
2026-09-02 01:29:53 -07:00
iamtoruk
2b3c106da8 fix(windows): the daily budget lives where the CLI keeps it
The app wrote the spend limit as a top-level dailyBudget in dollars. The
CLI's own config type has never had that key: it reads budget.daily, in
the display currency. So a limit set in the settings window did nothing
for `codeburn report`, and a limit set by the CLI did nothing for the
tray.

The limit is budget.daily now, written as the reader typed it, in
whatever currency they are looking at, which is what the CLI's other
budget tiers already mean. Every comparison converts it back to dollars
against the rate the app already has for the currency, because the
payload is in dollars; the popover re-reads it when the currency changes,
since what a limit is worth in dollars moves with the rate.

An existing dailyBudget is read once more and moved across on the first
read after this build, rate applied, old key removed, so a limit set
before this does not silently switch off. The token budget has no CLI
counterpart and stays exactly where it was.
2026-09-02 00:51:59 -07:00
iamtoruk
fee71400cd chore(windows): remove what nothing reads
None of this is reachable, and none of it is caught by the compiler:
tsc's noUnusedLocals only sees a name unused inside its own file, and CSS
has no such check at all.

- --scroll-height, a token no rule ever read.
- DropMenu's danger flag and the .dropmenu-danger rule behind it, which
  no caller has ever set.
- SunMoonIcon and PowerIcon, from a footer that has not looked like that
  for a while. FlameIcon stays: the Stats costliest-session row draws it.
- currentTheme, whose last caller went with the theme cycle, and
  settingsSnapshot, which never had one.
- .segmented, .segment and .segment-active, left behind by the period
  control that now draws its own tabs.
- A SystemTime import in usage_guard that only the tests use, which the
  ordinary build had been warning about since it landed.
2026-09-02 00:23:16 -07:00
iamtoruk
c9502bc7ff feat(windows): export into Downloads and reveal what came out
Export opened a console, ran the CLI in it and left the result in
whatever directory the app happened to be started from, which on this
platform is next to the executable. The mac has always written into
~/Downloads as codeburn-<timestamp> and then revealed the result in
Finder with it selected, so this does the same: no console, a folder of
CSVs or one JSON file with the mac's yyyy-MM-dd-HHmmss stamp, and
Explorer opening on it through /select, which is the Windows counterpart
of activateFileViewerSelecting.

The Downloads folder comes from the known-folder API rather than
%USERPROFILE%\Downloads, since it can be moved. The stamp is formatted in
the page because a local-time format in Rust would cost a dependency or a
hand-rolled timezone; it never reaches a shell, but it does become a
directory name, so Rust checks it is digits and hyphens of the right
length rather than trusting it.
2026-09-02 00:17:50 -07:00
iamtoruk
8a227ef87d feat(windows): the tray theme item cycles System, Light and Dark
The item said "Toggle Dark/Light" and flipped between two states, so a
reader who had ever chosen Light or Dark could not get back to following
the system, which is the only setting the mac has at all. It steps
System, Light, Dark now and names the state it moves to, because a menu
item reads as a verb. The popover's More menu runs the same cycle from
the same wording, so the two can never disagree.

The choice is written to the settings file from Rust rather than asked of
the popover, which means the item works with no popover on screen, and
every window retints from the broadcast that already exists. That made
the resolved-theme state in the popover dead, and the media listener that
kept it, so both are gone.

One bug had to go first. A tray's on_menu_event handler is registered
globally rather than against that tray, so registering it on both the
logo and the badge icon ran every menu item twice: the cycle skipped a
step, and Show Capacity Dock had been toggling itself straight back.
2026-09-02 00:03:52 -07:00
iamtoruk
6ed5b8f2fd feat(windows): the CLI timeout is a silence, not a stopwatch
A flat 60 s wall-clock timeout cannot tell a wedged child from a slow one. It
killed a first parse of a large corpus, which is the run that most needs to
finish, and it waited a full minute on a child that had died on its feet.

Port of CLIWatchdog.swift. Every read spawn now sets CODEBURN_PROGRESS=1,
under which the CLI's parser writes a keepalive line every 10 s for the whole
of a scan, so silence means something: 45 s of it stops the child, with the
mac's ten-minute cold budget until one payload has come back, and a
fifteen-minute ceiling for a child that chatters forever without finishing.
Every byte on either pipe restarts the clock, which is why both are drained in
chunks now rather than read to the end. Progress lines are stripped from
stderr before it can become the error a window shows.

The stop is asked for first: Ctrl+Break to the child's own process group,
which is what a console process has here instead of SIGTERM and which node
delivers as SIGBREAK, so the CLI unlinks its own refresh lock on the way out.
Only a child that ignores it inside five seconds is killed. The kill is
taskkill /T, because what this app spawns is codeburn.cmd and the process
doing the work is node, a generation below: killing the handle we hold ends
the shim and orphans the parse.
2026-09-01 23:44:55 -07:00
iamtoruk
da94b585d5 feat(windows): a background refresh with nothing new to read is skipped
The hidden loop spawned a Node process every two minutes whether or not a
single session file had been written since the last one. On an idle machine
that is the whole cost of the app for nothing.

Port of UsageDataChangeGuard.swift with the Windows spellings of the roots,
taken from each provider's own probeRoots in src/providers: the Claude config
directories and the desktop sessions, Codex, the Copilot store and the VS Code
family, Cursor, Gemini and Antigravity, and the two dozen smaller roots, each
honouring the environment override the CLI reads. Directory roots also stamp
their first level of children, which is where a new session lands; a provider
that keeps one file is watched as that file. It is deliberately not a
recursive scan: a tick must not replace a full Node parse with a full Rust
walk of the same corpus.

The list tracks the CLI's discovery by hand, so a provider missing from it
must degrade to "refreshes every half hour", never to "stale forever". That is
what the thirty-minute ceiling on a skip is for. The anchor is taken after a
payload comes back, never before, and an open popover or a refresh somebody
asked for is never skipped.

Debug builds say which ticks were skipped and why.
2026-09-01 23:27:35 -07:00