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 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.
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
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
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.
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.
* 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.
The daily budget could only be one of a few fixed presets. Add a "Custom…"
option to the budget picker that reveals a text field for an exact amount:
dollars for the cost metric, millions of tokens for the token metrics. The
typed value is stored in the same dailyBudget / dailyTokenBudget fields, so
the flame tint and banner pick it up unchanged. On open, a previously saved
non-preset value reselects "Custom…" and pre-fills the field.
The daily budget was always measured in dollars, so users tracking the
Tokens or Total Tokens metric had no token-based budget alert. Add a
separate token-denominated daily budget and route the flame tint and the
hero "budget exceeded" banner through one metric-aware check.
- New dailyTokenBudget (stored separately from the cost budget so switching
metric never reinterprets a threshold).
- Centralize the logic on AppStore: isTokenMetric, activeDailyBudget,
todayMetricTotal, isOverDailyBudget, dailyBudgetLabel.
- Settings shows a token-threshold picker (1M..100M) for token metrics and
the existing dollar picker for the cost metric, with metric-aware help text.
- Flame tint and the banner now use isOverDailyBudget / dailyBudgetLabel.
Without a model-savings mapping the Saved column was an unlabeled column of
dashes between Cost and Calls (menubar) and a dim '-' column (CLI models
report). Show it only when at least one model has savingsUSD > 0, so users
with no mapping keep the plain Cost/Calls layout. The menubar Hero caption was
already conditional.
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.
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.
The currency picker hardcoded 8 currencies while the SupportedCurrency
enum defines 18. New currencies added to the enum (like CNY in #430)
required a separate SettingsView edit to appear in the picker.
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.)
PR #393 accidentally deleted the RefreshBackoff integration from PR #388,
leaving RefreshBackoff.swift as dead code and tests calling nonexistent
methods. Delete the dead code and fix the broken test target.
Add exponential backoff to the loading watchdog (8s, 16s, 32s... up to
60s, max 6 attempts) so it stops hammering the CLI when it's unavailable.
After exhausting retries, show an error overlay with a Retry button.
Fix recoverFromStuckLoading to skip recovery when a fetch is already
in-flight (avoids killing healthy fetches via generation bump).
Fix selectedDay to return nil for multi-day selections, and pass days
through startInteractiveSelectionRefresh so the cache key matches
currentKey.
Commit 5800179 (multi-day calendar) accidentally removed stuck-loading
recovery. Re-add an 8-second watchdog loop in MenuBarContent that calls
recoverFromStuckLoading(). Change inFlightKeys from Set to timestamped
Dict so the watchdog can detect orphaned quiet-refresh entries. Stop
nuking payloadRefreshGeneration in the watchdog (it was killing healthy
in-flight fetches). Bump Package.swift platform to macOS 15 to match
NSAttributedString(attachment:) usage.
Adds "Icon Only" option to the Metric picker in Settings. When
selected, the menubar shows only the flame icon without any cost
or token text. Flame tinting for quota/budget alerts still works.
* 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.
The menubar status item can now track Today, Week, Month, or 6 Months
independently from the popover's active period. Setting is persisted
in UserDefaults and changeable from Settings or via `defaults write`.
Thanks @ozymandiashh. Closes#291.
* 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.
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
* 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.
* 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.
* Fix one-shot rate detection for Gemini, Vibe, Kiro, and Goose
Gemini and Mistral Vibe now emit per-assistant-message calls grouped
by user turn via turnId, so the classifier sees Edit->Bash->Edit as
retries instead of independent one-shot turns. Kiro and Goose record
per-message tool ordering via toolSequence for the same effect on
aggregated sessions.
Vibe now prefers meta.json.stats.session_cost over price-derived
estimates. Session cache bumped to v2. Insight pill switcher scrolls
horizontally instead of wrapping.
Closes#351.
* Remove dead geminiOrdinal variable and add toolSequence cache validation
* Restore geminiOrdinal for idx fallback dedup key
* 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.
Adds a `dormant` state to SubscriptionLoadState so the menubar never
prompts for keychain permission on launch. Users must navigate to the
plan tab and click "Load Quota" / "Connect" to trigger credential access.
Also fixes a flaky TZ-boundary test (cli-status-menubar) by widening the
time offset to avoid generating timestamps in the future at UTC hour 0.
Two related bugs in the macOS menubar `AgentTabStrip`:
1. With more detected providers than fit at the default 360pt popover
width (~7+), the off-screen provider chips were unreachable. SwiftUI's
horizontal `ScrollView` does not scroll from click-drag, and there
was no other affordance to reveal the hidden tabs.
2. Independent mouse wheels could not scroll the horizontal strip.
Standard wheels emit only vertical `deltaY` with
`hasPreciseScrollingDeltas == false`, and a horizontal SwiftUI
`ScrollView` ignores vertical-only deltas. Trackpads (which emit
horizontal deltas natively) already worked.
Fix:
- Wrap the strip in `ScrollViewReader` and add overflow-aware
left/right chevron buttons that programmatically scroll to the
next/previous visible provider via `proxy.scrollTo(_, anchor: .center)`.
Buttons only appear when `stripContentWidth > stripViewportWidth - 30`
and disable at the ends.
- Install an `NSEvent.addLocalMonitorForEvents(matching: .scrollWheel)`
in `.onAppear` (removed in `.onDisappear`). When the cursor is over
the strip and the event is non-precise (`!hasPreciseScrollingDeltas`)
with `deltaX≈0` and `deltaY≠0`, copy the `CGEvent` and transpose
`scrollWheelEventDeltaAxis1` / `PointDeltaAxis1` / `FixedPtDeltaAxis1`
onto axis 2 so the underlying NSScrollView receives a real horizontal
delta.
- Track strip hover via a `@MainActor` singleton
`AgentTabStripScrollState` so the local-monitor closure can read the
latest hover status without capturing stale SwiftUI state.
Trackpad events are passed through untouched, so vertical scrolling
elsewhere in the popover is unaffected.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add IBM Bob provider
* Add workspace extraction for Cline-family providers
Extract project name from workspace directory in api_conversation_history.json
so sessions show actual folder names instead of the provider display name.
Thread projectPath through ParsedProviderCall to avoid unsanitizePath mangling
hyphenated folder names.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
Co-authored-by: iamtoruk <hello@agentseal.org>