Commit graph

44 commits

Author SHA1 Message Date
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
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
61209bd8b5 fix(menubar): provider-correct plan tab copy; surface model-scoped weekly limits
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.
2026-07-02 19:37:51 +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
ozymandiashh
805d2ffa49
feat(menubar): add Local/Combined usage toggle (#566) (#568)
Some checks are pending
CI / semgrep (push) Waiting to run
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.
2026-06-28 22:01:01 +02:00
Resham Joshi
454d836f6e
fix(menubar): surface CLI stdout/stderr on decode failure (#515) (#547)
* 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.
2026-06-22 01:33:41 +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
694e8ce79b
fix(menubar): read Claude keychain via security CLI on silent refresh (#490) (#491)
Some checks failed
CI / semgrep (push) Has been cancelled
Background token refreshes re-read the "Claude Code-credentials" keychain
item via the Security framework. On macOS Sierra+, access is governed by the
item's partition list, not the legacy "Always Allow" ACL. Claude Code resets
that partition list every time it rotates the credential, dropping our app
from the allowed set, so the next read raises a fresh keychain password
prompt. On a heavy usage day this fires dozens of times. The LAContext
interactionNotAllowed flag we relied on does not suppress that prompt for a
plain generic-password item.

Route the silent path (proactive refresh and post-401 re-read) through
/usr/bin/security instead. The Apple-signed security binary sits in the
item's apple-tool: partition, so it reads the secret without prompting and
without depending on the user's ACL grant. It is read-only and never spends
the shared refresh token, preserving the existing invariant that the Claude
CLI owns the grant.

The user-initiated bootstrap keeps the framework read, where a single
consent prompt is expected. Drops the now-unused LocalAuthentication import.
2026-06-14 10:31:56 +02:00
Resham Joshi
f5d1a8513d
fix(menubar): await terminationHandler instead of blocking a queue thread; cap CLI spawns (#462)
The #426 fix moved waitUntilExit and its timeout onto the same global(qos:.utility)
queue. Under sustained load every utility worker blocked in waitUntilExit, so the
timeout could never be scheduled to kill them and the menubar wedged on Loading
forever (confirmed via sample after ~a week of soak). Await process.terminationHandler
(fires on a Foundation queue, blocks no worker) so the timeout always has a free
thread. Add an actor-based async semaphore capping concurrent CLI spawns at 6.
2026-06-09 21:27:32 +02:00
AgentSeal
5a41f39944 Merge PR #423 (local-model savings) onto main, re-homing payload savings into usage-aggregator
Resolves conflicts from the post-PR refactor that moved buildPeriodData,
hydrateCache, and the menubar payload builder out of main.ts into
usage-aggregator.ts. The PR's savings additions to those functions are
re-homed there; config.ts keeps both new fields; parser.ts keeps both imports;
redact.ts session details carry savingsUSD.
2026-06-06 22:14:13 +02:00
Resham Joshi
3cdd13881f
feat(menubar): configure CLAUDE_CONFIG_DIRS via Settings (#434) (#436)
Adds a "Config Directories" section under the Claude settings tab so users
can aggregate usage across multiple Claude config directories (work /
personal accounts) from the GUI. The menubar is an accessory app that
doesn't inherit the user's shell environment, so CLAUDE_CONFIG_DIRS was
previously unreachable from the app.

Rather than injecting the value as an env var into every spawned subprocess
(which would force arbitrary user paths through the shell/AppleScript
allowlist that deliberately excludes shell metacharacters), the list is
persisted to the shared ~/.config/codeburn/config.json. The CLI reads it
regardless of how it's launched, so both the menubar's data refresh and
terminal-launched `report`/`optimize` honor it consistently.

CLI: getClaudeConfigDirs() now reads a claudeConfigDirs array from config
as a fallback below the CLAUDE_CONFIG_DIRS / CLAUDE_CONFIG_DIR env vars
(env still wins for per-shell overrides), above the ~/.claude default.

Menubar: CLIClaudeConfig mirrors CLICurrencyConfig's flock-guarded write;
the Settings UI offers an add/remove list with a folder picker and forces
a refresh on edit.
2026-06-06 02:30:46 +02:00
iamtoruk
4ffec37861 fix(menubar): run CLI exit-wait and timeout off the cooperative pool
The menubar wedged on "Loading Today…" for hours after an idle period.
Root cause: DataClient.runCLI called the blocking process.waitUntilExit()
from an async function on Swift's cooperative thread pool. On a 16-core
machine, 16 concurrent slow `codeburn` subprocesses pinned all 16
cooperative threads inside waitUntilExit; the 45s timeout — itself a Task
on that same pool — could then never be scheduled to kill them, so the
deadlock was permanent. Confirmed via sample: 16/16 cooperative threads
parked in waitUntilExit. PR #412 (AppStore inFlightKeys bookkeeping) was a
layer above the OS-thread deadlock and could not fix it.

Move both blocking points off the cooperative pool: bridge waitUntilExit
through a global (overcommit) queue via a continuation, and drive the
timeout from a DispatchSource on a global queue so it fires even when the
pool is saturated. Extract runProcess for testability; add a concurrency +
timeout smoke test and an output/exit-code test.
2026-06-01 02:09:05 -07:00
Justin Gheorghe
8ded6ad6fd feat(cli): track local-model cost savings against a paid baseline (#421)
Add a new `localModelSavings` config and `codeburn model-savings` CLI
that maps a local-model name (e.g. llama3.1:8b) to a paid baseline
(e.g. gpt-4o). The local call still costs $0; the new `savingsUSD`
field tracks the counterfactual spend avoided by running locally and
is reported separately from `costUSD` everywhere a number is shown.

* Parser normalization (`applyLocalModelSavings`) runs on Claude
  parse, direct provider calls, and the cached-call path. It forces
  `costUSD` to 0 and attaches `savingsUSD` + `savingsBaselineModel`
  + `isLocalSavings` on the `ParsedApiCall`. Local-savings wins for
  actual cost even when the same model is also in `modelAliases`.
* Session, project, day, model, category, activity, skill, and
  subagent rollups all carry `savingsUSD` alongside `costUSD`.
* `status --format json` adds `today.savings` and `month.savings`.
* `status --format menubar-json` adds a `current.localModelSavings`
  block (totalUSD, calls, byModel, byProvider) plus savings on
  topModels, topProjects, topSessions, topActivities, and history
  daily entries. Schema fields default-decode for backward compat.
* `report --format json` adds savings across overview/daily/
  projects/models/activities/skills/subagents/topSessions, with
  the active paid baseline name on each model row.
* `models` command gains a `Saved` column on table/markdown/CSV
  and a `savingsUSD`/`savingsBaselineModel` pair in JSON. Default
  `--min-cost 0.01` filter now ORs in `savingsUSD >= minCost` so
  local models with $0 actual cost but >0 savings still surface.
* CSV/JSON exports add a `Saved (CODE)` column on summary/daily/
  models/projects/sessions.
* Dashboard TUI shows a green 'saved $X by local models' footer
  line in the overview when any savings are present.
* macOS Swift payload gains a `LocalModelSavings` Codable block
  and savings fields on every model/activity/session/daily
  struct. Hero shows a green leaf 'Saved $X' caption, models
  section gets a green `Saved` column. `swift build` clean.
* GNOME indicator adds 'saved $X' to the hero meta line and a
  `codeburn-model-saved` column to the model row.
* Daily cache schema bumped to v8 (`savingsUSD` on day/model/
  category/provider). `savingsConfigHash` invalidates the cache
  when the user changes their baseline mapping so historical
  saved-spend numbers never lie about a stale baseline.
* Defensive `Object.hasOwn` lookup in `getLocalSavingsBaseline`
  blocks the prototype-pollution test that previously surfaced via
  the savings path with a hostile `__proto__` model name.
* New tests (5 files, 25 tests, 549 lines) cover pricing helpers,
  end-to-end parser normalization, day aggregator savings,
  menubar payload savings, CLI set/list/remove, and
  daily-cache hash invalidation. Existing tests for daily-cache
  / day-aggregator / models-report updated for the new fields.
  Full vitest suite: 1028/1028 passing across 73 test files.
  `tsc --noEmit` clean. `npm run build` clean.
  (Note: `mac/Tests` has a pre-existing `no such module 'Testing'`
  environment error on the installed Swift toolchain, confirmed
  on `main` before this PR; not caused by these changes.)
2026-06-01 11:06:39 +03:00
iamtoruk
ca41021a51 fix(menubar): treat the CLI credential store as the source of truth
The menubar kept its own copy of each provider's OAuth grant and refreshed
it on a timer, racing the CLI. Both Claude and Codex use single-use refresh
tokens that rotate on every refresh, so the menubar's self-rotation could
invalidate the user's own CLI login and surfaced as "disconnected" after a
long idle period.

Codex: read ~/.codex/auth.json fresh each cycle; only self-refresh when
last_refresh is older than 8 days; on 401 re-read the source before spending
our token; write rotated tokens back to auth.json (atomic, preserving other
keys); recover from reuse/invalid_grant by re-reading instead of going
terminal.

Claude: never HTTP-refresh the CLI-owned token. On expiry/401 re-read the
keychain with a no-UI query (LAContext.interactionNotAllowed) to adopt a
token the CLI already rotated; when none is available yet, report a transient
sourceTokenStale rather than a terminal disconnect.
2026-05-31 05:01:19 -07:00
iamtoruk
515a7a1467 feat(menubar): drop launchd fetcher; GUI writes its own badge backstop
A launchd-spawned process gets separate TCC attribution from the LaunchServices
app, so the out-of-process refresher prompted for "access data from other apps"
on every run regardless of the GUI's grant. Remove it entirely: the in-app loop
(timer survives sleep, popover-open recovery) is the source of truth and now
writes menubar-status.json on each successful refresh. On upgrade, any leftover
com.codeburn.refresh LaunchAgent is unloaded and deleted.
2026-05-30 13:35:07 -07:00
iamtoruk
b158af09e7 fix(menubar): read period from standard defaults in headless refresh 2026-05-30 13:06:54 -07:00
iamtoruk
c350dd2a55 feat(menubar): refresh via headless app binary instead of node script
Run CodeBurn's own signed binary in --refresh-once mode under the LaunchAgent
so the spawned CLI inherits CodeBurn's TCC grant. The bare-node shell script
prompted "node would like to access data from other apps" because launchd-
spawned children lose the signed app's TCC attribution. Drops the generated
shell script, scriptEnvironment, and per-period regeneration; the plist is now
static and period is read from UserDefaults at refresh time.
2026-05-30 13:05:48 -07:00
iamtoruk
d9a2e829ae docs(menubar): note deliberate unquoted argv interpolation in refresh script 2026-05-30 12:40:28 -07:00
iamtoruk
1341b626a2 feat(menubar): generate self-contained menubar-refresh.sh
The script runs the CLI for the badge period and atomically writes
menubar-status.json, so a static LaunchAgent can refresh the badge
out of process.
2026-05-30 00:40:03 -07:00
iamtoruk
bdbd7b19f6 feat(menubar): add MenubarStatusCache read side for badge backstop 2026-05-30 00:01:07 -07:00
iamtoruk
11b3e1be58 fix(menubar): re-check CLI version on every update cycle 2026-05-27 06:35:00 -07:00
iamtoruk
af99516633 feat(menubar): show CLI update banner when a newer version is available 2026-05-27 06:27:49 -07:00
Resham Joshi
5800179da9
Add multi-day calendar selection with custom popover UI (#393)
* Add multi-day calendar selection with custom popover UI

Replace native DatePicker with a custom-themed CalendarPopover supporting
non-contiguous multi-day selection. Add --days CLI flag for comma-separated
date filtering that post-filters from the bounding range scan.

CLI: parseDaysFlag() parses and validates dates, filterProjectsByDays()
filters turns to only selected days. Verified that sum of individual --day
calls matches --days output across cost, calls, activities, models, and
providers.

Swift: DataClient passes --days for multi-day, with single-day fallback.
AppStore tracks selectedDays as Set<String> with PayloadCacheKey support.
CalendarPopover uses pending state committed only on Done tap.

* Clear + Done in calendar resets to current period

When user clears all selected days and taps Done, switch back to the
active period instead of silently keeping the old day selection.
2026-05-24 09:49:59 -07:00
森曜
da641eeb52
Pause menubar refresh after repeated stalls (#388) 2026-05-24 01:04:52 -07:00
Resham Joshi
33b1abbbef
Add tooling breakdowns to CLI and menubar (#373)
Some checks are pending
CI / semgrep (push) Waiting to run
* Fix Antigravity provider detection, Codex fork double-counting, and tab ordering

Antigravity:
- Handle ephemeral port (--https_server_port 0) via lsof fallback
- Force reparse when cached turns are 0 (server may have been unavailable)
- Persist precomputed costUSD in session cache for correct pricing
- Map gemini-pro-agent to gemini-3.1-pro pricing
- Add display names for gemini-pro-agent and gemini-3.5-flash-low

Codex:
- Detect forked sessions via forked_from_id in session_meta
- Skip replayed parent events within 5s of fork creation time
- Use parent session ID in dedup key so parent+fork don't double-count

Menubar:
- Sort provider tabs by cost descending instead of enum declaration order

* Add tooling breakdowns to CLI dashboard and menubar

Surface skills, subagents, tools, and MCP server usage in both the
text dashboard and menubar app. Parse subagent types from Agent/Task
tool calls, aggregate with skills into a merged "Skills & Agents"
panel. Remove Top Sessions panel from CLI dashboard.
2026-05-21 14:38:14 -07:00
Resham Joshi
0f55a446da
Fix per-provider data loss, history regression, and decode fragility (#362)
* Fix per-provider data loss, division-by-zero, and decode fragility

- Per-provider multi-day queries only merged cost/calls from cache,
  dropping categories/models/sessions/tokens. Remove broken cache
  shortcut and always do full parse for per-provider periods.
- Remove per-provider daily history double-counting from overlapping
  cache + live data.
- Guard maxCost against zero in ActivitySection and ModelsSection to
  prevent NaN in bar width calculations.
- Use offset-based ForEach ID in BarTooltipCard to avoid duplicate
  model name collisions.
- Make cacheHitPercent, topActivities, topModels, providers use
  decodeIfPresent for backward compat with older CLI versions.
- Skip currency switch when FX rate fetch fails with no cache,
  preventing rate/symbol desync.
- Use readSessionFile in Gemini parser for 128MB size cap.
- Truncate Codex userMessage to 500 chars like other providers.

* Restore cache-backed trend history for provider-filtered views

The previous commit removed the broken per-provider cache shortcut but
also dropped cache-backed daily history, causing provider-filtered views
to lose trend data outside the selected period range.

Use allCacheDays for historical days (cost/calls per provider is accurate
in cache) and today's entry from the full parse. No overlap since cache
ends at yesterday.
2026-05-20 04:16:48 -07:00
Resham Joshi
7cea9efb31
Add Optimize tab, token display modes, daily budget alerts, and project drill-down (#349)
* Add CodeBurn Pro Mac App Store app

SwiftUI MenuBarExtra with litellm-snapshot pricing, Claude/Codex/Copilot
parsers, session discovery, auto-refresh timer, and dashboard UI matching
the real menubar design.

* Add appstore/ to .gitignore

Private Mac App Store build, not for the public repo.

* Add Optimize tab, token display modes, daily budget alerts, and project drill-down

- New Optimize insight tab with Retry Tax and Routing Waste computations
  (pure math from session data, no LLM required)
- Retry tax: shows money wasted on failed edit retries, per-model breakdown
- Routing waste: counterfactual savings vs cheapest reliable model, per-model
- Token display modes: Cost ($), Tokens (up/down split), Total Tokens
- Daily budget alert: configurable threshold, flame turns yellow when exceeded
- Project drill-down: click project rows to see per-session cost, tokens, models
- Period-aware top sessions: 30-day view now shows 30-day costliest sessions
- Friendly project names: show directory name instead of full path, Home for ~
- Session cache TTL bumped to 180s to prevent re-parsing on non-today periods
- Audit fixes: array mutation, percentage rounding, ForEach ID collision,
  baseline minimum threshold, editTurns scoping, first-of-month edge case

* Fix model badge ForEach ID collision and budget warning provider filter

- SessionDetailsList model badges used id: \.name which silently drops
  duplicate model entries. Switched to enumerated offset-based ID.
- Hero budget warning compared against provider-filtered payload instead
  of todayPayload (all providers). Now matches the menubar flame tint.
2026-05-18 14:51:15 -07:00
Resham Joshi
58ccf84f02
Fix menubar per-provider performance (24s → 2s) and session cache safety (#344)
Some checks are pending
CI / semgrep (push) Waiting to run
Per-provider menubar calls now use loadDailyCache() instead of hydrateCache(),
splitting history into cache-based and fallback paths. Fixes session cache wipe
when scanProjectDirs/parseProviderSources receive empty dirs. Strips _dirty flag
before session cache serialization to prevent unnecessary 132MB rewrites. Removes
dead keychain code from both credential stores.
2026-05-17 13:54:38 -07:00
iamtoruk
b0131f698c Store credential cache in file instead of keychain, use cache for per-provider menubar
Credential cache: switched from keychain to file-based storage under
Application Support. Ad-hoc signed builds invalidate keychain ACLs on
every rebuild, causing repeated macOS password prompts. Existing
keychain entries are migrated to file on first read, then deleted.

Per-provider menubar: the Codex/Claude/etc tabs previously re-parsed
all sessions from scratch (22s). Now parses only today with the
provider filter and uses the daily cache for historical days, matching
the fast path the All tab already uses.

Daily cache bumped to v7 to force a clean rebuild after pricing and
provider changes since v6.
2026-05-17 07:18:58 -07:00
iamtoruk
909efcf989 Harden menubar refresh and installer 2026-05-14 18:32:15 -07:00
iamtoruk
f058f36dbd Normalize menubar version display 2026-05-11 11:21:39 -07:00
iamtoruk
1149ab6e43 Fix menubar wake recovery and release asset selection 2026-05-11 10:57:02 -07:00
iamtoruk
66316aba38 Fix menubar stuck loading with non-blocking pipe I/O and watchdog
Replace blocking availableData drain with non-blocking POSIX read
that respects Task cancellation. Handle EINTR from child SIGCHLD,
close pipe fds after drain to prevent deadlock on oversized output,
and escalate SIGTERM to SIGKILL after 0.5s grace period.

Add 60-second loading watchdog as safety net that auto-clears stuck
state on each refresh loop tick.

Fixes #282
2026-05-09 14:27:48 -07:00
Resham Joshi
8208cf8ff5
Quiet routine pricing warnings + menubar recovery from stuck-loading (#266)
* Quiet routine pricing warnings + menubar recovery from stuck-loading

CLI:

- Default `codeburn` invocation no longer prints "no pricing data for model"
  warnings on every run. Greeting a fresh user with three lines of stderr
  before the dashboard even draws looked like the tool was broken on first
  launch. The warning now requires --verbose, and the suppressed pricing
  miss still results in $0 cost (correct for unmapped models).
- Local-model heuristic skips the warning entirely for Ollama tags
  (`qwen3.6:35b-a3b-bf16`), GGUF/quantized fingerprints, and similar names
  that will never have public pricing. The "update codeburn" hint was
  actively misleading there.
- When the warning does fire (with --verbose), it points users at
  `codeburn model-alias <model> <known-model>` as the actual escape hatch
  alongside the package update suggestion.

Menubar:

- Replace perpetual "Loading…" spinner with a FetchErrorOverlay when the
  per-key fetch fails and the cache is empty. User sees the error and a
  Retry button instead of an infinite hang.
- Add diagnostic breadcrumbs (NSLog, invisible to normal users — Console.app
  / `log stream --process CodeBurnMenubar` only) for the four states that
  produce a stuck loading overlay:
    - subprocess timeout after 45s
    - fetch result dropped due to Task cancellation (rapid tab switch)
    - fetch result dropped due to mid-fetch calendar rollover
    - retry attempt where the last successful fetch is >2 min stale
- Track lastSuccessByKey separately from cache freshness so the staleness
  diagnostic survives day-rollover cache wipes.

* Stop flashing the compare-view loading screen on background refresh

When the 30s CLI tick updated `projects` while the user was reading the
model comparison results, the projects-watching effect always fired
setLoadTrigger, which flipped phase to 'loading' and re-ran the slow
scanSelfCorrections walk over every provider's session directory. The
user lost their scroll position and saw a loading flash mid-read.

Recompute the comparison rows in place when:
- the user is already on the results phase, AND
- both picked models still exist in the new aggregate.

Skip the corrections rescan on these in-place refreshes — corrections
drift slowly enough that holding the previous value until the user
re-enters compare is acceptable, and the rescan is the slow part of the
load. Initial selection and post-selection load still run the full
pipeline.
2026-05-08 20:33:48 -07:00
Resham Joshi
daa673449c
Menubar and CLI hardening from multi-agent audit (#257)
Some checks are pending
CI / semgrep (push) Waiting to run
Two passes of validators across CLI accuracy, dashboard UX, menubar Swift,
performance, security, and end-to-end smoke tests on real session data.

Data-correctness fixes:

- parseLocalDate rejects month/day overflow. JS Date silently rolled
  Feb 31 to Mar 3, so --from 2026-02-31 --to 2026-03-15 quietly dropped
  sessions on Feb 28 - Mar 2. Now throws "Invalid date" with a clear
  reason. Leap-day case covered (2024-02-29 valid, 2025-02-29 rejected).

- CSV/JSON exports use the active currency's natural decimal places. The
  previous round2 helper produced ¥412.37 in CSV while the dashboard
  rendered ¥412 — finance teams comparing the two surfaces saw a
  discrepancy. New roundForActiveCurrency consults Intl.NumberFormat for
  the right precision (0 for JPY/KRW/CLP, 2 for USD/EUR, etc).

- Copilot toolRequests is Array.isArray-guarded in both modern and legacy
  event branches. Previously a corrupt session with toolRequests=null or
  a string aborted the whole file's parse loop and silently dropped every
  legitimate call after it.

- Codex token_count dedup uses a null sentinel for prevCumulativeTotal so
  the first event is never confused with a duplicate. Sessions that emit
  only last_token_usage (no total_token_usage) report cumulativeTotal=0
  on every event; with the previous 0-initialized prev, the first event
  matched the dedup guard and was dropped.

- LiteLLM pricing values are clamped to [0, 1] per token via safePerTokenRate.
  Defense in depth against a tampered upstream JSON shipping negative or
  absurdly large per-token costs that would otherwise propagate into all
  cost totals.

Performance:

- Cursor SQLite parse no longer pegs at minutes on multi-GB DBs. Two
  changes: per-conversation user-message buffer uses an index pointer
  instead of Array.shift() (which was O(n) per call); and a real ROWID
  cutoff via subquery limits the scan to the most recent 250k bubbles
  with a stderr warning so power users get a partial report rather than
  a stalled CLI.

- Spawned codeburn CLI subprocesses are terminated when the calling Task
  is cancelled. Without this, rapid period/provider tab clicks in the
  menubar cancelled the Task but left the subprocess running to
  completion, piling up zombie processes.

UX:

- Dashboard period switch flips to loading and clears projects
  synchronously before reloadData runs, eliminating the frame where the
  new period label rendered over the old period's projects.

- Optimize findings tab paginates 3-at-a-time with j/k scroll. With 4
  new detectors plus 7 originals, 8-10 findings * 6 lines was scrolling
  the StatusBar off the alt buffer top.

- Custom --from/--to ranges hide the period tab strip and disable the
  1-5 / arrow keys so a stray period press no longer abandons the user's
  explicit range. A "Custom range: X to Y" banner replaces the tab strip.

- OpenCode storage-format warning is per-table-set, rate-limited to once
  per process, and points the user at OpenCode's migration step or the
  issue tracker. The previous all-or-nothing check fired the generic
  "format not recognized" string for any schema mismatch.

Menubar / OAuth:

- Both Claude and Codex bootstrap (Reconnect button) now honour the
  usageBlockedUntil 429 backoff that refreshIfBootstrapped respects.
  Spamming Reconnect during sustained rate-limit windows previously
  hammered the upstream endpoint on every click.

- Codex Retry-After HTTP header is parsed (delta-seconds plus IMF-fixdate
  fallback) so we don't over-back-off when ChatGPT tells us a shorter
  window than our 5-minute floor.

- Both credential cache files are written via SafeFile.write
  (O_CREAT | O_EXCL | O_NOFOLLOW with explicit 0600) so there is no race
  window where the temp file briefly exists at default umask, and a
  symlink at the destination cannot redirect the write. Reads now route
  through SafeFile.read with a 64 KiB cap, closing the symlink-follow gap
  on Data(contentsOf:).

CI signal:

- TypeScript strict typecheck (tsc --noEmit) is now zero errors. The
  six errors in src/providers/copilot.ts came from a discriminated-union
  catch-all branch whose `data: Record<string, unknown>` shape TS picked
  over the specific event branches when narrowing on `type`. Removed the
  catch-all; runtime falls through unknown event types via the existing
  if/else chain.

Tests added: 16 new (now 555 total)
- date-range-filter: month/day/year overflow rejection, leap-day correctness
- currency-rounding: convertCost no-rounding contract, roundForActiveCurrency
  for USD/JPY/KRW/EUR
- providers/copilot: malformed toolRequests does not abort the parse
- providers/cursor-bubble-dedup: re-parse after token mutation does not
  double-count, single parse yields one call per bubble
- providers/codex: first event with cumulativeTotal=0 not dropped,
  consecutive zero-cumulative duplicates still deduped
2026-05-06 22:15:11 -07:00
Resham Joshi
efac2bfa15
Live quota bar inside AgentTab + Claude OAuth refresh gate (#255)
Some checks are pending
CI / semgrep (push) Waiting to run
* Gate Claude OAuth refresh attempts on terminal failures

Anthropic returns invalid_grant (HTTP 400) when the user's refresh token has
been revoked or rotated, typically after they re-ran claude login on another
device. The previous code rethrew the raw error every refresh cycle, leaving
the Plan UI stuck on a Swift error string and pummeling Anthropic's token
endpoint forever.

The new SubscriptionRefreshGate captures a fingerprint of
~/.claude/.credentials.json on terminal failure and stops trying until that
fingerprint changes (the user re-logs-in). Transient 5xx/network failures
get exponential backoff capped at 6 hours.

Two new SubscriptionError cases let the UI distinguish "user must reconnect"
from "Anthropic is flaky right now" and show a clean reconnect CTA instead
of raw HTTP guts.

* Inline live-quota progress bar inside each AgentTab chip

When a provider exposes a live quota source, the AgentTab chip grows by ~3pt
to host a thin weekly-utilization bar directly under the label. Hovering the
chip reveals a popover with all four Anthropic windows (5-hour, weekly, weekly
Opus, weekly Sonnet) plus reset countdowns. Click still switches the tab as
before.

Today only Claude has a quota source (the existing /api/oauth/usage path);
other providers' chips render unchanged. The QuotaSummary abstraction lets
us bolt on Cursor/Copilot/Codex meters in follow-up commits.

Subscription is now refreshed eagerly on the periodic loop so the bar lights
up without forcing the user to open a deep view first. The previous
SubscriptionRefreshGate keeps a dead refresh token from spamming Anthropic.

Adds two new SubscriptionLoadState cases (terminalFailure, transientFailure)
so the deep Plan view shows a "reconnect" message instead of a raw Swift
error string when the user's claude login expired.

* Replace SubscriptionClient with credential-store + service architecture

The previous SubscriptionClient never persisted refreshed access tokens, so
every 30s tick read the expired token from Keychain, refreshed it (1 call),
fetched usage with the new token (2nd call), and threw the new token away —
3 API calls per cycle, which burned through Anthropic's per-account rate
budget and produced the 429s and `invalid_grant` loops users were seeing.

The replacement mirrors CodexBar's proven pattern:

- ClaudeCredentialStore owns the credential lifecycle. Bootstrap is strictly
  user-initiated (Connect button in the Plan tab); the menubar does not touch
  Claude's keychain at startup. After bootstrap, refreshed tokens — including
  rotated refresh tokens — are persisted to a local cache file under
  ~/Library/Application Support/CodeBurn (mode 0600). Using a file instead of
  our own keychain item means rebuild signature changes don't trigger a
  startup keychain prompt; the only prompt the user ever sees is the one for
  Claude Code-credentials on Connect.

- ClaudeUsageFetcher (folded into the service) is a pure /api/oauth/usage
  call with one allowed 401-recovery roundtrip. 429s record an explicit
  backoff window honouring Retry-After.

- ClaudeSubscriptionService orchestrates bootstrap / refresh / disconnect,
  applies the 429 backoff, and surfaces terminal vs transient failures so
  the UI can show the right CTA.

- Reading Claude's keychain now tries the entry keyed by NSUserName() first
  and falls back to the unscoped query, so users who re-ran /login and ended
  up with two Claude Code-credentials items pick up the fresh one. This was
  the actual cause of "I logged in but the menubar still shows stale data".

User-facing additions:

- A proper Settings window (right-click → Settings…) with General / Claude /
  About tabs. Provider quota cadence is configurable (Manual / 1m / 2m / 5m /
  15m). New providers plug in as additional tabs.

- Plan tab: notBootstrapped → "Connect Claude subscription" CTA;
  terminalFailure → "Reconnect Claude" with the correct /login instruction
  for Claude Code 2.1; transientFailure preserves the last loaded view with
  a retrying badge.

- AgentTab quota bar slot is always reserved so chip height doesn't jitter
  when the user connects for the first time. Hover popover has 250ms enter
  / 150ms exit debounce so swiping across chips doesn't pop a popover for
  every chip touched.

- Disconnect requires confirmation, clears capacityEstimates and the
  subscription snapshot store so a reconnect under a different account
  doesn't surface "Based on last cycle" projections from the old account.

Validator findings applied: cadence anchor only updates on successful
refresh (not every attempt), refresh-token rotation persists in memory
before keychain write so a write failure doesn't lock the user out, server
error bodies are sanitized (token redaction + 240-char cap) before they
reach the UI or NSLog, and Refresh Now refreshes both the menubar payload
and quota.

* Add Codex live quota + multi-provider warning, with validator fixes

CodexCredentialStore reads ~/.codex/auth.json (ChatGPT-mode only) on
user-initiated Connect, caches under Application Support like Claude.
CodexSubscriptionService hits chatgpt.com/backend-api/wham/usage with
the bearer token + ChatGPT-Account-Id header, parses primary/secondary
windows, additional per-model rate limits (e.g. GPT-5.3-Codex-Spark),
and credits balance with a Double-or-String fallback.

Plan-tier enum captures the full ChatGPT plan list including prolite,
free_workspace, education, quorum, k12, plus an unknown(String) case
that preserves the raw plan name when OpenAI ships a tier we haven't
mapped yet.

Multi-provider warning system:
- Menubar flame tints from neutral to yellow (70%) → orange (90%) →
  red (100%) based on the worst-affected connected provider's worst
  window. Uses NSImage.SymbolConfiguration palette colors.
- Popover header gains a warning row when any provider is at 70%+.
  "Claude 79% of quota used", "Claude 79% · Codex 92%", or
  "Claude over limit (105%)" when severity hits .danger.
- Hover popover gains a plan-name badge in the top-right corner so
  users know which subscription is feeding the bar.
- Codex chip surfaces the credits balance and any non-zero per-model
  additional rate limits as footer rows.

Validator fixes applied in the same commit:

- Provider-specific reconnect / disconnected copy in QuotaDetailPopover
  (was hardcoded to Claude).
- Generation-token guard on refreshSubscriptionReportingSuccess and
  refreshCodexReportingSuccess so a Disconnect during an in-flight
  fetch can't resume after the await and re-populate the cleared state.
- Codex codexQuotaSummary promotes secondary to primary when only one
  window is returned, so free / guest tiers don't render an empty bar.
- Memory-cache TTL is now actually consulted in currentRecord (the
  isFresh check was dead code, leaving cached records valid forever).
- sanitizeForUI now redacts OpenAI sk-* keys, JWT tokens, and Bearer
  headers in addition to Claude sk-ant-*.
- Removed diagnostic NSLog that wrote raw chatgpt.com response bodies
  to the unified log.
- Codex Connect / Reconnect copy in Settings explains the auth.json
  prerequisite and the API-key vs ChatGPT-mode distinction.
- Disconnect dialogs now state explicitly that the auth.json /
  credentials keychain entry is left untouched.
- Plan badge in the popover gets line-limit + truncation + max-width
  so a long unknown plan name can't overflow the row.
- Renamed shadowing `let max` to `let worst` in aggregateQuotaStatus.

* Add Codex Plan tab + size plan badge to content

The Plan tab is now visible when the Codex chip is selected, mirroring
the Claude tab's deep view. CodexPlanInsight renders the user's plan
tier ("Pro Lite", "Plus", etc.), the primary and secondary rate-limit
windows with reset countdowns, and any non-zero per-model additional
limits (e.g. GPT-5.3-Codex-Spark) so power users see them.

The "On pace at reset" projection that Claude's Plan view shows is not
included here — that math feeds from local Claude per-message spend
extrapolated against API quota windows, and our local Codex spend is
not a 1:1 signal for the ChatGPT-subscription rate windows reported by
wham/usage. Wiring a Codex extrapolator is a follow-up.

Drop the maxWidth=90 frame on the plan badge in the hover popover. It
was stretching short labels like "Pro Lite" to fill the full 90pt slot;
fixedSize makes the badge hug the text. Plan names are bounded short
strings, so truncation is a non-issue in practice.
2026-05-06 19:57:17 -07:00
iamtoruk
869474b3b4 Fix update button stuck spinning after successful install
Some checks are pending
CI / semgrep (push) Waiting to run
terminationHandler only reset isUpdating on non-zero exit, assuming
the app would be killed and relaunched on success. If pkill fails
silently the old process survives with isUpdating stuck true. Now
always resets on termination and clears the update badge on success.
2026-05-05 11:38:54 -07:00
iamtoruk
6702d55345 Fix menubar provider view showing $0.00 after idle and refresh race condition
CLI timeout increased from 20s to 45s to handle cold file-cache latency on
provider-specific queries. Loading overlay now appears when the all-provider
payload confirms a provider has spend but its dedicated data hasn't loaded yet.
Manual refresh (force: true) bypasses the in-flight guard so users can always
re-fetch. Tab strip prefers the provider-specific payload cost when available
so it stays in sync with the hero section.
2026-05-03 12:00:03 -07:00
AgentSeal
d3c4de0375 Reduce CLI timeout from 60s to 20s for faster recovery
Some checks are pending
CI / semgrep (push) Waiting to run
2026-04-24 05:53:52 +02:00
iamtoruk
3d063c9100 fix(menubar): keychain account filter + App Nap hardening + single query
Remove hardcoded "default" account allowlist from keychain credential
lookup. Claude Code 2.1.x writes the macOS login username, not
"default", so the filter silently dropped valid credentials on every
install.

Collapse the two-phase keychain enumeration into a single
SecItemCopyMatching call (one keychain prompt instead of four on
debug builds).

Harden App Nap opt-out: disable automaticTerminationSupport and
suddenTermination at the process level so AppKit cannot override
the beginActivity token.

Closes #115
2026-04-22 05:27:07 -07:00
iamtoruk
12f0833bef fix(menubar): use numeric version comparison for update check
Compare versions with .orderedDescending instead of != to prevent
showing update button when installed version is newer than cached.
2026-04-19 16:33:52 -07:00
iamtoruk
dba33428ca fix(mac): update badge always visible due to version prefix mismatch
GitHub asset name includes a v prefix (v0.8.0) while
CFBundleShortVersionString does not (0.8.0). Strip the prefix
before comparing. Also capture stderr on update failure so the
button doesn't hang on "Updating..." forever.
2026-04-19 13:00:20 -07:00
iamtoruk
bc92b49c1b feat(mac): auto-update checker and Plan pane button cleanup
Remove the broken "Connect Claude" / "Reconnect Claude" buttons from
the Plan pane -- they opened a terminal session that did nothing useful
for already-logged-in users. Keep only the "Retry" button.

Add an auto-update checker that queries GitHub releases every 2 days in
the background. When a newer menubar build is available, an "Update"
pill appears in the header. Clicking it runs the existing installer
flow (download, replace, relaunch) with no manual steps.
2026-04-19 03:33:37 -07:00
Resham Joshi
495a254338 feat(mac): native Swift menubar app + one-command install
Introduces mac/ with a native SwiftUI menubar app that replaces the
previous SwiftBar plugin entirely. Install via `npx codeburn menubar`,
which downloads the .app from GitHub Releases, strips Gatekeeper
quarantine, and drops it into ~/Applications.

Highlights

- mac/ SwiftUI app: agent tabs, Today/7/30/Month/All period switcher,
  Trend/Forecast/Pulse/Stats/Plan insights, activity + model
  breakdowns, optimize findings, CSV/JSON export, Star-on-GitHub
  banner, live 60s refresh, instant currency switching with offline FX
  cache.
- Security: CodeburnCLI argv-based spawn (no shell interpretation),
  SafeFile symlink guards + O_NOFOLLOW writes, FX rate clamping to
  [0.0001, 1_000_000], keychain filtered to account == "default",
  removed byte-window credential log, in-flight refresh guard, POSIX
  flock on config.json writes, TerminalLauncher validates argv before
  AppleScript interpolation.
- Performance: shared static NumberFormatter (thousands of allocations
  per popover redraw eliminated), concurrent pipe drain with 20 MB cap
  + 60s timeout in DataClient, Observation-tracked reactive UI, 5-min
  payload cache keyed on (period, provider).
- CLI: new `codeburn menubar` subcommand that downloads + installs +
  launches the .app (no clone, no build). New `status --format
  menubar-json` payload builder. `export` rewritten to produce a
  folder of one-table-per-file CSVs with a `.codeburn-export` marker
  so arbitrary -o paths cannot be silently deleted.
- Removed: src/menubar.ts (SwiftBar plugin generator),
  install-menubar / uninstall-menubar subcommands, `status --format
  menubar` directive output, tests/menubar.test.ts,
  tests/security/menubar-injection.test.ts.
- Release: .github/workflows/release-menubar.yml builds universal
  binary, assembles .app, ad-hoc signs, zips, uploads on mac-v* tag
  push. Runs on the free macos-latest runner.

Tests

- 230 TypeScript tests pass
- 10 Swift CapacityEstimator tests pass
- TypeScript typecheck clean
- Swift release build clean
2026-04-17 16:55:56 -07:00