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.
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.
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
Set accessory activation policy in willFinishLaunching before the
focus chain forms. Debounce observation tracking to coalesce rapid
property changes into a single status bar refresh.
- Fix refresh loop: proper while loop with 30s sleep and force:true
instead of single-fire Task that never repeated
- Fix loading overlay: counter-based isLoading so concurrent fetches
don't flicker the overlay on/off
- Fix rapid tab switching: cancel previous switchTask, check
Task.isCancelled after CLI returns to discard stale results
- Fix tab strip vs hero desync: fetch provider-specific and all-provider
data in parallel so costs arrive from same data snapshot
- Fix stale menubar icon after wake: forceRefresh now fetches today/all
in parallel alongside the current selection
- Fix accent color: ThemeState is now @Observable so color changes
propagate via observation, removing .id() view hierarchy teardown
- Fix currency flash: defer store.currency and symbol update until a
rate is available so symbol and rate apply atomically
- Fix export: terminationHandler instead of waitUntilExit (no UI freeze),
HHmmss in filename to prevent overwrite on double-export
- Fix CurrencyState: @MainActor isolation with proper Sendable
conformance, nonisolated on pure static functions
- Fix streak count: iterate calendar days instead of sparse history
entries so gaps are counted as streak-breakers
- Fix TrendBar identity: stable date-based id instead of UUID
- Add GPT-5.3 and DeepSeek model display names
forceRefresh() was missing force:true, so the cache TTL guard
silently skipped every LaunchAgent and wake-triggered refresh.
Also adds right-click context menu and version label in footer.
On macOS 26.4+, accessory apps may fail to render their status item if
the activation policy is set to .accessory before the status item is
created and populated. The status item draw code executes but nothing
appears in the menu bar.
Workaround:
1. Start as .regular app and call activate() to ensure window server
registration
2. Set a simple SF Symbol image immediately on the status button
3. Defer the attributed title setup to ensure initial render completes
4. Switch to .accessory policy after setup to hide from Dock
Fixes#146
Fixes crash when switching timeframes or providers by handling
duplicate dates in history data gracefully.
Adds LaunchAgent that posts a distributed notification every 15
seconds to keep prices fresh even after long idle periods.
The menubar price would freeze after the app was idle for a while because macOS App Nap suspends Task.sleep even with beginActivity.
Replace Task.sleep with DispatchSourceTimer which is more robust for background execution. Also add observers for system wake and screen wake events to force a refresh when the Mac resumes from sleep.
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
UserDefaults key CodeBurnMenubarCompact toggles a tighter menubar
display: no decimals, no leading space, variable width that hugs
the rendered text instead of the fixed 130pt slot.
Closes#129
Confirmed in the system log: while the menubar icon sits idle in the
background, macOS flips _kLSApplicationWouldBeTerminatedByTALKey to 1,
which is the Automatic Termination and App Nap subsystem declaring the
app a suspend candidate. Once that happens the 15s refresh Task's sleep
stretches arbitrarily, so the status bar label freezes until the user
clicks the icon (which triggers NSApp.activate and wakes everything up).
Hold a ProcessInfo.beginActivity handle for the life of the app with
.userInitiated + .automaticTerminationDisabled + .suddenTerminationDisabled
so macOS leaves the app alone. Released implicitly when the app exits.
Two independent causes for the stuck-label / only-refreshes-on-click
behaviour, both fixed here.
1. NSStatusItem button defers the status bar paint for accessory apps
that are not foreground, so after refreshStatusButton sets the new
attributed title the menu bar visually froze until the user opened
the popover (which triggers NSApp.activate and a forced redraw
cycle). Explicit needsDisplay + display() forces the paint every
cycle.
2. The codeburn subprocess inherited the accessory app's default QoS,
which macOS background-throttles. That could stretch a sub-1-second
parse into tens of seconds on large corpora and overrun the 15s
refresh cadence. Set .userInitiated so the CLI runs at the same
priority it does from a user-interactive terminal.
The detached prefetch spawned four codeburn subprocesses (week, 30days,
month, all) that competed with the main refresh loop's Today fetch for
disk and parser time. On large session corpora that starved the Today
refresh for minutes at a time, and the status label drifted stale until
the prefetch queue finally drained. Users perceived this as the menubar
"stopping" until they clicked a provider tab, which coincided with the
queue clearing.
Drop prefetchAll. Period tabs in the popover now fetch lazily on first
click (3-10 seconds once per app lifetime per period), same as they did
in 0.8.1. Today's label stays fresh because nothing else is hammering
the parser in the background.
prefetchAll was awaited inside the refresh loop after the first cycle.
On large corpora the All Time parse takes 60+ seconds and kept the Today
refresh blocked for that whole window, so the status label got stuck for
minutes at a time while the CLI was returning fresh numbers.
Detach prefetchAll into its own @MainActor task. The popover still gets
the benefit of pre-warmed period caches, the refresh loop ticks every 15
seconds regardless.
Loading overlay no longer flashes on every 15s poll. isLoading now only
toggles when the cache is cold, and all periods prefetch once on launch
so tab switching is instant.
Heatmap tooltip, trend bars, forecast, and all-time stats were computing
on UTC dates while the CLI reports on local dates, so the two disagreed
at day boundaries. Switched every date formatter and calendar in these
paths to .current so the menubar matches codeburn today output.
Menubar: reduce cache TTL from 300s to 30s, background refresh from
60s to 15s, always fetch fresh data on tab switch instead of serving
stale cache. TUI: default auto-refresh to 30s (--refresh 0 to disable).
Closes#107
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.
The refresh loop previously skipped `refreshQuietly(.today)` when the
user was already viewing the Today period. That guard meant while the
user was on (today, claude) or any other non-.all provider, the
(today, all) cache went stale. The menubar title and the agent tab
strip both read from that cache, so they displayed stale costs while
the hero section (which reads the currently-viewed payload) showed
the correct fresh value.
Remove the guard so the (today, all) cache refreshes every cycle
regardless of the currently selected period/provider.
Shipped as mac-v0.7.4.