Commit graph

109 commits

Author SHA1 Message Date
Resham Joshi
517058c117
feat(update): menubar pill checks and updates through the engine (#1742)
When the CLI is 0.9.27 or newer, the menu bar update pill checks and updates every surface through codeburn update, and polls every 6 hours. Older CLIs keep the previous path.
2026-10-11 04:35:30 +02: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
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
Resham Joshi
246cb37c83
desktop: light theme default, refreshed type and spacing, plugin and plan cards (#1485)
Fresh installs now default to the light theme; an explicit theme choice is still honored. Type and spacing move one step up (larger body, titles, sidebar rows and page padding), sidebar shortcut chips move to hover tooltips, and primary text gets full contrast in both themes.

The Plugins page shows the Menu bar and Teams plugins as two cards side by side with embossed background art. The Menu bar card carries the Capacity Dock switch and Settings, Quit and Uninstall as icon buttons with tooltips, an info modal describing the menu bar and the Capacity Dock with the real supported-provider list, and it can install, update, open, quit and uninstall the menu bar app, versioned so an older one shows Update, with a Settings action that opens the menu bar app's own settings. The Teams card shows what is planned in an info modal and a Register for beta testing link.

The Plans page is redesigned as provider cards in a responsive grid, each with its own logo embossed. Connection status is now stable: a single empty or slow poll no longer flips a connected provider to disconnected, a connected provider keeps its quota bars through a transient refresh, and a stuck waiting resolves to an actionable state after twenty seconds. A login-expired error offers Connect, and Gemini's expired login is now a terminal actionable state instead of an endless wait, matching the menu bar.

CLI report, models and menubar-json numbers are identical to the cent before and after; only the Gemini quota error string changes.
2026-09-18 15:09:01 -07:00
Resham Joshi
bf06d6c0a0
menubar: keep retrying parked status item on a persistent menu bar (#1461) 2026-09-16 18:46:13 +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
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
09ba2d1569
Merge pull request #1360 from getagentseal/feat/menubar-refresh-on-rollover
menubar: refresh quota the moment a window rolls over
2026-09-13 23:58:50 +03:00
iamtoruk
1be3bb865c Force a quota refresh when a window rolls over
The menubar showed a window's pre-reset percentage for up to five minutes
after its reset instant. The 30s tick now compares the reset dates it
already holds for Claude, Codex and the Capacity Dock providers against
the previous tick and forces a refresh for the group that rolled over.
2026-09-13 13:49:46 -07:00
iamtoruk
153df14ffa menubar: retire older instances at launch
A second copy of the app stacks a second Capacity Dock on the screen
edge. At launch the app now terminates instances of its own bundle
identifier that launched strictly before it, so two simultaneous
launches cannot quit each other, and forces out one that ignores the
polite terminate after a few seconds.
2026-09-13 13:49:03 -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
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
a0d08c81f0 fix(menubar): pick the second row's provider by its worst quota window
The "quota remaining" row selected each provider's headline window, which
is a billing horizon: weekly, else monthly, and only then the busiest
window. That is the right number for the Capacity Dock rail and the wrong
one for a row whose whole job is to name the provider nearest its limit.
On a real machine it discarded Cursor's API window at 100% and Claude's
5-hour window at 18% in favour of Monthly 9.5% and Weekly 5%, so the row
read as if nothing were near a limit — while the flame beside it was
tinting off the worst window across every provider. The comment claiming
the row followed the flame was wrong on both counts.

Each provider now contributes its worst window, per-model rows included,
which is exactly the rule aggregateQuotaStatus tints by, so the row and
the flame agree and README, CHANGELOG and the Settings help become true
as written. A provider in transientFailure keeps its last-known window
instead of dropping out, matching the dock, which shows the same data
dimmed during a backoff: the row no longer vanishes and resizes the
status item for the length of a retry.

Three smaller fixes from the same review: the row is capped at 24
characters — about the widest the single row gets at 13pt, since the
second line renders at 9pt — shortening a long provider name rather than
letting "GitHub Copilot 12% left · 6d 3h" more than double the item's
width; the two-line title gets a VoiceOver label that reads as one phrase
instead of a string with a newline in it; and the snapshot feeding the
row is no longer built on refreshes where the feature is off. With the
setting off the title is byte-for-byte the single-row composition it has
always been.

Closes #1310
2026-09-12 20:29:35 +03:00
ozymandiashh
a08dfec842 fix(menubar): make the two-row height a fit contract, not an exact figure
CI's swift test on macos-latest measured the two-row title at 21pt where this
machine measures 20pt, so the exact-equality assertion failed 16 times. The
figure is release-dependent: SF Symbol metrics and line rounding move between
macOS versions. Nothing positions the rows off it — AppKit's button cell
centres whatever it measures inside the status item — so 21pt is centred just
as correctly as 20pt, and the requirement was always "fits a 22pt menu bar",
not "measures exactly 20pt". The test now asserts that band, and
twoRowMeasuredHeight is replaced by twoRowMaximumHeight (the bar) and
twoRowMinimumHeight (a floor, so a lost paragraph clamp or a dropped second
line is still caught rather than passing as "fits").

The 1pt delta does expose a real risk the constant was hiding: the first line's
height follows the flame image, and the image size for a point size is the
system's business. A release that renders it taller could push the title past
the bar. The two-row attachment box is now computed by
twoRowAttachmentBounds(imageSize:), which scales the image down to the clamped
line height — never up — so the first line cannot outgrow its clamp. Verified
by composing the real title with the flame forced to 11, 13, 16 and 24pt tall:
every combination still lays out at 20pt here. The single-row path keeps the
raw image size and its -3pt offset, so it stays byte-identical.
2026-09-12 06:37:46 -07:00
ozymandiashh
016b8edaef feat(menubar): optional second row in the macOS menu bar item
The menu bar item could show one figure, so seeing subscription quota and
today's local usage at the same time meant opening the popover. This adds an
optional second line, off by default, configured from the existing
Settings > General > Display section: a "Second row" toggle plus a picker for
what it shows. All four metrics are figures the app already computes for the
popover, so nothing here adds a fetch or a refresh cadence: quota remaining
with its reset countdown for whichever connected provider is nearest its limit
(the same provider the flame already tints for), today's all-provider cost,
today's total tokens, or running sessions.

The rows are one attributed title. A paragraph style clamps text line height
but not an attachment, so the inline flame at the text's own point size dragged
the title to 24pt and AppKit clipped the second line away; the flame is
requested at 8pt and seated back inside the line, which puts every row
combination at exactly 20pt inside the standard 22pt menu bar, centred with 1pt
of slack above and below. Measured with NSAttributedString.boundingRect and
asserted in the tests, since those four numbers only work together.

With the setting off, or whenever the chosen metric has no data yet (no
connected provider with a quota window, no today payload, a CLI that predates
the liveSessions block), the title falls back to the previous single-row
composition unchanged: same font, same attributes, no paragraph style. The row
formatting is a pure type over the row settings and a plain-value payload
snapshot, so it is covered without an AppStore or a status item.

This is a deliberately reduced slice of the request. Not included: the layout
editor, presets, live preview, a third row, multiple items or aligned columns
per row, per-item provider/period/scope, optional labels, decimal precision,
reordering and text size. Keeping the formatter pure leaves room for those
later without touching the rendering path.

Closes #1252
2026-09-12 06:37:46 -07:00
iamtoruk
8b3cb68255 fix(mac): keep dock quota fresh when the payload tick is skipped
The background tick skips the whole refresh when local session files are
unchanged, but quota is server-side state, so the dock rings went stale
until the popover was opened. Run the quota half on skipped ticks, still
throttled by the subscription cadence.

Also refresh the dock providers before the Claude/Codex awaits so a slow
provider cannot delay them, anchor lastCodexRefreshAt on attempt like its
siblings, and query display sleep live instead of latching a flag that a
missed wake notification leaves stuck.
2026-09-12 03:47:46 -07:00
iamtoruk
5e8236f77f feat(mac): notify when an update is available 2026-09-10 12:39:38 -07:00
iamtoruk
fdd18e7bb4 fix(menubar): rebuild the popover content on every open and drop it on close
A popover content view that outlived its window kept re-laying itself out
at display cadence once the popover had been shown during the first load
(loading overlay 520pt tall, then the real height): 6 to 7 percent idle
CPU until relaunch, reproduced on demand. Limiting the hosting controller
to preferredContentSize was not enough on its own. Building the 360pt view
fresh per open and releasing it on close removes the survivor entirely;
the dock's hidden bubble panel gets the same treatment.

Reproduction on the same machine (minute logs, popover opened during the
first load and closed): before 6.1 to 7.1 percent sustained; after 0.1
percent sustained through the data load.
2026-09-01 02:49:01 -07:00
iamtoruk
c19d5621e3 fix(menubar): stop rendering the closed popover around the clock
The status-item popover's hosting view is created once and lives for the
app's lifetime, and the flame wordmark inside it ran a repeat-forever
shimmer, so SwiftUI re-rendered the closed popover at display cadence
continuously: 5 to 7 percent idle CPU and roughly 650 wakeups a second
(Activity Monitor showed a 12-hour energy figure near two thirds of
Chrome's). The wordmark sweep and the loading flame pulse now start and
stop with popover visibility via a store flag set by the popover
delegate.
2026-08-31 10:46:37 -07:00
Aditya Vikram Singh
84fea9d550
feat(menubar): add native Capacity Dock (#1167)
* feat(menubar): add native capacity dock

* fix(menubar): harden dock interaction boundaries

* feat(menubar): polish native Capacity Dock for launch

- Flush top-edge docking via NSHostingView safe-area opt-out; shielding
  window level so the rail necks into the physical top like the system notch
- Liquid Glass legibility scrim so labels stay readable on light wallpapers
- Weekly-else-monthly headline window; unified severity colour bands
  (green <50 / yellow / orange / red) across rings and detail bars
- Squircle gauge + graphite theme defaults; auto-show connected
  subscriptions (up to 5) until the user edits the set, then respect it
- Capacity Dock Settings menu item; re-centre the settings window; info
  glyph for the About row
- Trim dead provider-credential override fields down to apiKey only
- Reconcile and extend tests for all of the above

---------

Co-authored-by: iamtoruk <hello@agentseal.org>
2026-08-29 00:37:30 -07:00
Aditya Vikram Singh
daafb593ea
fix(menubar): recover parked status item (#1161) 2026-08-28 13:20:48 -07:00
iamtoruk
c6ba1d5189 polish(menubar): brand and window refinements from live review
Status item returns to the SF flame (the 0101 mark blurs at menubar
size); About hero shows the full-color binary flame and the About
sidebar row its bare white template, no chip; footer credits Resham
Joshi (iamtoruk) / AgentSeal, MIT; About tagline matches the website
(Your AI Bill, Itemized); UI copy drops em-dashes (currency rows use
middots); Settings window is resizable and fullscreen-capable and
recenters when restored off-screen.
2026-08-24 09:41:53 -07:00
iamtoruk
0213f7e9f8 feat(menubar): live Antigravity quota — local language-server probe, Settings pane, Plan tab
Ports the desktop app's Antigravity flow: ps+lsof discovery of the
local language server, Connect-RPC quota/status probes over loopback
HTTPS with the self-signed-cert trust override scoped strictly to
127.0.0.1, disconnected-state UX when the app is not running. Also
swaps all provider sidebar marks to the official LobeHub icon set.
2026-08-24 09:40:04 -07:00
iamtoruk
8908bce69e feat(menubar): CodexBar-faithful Settings shell, binary-flame status icon, resource-bundle packaging
Settings window now replicates CodexBar's System Settings layout
(880x620 fullSizeContentView window, transparent titlebar, fixed 260pt
sidebar, SF Symbol gradient chips, native search field, grouped About
pane with Updates/Links). Status item renders the 0101 flame template
with severity tints. package-app.sh ships the SwiftPM resource bundle,
fixing the Bundle.module launch crash. Sidebar layout modeled on
CodexBar (MIT, steipete/CodexBar).
2026-08-24 09:22:21 -07:00
iamtoruk
826342179e feat(menubar): live GitHub Copilot quota — service, Settings pane, Plan tab
Ports the desktop app's Copilot flow (editor-plugin OAuth token from
~/.config/github-copilot, copilot_internal/user snapshot) read-only,
with github.com-host preference and UI-string sanitization.
2026-08-24 09:10:09 -07:00
iamtoruk
05fb99d9af feat(menubar): live Gemini quota — service, Settings pane, popover Plan tab
Ports the desktop app's Gemini Code Assist quota flow (loadCodeAssist
tier + retrieveUserQuota buckets, ~/.gemini/oauth_creds.json read-only,
in-memory-only refresh) to the menubar. Plan tab and empty-state gating
now treat Gemini as plan-capable.
2026-08-24 08:43:47 -07:00
iamtoruk
272da806e4 fix(menubar): no-output watchdog, lock signal cleanup, dead-pid takeover, serve orphan reaping
The 0.9.20 menubar never completed a fetch on a large corpus: the cache
version bump forced a full rehydration, DataClient's fixed 45s kill ended
it mid-transaction, cache-refresh-lock had no signal cleanup so the dead
holder's lock survived, and DEFAULT_WAIT_MS (30s) < DEFAULT_STALE_MS (90s)
meant no waiter could ever recover it.

A. Swift port of #1096's watchdog. CLIWatchdog holds the constants and the
   pure verdict; spawns and the resident serve child set CODEBURN_PROGRESS=1;
   the window restarts on any stdout/stderr byte; 45s silence, 10min cold
   floor until the first payload, 15min ceiling, SIGTERM then SIGKILL after
   5s. ServeConnection's fixed 60s warm cap becomes the same silence window,
   re-armed by each progress frame, and a spent death budget is now a
   5-minute cooldown instead of disabling the resident for the app run.

B. cache-refresh-lock arms SIGINT/SIGTERM cleanup the way session-cache does
   for hydrating.lock, so a SIGTERMed holder unlinks its own lock.

C. Staleness also opens on a dead holder pid, and the waiter budget derives
   from staleMs so it can never expire before the gate it waits for. A live
   holder - fresh heartbeat, pid answers signal 0 - is never taken from.
   parser.ts heartbeats through the lock wait, the one silent stretch left.

D. The app closes its end of a retired child's stdin (dropping the handle
   left the pipe alive inside the Process), reaps every serve child
   synchronously at quit, and records pid+argv so a crash-orphaned child is
   reaped next launch. serve's final exit no longer runs through a
   monkeypatched process.exit, and its post-drain cleanup is bounded.

Closes #1117
2026-08-23 05:35:17 -07:00
iamtoruk
185d6b3b31 mac: register the login item with SMAppService
The menubar told System Events to make its login item, so macOS asked for
Automation access on first launch. SMAppService.mainApp does it in-process
with no Automation grant. No AppleScript fallback: a failure must not bring
the prompt back. Package floor is macOS 14, so the 13+ API needs no
availability guard.

Fixes #1026
2026-08-18 07:48:03 -07:00
ozymandiashh
d8d343e83a perf(desktop): share cache state and eliminate duplicate cold hydration 2026-08-12 17:16:41 +03:00
iamtoruk
df05d7792e perf: web dashboard period prefetch + menubar serve client
Two more surfaces adopt the resident-serve pattern the desktop app got:

- Web dashboard: every period tab is prefetched sequentially right after
  startup, so the first click on 7d/30d/Month answers from the payload
  cache instead of paying a full parse; stale-while-revalidate rebuilds
  behind a served payload past 75% of the TTL so expiry never lands its
  multi-second parse on a user's click. Lifetime prefetches last.
- Menubar: ServeConnection (Swift actor) holds one codeburn serve --stdio
  child; status payload fetches route through it once warm, with the same
  contract as the app client — cold start and every failure keep the
  spawn path, three child deaths disable serve for the run, requests
  time out by killing the child, app termination shuts it down, and a
  pre-serve CLI (0.9.19) simply dies into permanent spawn fallback, so
  mixed-version installs degrade gracefully.

swift build clean, swift test 156/156, CLI tsc clean; verified live with
both the Electron app's and the menubar's serve children resident and
answering.
2026-08-10 11:17:59 -07:00
iamtoruk
1ba09dc423 docs(menubar): correct the activation-policy comment the #868 probe disproved 2026-08-10 06:21:37 -07:00
ozymandiashh
0c8cb9e8eb experiment(menubar): activate before setupStatusItem, no policy transition
Test build for #868. Keeps the activation policy pinned at .accessory
(LSUIElement makes the :92-96 call a no-op in packaged builds anyway) and
restores only the NSApp.activate(ignoringOtherApps: true) half of the
#147 fix, immediately before the status item is created.
2026-08-10 06:20:49 -07:00
AgentSeal
6530546f73 fix(mac): use regular weight for the menubar title text
Render the macOS menu-bar title with `.regular` instead of `.medium`, matching
the visual weight of SwiftBar / MeetingBar. The flame symbol keeps its `.medium`
config. Applies the one-line change from #851.

Co-authored-by: Tim De Pauw <timdp@users.noreply.github.com>
2026-08-03 22:52:22 +02:00
Marc Reynolds
f19d9d8d4b
feat(menubar): mark badge when a paired device is unreachable in combined scope
Under combined scope the badge falls back to the local figure whenever a
paired device doesn't report (asleep / off-network), which read as a glitch.
Append a dimmed "reachable/total" marker (e.g. "$95.94 /mo · 1/2") and a
matching tooltip so the reduced total is legibly "peer unreachable" instead.
The marker clears the instant every paired device reports again.

Adds AppStore.menubarBadgeDeviceShortfall plus tests.
2026-07-31 15:13:38 -06:00
Marc Reynolds
61581edb18
feat(dashboard): add combined-device scope to desktop app and menubar badge
Fixes #795: the desktop Dashboard showed only local-device cost while the
menubar and web GUI aggregated across paired devices.

Desktop app:
- Add a Local/Combined Scope setting (Settings > General), mirroring the
  macOS menubar. Combined passes `--scope combined` to the CLI, forces the
  provider filter to all, and persists to localStorage.
- Overview hero shows the paired-device aggregate (cost/calls/sessions) with
  a "Combined · N devices" caption and per-device breakdown when Combined is
  selected; detailed panels remain local (the combined payload carries totals
  only).

Menubar:
- The badge figure now reflects Combined scope instead of always showing the
  local total: refreshMenubarBadge fetches the combined payload for the badge
  period and the badge renders the cross-device aggregate, falling back to
  local when no combined payload is available.
2026-07-31 15:12:29 -06:00
Resham Joshi
2e2082061e
Merge pull request #803 from hyknerf/fix/802-menubar-right-click-menu
fix(mac): keep right-click status-item menu open and stable
2026-07-26 06:58:51 -07:00
Frenky Harry S. Sinaga
8e8b58626a
test(mac): lock status-item context menu policy (#802)
Extract StatusItemContextMenuPolicy (event mask, debounce, presentation
mode) and unit-test it so rightMouseUp + statusItemMenu cannot regress
to the flash/scroll-jump paths without a failing test.
2026-07-24 19:27:25 +07:00
Frenky Harry S. Sinaga
f7eafecb7f
fix(mac): keep right-click status-item menu open and stable
Present the context menu on rightMouseUp (not down) so the matching
mouse-up no longer dismisses it, and open via statusItem.menu +
performClick so AppKit tracks the menu under the status item instead of
manual popUp (which scrolled the Today row away on mouse move).

Clears statusItem.menu in menuDidClose so left-click still opens the
popover. Debounce + legacy rightMouseUp path retained for macOS <= 26.

Closes #802
2026-07-24 19:22:24 +07:00
reviewer
576f47d854 menubar: add Kimi Code subscription quota tracking
Read ~/.kimi-code credentials directly and poll api.kimi.com
/coding/v1/usages for the weekly quota and rate-limit windows,
mirroring the existing Codex/Claude quota services.

- New KimiSubscriptionService with lenient decoding (string/number
  values, enum-style time units, derived used from remaining) and
  429 backoff
- Quota chip on the Kimi Code tab, Plan insight, and a Kimi tab in
  Settings with connect/disconnect
- Independent refresh cadence anchor so Kimi-only setups don't poll
  on every payload tick
- Expired tokens surface as terminal with automatic recovery once
  the CLI refreshes the credential file
- Keep insights visible for quota-capable providers on empty days
- Widen Settings so six tabs don't collapse into the overflow menu
2026-07-23 21:31:55 +02:00
Aditya Vikram Singh
71a8e8e250
menubar: skip unchanged background refreshes, 120s AC idle cadence, utility QoS for tick spawns (#703) (#704)
Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
2026-07-16 15:11:25 -07:00
Aditya Vikram Singh
4689b5afa9
menubar: honor Retry-After on Claude quota 429s, back off failed refreshes (#702)
* menubar: honor Retry-After on Claude quota 429s, back off failed refreshes (#701)

* Fix operator precedence in claudeDue fallback

---------

Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
2026-07-16 14:52:47 -07:00
AgentSeal
6b6b6b8df6 feat(menubar): Claude config selector, hardened (supersedes #635)
Scopes the menubar to one Claude config for multi-config (CLAUDE_CONFIG_DIRS)
setups, with All as the default. Rebased onto main and fixed the review
findings from the original #635:

- Fix a TS2206 build break (a 'type' modifier inside an import type block).
- Reject --claude-config-source with a non-Claude --provider, and scan Claude
  only in the scoped branch (a config is Claude-only): fixes provider data
  leaking into a scoped query and avoids parsing every provider's corpus.
- Scope the macOS menu-bar figure to the selected config (badge matched the
  popover), clear the selection when switching to a non-Claude provider tab,
  and stop the on-disk badge fallback from showing an unscoped number while
  scoped.
- Tag Claude Desktop / Cowork sessions as their own 'claude-desktop' source so
  they are a selectable bucket instead of silently vanishing from per-config
  views (sum of options now equals All).
- Skip the redundant Claude discovery walk for plain single-config users while
  keeping idle configs and Claude Desktop selectable.

Reviewed by Codex 5.6; all findings addressed. Full suite: 1581 TS tests, 76
Swift tests, tsc clean.
2026-07-10 01:44:51 +02:00
AgentSeal
fd3ec8707c fix(menubar): manual cadence never respawns from stuck-refresh cleanup
Stale-force or stale-loading recovery could set shouldForceRefresh even
with a nil interval, so Manual mode (and its wake path) still spawned
after a stuck fetch. Cleanup now clears state without respawning unless
the cadence allows auto-spawns; the next user interaction fetches.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
b4c04e7beb feat(menubar): user-selectable usage refresh cadence (Auto / 1m / 5m / 15m / Manual)
Settings gains a Usage Refresh picker mirroring the quota cadence
pattern. Auto keeps the adaptive default (30s active, battery and Low
Power backoff); fixed cadences ignore power state but an open popover
always gets the active 30s; Manual never auto-spawns, refreshing only
on popover open, Refresh Now, and first launch (wake included: manual
wakes run stuck-state cleanup without spawning). Stored in UserDefaults
as CodeBurnMenubarRefreshSeconds (-1 auto, 0 manual, else seconds) and
documented in the README with the defaults-write escape hatch.

Refs #647
2026-07-09 22:48:07 +02:00
AgentSeal
6a08a8f31c fix(menubar): cut idle energy by gating spawns on popover, power, and display state
Every 30s tick spawned 2-3 concurrent Node CLI processes at 100%+ CPU,
and an app-lifetime activity assertion disabled App Nap forever, putting
the menubar's energy use in the video-call class (#647).

- Closed popover now fetches only what the status item renders (menubar
  period plus today); the popover's selected key refreshes on open via
  the existing recovery path, which now also catches up the status
  figure and clears a latched display-sleep flag.
- Spawn cadence stretches to 150s on battery and 300s in Low Power Mode
  while the popover is closed; the 30s timer itself keeps running.
- The app-lifetime beginActivity is gone; each in-flight refresh
  (including manual Refresh Now) holds its own scoped activity, and an
  NSBackgroundActivityScheduler backstop tick every ~3min bounds status
  staleness once App Nap starts coalescing the timer.
- Display sleep skips ticks entirely; wake and popover open clear it.
- The status-payload fallback honors the same spawn interval so the 30s
  cache TTL cannot defeat the backoff.

Child process QoS stays .userInitiated on purpose: lowering it is
documented to slow parses 5-10x and starve the cadence; the win here is
spawn frequency, not priority. Measured on an idle machine: two
concurrent 100%+ CPU spawns per 30s window before, one spawn in 72s
after, with App Nap coalescing the rest.

Fixes #647
2026-07-09 22:47:30 +02:00
AgentSeal
256511618d fix(menubar): refuse menubar updates from CLIs older than 0.9.9
CLIs before 0.9.9 resolve /releases/latest for menubar --force, which can
point at a CLI-only release with no menubar asset, so the update they run
installs nothing. The update dialog and performUpdate now detect an
installed CLI older than the installer fix (909efcf, first shipped in
0.9.9) and direct the user to upgrade the CLI first, with the exact
command, instead of suggesting a command that cannot work. Unknown CLI
versions are not flagged.
2026-07-02 05:30:19 +02:00
Resham Joshi
844fdde4cd
fix(menubar): drop the ' tok' suffix from the Total Tokens metric (#511)
Per #497 feedback: with Total Tokens selected the bar showed e.g. '0 tok'.
The user picked the metric, so the unit is redundant; show just the number
(the flame already disambiguates it from cost). Compact mode already had no
suffix; this only changes the wide rendering.
2026-06-18 17:59:03 +02:00
Resham Joshi
a3da2ded2a
feat(codex): compute Codex credit usage (#408, #495) (#510)
* feat(codex): compute Codex credit usage (#408, #495)

Codex/ChatGPT subscription users consume credits, a unit separate from API
dollars: usage is billed as credits-per-million-tokens at per-model rates that
differ from the API USD pricing CodeBurn uses for cost. So the reported dollar
cost does not match what credits actually consume.

Add a credit engine sourced from the official Codex credit rates
(developers.openai.com/codex/pricing): GPT-5.5 125/12.5/750, GPT-5.4
62.5/6.25/375, GPT-5.4 mini 18.75/1.875/113 credits per 1M input/cached/output
tokens. Surface per-model credit usage in `codeburn models` JSON output
(credits field; null for non-Codex or unknown models). models-report already
folds reasoning into output and keeps non-cached input + cached-read separately,
which is exactly what the credit rates expect, so the figure is exact.

Engine + computation are unit-tested. UI display surfaces (the models table,
the TUI dashboard, the menubar "credits" view) are intentionally left for a
follow-up so the display choice can be decided.

* feat(menubar): opt-in Codex credits display metric (#408, #495)

Surface Codex credit usage in the menubar as a selectable metric, without
changing the default. Cost ($) stays the default in both the menubar and the
CLI; credits only appear when explicitly chosen.

- TS: buildMenubarPayloadForRange computes the period's Codex credits (via the
  tested aggregateModels, so reasoning/cached are handled) and exposes
  current.codexCredits in the menubar JSON.
- Swift: new DisplayMetric.credits, a "Credits (Codex)" option in the metric
  picker, decodes codexCredits, and renders it in the menu-bar title. Default
  metric remains .cost.
2026-06-18 17:03:46 +02:00
Resham Joshi
96d74a04b3
feat(menubar): polish the status-item menu and About tab (#509)
Right-click menu:
- Add a dimmed "Today · $X · N calls" usage row at the top.
- Give Settings an explicit gearshape icon (instead of relying on the macOS
  Tahoe auto-decoration).
- Add an "About CodeBurn" item that opens Settings on the About tab.
- Drop the per-item keyboard-shortcut labels and the section separators for a
  flat, compact list.
- Open the menu a few px below the status item so it clears the menu bar
  (anchoring flush clipped the top edge and triggered menu scrolling on hover).

Settings:
- Add a settingsTab selection to AppStore and bind SettingsView's TabView to
  it (tagged tabs) so "About CodeBurn" can jump straight to the About tab.
- About tab: add a "Star on GitHub" button and a "Sponsor" link.
2026-06-18 14:25:29 +02:00