Commit graph

163 commits

Author SHA1 Message Date
Resham Joshi
b9931b1adb fix(commandcode): show the monthly window
Command Code's credits endpoint only reports monthly credits left, so the
monthly window was dropped. For an active subscription on a known plan the
plan price now comes from a small table (the API has no monthly cap field):
pool = max(plan, left), used = pool - left, reset = currentPeriodEnd.
Unknown plans and purchased/free credits stay out. CLI and menubar mirrored.
2026-10-10 10:45:15 -07:00
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
5e3308e0b8
feat(menubar): sign and notarize in CI, pin updates to our team, fixed update feed (#1684)
* feat(menubar): CI Developer ID signing, team-pinned installs, update feed

- release-menubar.yml imports the Developer ID cert, signs with hardened
  runtime and timestamp, notarizes with an App Store Connect key, staples,
  and fails when the signing secrets are missing
- the installer refuses bundles not signed by team XRVP7P7F9M or rejected
  by Gatekeeper
- a rolling update-feeds prerelease carries menubar-latest.json, written
  last after the published zip is checked; installer and update check read
  it before scanning mac-v releases
- the update pill skips the CLI step for the desktop app's bundled CLI or
  a CLI with no package manager, explains npm permission errors, and
  allows longer update timeouts
- RELEASING.md matches what CI does and lists the required secrets

* docs(menubar): DISTRIBUTION.md points at CI signing; retry the feed's asset check

* fix(menubar): popover shows only the Update button; command footer appears only after a failed update
2026-10-08 04:52:56 +02:00
Resham Joshi
4332b1762a
feat: Google AI plan presets and the Antigravity tier label (#1683)
* feat: Google AI plan presets and the Antigravity tier label (#1614)

Plan presets google-ai-pro, google-ai-ultra-5x and google-ai-ultra-20x
at Google's US monthly prices, scoped to Antigravity usage.

The Antigravity quota readers (CLI, desktop, menu bar) now also ask
GetUserStatus when the summary answers and label the windows with
userTier.name. planStatus.planInfo is not used: it says Pro on a free
tier account.

* docs: Antigravity probe comments and README preset list
2026-10-07 23:34:49 +02:00
Resham Joshi
39b9b3adcd
feat(quota): Command Code quota and ClinePass from the Cline sign-in (#1682)
* 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
2026-10-07 23:34:31 +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
b2be8eba56
feat: mark estimated dollars the same way in every view (#1657)
* 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.
2026-10-06 22:27:13 +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
aded4c2326
feat(devin): plan and quota from the Devin CLI cache (#1606)
Some checks failed
Tests / desktop (push) Has been cancelled
Tests / test-platforms (windows-latest) (push) Has been cancelled
Windows Menubar CI / check (ubuntu-latest) (push) Has been cancelled
Windows Menubar CI / check (windows-latest) (push) Has been cancelled
CI / semgrep (push) Has been cancelled
Tests / test (22) (push) Has been cancelled
Tests / test (22.13.0) (push) Has been cancelled
Tests / test (26) (push) Has been cancelled
Tests / test-platforms (macos-latest) (push) Has been cancelled
Tests / test (24) (push) Has been cancelled
macOS Menubar CI / test (push) Has been cancelled
* 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
2026-10-02 16:43:15 +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
yi111
162bc5e937
fix(menubar): pick the CLI package manager from the resolved symlink (#1563)
* fix(menubar): pick the CLI package manager from the resolved symlink

Homebrew's node sets the npm global prefix to the Homebrew prefix, so a
global npm install puts the codeburn launcher at <prefix>/bin/codeburn
pointing at <prefix>/lib/node_modules/codeburn. The directory substring
test saw /homebrew/ there and chose brew upgrade, which exits non-zero
with "codeburn not installed". Resolve the launcher first and branch on
the target: /Cellar/codeburn/ is brew, /node_modules/codeburn/ is npm.
The manual hint used the same substring test, so route it through the
same decision.

refs #1557

* test(menubar): pin symlink resolution in brew path tests

---------

Co-authored-by: Yi-111-a <>
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
2026-09-27 17:17:51 +02:00
Resham Joshi
82469eab68
fix(cursor): connect Enterprise team seats on the team on-demand meter (#1553)
usage-summary for an Enterprise seat has no individualUsage.plan, a null
individualUsage.overall.limit and teamUsage.onDemand instead of pooled, so
decodeCursorUsage returned null and the adapter reported an unrecognized
response. Fall back to the team on-demand meter, list it beside the
monthly window when both exist, and decode meter amounts as Double in the
menu bar so a fractional amount cannot fail the whole decode.

Settings no longer appends sign-in guidance to transient failures.

Reported in #1546.
2026-09-25 01:33:41 +02:00
Resham Joshi
b8cc0edcbf
0.9.25 round 2: fixes found testing the built apps (#1517)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / desktop (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Windows Menubar CI / check (windows-latest) (push) Waiting to run
Windows Menubar CI / check (ubuntu-latest) (push) Waiting to run
* 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.
2026-09-21 13:53:45 +02:00
Resham Joshi
2fc62f7876
0.9.25: correctness, energy, menu bar and installer fixes (#1511)
* 0.9.25: correctness + provider fixes, energy guard, logo, default DMG

CLI/engine fixes: local-model savings count reasoning; ZCode reasoning billed;
codex-credits version-boundary tiering; GLM alias repointed to z.ai discounted rate;
openclaw/pi/qwen timestamp mtime fallback; hermes delta timestamp;
sqlite/warp/hermes busy+permission reclassified as failed (no sealed empty);
codex-cache dev+ino; cursor-cache WAL; antigravity server TTL; quota timeout+
grokbot footer+claude near-expiry; serve RSS guard env+hysteresis; CSV export
subfolder; providers pane fixed window; Warp credit-based cost.
Desktop/menubar: unified flame logo; default DMG; docs signed+notarized.

* menubar: one running instance, self-relaunch on language change

The single-instance guard only retired strictly older copies, so two login
items starting in the same instant both survived, and a copy whose bundle
identity did not survive install was never seen at all. The decision is now
total (newest launch wins, ties break on pid) and peers are matched on bundle
id or executable name. --keep-both opts one launch out.

Language changes no longer have the desktop app quit and reopen the menu bar,
which made macOS attribute the new process to the desktop app and re-ask for
the data-access grant. The menu bar relaunches itself via the existing remote
command key; older menu bars that do not watch the key fall back to the old
path after 5s.

* desktop: export toast shows the saved path; resource buckets on app_close

The export toast showed the folder that was picked, not the dated subfolder
the files land in. The handler already parsed the CLI's saved line; it now
returns that path and the toast uses it.

app_close carries cpu/mem (and serveCpu/serveMem when serve reported) as bucket
labels, sampled on the existing flush interval. serve adds cpuSec and peak rssMb
to its answer frames. Unmeasured figures are omitted, never sent as zero.

* menubar guard never leaves zero instances; installer replaces in place; JSON export into folder

Guard: an unreadable launch date on either side could make two copies retire
each other. Unreadable peers now count only when their pid is lower, and a copy
that cannot read its own date retires lower pids only, so a disagreement keeps
at least one instance.

Installer: an existing bundle in /Applications or ~/Applications is replaced
where it lives instead of a second copy being added. The old bundle is moved
aside and restored on any failure; a cross-volume move falls back to a copy that
is re-verified with codesign. An unwritable location is never escalated: the
install goes to ~/Applications and the leftover is named.

Export: -o pointing at an existing folder writes the JSON inside it. Short
sessions omit the cpu buckets. setLanguage reopens the menu bar if its own
relaunch never comes back. AppRelaunch passes the bundle path as an argument.

* menubar guard orders by kernel start time; installer opens the copy that exists and recovers a killed install

Guard: launch dates could be unreadable and pid order can disagree with start
order after pid reuse, so two copies could still retire each other. Ordering now
uses the kernel-reported process start time, which both sides read identically.
A copy whose start time cannot be read is left out, so the worst case is two
menu bars, never none.

Installer: with the only copy in an unwritable /Applications, a plain run tried
to open a path that did not exist. It now opens the installed copy. A placement
killed mid-way is recovered on the next run, a concurrent install is refused,
and a failed restore names where the previous bundle is. Process matching uses
the exact executable name.

Export: a trailing separator names a folder. Telemetry cpu is measured from
telemetry start.

* installer: a recycled pid can no longer block installs; language change waits for the replacement process

An aside left by an earlier install counted as an active install whenever its
pid was live, so once that pid was reused every install was refused for good.
It now also has to be recent (ctime within 10 minutes), a pid we may not signal
is not ours, the refusal names the file, and an orphan that cannot be cleared is
reported instead of aborting the install.

setLanguage confirmed the relaunch with a matcher the dying process still
satisfied, so the reopen fallback never fired. It now waits for a pid that was
not running before.

* desktop: only the headline refreshes live; act, yield and the optimize scan move to slower tiers

An open window ran act report (a fresh CLI process each time), yield and an
optimize scan on every refresh. The headline poll now passes --no-optimize and
takes the snapshot path; act report refreshes on mount, on manual refresh and
every 10 minutes, yield every 5. The optimize scan runs at most once a day per
query scope, is stored in userData with its time, and is forced fresh by the
Optimize page or a manual refresh. Figures that are not live show their age,
and no stored scan means no savings clause rather than a zero.

On battery the live interval doubles; the chosen cadence stays the base.

* desktop: a stored optimize scan is only valid on the day it was computed; cadence changes no longer refetch

The scan cache is keyed on the argv, which says 'today' or '30days', not a date,
so yesterday's scan was served as today's. It is now reused only on the same
local day. Stored findings in Signals carry their age, a failed scan shows the
error instead of scanning forever, and the cold-boot label uses the time the app
received the payload.

usePolled fetched immediately whenever its interval changed, so every power
transition (and launching on battery) refetched every live hook and could spawn
an extra act report. Fetching and timer arming are now separate effects.
Optimize's yield is on the same 5 minute tier as Overview.

* desktop: re-ask for the optimize scan on the first render of a new local day

An app left open across midnight kept yesterday's stored savings under Today,
because the scan was only requested on mount, scope change or manual refresh.
The local day is now one of the hook's triggers; the previous day's figure is
dropped while the new scan runs.

* serve: a menubar poll is never answered from the status snapshot

Inside its settle window the status snapshot returns the pre-change payload on
purpose. The resident serve process then memoized that answer as clean, so after
a single write followed by quiet the headline stayed on the old figure until the
next write or the five minute memo cap. The snapshot is for one-shot runs; the
resident process already holds the parsed state, so it now skips it.

* tests stop writing the real serve pid file; superseded caches are swept; installer notes reach the desktop card

ServeConnection takes its pid file as a parameter, so the Swift tests no longer
share ~/.cache/codeburn/menubar-serve.pid with a running menu bar, and the
reaper only signals a record that is a serve --stdio command.

Superseded session-cache, codex-results and cursor-results files (100MB+ each)
are removed once untouched for 14 days, at most once a day. daily-cache and the
hermes ledger are never swept: they carry sealed days a version bump adopts.

The menu bar installer's leftover-copy advice is emitted as a marked line under
CODEBURN_PROGRESS and shown on the desktop card. The skipped-refresh NSLog is
debug only. The app's vitest workers turn off Node's built-in web storage.

* menubar: anonymous usage statistics, under the same consent as the desktop and the Windows tray

The Swift menu bar sent nothing, so every codeburn-menubar row was a Windows
tray. It now follows the desktop's telemetry.v1.json when that exists (read
only: same install id, same on/off, onboarded required) and otherwise keeps its
own state with the same region default (off in EU/EEA/UK/CH and when the region
is unknown). Settings gains one Privacy toggle, disabled when the desktop owns
the choice. Events use the tray's names; the daily usage snapshot is the CLI's
block passed through a sanitizer and is left to the desktop when it is the
consent source. Debug builds never send.

A resident CLI that gets disabled for the run now says so in the log.

* menubar statistics: private network session, abandoned batch on opt-out, a desktop No survives the desktop going away

The sender used URLSession.shared, so a server or CDN cookie and the default
User-Agent rode along and would have outlived an opt-out. It now owns an
ephemeral session with no cookies, no cache, a fixed agent string and no
redirects. A batch that is out when the toggle goes off is dropped rather than
re-queued under the fresh id. The desktop's answer is recorded locally, so a
missing or damaged state file never falls back to the region default, and the
standalone id is persisted when minted. The toggle stays usable as a local off
switch under desktop consent. Copy states what this app actually sends.

* vercel gateway spend is shown on its own row and kept out of totals by default

The gateway reports daily aggregates per model, with nothing to match against
the local tools that were pointed at it, so every headline counted that traffic
twice. Totals now leave the gateway slice out when all providers are shown; the
row keeps its full amount and is marked not in total, --provider vercel-gateway
is unchanged, and codeburn gateway-totals include folds it back in. The daily
cache still stores the slice, so the choice applies retroactively. Reports with
no gateway credential are byte-identical.

The gateway's request_count now feeds the call count instead of each daily row
counting as one call. Hermes: a mid-session price change lands on the day it is
observed and sealed days are never rewritten; stated in the code and changelog.

* gateway exclusion applies to every report through the parse, and the detail panels follow the headline

The first pass narrowed only the day totals, so By tool, top models, top
projects, activities, status json projects and any project-filtered table still
carried gateway spend next to a headline that did not, and By tool shares could
pass 100%. The rule now lives in parseAllSessions: with all providers in scope
and the opt-in off, aggregate-only sessions are dropped after the memo, and the
callers that derive cached days opt out so stored slices are unchanged.
day.projects is reduced like models. The gateway stays visible once, as a
labelled not-in-total row with no share, and standalone reports note the
excluded amount on stderr so machine output is unchanged.

* dashboard: the background history index follows the gateway exclusion too

The index keeps the whole corpus so it can fill the daily cache, but its
per-period projection handed that unfiltered list to the project, model and
activity panels, so they regained gateway spend once the index landed.

* ci: a hung lock suite fails in 90s and retries once with a warning; slow Windows tests get measured limits

The cache-lock step failed 6 of the last 60 runs with no output for five minutes
after the vitest banner; passing runs take 12-18s on every Node version. Each
attempt is now capped at 90s and retried once, with a warning whenever that
happens. On windows-latest a single CLI spawn takes 1.4-12s, so the four-spawn
status snapshot test and the c6 parser suite get limits taken from those runs,
each with its reason. Both test jobs print their ten slowest cases.

* desktop: Privacy & data uses the same rows as the rest of Settings

The pane mixed two static claims with two controls in its own layout. It now
uses the about-row pattern every other pane uses: title and hint on the left,
one control on the right. The claims became the subtitle. Rows: anonymous usage
statistics (toggle, link to what is sent), device sharing state with Manage,
local report snapshots with Clear, export with Open export.

* desktop: Privacy & data keeps one bounded column and rows that never squeeze

The card stretched with the window, stranding each control hundreds of pixels
from its text. This pane's heading, subtitle and card now share a 760px column
and one text measure. Rows stack on their own width, because the window can
never be narrow enough for the old viewport rule to fire; row buttons do not
wrap. The what-is-sent link sits on its own line, and the snapshot and export
descriptions fit their measure and say this machine.

* menubar tests clean up their scratch defaults and leave the real Keychain alone

Four test files never removed their UserDefaults suites and the rest removed the
domain but left the plist, so thousands of files piled up in
~/Library/Preferences. A shared helper forgets a suite and deletes its file, and
every test that makes a suite now uses it. cfprefsd still rewrites an empty plist
when the test process exits, so a run leaves about a hundred 42-byte files with
no test data in them. The real-Keychain round trip only runs with
CODEBURN_TEST_REAL_KEYCHAIN=1.

* desktop: the resident parser retires after 15 idle minutes and restarts on demand

The renderer stops polling while the window is hidden, but the serve child stayed
resident with its parsed cache, one to two GB on a large corpus. After 15 minutes
without a request it is stopped the way a settings restart stops it, without
spending the unexpected-death budget, and the next request starts a new resident
through the existing lazy path, never a one-shot. CODEBURN_SERVE_IDLE_MS tunes or
disables it.

* desktop: every settings pane keeps a bounded column, and dark buttons have an edge

Cards stretched with the window, so on a wide display controls sat up to 1300px
from their labels and intro lines ran past 140 characters. All panes now share
one left-aligned column (760px, 980px for projects, aliases and pricing, which
hold wide rows) and one text measure. Rows stack on their own width instead of a
viewport rule the 900px minimum window could never reach, and a long path in
Projects no longer overflows the pane.

In dark mode the secondary button matched or sat below its surface, and was
invisible on the plugin install dialog. It gets a face and a border from two new
tokens; light is unchanged. The onboarding consent label matches Settings.

* desktop: this device shows its fingerprint on its own line

The local name and the fingerprint are two adjacent inline spans, so they read as
one run of text.

* menubar: the resident parser retires after 15 idle minutes and restarts on demand

The menu bar kept its serve child, and the parsed cache with it, for the whole
run, even with the popover closed and nothing changing. After 15 minutes without
a request the child is retired without counting as a death, and the next request
starts a new resident through ensureStarted, never the one-shot fallback, whose
snapshot settle window would make the figure lag a whole tick. The window sits
above the longest gap between real requests, so a child stays warm while work is
happening. CodeBurnServeIdleSeconds tunes or disables it.

* serve: a poll re-checks only what the watcher saw change, and shard writes are coalesced

With a session being written, every poll re-listed about 2,400 directories and
re-fingerprinted about 6,000 transcripts, then rewrote the whole current-month
shard (49MB on a large corpus) and read it back on the next poll. The resident
process now reuses the previous listing and fingerprints for paths its root
watcher proves untouched, and falls back to the full sweep whenever the watcher
is missing, unhealthy, overflowed, armed after the sweep, does not cover the
path, the memo is older than 4.5 minutes, or the process was suspended. Shard
publication is held for up to 90s and flushed when stdin closes; a held write is
dropped rather than written over something another process published. One-shot
runs are unchanged. Active poll cost 0.71 to 0.33 CPU-s at a 30s cadence;
time-to-reflect unchanged at about 1.2s across append, new dir, new file, delete,
rename-over, in-flight, watcher overflow and unhealthy watcher.

Changelog: state the telemetry default as it is in this version.

* a retired parser gets to publish its held shards, and durable providers are never held

Both apps retired the serve child with SIGTERM before its stdin-close flush could
run, so every retire discarded the coalesced window. They now close stdin, give
the child a bounded grace to publish and exit, and signal it only if it is still
running; a settings restart does the same. Measured: flushed 0/5 before, 5/5
after, for both clients.

copilot's cache entries are the only record once Copilot prunes its own files, so
a held window that was dropped (retire, crash, kill -9) before the source was
pruned lost that spend for good. A window that a durable provider dirtied is
published on the poll that parsed it, as before coalescing existed. The RSS guard
publishes a held window before it clears the caches instead of discarding it.

* serve: release the lifetime parse after the fill, and make the collection real

The RSS guard dropped its memos and then called globalThis.gc, which exists only
under --expose-gc; no launcher passes it, so the collection had never run and the
freed pages never went back. On a cold start the resident idled at 2.2GB with
364MB of live heap: clearing every memo moved RSS by 0MB, a collection alone by
700MB.

After the background fill has sealed its summaries the process now publishes any
held shard window, drops the memos it can re-read from disk (the cold start had
loaded the cache with scope 'all', which then served every later poll), and
collects; it collects again a minute later for as long as a pass returns 64MB,
at most four times. Cold idle 2198MB to 700MB, warm 900MB to 781MB. Reports are
leaf-identical cold and warm, time-to-reflect unchanged, a released month scope
reloads in 0.3-0.5s.

* warp: the billed cost reaches reports and survives the cache

The provider derived a dollar cost from Warp's own billing record (provider cents,
charged usage, or credits), but warp was missing from the reported-cost
pass-through, so the cached call kept no cost and every report, cold or warm,
priced the conversation from its tokens. A fixture billed at $1.22 reported
$0.15. A call whose cost came from a billing record now keeps it; a call that
only has the token floor stores none and is still re-priced when prices change.

Warp entries re-parse once (provider parse version), and the daily cache moves to
v34 so days whose Warp source still exists re-derive; call counts do not move, so
nothing is double counted or dropped.

* openclaw and pi: a reported cost survives the cache like every other reported cost

Both providers read the cost their tool recorded for a message, and both were
missing from the reported-cost pass-through, so reports priced every call from
tokens instead. Fixtures reported $0.011 against $0.056 (openclaw) and $0.013
against $0.410 (pi). A call with a reported cost now keeps it; a call with none,
or an explicit zero, stores none and is still re-priced when prices change. omp
had been added to the list in #1150 and pi, the same parser, was never revisited.

The provider list is one named set, the unreachable providerCallToTurn is gone,
and both providers re-parse once; the v34 daily cache re-derives their days with
call counts unchanged.

* test: pin parse count per report and per resident poll
2026-09-21 00:16:21 +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
43608c2558
desktop and menu bar: six-language localization (#1493)
The desktop app and the macOS menu bar can now run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the choice is shared so the menu bar switches with it, relaunching to pick up the language with a retry so a switch never leaves it down.

Every user-facing string in the desktop renderer goes through a local lookup table, 1,249 keys per language, no framework and no network. The menu bar gains four .lproj to join English and Simplified Chinese, 626 keys each. A completeness gate fails the build if any language is missing a key, in both the desktop catalogs and the menu bar coverage suite. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. English output is byte-identical.

Strings produced by the CLI engine stay English until the CLI is localized, a separate effort. Validated live in all six languages: no missing text, no clipping, correct fonts, numbers unchanged, and the menu bar switching for every language.
2026-09-19 03:25:46 -07:00
ozymandiashh
3e87863d44
feat(quota): add ZCode (z.ai coding plan) live quota provider (#1347)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Windows Menubar CI / check (ubuntu-latest) (push) Waiting to run
Windows Menubar CI / check (windows-latest) (push) Waiting to run
Adds a live quota gauge for the z.ai ZCode coding plan on all three surfaces: the CLI plan view, the desktop Plans page, and the mac menu bar. It reads the provider's own remaining-allowance endpoint, so it is quota-only and touches no cost or spend paths. One decoder is shared between Z.ai and ZCode, and ZCode is hidden when a Z.ai login already covers the same endpoint. The fetch is bounded with graceful disconnected, waiting and error states, and no socket is opened without a login. The logo shows on every surface, and ZCode is listed in the desktop supported-provider list to match the menu bar catalog.
2026-09-18 18:11:08 -07:00
ozymandiashh
1f6c3846c0
fix: name the usage that prices at $0 instead of hiding it (#1491)
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.
2026-09-18 16:48:26 -07:00
ozymandiashh
1e6d43d836
menubar: early-reset notification for Codex (#1339)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Codex hands its quota windows to the same early-reset monitor Claude uses, on refresh and on bootstrap, and a Codex disconnect clears its record. Window identity comes from a pre-localization storage label, so a language switch or the limit-reached suffix cannot drop the stored baseline or flip the key at the boundary. A window with no validated cycle length stays silent, and when a provider reports absolute usage a spend-cap increase no longer reads as a cleared counter.
2026-09-17 14:33:35 +02:00
ozymandiashh
dd1f11f528
fix: uncap the Overview model table, billions token rung, unpriced calls dimming (#1443)
The period model list the Overview table reads is no longer cut at 20 rows, so
every model with usage in the period shows up instead of only the costliest
twenty - the rows that dropped out were exactly the $0 local and free models.

formatTokens gains a billions rung, so a codeburn models totals row reads 29.9B
instead of 29868.7M; the menubar row formatter gains the same rung, and compare
now reuses formatTokens instead of its own K/M copy.

The desktop Models tables stop dimming the calls column on unpriced rows. In the
shipped app those rows do not reach the table yet - codeburn models defaults to
minCost 0.01 and getModels passes no override - so the change takes effect once
that filter is addressed, tracked separately.

Also re-lands the three #1315 commits that never reached main: per-model token
counts beside per-model cost in the Overview and the macOS menubar Models rows.
Status snapshot revision goes to 9 so a warm snapshot cannot keep serving the
truncated list. Fixes #1318.
2026-09-17 13:53:10 +02:00
Resham Joshi
3684f1d5f0
Grok Bot: sessions, estimated spend and the weekly allowance on all three surfaces (#1462)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Adds xAI's Grok Bot desktop app as a provider: per-bot sessions with estimated usage, and the weekly allowance read through the Cursor IDE login on the CLI, desktop Plans screen and menubar. No row unless the app is installed. Two independent review rounds on real data.
2026-09-16 21:17:48 +02:00
Resham Joshi
31bd0ff2cb
menubar: drop the early-reset dock band and history caption (#1452)
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
* fix(menubar): drop the early-reset dock band and history caption

* menubar: drop stale band references left by the caption removal
2026-09-16 11:29:38 +02:00
ozymandiashh
a208c25f86 fix(menubar): pin multi-argument catalog keys to positional specifiers
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
The catalog comparison matched specifier lists, so a key whose
placeholders are all %@ passed even when the Chinese value bound the
arguments into the wrong slots — the drift that shipped in three
early-reset keys and was only caught by rendering real data bilingually
(#1331).

All 42 multi-argument keys in both tables now use positional specifiers
(%1$@, %2$lld, ...); the parity check compares slot to verb so Chinese
word order stays free while each slot keeps its type; a new test fails
on any multi-argument key that is not positional.

The source scanner closes its three expression gaps: ternary branches,
concatenated literals, and let-bound literals passed by name.
Text(verbatim:) stays the explicit opt-out; the four provider-name
fallbacks the new rules surfaced in HeatmapSection route through the
catalog as verbatim entries.

Closes #1331
2026-09-15 01:33:28 +03:00
iamtoruk
b9e70bedcb Raise a live session's terminal from the Capacity Dock card
Clicking a session row finds the row's process — Claude Code writes its
pid into ~/.claude/sessions/<pid>.json, Kimi Code is found by the one
`kimi` working in the directory its state.json records — walks the
process tree to the application that owns it, selects the tab by tty in
Terminal.app and iTerm2, and raises the app everywhere else. The
AppleScript runs off the main actor with a 3s cap.

A row whose process cannot be found keeps its layout and stays inert: no
pointer change, no tooltip.
2026-09-14 01:38:01 -07:00
iamtoruk
a496cf646d Notify when a quota window crosses 80% and 100%
Every connected provider's windows are checked on the existing quota
refresh. A crossing fires once per (provider, window, reset instant);
the next cycle re-arms it. Authorization is requested on the first real
crossing, not at launch. Settings gains a toggle, default on.
2026-09-13 13:59:31 -07:00
Resham Joshi
0800471dfd
Merge pull request #1356 from getagentseal/fix/menubar-change-guard-cap
menubar: cap unchanged-refresh skips at five minutes and watch the Kimi Code store
2026-09-13 23:24:49 +03:00
iamtoruk
42c095e36c menubar: cap unchanged-refresh skips at five minutes and watch the Kimi Code store 2026-09-13 13:16:12 -07:00
iamtoruk
89213cb13b menubar: one set of quota colour bands, 60/70/95, for the flame, popover and dock 2026-09-13 12:50:25 -07:00
iamtoruk
a858014bf7 menubar: drop the pace caption from the popover quota rows and delete the caption builder 2026-09-13 12:39:36 -07:00
ozymandiashh
8222c734d9 fix(menubar): make the quota warning legible and name the window it means
The popover's quota warning painted its text in the same system yellow,
orange or red as its 12% pill. In light mode that measured 1.23:1 to 1.43:1
for yellow and at best 3.04:1 for red, and dark-mode red fell to 3.33:1. The
pill keeps its hue; the text and its glyph now take a per-appearance
foreground from QuotaWarningPalette that measures at least 5.29:1, and a test
holds every severity at WCAG AA against the pill composited over both ends of
the popover's background in both appearances.

The sentence named no window. AggregateQuotaStatus reduced each provider to
its worst window and kept only the percentage, so "Claude 70% of quota used"
was the 5-hour limit read next to a weekly 34%, and looked wrong. Each warning
now carries the winning window's label and reset, and the row reads
"Claude · 5-hour 71% · resets in 3h 12m" with the Capacity Dock's countdown,
one line per provider. The countdown is left off when there is no reset time
or it has passed, and "over limit" is kept for a window at or past 100%
instead of everything the danger band (90% and up) covers. The windows
compared, the 70% threshold and the flame's severity are unchanged.

"%@ of quota used" has no caller left and is retired; "resets in %@" is added
for en and zh-Hans.
2026-09-13 06:29:21 -07:00
iamtoruk
d4f3d6eb7a feat(menubar): translate the Codex banked-reset copy and the cache-read tooltip
The notification, the Plan-tab detail and the hover-card line #725 added were
composed in a presentation type and shipped English to a zh-Hans user. Each is
one key now. reset_type is vendor data with no fixed vocabulary, so the four
cadence words OpenAI sends are translated and anything else rides through
verbatim inside a translated frame. compactAge stays character-identical with
the TypeScript side in English, which is what the contract was about: en is an
identity table. The dock's cache-read tooltip was bound to a let before its
call site, which is why the scan never saw it.
2026-09-12 16:12:11 -07:00
iamtoruk
e53dca1350 feat(menubar): translate the early-reset notification, band and history
Every sentence #725 added is now one key with placeholders. The window label
keeps its English form in the persisted event and is translated at render
through a lookup on that form, because "weekly limit" -> "weekly" is a suffix
rule only English obeys; a label outside the known set reads through as before.
The 697 existing early-reset expectations pass untouched.
2026-09-12 16:12:11 -07:00
iamtoruk
008aa1ba11 feat(menubar): translate the Capacity Dock pace captions
Every caption and help sentence #1267 added rendered English in a zh-Hans
build: they are composed in a presentation type, not at a view call site, so
neither the catalog diff nor the source scan could see them. Each sentence is
now one key with placeholders. The two stage captions reuse the Plan tab's
existing keys, and the countdown reuses the popover's. English output is
unchanged: the 329 existing pace expectations pass untouched.
2026-09-12 16:12:11 -07:00
iamtoruk
b94338a75c feat(menubar): add a language picker to Settings
Settings > General > Language offers System, English and 简体中文. It writes
AppleLanguages into the app's own preferences domain, the same key System
Settings > Language & Region > Applications writes, so the two surfaces are one
setting. The current value is read back from that domain rather than through
UserDefaults.standard, which falls through to the global domain and would
report the system language as CodeBurn's own override: System could then never
show as selected. An inline row offers the relaunch that applies it.
2026-09-12 16:12:11 -07:00
iamtoruk
f995593f2a fix(menubar): carry the localization catalog onto current main
Rebase resolution for #1289 onto main (#1306, #1313, #1314, #1315, #1328,
#1329). Conflicts kept both intents: main's second-row title composition,
glance-window accessibility actions, cache-read line, host field and merged
Notifications section keep their behaviour and now resolve through L(…).

Adds 14 keys to both tables, drops 4 that no longer have a call site, and
retranslates the two Copilot explanations main rewrote.
2026-09-12 16:12:11 -07:00
ozymandiashh
e8ae6cb9c4 feat(menubar): translate the strings the two new menubar features added
and #1286's Copilot host fix all shipped user-facing copy that bypasses the
catalog — neither of the first two PRs' diffs contains a single `L(` call. In a
zh-Hans build those strings render in English.

Sweeping mac/Sources the way the original extraction did brings 18 new keys,
and retires 2 that main reworded (the Copilot help paragraph gained the
GitHub Enterprise Cloud host rule; the connection detail now names the host
that answered rather than hardcoding api.github.com). 533 keys become 551.

Routed here:

- Second row: the Settings toggle, the metric picker and its four option
  names, the help paragraph, and the row text itself — today's cost, today's
  tokens, and the quota row with and without a provider label. The reset
  countdown reuses the popover's own `%lldd %lldh` / `%lldh %lldm` / `%lldm`
  keys rather than minting a second set, so the two countdowns cannot
  disagree in a translated build.
- Capacity Dock glance: the switch hint and the named VoiceOver action.
- Copilot: the reworded help paragraph and the host-aware connection detail.

Kept verbatim, per the rules in the catalog header: provider, model and plan
names; `tok`; the `%.2f` amount and its currency symbol; and the window label
the gauge reports, which is provider data and doubles as the matching needle
in CapacityDockGlanceWindow — translating the generic fallback alone would
make one VoiceOver sentence half-Chinese depending on the provider selected.

Also localizes CodexUsage's credit-limit labels. Those are not new, they were
missed by the original extraction and the guard test in the next commit finds
them; leaving them would mean shipping a test that fails on main.

Verified with a standalone swiftc harness over both tables (`swift test`
cannot run on this host): key parity, no blank values, en identity, and
argument/literal-percent specifier parity, plus the new rows formatting
correctly in both locales.
2026-09-12 15:22:47 -07:00
ozymandiashh
d62857ea4e feat(menubar): route every user-facing string through a strings catalog
Groundwork for #1219. The menubar app hardcoded ~530 English strings across
Views/, the presentation structs, the status-item menu and the update alerts,
so there was nothing for a translator to translate.

Every user-facing literal now goes through `L(_:)` / `L(_:_:)`, which look the
string up in `Localizable.strings` in the SwiftPM target resource bundle. The
key *is* the English copy, so English stays the development language and a
missing translation degrades to correct English instead of a dotted
identifier. `en.lproj` is therefore an identity table; it exists so the bundle
advertises `en` and so a translation can be diffed against it.

Why the lookups are explicit rather than relying on SwiftUI's implicit
`LocalizedStringKey`: SwiftPM emits target resources into a sibling bundle
(`CodeBurnMenubar_CodeBurnMenubar.bundle`) that the packaging scripts copy into
`Contents/Resources`. `Bundle.main` has no `.lproj` at all, so `Text("literal")`
would always miss. Naming `Bundle.module` is the one form that resolves the
same way in `swift run`, in `swift test` and in the packaged `.app`.

Enum raw values that double as identity (`Period`, `MenubarScope`,
`InsightMode`, `AccentPreset`, `ProviderFilter`) keep their raw value and gain
a `displayLabel`, so persistence and cache keys are untouched by translation.

Three display-only date formatters that were pinned to `en_US_POSIX` with fixed
patterns now use `setLocalizedDateFormatFromTemplate`, and the calendar
popover's weekday row comes from the locale's own symbols. The `yyyy-MM-dd`
formatter stays POSIX: it parses and builds data keys, not display text.

Not extracted, deliberately: provider, model and plan names; units; currency
codes; shell commands and paths; quota window labels that policy code matches
on by English substring (`QuotaSummary.headlineWindow`); and anything the
`codeburn` CLI produces.

Also copies the SwiftPM resource bundle into the app in build-local.sh, using
the named-bundle + `[[ -d ]]` pattern package-app.sh already uses. Without it
the assembled app finds no strings table (and already trapped on first icon
load, the resource-bundle half of #1262).
2026-09-12 15:21:49 -07:00
iamtoruk
cbcd984929 fix(mac): say what a usage-drop early reset actually did
Both early-reset signals shared one sentence, so a window whose counter
the vendor emptied mid-cycle was announced as "reset 18h early" even
though its reset time had not moved and the cycle still ended when it
always would have. Replaying this Mac's own 30 days of snapshots through
the monitor produces exactly that: on 2026-09-01 the weekly counter went
81% to 0% with resets_at still at 2026-09-04T07:00:00Z, and the copy
promised a new window that was not coming.

usageDropped now gets its own title, body, band and help text; the
reset-moved-forward wording is unchanged. lead() rounds to the unit it
prints instead of truncating, so a 1h57m lead reads "2h", not "1h".

Tests pin both, assert a usage-drop event never says "reset early", and
pin the notice expiry and the cycle anchor tolerance by value rather
than by reading the constants back.
2026-09-12 14:58:42 -07:00
ozymandiashh
c9e8547dae feat(mac): decide early quota resets in pure, testable types
Vendors sometimes reset a usage window ahead of schedule as a goodwill
gesture. Nothing in the app noticed, so the capacity that was handed back
went unused.

The decision is two pure pieces so it can be tested without a view or a
network: a detector that takes the previous and current reading of one
window and returns an optional event, and a summariser that reads a
window's early-reset pattern out of the snapshots already on disk.

Detection needs both signals to be defensible on their own. A reset time
that jumps to a genuinely new cycle while the stored one still had time
to run is one; usage emptying while the advertised reset time stands
still is the other, for vendors that grant the reset without moving the
time. Both carry the same identity for the cycle they cut short, so the
second cannot re-announce what the first already said.

The thresholds are deliberately conservative, because a false "you have
your capacity back" is worse than silence. Inside a cycle reported usage
only rises, moving down by a point or two of rounding, so the drop signal
asks for a fall of at least 40 points that lands at or under 10% — far
outside rounding, with room for a refresh interval of real use after the
reset. A new cycle cannot start before our last look at the old one, so
its reset time must sit about a window past that look; a reset time that
merely creeps forward is a rolling window, not a reset.

Silence is the answer for a scheduled reset, a plan change, clock or
timestamp skew (the discipline QuotaPace already applies), a window
appearing or disappearing, a window's first observation, a reconnect
after a failure, and a reading that is missing or malformed.

The monitor around it keeps the previous fetch's readings, the plan label
and the reset times it has already announced, and delivers through the
existing UpdateNotifier rather than a second notification stack. The
snapshot store keeps each cycle's highest reading, which is right for
"what did the cycle end at" and wrong for "what did the last fetch say",
so the baseline is kept alongside the announcement record instead.

History is derived from the stored reset times alone: a fixed window
starting at t ends at t + length, so a cycle ending sooner than a full
window after the previous cycle's scheduled end began early by the
difference. No network, no external feed.
2026-09-12 14:58:17 -07:00
iamtoruk
84934ca71f fix(macos): only install an update when the update notice is tapped
The notification delegate ran performFullUpdate() for any notification
tapped. With a second notification kind, tapping "Codex banked a limit
reset" would start a CLI and app update.

Gate the handler on the UpdateChecker identifier prefix, shared as a
constant so the poster and the handler cannot drift.
2026-09-12 14:48:50 -07:00
ozymandiashh
79d1482951 feat(codex): notify once when OpenAI banks a limit reset
A banked reset is the one capacity signal a user can act on before it is spent,
and it arrives silently. CodeBurn already sees it: the reset-credit inventory
rides every successful Codex quota refresh. Comparing that inventory against the
previous reading is all it takes to notice a grant, so there is no new endpoint,
no new cadence and no new data source here.

The detection is a pure function over an observation and the state it is judged
against, which is what makes the awkward cases testable rather than hopeful:

- The first-ever observation is a baseline. Everything the account already holds
  is history, not a burst of notifications.
- A credit that disappears was spent or expired, and is never an event. Its id
  is kept for the retention window so a payload that lists it again — a flicker,
  not a grant — cannot re-announce it.
- An absent or malformed credits payload is *no opinion*: no event, and no state
  change at all. Treating it as an empty account would re-baseline on every
  failed fetch and re-announce the whole inventory on reconnect.

Seen ids persist to `codex-banked-resets.json` in the CodeBurn cache directory,
written through SafeFile behind an actor exactly the way SubscriptionSnapshotStore
writes its snapshots — the same mechanism, not a second one — so a relaunch does
not repeat a notice. State is written before the post, not after: a notice we
drop is a smaller failure than one that repeats forever. Disconnecting clears the
file, so a reconnect under another account baselines again.

The notification goes out on the existing UpdateNotifier path, one per new
credit, naming what was granted, when, and how many are usable. Never the credit
id, never a token. Settings gains a toggle, default on; with it off the state is
still recorded, so turning it back on does not release a backlog.

Nothing here ever spends a credit. It is a notice.
2026-09-12 14:37:24 -07:00
ozymandiashh
53dd90a4a7 feat(codex): read the per-credit fields the reset-credits payload already carries
The reset-credits decoder kept only `available_count` and the soonest expiry,
and the inline block on the usage payload kept only `available_count`. The
response has always carried more: `id`, `reset_type` and `granted_at` per
credit, and `applicable_available_count` alongside the total. Those are exactly
the fields needed to tell a newly granted credit from one the account was
already holding, so they are decoded now instead of discarded.

Identity comes first: a credit is reduced to a grant only if it carries an `id`
or, failing that, a `granted_at` to be named by. One that carries neither still
counts toward the server's authoritative total but never becomes a grant,
because it could not be told apart from the next one and would re-announce
itself on every refresh. Only `status == "available"` credits become grants, so
a redeemed one is spent history rather than news.

`applicable_available_count` is unknown when absent, never zero. The companion
endpoint does not always send it and the usage payload sometimes does, so the
usage response we are already holding fills it in when — and only when — the
authoritative source said nothing. No extra request either way.
2026-09-12 14:37:24 -07:00
Resham Joshi
9715637bec
Merge pull request #1314 from getagentseal/fix/dock-pace-followups
feat(menubar): cache-read usage and quota pace in the Capacity Dock
2026-09-12 22:40:46 +03:00
Resham Joshi
d5e2d22880 fix(menubar): stop the dock resizing and claiming a refresh it is not doing
Reserving the pace slot only while connected made the panel relayout on
every refresh: the slot collapsed and the staleness band appeared. The
reservation is a property of the window shape, so it no longer asks the
connection. Windows block on the real Claude shapes, connected vs stale:
91/77 -> 91/91 at one and two windows, 164/136 -> 164/164 at three.

A Claude or Codex summary now reads stale on sample age alone, and the
manual cadence never refreshes, so "Last known usage - refreshing" and
the Settings "Refreshing..." became permanent lies. Both now ask whether
a fetch is actually in flight.
2026-09-12 12:19:29 -07:00
ozymandiashh
53cd44daf2 fix(menubar): give the pasted Copilot token a GitHub host of its own
The last rung of the chain is a token the user pastes into Settings, and an
enterprise personal access token pasted there was still sent to
api.github.com. Settings now takes the host beside the token, defaulting to
github.com, and saves both in the same provider Keychain record so the host a
request is addressed to always comes from the entry that carried the token.

A host no endpoint can be derived from is refused at the field instead of
being stored and reported later as a terminal fetch failure, and the crafted
values #1286 guarded against are refused there too. Clearing the token clears
the host with it, so a stale tenant can never be paired with the next token.

The record's decoding moves into an extension and reads every key with
decodeIfPresent: Swift's synthesized init(from:) ignores a property's default
and throws on a missing key, so adding a field would otherwise make every
credential saved before it unreadable — for every provider, not just Copilot.

The dormant connection row no longer promises api.github.com before anything
has been fetched; it names a host only once one has answered.

Tests cover the three new host sources with a hostile value each, the rung
wiring for all five rungs, the gh hosts.yml key scan, and that a record
written before the host field still decodes.
2026-09-12 12:02:13 -07:00
ozymandiashh
1b4c00f8d5 fix(menubar): follow GH_HOST and gh's own host for the Copilot env and gh rungs
#1286 taught the credential files to carry the GitHub host their token came
from, but the environment and `gh auth token` rungs carried none, so they were
always addressed to api.github.com. A user whose only Copilot credential is a
GitHub Enterprise Cloud login — GH_TOKEN exported alongside GH_HOST, or
`gh auth login --hostname <tenant>.ghe.com` — got a 401 and a terminal failure
with nothing to act on.

The environment rung now reads GH_HOST out of the same environment as the
token. The gh rung resolves the host from gh's own hosts.yml, found via
GH_CONFIG_DIR, then XDG_CONFIG_HOME/gh, then ~/.config/gh, and picked the way
gh picks the host for `gh auth token`: GH_HOST, else the single configured
host, else dotcom. That is a file read rather than a second `gh auth status`
spawn, because gh writes hosts.yml for every login, keyring-backed ones
included.

Both are new untrusted inputs, so they stay raw all the way to
`apiHost(for:)`, where #1286's character validation refuses a crafted value
such as `evil.com?.ghe.com` before any URL exists — that host passes the
`.ghe.com` suffix check but builds a URL whose host is api.evil.com, which
would then receive the Authorization header.
2026-09-12 12:02:13 -07:00
ozymandiashh
610cae48c7 feat(menubar): plain pace verdict, also on the agent-tab quota card
Ports the two parts of #1287 the maintainer asked to keep, expressed
through this branch's `QuotaPacePresentation` rather than through
`QuotaPace.Verdict`:

- Placement: `QuotaDetailRow` in the agent-tab quota hover card becomes
  a VStack and draws the pace line under the bar, indented past the
  label column, with the three tones the dock already uses. The card is
  fitted, so a silent caption collapses there instead of reserving a
  slot the way the dock's computed frame must.
- Wording: the long-window caption drops "est. N% at reset" /
  "est. out in X" for the plain "Lasts until reset" / "Runs out in X",
  and the exhausted state becomes "Limit reached". The projected
  percentage it came from stays in the hover text, which has room for
  the reasoning. Short windows keep the on-pace / deficit / reserve
  stage (#726), and the freshness, connection and duration gates are
  untouched.

`QuotaPace.inferredWindowSeconds(label:)` is deliberately NOT ported.
Grok Build chooses its window label from the distance to the reset, so
a monthly cycle passing through the 4-12 day band is labeled "Weekly"
and a label round trip paces a month's budget against seven days. The
adapter's real `windowSeconds` is the only source here, and a new test
pins that shape: 72% used with six days to a monthly reset reads
"Lasts until reset", while the same sample against an inferred 7-day
window overflows and would have printed "Runs out in 9h 20m". The same
test asserts a window with no duration metadata stays silent rather
than falling back to its label.

Verified with a standalone swiftc harness over QuotaPace,
QuotaPacePresentation and QuotaSummary: this host has Command Line
Tools only, so `swift test` cannot load the Testing module.

Closes #1267
2026-09-12 20:32:14 +03:00
ozymandiashh
75f537e974 Merge origin/main into cache-read and quota pace
Main took render revision 6 for `sessionCountBasis` and changed the
provider-slice session fold from a sum to a per-day max. Both had to
survive the merge, so the resolutions are unions rather than sides:

- `addProviderSlice` keeps main's `Math.max` for `sessions` (per-day
  ticks are not distinct identities) and adds the cache-read sum beside
  it, with a comment saying why one is additive and the other is not.
- `ProviderSliceTotal`, `ProviderCost`, `providerDetails` and the Swift
  `ProviderDetail` carry both `sessionCountBasis` and `cacheReadTokens`.
- The snapshot revision stays 7, which is still the next free number
  above main's 6. Its doc comment said v6 was PR1265's per-model counts;
  it is main's `sessionCountBasis`, so the comment now says that.

The status-menubar suite hard-codes render revisions. Main's assertion
that a rejected v5 snapshot is rewritten as `:render-6:` auto-merged
cleanly while this branch moved the current revision to 7, which would
have failed silently on the next bump too. Those assertions now derive
the tag from `STATUS_SNAPSHOT_RENDER_VERSION`, so the current and
previous revisions follow the constant; the deliberately historical
`:render-5:` and `:render-2:` plants stay literal.
2026-09-12 20:27:33 +03:00
ozymandiashh
5b5dc5aceb feat(menubar): switch the Capacity Dock gauge between usage windows
The resting rail shows one number per provider, and that number was always
the weekly (else monthly) billing window. A user watching Claude's 5-hour
limit had to expand the dock and read the glance card every time, which
defeats the point of an always-visible usage monitor.

Clicking the provider that already rests in the rail now switches its gauge
to the provider's short rolling window and back. The preference is stored per
provider rather than as a Claude-only switch: the dock's collapsed row is
provider-agnostic, and Codex publishes a 5-hour and a weekly window too, so
the question is "which horizon for this provider", stored under
CodeBurnCapacityDockGlanceWindows alongside the other dock preferences.

The two horizons are named rather than labelled, so the policy works for any
adapter: billing reuses QuotaSummary.headlineWindow, the app's single
definition of the weekly/monthly glance, and burst matches the short rolling
rows the adapters emit (5-hour, Hourly, Daily, session) while excluding
anything weekly or monthly, so Claude's per-model "Weekly · Opus" row can
never be read as a short window. CapacityDockGlanceWindow keeps that policy
pure and testable, including the fallbacks: a provider with one window shows
it whichever horizon is stored, and a stored horizon the provider stops
reporting resolves to the one it does report instead of blanking the gauge.

The click cannot fight the hover expansion: this branch only ever pins, never
collapses, and it is reached only when the provider actually has two windows,
so a single-window provider keeps the existing click-to-pin toggle. Escape
and an outside click still unpin. Keyboard and VoiceOver users get the same
switch as a named accessibility action on the provider cell, and the cell's
accessibility value now names the window feeding the ring. The rail's
geometry is untouched.

Closes #1243
2026-09-12 06:38:34 -07:00
iamtoruk
0225088984 fix(quota): reject a Copilot host carrying a URL delimiter
The `.ghe.com` suffix check ran on a normalized string that still allowed
`?`, `#`, `\` and spaces, and that string was interpolated straight into
the request URL. `evil.com?.ghe.com` in a hosts.json key passes the
suffix check but builds a URL whose host is `api.evil.com`, so the
Authorization header lands on a host that was never approved. Verified
on macOS: URL(string:) parses it as host `api.evil.com`, path `/`.

Both readers now require the normalized host to look like a DNS name
before an endpoint is derived; anything else fails as an unsupported
host, with no request sent.
2026-09-12 06:14:26 -07:00