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.
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
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>
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 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.
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 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.
A quiet refresh torn down across sleep/wake (or a generation reset) can
leave an orphaned inFlightKeys entry for the current key. The stuck-loading
recovery guard bailed whenever any in-flight entry existed, so the popover's
retry loop no-oped forever and the spinner ("Loading Today...") never
cleared — observed on a long-lived instance that crossed the midnight day
rollover.
Clear stale loading/in-flight state via the existing watchdog before the
in-flight guard, so an orphaned entry can no longer trap recovery. A healthy
in-flight fetch (younger than the watchdog) is still respected.
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.
* 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.
When the user clicks Load Quota from dormant state after a clean
reinstall, the cached keychain item may not exist. Using the refresh
path triggered a macOS keychain prompt because it assumed the cache
was present. Now calls bootstrapSubscription/bootstrapCodex which
properly re-reads credentials from source.
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.
* 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>