Skip to content

Commit 109fa07

Browse files
yyq1025claude
andcommitted
menubar: poll plan usage on a background timer so opens show fresh data
The tray fetched plan usage only on open + at startup, but macOS never repaints an open NSMenu — so a click could only paint an already-cached snapshot and the in-flight fetch landed for the *next* open. Result: the menu always showed the previous open's numbers (most visibly a stale Weekly % lingering after the window reset). Add a 2-min setInterval poller (cleared in before-quit) that keeps the cached snapshot warm independent of tray opens. The daemon's 30s TTL + single-flight coalesce anything faster, and plan usage drifts slowly, so 2 min is plenty. Startup + per-open refreshes stay (free under the TTL); the poll is just what makes the displayed number current. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9994073 commit 109fa07

1 file changed

Lines changed: 18 additions & 4 deletions

File tree

packages/menubar/electron/main.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,23 @@ let keepAwakeId: number | null = null;
3131

3232
// --- Plan usage (daemon-fetched, menu-cached) ---
3333
//
34-
// Last fetch result, rendered by buildMenu. Refresh is fire-and-forget on
35-
// tray click + once at startup: macOS doesn't repaint an open NSMenu, so a
36-
// click shows the previous snapshot and the fetch lands for the next open
37-
// (the daemon's 30s cache + single-flight make spamming clicks free).
34+
// Last fetch result, rendered by buildMenu. macOS doesn't repaint an open
35+
// NSMenu, so the open itself can only ever paint an ALREADY-cached snapshot —
36+
// a tray click can't show its own in-flight fetch. A background poll
37+
// (PLAN_USAGE_POLL_MS) keeps the cache warm so each open shows near-current
38+
// data; without it an open showed the *previous* open's snapshot. The
39+
// startup + per-open `refreshPlanUsage()` calls stay (the daemon's 30s cache
40+
// + single-flight make the extra fetches free), but the poll is what makes
41+
// the displayed number fresh.
3842
// Token Stats was CUT from V0: its planned source (stats-cache.json) turned
3943
// out to be lazily written — only when the user runs /stats — so honest
4044
// numbers require Desktop-style JSONL aggregation; deferred.
4145
let planUsage: PlanUsageResult | null = null;
46+
// Background poller handle, cleared on quit. 2 min: the daemon's 30s TTL
47+
// coalesces anything faster, and plan usage drifts slowly, so a tighter
48+
// interval would just burn requests for no visible gain.
49+
const PLAN_USAGE_POLL_MS = 2 * 60_000;
50+
let planUsageTimer: ReturnType<typeof setInterval> | null = null;
4251

4352
function refreshPlanUsage(): void {
4453
if (!daemon) return;
@@ -349,6 +358,10 @@ app.whenReady().then(async () => {
349358

350359
refreshPlanUsage();
351360
refreshMenu();
361+
// Keep the cached snapshot warm independent of tray opens, so the next
362+
// open paints current data instead of the previous open's (NSMenu can't
363+
// repaint while open). Cleared in before-quit.
364+
planUsageTimer = setInterval(refreshPlanUsage, PLAN_USAGE_POLL_MS);
352365
console.log("[main] tray + menu ready");
353366

354367
// electron-updater: auto-download + drive the update menu items. Rebuilds the
@@ -360,6 +373,7 @@ app.on("before-quit", (event: Electron.Event) => {
360373
if (isQuitting) return;
361374
event.preventDefault();
362375
isQuitting = true;
376+
if (planUsageTimer) clearInterval(planUsageTimer);
363377
console.log("[main] before-quit: stopping daemon...");
364378
const stopPromise = daemon ? daemon.stop() : Promise.resolve();
365379
void stopPromise.then(() => {

0 commit comments

Comments
 (0)