Skip to content

Commit 12ae7ed

Browse files
committed
feat: electron-updater + Codex CLI monitoring (opt-in)
Auto-update - electron-updater checks sooua/TokenWatch on GitHub Releases at startup (15s delay) and every 4 hours; active only in packaged builds - autoDownload=false: user confirms before the download begins - autoInstallOnAppQuit=true: installs silently on next graceful exit - UpdateBanner shows four states (available / downloading / downloaded / error) above the main window header with Download / Install & restart buttons and a dismiss X - Settings: toggle for automatic checking + "Check for updates now" btn - electron-builder.json gains a github publish provider (owner=sooua) Codex CLI monitoring (opt-in, off by default) - New CodexService tails ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl, extracts the latest event_msg.type=token_count event, and surfaces the primary (5h) + secondary (7d) rate-limit windows that Codex itself records — no estimation needed - New CodexCard renders two progress bars (used_percent + reset-in), last session tokens, context window %, total sessions, last event timestamp; warm palette consistent with the Claude sections - Auto-hides when ~/.codex/sessions is missing, so enabling without Codex installed is a no-op instead of a broken card - Settings: "Show Codex CLI card" switch — off by default so Claude stays the hero of the dashboard - Tail-read implementation (64 KB from file end) to avoid loading multi-MB session transcripts just to see the latest token counts UI polish - Dashboard hero title renamed to "Claude Code — usage overview" / "Claude Code — 用量概览" so the page's provider context is explicit - i18n keys added under `codex.*` and `update.*`; both en and zh
1 parent 86fcacb commit 12ae7ed

15 files changed

Lines changed: 1028 additions & 26 deletions

‎electron-builder.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"directories": {
55
"output": "release"
66
},
7+
"publish": [
8+
{
9+
"provider": "github",
10+
"owner": "sooua",
11+
"repo": "TokenWatch",
12+
"releaseType": "release"
13+
}
14+
],
715
"files": [
816
"dist/**/*",
917
"assets/**/*",

‎main.ts‎

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ import * as fs from 'node:fs';
22
import * as os from 'node:os';
33
import * as path from 'node:path';
44
import { fileURLToPath } from 'node:url';
5-
import {
6-
BrowserWindow,
7-
Menu,
8-
Tray,
9-
app,
10-
ipcMain,
11-
nativeImage,
12-
screen,
13-
} from 'electron';
5+
import { BrowserWindow, Menu, Tray, app, ipcMain, nativeImage, screen } from 'electron';
6+
import electronUpdater from 'electron-updater';
147
import { CCUsageService } from './src/services/ccusageService.js';
8+
import { CodexService } from './src/services/codexService.js';
159
import { NotificationService } from './src/services/notificationService.js';
1610
import { SettingsService } from './src/services/settingsService.js';
1711

12+
const { autoUpdater } = electronUpdater;
13+
1814
const __filename = fileURLToPath(import.meta.url);
1915
const __dirname = path.dirname(__filename);
2016

@@ -76,6 +72,7 @@ class TokenWatchApp {
7672
private tray: Tray | null = null;
7773
private window: BrowserWindow | null = null;
7874
private usageService: CCUsageService;
75+
private codexService: CodexService;
7976
private notificationService: NotificationService;
8077
private settingsService: SettingsService;
8178
private updateInterval: NodeJS.Timeout | null = null;
@@ -93,9 +90,12 @@ class TokenWatchApp {
9390
private miniHudContent: 'percentage' | 'percentageCost' | 'percentageCostBurn' =
9491
'percentageCost';
9592
private miniHudSavePositionTimer: NodeJS.Timeout | null = null;
93+
private autoUpdateTimer: NodeJS.Timeout | null = null;
94+
private autoUpdateInitialized = false;
9695

9796
constructor() {
9897
this.usageService = CCUsageService.getInstance();
98+
this.codexService = CodexService.getInstance();
9999
this.notificationService = NotificationService.getInstance();
100100
this.settingsService = SettingsService.getInstance();
101101
}
@@ -117,6 +117,7 @@ class TokenWatchApp {
117117
});
118118

119119
this.applyLaunchOnStartup(settings.launchOnStartup === true);
120+
this.initAutoUpdate(settings.autoCheckUpdates !== false);
120121

121122
this.createTray();
122123
this.createWindow();
@@ -320,6 +321,83 @@ class TokenWatchApp {
320321
}
321322
}
322323

324+
// --- Auto-update (electron-updater against GitHub Releases) -------------
325+
// Only active in a packaged build — in dev there's no installed copy to
326+
// replace. Events are forwarded to the renderer so the UI can show a banner
327+
// and progress.
328+
329+
private initAutoUpdate(enabled: boolean) {
330+
if (!app.isPackaged) return;
331+
332+
if (!this.autoUpdateInitialized) {
333+
autoUpdater.autoDownload = false; // User confirms before the download starts
334+
autoUpdater.autoInstallOnAppQuit = true;
335+
336+
autoUpdater.on('checking-for-update', () => this.emitUpdate({ status: 'checking' }));
337+
autoUpdater.on('update-available', (info) =>
338+
this.emitUpdate({
339+
status: 'available',
340+
version: info?.version,
341+
releaseNotes:
342+
typeof info?.releaseNotes === 'string' ? info.releaseNotes : undefined,
343+
releaseDate: info?.releaseDate,
344+
})
345+
);
346+
autoUpdater.on('update-not-available', () =>
347+
this.emitUpdate({ status: 'not-available' })
348+
);
349+
autoUpdater.on('download-progress', (progress) =>
350+
this.emitUpdate({
351+
status: 'downloading',
352+
percent: progress.percent,
353+
bytesPerSecond: progress.bytesPerSecond,
354+
transferred: progress.transferred,
355+
total: progress.total,
356+
})
357+
);
358+
autoUpdater.on('update-downloaded', (info) =>
359+
this.emitUpdate({ status: 'downloaded', version: info?.version })
360+
);
361+
autoUpdater.on('error', (err) =>
362+
this.emitUpdate({ status: 'error', error: err?.message || String(err) })
363+
);
364+
365+
this.autoUpdateInitialized = true;
366+
}
367+
368+
// Clear any existing periodic check before re-arming with the current
369+
// preference — the setting can flip at runtime.
370+
if (this.autoUpdateTimer) {
371+
clearInterval(this.autoUpdateTimer);
372+
this.autoUpdateTimer = null;
373+
}
374+
375+
if (enabled) {
376+
// First check runs shortly after boot so it doesn't compete with
377+
// ccusage's cold-start parsing for network bandwidth.
378+
setTimeout(() => {
379+
autoUpdater.checkForUpdates().catch((err) => {
380+
console.error('Initial update check failed:', err);
381+
});
382+
}, 15_000);
383+
384+
this.autoUpdateTimer = setInterval(
385+
() => {
386+
autoUpdater.checkForUpdates().catch((err) => {
387+
console.error('Periodic update check failed:', err);
388+
});
389+
},
390+
4 * 60 * 60 * 1000 // 4 hours
391+
);
392+
}
393+
}
394+
395+
private emitUpdate(payload: Record<string, unknown>) {
396+
if (this.window && !this.window.isDestroyed()) {
397+
this.window.webContents.send('update-status', payload);
398+
}
399+
}
400+
323401
private applyLaunchOnStartup(enabled: boolean) {
324402
// On Windows this writes HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
325403
// On macOS it uses the LSBackgroundOnly launchd item. Linux is a no-op.
@@ -599,6 +677,34 @@ class TokenWatchApp {
599677
ipcMain.handle('mini-hud-open-main', () => {
600678
this.showWindow();
601679
});
680+
// Auto-update — renderer can trigger a check, start the download after
681+
// we've announced an available version, or install what's already
682+
// downloaded.
683+
ipcMain.handle('update-check', async () => {
684+
if (!app.isPackaged) {
685+
this.emitUpdate({ status: 'not-available', info: 'dev build' });
686+
return;
687+
}
688+
try {
689+
await autoUpdater.checkForUpdates();
690+
} catch (err) {
691+
console.error('update-check failed:', err);
692+
this.emitUpdate({ status: 'error', error: (err as Error)?.message || String(err) });
693+
}
694+
});
695+
ipcMain.handle('update-download', async () => {
696+
try {
697+
await autoUpdater.downloadUpdate();
698+
} catch (err) {
699+
console.error('update-download failed:', err);
700+
this.emitUpdate({ status: 'error', error: (err as Error)?.message || String(err) });
701+
}
702+
});
703+
ipcMain.handle('update-install', () => {
704+
this.isQuitting = true;
705+
autoUpdater.quitAndInstall(false, true);
706+
});
707+
602708
ipcMain.handle('mini-hud-close', () => {
603709
// Called by the close-button inside the HUD itself. Persist the flip so
604710
// the HUD stays off across relaunches.
@@ -618,6 +724,15 @@ class TokenWatchApp {
618724
}
619725
});
620726

727+
ipcMain.handle('get-codex-stats', async () => {
728+
try {
729+
return await this.codexService.getStats();
730+
} catch (error) {
731+
console.error('Error getting codex stats:', error);
732+
return { installed: false } as const;
733+
}
734+
});
735+
621736
ipcMain.handle('get-cached-usage-stats', () => {
622737
try {
623738
return this.usageService.loadPersistedStats();
@@ -702,6 +817,10 @@ class TokenWatchApp {
702817
this.applyLaunchOnStartup(settings.launchOnStartup);
703818
}
704819

820+
if (typeof settings.autoCheckUpdates === 'boolean') {
821+
this.initAutoUpdate(settings.autoCheckUpdates);
822+
}
823+
705824
if (typeof settings.miniHud === 'boolean' && settings.miniHud !== this.miniHudEnabled) {
706825
this.miniHudEnabled = settings.miniHud;
707826
if (this.miniHudEnabled) {

0 commit comments

Comments
 (0)