Full Report and Optimize always opened Terminal.app. Add a closed
PreferredTerminal enum (Terminal.app, iTerm2), a General settings picker,
and graceful fallback: chosen terminal -> Terminal.app -> headless spawn.
Defaults to Terminal.app so existing users see no change.
The terminal is selected from a closed enum, never a user string, so the
`tell application "..."` target stays a compile-time literal. Commands are
still whitespace-joined argv validated token-by-token by CodeburnCLI.isSafe
before any interpolation, preserving the shell-injection invariant.
Only terminals with a real "run in a live window" scripting verb are listed:
Terminal.app has `do script`, iTerm2 has `write text` on a session. Ghostty,
WezTerm, Warp, Alacritty and kitty expose no equivalent, so they keep the
existing headless fallback rather than shipping a window that closes on exit.
The iTerm2 script targets `application "iTerm"`, not `"iTerm2"`. AppleScript
resolves the name of a not-yet-running app through LaunchServices by bundle
file name, and the bundle is iTerm.app. Measured on iTerm2 3.6.11: with the
app quit, `tell application "iTerm2"` fails to compile (-2741) while
`tell application "iTerm"` compiles, cold-launches iTerm2 and runs the
command. The `"iTerm2"` spelling only works while the app already happens to
be running.
Fallback is a chain that checks results rather than a single fire-and-forget
pick, because "installed" does not imply "scriptable": osascript can still
fail on a missing Automation approval or a broken bundle. Each candidate is
run, waited on and its exit status checked, off the main thread so the
popover stays responsive; only once every candidate has failed do we spawn
headless. Every step logs via NSLog, so a user who sees no window has a trail
in Console.app instead of an app that looks dead. The decision logic is
extracted into terminalChain/runFirstWorking so tests exercise
"primary failed -> fell back" without launching anything.
Document the setting in the README next to the other menubar defaults keys.
Closes#877
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>
The desktop dollar-balance footer used toFixed, which drops thousands
separators, while the new menubar footer groups via a currency
NumberFormatter, so the two clients showed $12500.00 vs $12,500.00 for
the same balance. The menubar credit footer also rounded half-even while
the desktop uses Math.round (half-up), disagreeing on exact-half values.
Render the desktop dollar path through the en_US currency locale, and pin
the menubar footer formatter to half-up. Adds a grouping assertion (TS)
and an exact-half rounding test (Swift).
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.
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.
Kimi Code tokens live ~15 min and only the CLI renews them, so the load
state sits in .terminalFailure as its dominant steady state between CLI
uses. The Plan tab and tab-strip chip flapped to a reconnect screen every
cycle even with a good last snapshot on hand.
Extract the display decision into a pure KimiQuotaPresentation helper:
terminal-with-data shows the usage bars (flagged idle) and only the
no-data case falls through to reconnect. loadedBody stamps an 'as of
<time>' caption once a snapshot is older than 10 min, in every state that
renders it. The chip's kimiQuotaSummary downgrades terminal-with-data to
.stale so the bar and popover keep the last-known rows instead of the
reconnect card. Settings' connect/disconnect pane still explains the
terminal reason.
Adds KimiQuotaPresentationTests.
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.
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
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
- Resolve k3/k3-agent/k2d6-agent model aliases to canonical Kimi names
- Discover sessions across all Kimi Code homes (CLI + desktop runtime)
- Accept conv-*/ctitle-* session directory naming, not just session_*
- Add Kimi Code provider tab with brand color to the menubar
- Show short model names (Kimi K3, Kimi K2.6) in the menubar payload
Compact one-row strip: correction rate with count, median time to first
edit, and the top reworked file, plus one coaching note derived locally
with the same thresholds and copy as the CLI's workflow-insights
buildCoachingNotes. Decodes two new optional payload blocks
(current.workflow and current.topReworkedFiles), so payloads from older
CLIs still parse. The section hides entirely when there is no signal and
individual stats never render as zero placeholders. It reads the store's
current payload, so it follows the selected agent tab automatically.
Adds unit tests for the duration formatter, note selection, model
derivation, and payload decoding.
Co-authored-by: reviewer <review@local>
The desktop app's Life tab failed with 'invalid period': the renderer
and CLI both learned lifetime, but the electron IPC allowlist between
them did not. The menubar's period row also overflowed once Lifetime
joined it; labels compact to the desktop strip's forms (7D, 30D, 6M,
Life). Period selection is not persisted by raw value, so the label
change is safe. Follow-up worth doing: derive the IPC allowlist from
the renderer's period module so a new period cannot miss one layer of
three again.
Co-authored-by: reviewer <review@local>
The Update badge previously replaced only the menubar (via the CLI's
menubar --force installer), and the CLI banner only copied the update
command to the clipboard. One click now runs the whole sequence: update
the CLI in place through the package manager it was installed with
(brew upgrade, or npm install -g codeburn@latest --force; the npm next
to the codeburn launcher wins so nvm/volta/asdf installs update inside
their own toolchain), re-read the installed version, then run the app
replacement from the freshly updated CLI. No recognizable package
manager surfaces the manual command instead of guessing at a mutation;
a homebrew CLI without a findable brew never falls through to npm,
which would create a second conflicting install. The badge now also
appears for CLI-only updates, and the banner's primary action is Update
now with the copyable command kept as secondary. Six resolution tests,
mutation-verified.
Co-authored-by: reviewer <review@local>
The Codex tab's Plan section reused two Claude-hardcoded views:
PlanLoadingView always showed "Reading Claude credentials..." and
PlanFailedView's Retry always called store.refreshSubscription(), so a
Codex user retried the wrong provider.
Extends the 61209bd parameterization to the two remaining shared views:
- PlanLoadingView(message:) - Claude sites keep the current copy, Codex
sites show "Reading Codex CLI credentials..."
- PlanFailedView(error:onRetry:) - drops the internal AppStore refresh
dependency; Claude sites pass refreshSubscription(), Codex sites pass
refreshCodex()
The failed-state accent stays Theme.brandAccent, matching every sibling
plan view and the section's single-palette convention.
Review follow-up on the Sonoma deployment-target fix:
- The release minos guard was fail-open: if vtool returned nothing (missing
binary, tool change) the check passed vacuously and could green-light a bad
release. Now it fails closed when no minos is reported.
- The libswift_errno check now excludes weak links, so it only fails on a
breaking STRONG link. dyld tolerates a missing weakly-linked dylib, so a
macOS 15.x SDK runner that weak-links errno at the .v14 target no longer
causes a spurious release failure.
- Removed em-dashes from the changed files per repo style.
Verified: fresh .v14 build is minos 14.0 with no strong errno link; the guard
now fails closed on an unreadable binary and passes on the real one; 71 Swift
tests pass.
- README.md / build-local.sh: the -10825 fix is the Package.swift deployment
target, not the SDK used to build (ld64's $ld$previous drops
libswift_errno.dylib based on minos, so it already applies to the
CI-distributed release too). build-local.sh exists only for building on a
Sonoma machine with just the Command Line Tools.
- build-local.sh: build arm64 and x86_64 separately and lipo them into a
universal binary (`--arch arm64 --arch x86_64` together needs xcbuild,
which CLT doesn't ship); assert the active SDK is actually 14.x instead of
silently trusting `xcrun`; use `pkill -x` instead of `-f` to avoid matching
unrelated processes; broaden the @MainActor patch regex to slurp mode so it
also covers multi-line struct headers and `extension X: View`.
- package-app.sh: fail the build if the packaged binary's minos isn't 14.0
for every arch slice, or if it links libswift_errno.dylib, so a future
deployment-target regression is caught in CI instead of a user's crash report.
The packaged app set Package.swift to .macOS(.v15), producing a binary with
minos 15.0 that LaunchServices refuses on macOS 14.x with
kLSIncompatibleSystemVersionErr (-10825) — even though Info.plist, the CLI
install guard (MIN_MACOS_MAJOR=14), and the README all advertise macOS 14+.
The .v15 bump was attributed to NSAttributedString(attachment:), which is
actually AppKit since macOS 10.0, so the floor must not exclude Sonoma.
Separately, a stock macOS-15-SDK CI build hard-links libswift_errno.dylib
(macOS 15 only), which dyld cannot resolve on Sonoma even once minos passes.
Building against the macOS 14 SDK avoids that dependency.
- Package.swift: .macOS(.v15) -> .macOS(.v14)
- Scripts/build-local.sh: build on a Sonoma machine (which only has the
macOS 14 SDK, lacking the SwiftUI @MainActor inference the macOS 15 SDK
adds to the View protocol) using a standalone swift.org Swift 6.x toolchain,
patching @MainActor onto views in a scratch copy so repo sources stay clean.
- README: document the Sonoma local-build path.
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.
The computed Binding wrote UserDefaults but nothing invalidated the
view, so the picker visually stayed on Auto while the value silently
landed. Back it with AppStorage so selection updates render.
Refs #647
It controls the app-wide payload refresh loop, not anything
Claude-specific; Quota Refresh stays in the Claude tab where the
Anthropic rate-limit note applies.
Refs #647
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
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
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
The CLI already supports any ISO 4217 code dynamically via Intl.NumberFormat,
but the macOS menubar app (SupportedCurrency enum) and GNOME extension
(hardcoded CURRENCIES array) had fixed lists. RON was missing from both.
- Add RON case to SupportedCurrency enum + displayName in AppStore.swift
- Add RON symbol override ('lei') in CurrencyState.swift
- Add RON entry to CURRENCIES array in gnome/indicator.js
The Codex plan tab reused the shared connect, no-credentials and reconnect
views whose copy was hardcoded for Claude, so the Codex tab showed Connect
Claude subscription. Worse, the no-credentials Try Again button always
re-ran the Claude bootstrap. The three views now take title, message and
action from the call site.
The oauth usage endpoint gained a limits array carrying model-scoped
weekly buckets (currently Fable). Parse weekly_scoped entries generically
by their display name and show them in the Claude hover popover and the
plan tab, with the usual pace projection. Parser now has test coverage
using the captured live response shape.
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.
Mirror the combined multi-device dashboard in the macOS menubar via a
Local/Combined scope toggle (Settings -> Display). Builds on the menubar-json
`--scope combined` contract.
- MenubarScope enum (.local default) persisted in UserDefaults; PayloadCacheKey
gains a scope dimension so local and combined payloads cache separately.
- DataClient.fetch(scope:): a pure statusSubcommand builder appends
`--scope combined` only for combined, forces `--provider all` (the CLI rejects
combined with a provider/project/exclude filter), and coerces multi-day
selections to local (the CLI rejects combined + --days).
- MenubarPayload decodes the optional `combined` block (per-device + totals);
nil when absent (back-compat).
- The badge always stays on the local payload; the combined network pull lives
on a separate cache key and falls back to local with a "Combined unavailable"
indicator, so a slow/offline peer never blocks the badge.
- Hero shows combined totals + a per-device breakdown in Combined scope; the
token metric stays input+output (cache excluded) to match local; the
per-device daily-budget warning is suppressed for a multi-device total;
selecting Combined resets the provider tab to All.
Tests run via `swift test` (CommandLineTools Testing.framework on the rpath, no
Xcode): scope persistence, argv (local/combined/provider-forcing/multi-day),
cache-key partition, badge-survives-combined-failure, combined decode, token
consistency, provider reset, budget suppression.
The agent tab strip never showed Grok, Hermes, or ZCode: ProviderFilter had no cases for them, so they could not become tabs. It also derived the visible tabs from today's providers rather than the range the user actually has selected, so an agent used in the selected window but not today never appeared.
Add the three missing cases (filter keys, CLI arg, accent color) and rebuild visibleFilters to source from the selected period via cost(for:). Every agent with usage in the range now appears, ordered by spend descending, and agents with no usage in the range are omitted.
* fix(menubar): surface CLI stdout/stderr on decode failure (#515)
A failed decode of the CLI menubar-json output threw an opaque
DataClientError.decode(error) that surfaced only "not valid JSON", hiding
whether stdout was empty or carried a non-JSON prefix (e.g. a stray Node
banner on stdout, the root cause in #515). Wrap it in a CLIDecodeFailure that
carries a bounded stdout snippet, the stdout byte count, and stderr, so
String(describing:) is self-diagnosing in logs and the UI.
* test(menubar): add missing codexCredits arg so the Swift test target compiles
Two tests built CurrentBlock without the codexCredits parameter added in #510,
so the Swift test target failed to compile (CI does not run the Swift tests, so
it went unnoticed). Add codexCredits: nil to both.
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.
* 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.
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.
Two follow-ups from the budget review:
- Currency consistency: the daily cost budget is defined in USD (the presets and
the custom field are labeled "$"), but the "exceeded" banner ran the value
through the display-currency rate, so a non-USD user saw the field and banner
disagree (e.g. field "$100", banner "EUR 92"). Render the budget label in USD
via a new asUSD() helper so the picker, field, and banner all agree. The
over-budget comparison was already USD vs USD and is unchanged.
- Empty custom cue: selecting "Custom..." and leaving the field blank stores 0,
which silently disables the alert while the picker still shows "Custom...".
The help text now says "Enter an amount above, or the alert stays off." in
that state so it does not look armed when it isn't.