Commit graph

65 commits

Author SHA1 Message Date
Aditya Vikram Singh
71a8e8e250
menubar: skip unchanged background refreshes, 120s AC idle cadence, utility QoS for tick spawns (#703) (#704)
Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
2026-07-16 15:11:25 -07:00
ozymandiashh
df78e50052
feat: add CodeWhale provider support (#674)
Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 03:12:28 -07:00
Resham Joshi
5093d232e1
Merge pull request #600 from getagentseal/feat/add-ron-currency
Add Romanian Leu (RON) currency to menubar and GNOME extension
2026-07-10 01:33:53 -07:00
AgentSeal
6b6b6b8df6 feat(menubar): Claude config selector, hardened (supersedes #635)
Scopes the menubar to one Claude config for multi-config (CLAUDE_CONFIG_DIRS)
setups, with All as the default. Rebased onto main and fixed the review
findings from the original #635:

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

Reviewed by Codex 5.6; all findings addressed. Full suite: 1581 TS tests, 76
Swift tests, tsc clean.
2026-07-10 01:44:51 +02:00
ZacharyHu0
a485087b5b Add LingTai TUI provider support 2026-07-09 23:47:59 +02:00
ozymandiashh
c913fc5cf6 Add Romanian Leu (RON) currency support to menubar and GNOME extension
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
2026-07-03 01:24:46 +03: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
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
f26f4ad1ba
fix(menubar): show every active agent as a tab, ordered by usage for the selected range (#549)
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.
2026-06-22 03:20:02 +02:00
Resham Joshi
a3da2ded2a
feat(codex): compute Codex credit usage (#408, #495) (#510)
* feat(codex): compute Codex credit usage (#408, #495)

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

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

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

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

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

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

Settings:
- Add a settingsTab selection to AppStore and bind SettingsView's TabView to
  it (tagged tabs) so "About CodeBurn" can jump straight to the About tab.
- About tab: add a "Star on GitHub" button and a "Sponsor" link.
2026-06-18 14:25:29 +02:00
Resham Joshi
dbe4e94322
fix(menubar): keep cost budget in USD and flag empty custom budget (#508)
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.
2026-06-18 13:30:58 +02:00
Resham Joshi
dab1297001
feat(menubar): make the daily budget alert respect the display metric (#497) (#505)
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.
2026-06-18 13:07:28 +02:00
Tiago Santos
cf35854b09
feat: add devin provider (#444)
* feat: add devin provider

* feat: add devin to gnome extension and improve dev scripts

* fix: fix local installer

* chore: cleanup unecessary changes and address pr comments
2026-06-09 21:58:31 +02:00
Porun
42ef1ff97e
Add contribution heatmap insight (#437)
* Add contribution heatmap insight

* fix(menubar): heatmap layout polish — labels, spacing, pill order

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-06-06 02:49:08 +02:00
Misaka
906c5338e4 Add CNY currency support 2026-06-03 10:45:39 +08:00
iamtoruk
372681cae2 fix(menubar): recover from stuck loading when in-flight entry is orphaned
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.
2026-05-28 14:06:13 -07:00
iamtoruk
3751b3381c fix(menubar): add watchdog backoff, remove dead RefreshBackoff code
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.
2026-05-27 04:38:42 -07:00
iamtoruk
ac35746209 fix(menubar): recover from stuck loading state
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.
2026-05-27 04:06:48 -07:00
Resham Joshi
ce043631ed
Add icon-only menubar display mode (#396)
Some checks are pending
CI / semgrep (push) Waiting to run
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.
2026-05-24 18:05:15 -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
iamtoruk
aa69857e30 Add configurable menubar status period (#302)
Some checks are pending
CI / semgrep (push) Waiting to run
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.
2026-05-24 03:15:39 -07:00
森曜
da641eeb52
Pause menubar refresh after repeated stalls (#388) 2026-05-24 01:04:52 -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
iamtoruk
9ad137f2b8 Fix dormant activation to bootstrap instead of refresh
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.
2026-05-17 05:27:49 -07:00
iamtoruk
bf0c7cc993 Defer keychain access until user explicitly connects on plan tab
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.
2026-05-17 00:31:33 -07:00
iamtoruk
5696a74571 Merge main into feat/kimi-code-provider to resolve conflicts 2026-05-16 07:25:12 -07:00
iamtoruk
59a4d95b18 Merge main into feat/cline-provider to resolve conflicts 2026-05-16 05:58:10 -07:00
iamtoruk
909efcf989 Harden menubar refresh and installer 2026-05-14 18:32:15 -07:00
iamtoruk
478131d5b7 Harden menubar status refresh timer 2026-05-14 17:57:36 -07:00
iamtoruk
00c55873a4 Fix menubar tab refresh recovery 2026-05-14 11:26:15 -07:00
iamtoruk
403efd4727 Merge remote-tracking branch 'origin/main' into fix/menubar-version-prefix
# Conflicts:
#	package.json
#	src/parser.ts
2026-05-13 20:32:22 -07:00
iamtoruk
c626fc4a45 Fix menubar stale cache recovery 2026-05-13 20:22:15 -07:00
AgentSeal
03e22ecb80
Add IBM Bob provider with workspace extraction (#316)
Some checks are pending
CI / semgrep (push) Waiting to run
* 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>
2026-05-11 20:54:13 -07:00
iamtoruk
b4b28becc8 Harden menubar refresh recovery 2026-05-11 20:44:06 -07:00
ozymandiashh
9187bc590a Add Cline provider 2026-05-12 00:31:41 +03:00
iamtoruk
ce0e1eb116 Make menubar refresh now reset stale state 2026-05-11 11:05:50 -07:00
ozymandiashh
78cbfd3798 Add Kimi provider 2026-05-11 19:02:28 +03:00
Resham Joshi
180e14da82
Merge pull request #293 from getagentseal/fix/menubar-loading-watchdog
Some checks are pending
CI / semgrep (push) Waiting to run
Fix menubar loading recovery deadlocks
2026-05-10 10:32:56 -07:00
iamtoruk
d79deefaae Fix menubar refresh recovery deadlock 2026-05-10 03:30:56 -07:00
Resham Joshi
4c29f6b880
Add Crush provider plus per-provider icon column in README (#286)
Closes #278.

Adds Charmbracelet Crush as a lazy-loaded provider:
- src/providers/crush.ts: walks ~/.local/share/crush/projects.json
  (XDG_DATA_HOME and CRUSH_GLOBAL_DATA aware), opens each project's
  crush.db read-only, queries root sessions where parent_session_id
  IS NULL. Emits one ParsedProviderCall per session with real
  prompt_tokens, completion_tokens, cost (dollars), and the
  dominant model resolved from messages.model.
- src/providers/index.ts: register crush alongside cursor, goose,
  opencode, antigravity, cursor-agent in the lazy import path.
- tests/providers/crush.test.ts: 10 fixture-based tests covering
  discovery, parsing, missing-registry, malformed JSON, missing db,
  child session exclusion, dominant model selection, dedup, and
  array-shaped legacy registry.

Schema source: charmbracelet/crush@v0.66.1
internal/db/migrations/20250424200609_initial.sql, verified by
spawning a research agent against upstream. The schema *comments*
in that migration claim millisecond timestamps but every actual
INSERT/UPDATE uses strftime('%s', 'now') which returns Unix
seconds; the parser treats values as seconds. Tokscale's
parser (junhoyeo/tokscale#346) gets this wrong and is off by
1000x, plus its parser misses the prompt_tokens/completion_tokens
columns that exist in Crush's schema. Our integration uses both,
so Crush sessions get real per-model attribution.

Menubar:
- mac/Sources/CodeBurnMenubar/AppStore.swift: add .crush case to
  ProviderFilter and its cliArg switch.
- mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift: add
  Crush color to the per-tab color extension. The visibleFilters
  computed property already filters by detected providers, so the
  Crush tab appears automatically when a user has Crush data.

README:
- Replace the provider table with an icon-led layout. Icons live
  under assets/providers/<name>.<ext>. 14 icons sourced from
  junhoyeo/tokscale (MIT) under nominative fair use, 4 sourced
  separately: codex (OpenAI org avatar), cursor-agent (reuses the
  Cursor icon), kiro (kiro.dev favicon, ico->png via sips), omp
  (can1357/oh-my-pi icon.svg, MIT). Attribution line added.
- Add Crush row.

Docs:
- docs/providers/crush.md: full per-provider doc with verified
  schema excerpt, the seconds-vs-milliseconds quirk, and a
  "when fixing a bug here" checklist.
- docs/architecture.md: provider count 17 -> 18, test count
  41 -> 42, and crush in the lazy list.
- docs/providers/README.md: add Crush row to the lazy index.
- CONTRIBUTING.md: bump test count to 568 (was 558).

All 568 tests pass locally; swift build clean.
2026-05-09 20:47:56 -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
iamtoruk
b317009181 Fix menubar stuck loading after sleep, double-click on pill tabs, oversized disconnected tabs
Sleep: track forceRefreshTask and cancel it on willSleep alongside
refreshLoopTask. Reset loadingCount on wake so orphaned fetches from
before sleep cannot leave the loading bar stuck.

Tabs: replace Button wrapper with onTapGesture on AgentTab so the
NSPopover hover tooltip does not eat the first click. Add
clickDismissed guard to prevent the popover from re-showing while
the mouse is still over the tab after a tap.

Tab size: only render the quota bar slot when quota data exists
(connected), not for every provider that supports quota. Disconnected
Claude/Codex tabs are now the same height as other tabs.
2026-05-08 13:00:35 -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
3c8ce32bf3 Fix popover anchor, tab strip flicker, and stale-data refresh
Five interleaving menubar regressions traced back to the cache-wipe and
showLoading additions in 18c3c8f, surfaced by adversarial multi-agent
review against the v0.9.6 baseline.

- forceRefresh no longer calls store.invalidateCache(). Wiping the
  whole cache on every wake or manual refresh emptied todayPayload,
  flipped showAgentTabs to false, and made cache[key] == nil for all
  keys, which forced the full-popover loading overlay over already
  rendered data. The day-rollover guard inside refresh() still wipes
  the cache when the calendar date changes, so the legitimate part of
  18c3c8f is preserved.

- Overlay condition is now !store.hasCachedData. Without this, the
  popover briefly rendered $0.00 placeholders before the overlay slid
  in on a cold key, and reflashed the overlay on every manual refresh
  even when fresh data was on screen.

- refreshStatusButton skips while popover is anchored. Rewriting the
  button's attributedTitle changes its intrinsic width, which makes
  macOS reflow the status item and detaches the anchored popover to
  the screen's top-left default position. popoverDidClose runs the
  refresh once so the menubar title catches up immediately on
  dismiss.

- showAgentTabs is sticky via hasAnyProvidersInCache. Prevents the
  one-frame flicker where the tab strip vanished while the new key's
  payload had not yet arrived.

- observeStore tracks store.currency. Without this, switching
  currency did not propagate to refreshStatusButton until the next
  30s payload tick, leaving the menubar showing the old currency
  symbol and rate.

- Day-rollover race in refresh and refreshQuietly: capture cacheDate
  at fetch start, drop the write if the calendar date changed during
  the await. Prevents an in-flight fetch from yesterday polluting
  today's freshly cleared cache.

- Manual refresh button passes showLoading: true again. Safe now that
  the overlay is gated on cache state instead of isLoading; the
  refresh button icon swaps to the spinner glyph for visible feedback,
  while the popover body keeps the existing data and updates when the
  fetch lands.
2026-05-06 10:59:42 -07:00
iamtoruk
18c3c8f908 Fix stale menubar data after sleep and silent refresh button
Cache now tracks the calendar date and clears on day rollover so
overnight sleep no longer shows yesterday's numbers. Wake-from-sleep
invalidates the entire cache before fetching. Manual refresh and wake
explicitly request loading feedback so the spinner is visible even
when stale data exists.
2026-05-05 11:35:38 -07:00
iamtoruk
c706cd2de2 Strip optimize from menubar, fix stuck loading spinner
The menubar ran --optimize on every 30-second CLI invocation. As
sessions accumulated throughout the day, optimize got heavier until
it exceeded the 45-second timeout. When the fetch failed with no
cached data, the loading overlay had no escape hatch and stayed
forever.

- Never pass includeOptimize from the menubar (background loop,
  forceRefresh, tab/period switches, manual refresh button)
- On fetch failure with empty cache, retry without optimize as
  fallback so the spinner always clears
- refreshQuietly also skips optimize
2026-05-04 23:11:42 -07:00
iamtoruk
bfa5fe7fa0 fix(labels): update remaining 'all' period labels to '6 Months'
PR #221 unified the period logic but missed the TUI hotkey bar,
GNOME indicator popup, and macOS menubar app. All surfaces now
consistently show '6 Months' instead of 'All' or 'all time'.
2026-05-04 19:46:20 -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