-
Notifications
You must be signed in to change notification settings - Fork 178
feat: Add Windows/Linux cross-platform Electron port with local CLI usage parsing #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Initial scaffolding for cross-platform CodexBar port: - Electron 28 + React 18 + TypeScript 5.3 + Vite - System tray app with usage menu - All 12 providers ported (Codex, Claude, Cursor, Gemini, Copilot, Antigravity, Factory, z.ai, Kiro, Vertex AI, Augment, MiniMax) - Settings window with provider toggles - Auto-updates via electron-updater - CLI tool (codexbar status/list/refresh) - Persistent settings and usage history Addresses steipete#151
- Add setAny() method to SettingsStore for IPC string keys - Add pnpm.onlyBuiltDependencies for electron, esbuild, keytar, node-pty - Add pnpm-lock.yaml
- Clean, minimal dark design with smooth gradients - Card-based provider list with staggered animations - Large percentage display with color-coded warnings - Modern toggle switches with hover states - Custom scrollbars and proper typography - Polished About page with logo and links - Responsive hover effects throughout
- GeminiProvider: parse session files from ~/.gemini/tmp/*/chats/ - ClaudeProvider: read stats from ~/.claude/stats-cache.json - AntigravityProvider: count conversations from ~/.gemini/antigravity/ - CopilotProvider: detect GitHub CLI auth and VS Code extension - CodexProvider: check CLI version (no local usage storage) - CursorProvider: enhanced auth token detection across platforms - VertexAIProvider: integrate with gcloud CLI for project info - FactoryProvider: check factory and droid CLI versions - ZaiProvider, KiroProvider, AugmentProvider, MiniMaxProvider: enhanced detection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfdd91e459
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Update settings | ||
| ipcMain.handle('set-setting', async (_event, key: string, value: unknown) => { | ||
| settingsStore?.setAny(key, value); | ||
| await updateTrayMenu(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apply refresh interval changes to polling loop
Changing the refresh interval in settings only updates the stored value and the tray menu, but the polling loop keeps running at the original interval because ProviderManager.startPolling() reads the interval once and nothing calls updatePollingInterval() when the setting changes. As a result, users who pick a new interval won’t see any effect until they restart the app. Consider detecting the refreshInterval key here and restarting polling (or delegating to ProviderManager.updatePollingInterval).
Useful? React with 👍 / 👎.
| <div className="setting-label">Start at Login</div> | ||
| <div className="setting-desc">Launch CodexBar on startup</div> | ||
| </div> | ||
| <label className="toggle"> | ||
| <input type="checkbox" checked={settings.startAtLogin ?? false} onChange={e => updateSetting('startAtLogin', e.target.checked)} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wire Start at Login toggle to OS login items
The UI exposes a “Start at Login” toggle, but the main process never applies it (no app.setLoginItemSettings or platform equivalent is called), so this checkbox only persists a setting and never changes actual login behavior. Users will think they enabled startup but the app still won’t auto-launch. This needs wiring in the main process when the setting changes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a comprehensive cross-platform Electron port of CodexBar for Windows and Linux users. The application provides system tray integration for monitoring AI provider API usage limits by parsing local CLI configuration files rather than making direct API calls.
Changes:
- Complete Electron application with system tray integration and React-based settings UI
- 12 AI provider implementations that read usage data from local CLI config files (Gemini, Claude, Codex, Cursor, Copilot, Antigravity, Factory, z.ai, Kiro, Vertex AI, Augment, MiniMax)
- Persistent storage using electron-store, auto-updater support, and comprehensive logging infrastructure
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 31 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Project configuration with Electron 28, React 18, TypeScript 5.3, and build tooling |
| tsconfig.json, tsconfig.main.json, tsconfig.renderer.json | TypeScript configurations for main process, renderer process, and shared code |
| vite.config.ts | Vite bundler configuration for renderer process |
| src/main/index.ts | Main process entry point with tray, IPC, and provider management |
| src/main/preload.ts | Secure IPC bridge between main and renderer processes |
| src/main/updater.ts | Auto-updater configuration using electron-updater |
| src/main/providers/BaseProvider.ts | Base provider interface and utilities |
| src/main/providers/ProviderManager.ts | Orchestrates provider polling and state management |
| src/main/providers/*/Provider.ts | Individual provider implementations for 12 AI services |
| src/main/store/SettingsStore.ts | Persistent settings management |
| src/main/store/UsageStore.ts | Usage data storage and history tracking |
| src/main/utils/logger.ts | Winston-based logging infrastructure |
| src/main/utils/subprocess.ts | CLI command execution utilities |
| src/main/tray/TrayMenu.ts | System tray context menu builder |
| src/renderer/App.tsx | Main React application component |
| src/renderer/components/ProviderList.tsx | Provider list with usage visualization |
| src/renderer/components/GeneralSettings.tsx | Settings UI component (unused) |
| src/renderer/styles.css | Vercel-inspired dark theme styles |
| src/cli/index.ts | Command-line interface implementation |
| README.md | Comprehensive documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| /** | ||
| * Set a setting by string key (for IPC) | ||
| */ | ||
| setAny(key: string, value: unknown): void { | ||
| if (key in defaults) { | ||
| this.store.set(key as keyof SettingsSchema, value as any); | ||
| logger.debug(`Setting updated: ${key} = ${JSON.stringify(value)}`); | ||
| } |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'setAny' method performs type coercion with 'as any' without proper validation. If an invalid key or value type is passed via IPC, this could cause runtime errors or data corruption. Consider adding runtime type validation or using a type guard.
| /** | |
| * Set a setting by string key (for IPC) | |
| */ | |
| setAny(key: string, value: unknown): void { | |
| if (key in defaults) { | |
| this.store.set(key as keyof SettingsSchema, value as any); | |
| logger.debug(`Setting updated: ${key} = ${JSON.stringify(value)}`); | |
| } | |
| /** | |
| * Runtime guard to ensure the key is a known setting key. | |
| */ | |
| private isKnownSettingKey(key: string): key is keyof SettingsSchema { | |
| return Object.prototype.hasOwnProperty.call(defaults, key); | |
| } | |
| /** | |
| * Runtime validation to ensure the value matches the expected type | |
| * for the given setting key. | |
| */ | |
| private isValidSettingValue(key: keyof SettingsSchema, value: unknown): boolean { | |
| switch (key) { | |
| case 'enabledProviders': | |
| return Array.isArray(value) && value.every((v) => typeof v === 'string'); | |
| case 'refreshInterval': | |
| return typeof value === 'number' && Number.isFinite(value); | |
| case 'showNotifications': | |
| case 'startAtLogin': | |
| case 'showInDock': | |
| return typeof value === 'boolean'; | |
| case 'theme': | |
| return value === 'system' || value === 'light' || value === 'dark'; | |
| default: | |
| // Should be unreachable if key is keyof SettingsSchema | |
| return false; | |
| } | |
| } | |
| /** | |
| * Set a setting by string key (for IPC) | |
| */ | |
| setAny(key: string, value: unknown): void { | |
| if (!this.isKnownSettingKey(key)) { | |
| logger.warn(`Attempt to set unknown setting key via IPC: ${key}`); | |
| return; | |
| } | |
| if (!this.isValidSettingValue(key, value)) { | |
| logger.warn( | |
| `Attempt to set setting with invalid value type via IPC: ${key} = ${JSON.stringify( | |
| value, | |
| )}`, | |
| ); | |
| return; | |
| } | |
| // Value has been validated against SettingsSchema for this key. | |
| this.store.set(key, value as any); | |
| logger.debug(`Setting updated: ${key} = ${JSON.stringify(value)}`); |
| "electron-store": "^8.1.0", | ||
| "electron-updater": "^6.1.7", | ||
| "node-fetch": "^3.3.2", | ||
| "keytar": "^7.9.0", |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'keytar' package in dependencies is marked as a build dependency in pnpm config but is also in regular dependencies. Keytar is a native module that can be challenging to build. Verify if it's actually used in the codebase, as I don't see any imports for it. If unused, it should be removed to reduce installation complexity.
| "keytar": "^7.9.0", |
| "tough-cookie": "^4.1.3", | ||
| "winston": "^3.11.0", | ||
| "commander": "^11.1.0", | ||
| "node-pty": "^1.0.0" |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'node-pty' package is listed in dependencies and build dependencies but doesn't appear to be used in the codebase. This is another native module that can complicate builds. Consider removing it if it's not needed.
| import React from 'react'; | ||
|
|
||
| interface Settings { | ||
| refreshInterval: number; | ||
| showNotifications: boolean; | ||
| startAtLogin: boolean; | ||
| theme: 'system' | 'light' | 'dark'; | ||
| } | ||
|
|
||
| interface Props { | ||
| settings: Partial<Settings>; | ||
| onChange: (key: string, value: any) => void; | ||
| } | ||
|
|
||
| export function GeneralSettings({ settings, onChange }: Props) { | ||
| return ( | ||
| <div className="general-settings"> | ||
| <div className="settings-section"> | ||
| <h3>Data</h3> | ||
|
|
||
| <div className="setting-row"> | ||
| <div> | ||
| <div className="setting-label">Refresh Interval</div> | ||
| <div className="setting-description">How often to sync usage data</div> | ||
| </div> | ||
| <select | ||
| className="setting-input" | ||
| value={settings.refreshInterval || 300} | ||
| onChange={(e) => onChange('refreshInterval', parseInt(e.target.value))} | ||
| > | ||
| <option value={60}>Every minute</option> | ||
| <option value={120}>Every 2 minutes</option> | ||
| <option value={300}>Every 5 minutes</option> | ||
| <option value={600}>Every 10 minutes</option> | ||
| <option value={900}>Every 15 minutes</option> | ||
| <option value={1800}>Every 30 minutes</option> | ||
| </select> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="settings-section"> | ||
| <h3>Notifications</h3> | ||
|
|
||
| <div className="setting-row"> | ||
| <div> | ||
| <div className="setting-label">Usage Alerts</div> | ||
| <div className="setting-description">Notify when usage exceeds 80%</div> | ||
| </div> | ||
| <label className="provider-toggle"> | ||
| <input | ||
| type="checkbox" | ||
| checked={settings.showNotifications ?? true} | ||
| onChange={(e) => onChange('showNotifications', e.target.checked)} | ||
| /> | ||
| <span className="toggle-slider" /> | ||
| </label> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="settings-section"> | ||
| <h3>System</h3> | ||
|
|
||
| <div className="setting-row"> | ||
| <div> | ||
| <div className="setting-label">Launch at Startup</div> | ||
| <div className="setting-description">Start CodexBar when you log in</div> | ||
| </div> | ||
| <label className="provider-toggle"> | ||
| <input | ||
| type="checkbox" | ||
| checked={settings.startAtLogin ?? false} | ||
| onChange={(e) => onChange('startAtLogin', e.target.checked)} | ||
| /> | ||
| <span className="toggle-slider" /> | ||
| </label> | ||
| </div> | ||
|
|
||
| <div className="setting-row"> | ||
| <div> | ||
| <div className="setting-label">Appearance</div> | ||
| <div className="setting-description">Color theme preference</div> | ||
| </div> | ||
| <select | ||
| className="setting-input" | ||
| value={settings.theme || 'system'} | ||
| onChange={(e) => onChange('theme', e.target.value)} | ||
| > | ||
| <option value="system">System</option> | ||
| <option value="dark">Dark</option> | ||
| <option value="light">Light</option> | ||
| </select> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GeneralSettings component is imported in the file but never used. The settings UI is implemented inline in App.tsx instead. This creates a misleading import and dead code. Either use the GeneralSettings component or remove it.
| import React from 'react'; | |
| interface Settings { | |
| refreshInterval: number; | |
| showNotifications: boolean; | |
| startAtLogin: boolean; | |
| theme: 'system' | 'light' | 'dark'; | |
| } | |
| interface Props { | |
| settings: Partial<Settings>; | |
| onChange: (key: string, value: any) => void; | |
| } | |
| export function GeneralSettings({ settings, onChange }: Props) { | |
| return ( | |
| <div className="general-settings"> | |
| <div className="settings-section"> | |
| <h3>Data</h3> | |
| <div className="setting-row"> | |
| <div> | |
| <div className="setting-label">Refresh Interval</div> | |
| <div className="setting-description">How often to sync usage data</div> | |
| </div> | |
| <select | |
| className="setting-input" | |
| value={settings.refreshInterval || 300} | |
| onChange={(e) => onChange('refreshInterval', parseInt(e.target.value))} | |
| > | |
| <option value={60}>Every minute</option> | |
| <option value={120}>Every 2 minutes</option> | |
| <option value={300}>Every 5 minutes</option> | |
| <option value={600}>Every 10 minutes</option> | |
| <option value={900}>Every 15 minutes</option> | |
| <option value={1800}>Every 30 minutes</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="settings-section"> | |
| <h3>Notifications</h3> | |
| <div className="setting-row"> | |
| <div> | |
| <div className="setting-label">Usage Alerts</div> | |
| <div className="setting-description">Notify when usage exceeds 80%</div> | |
| </div> | |
| <label className="provider-toggle"> | |
| <input | |
| type="checkbox" | |
| checked={settings.showNotifications ?? true} | |
| onChange={(e) => onChange('showNotifications', e.target.checked)} | |
| /> | |
| <span className="toggle-slider" /> | |
| </label> | |
| </div> | |
| </div> | |
| <div className="settings-section"> | |
| <h3>System</h3> | |
| <div className="setting-row"> | |
| <div> | |
| <div className="setting-label">Launch at Startup</div> | |
| <div className="setting-description">Start CodexBar when you log in</div> | |
| </div> | |
| <label className="provider-toggle"> | |
| <input | |
| type="checkbox" | |
| checked={settings.startAtLogin ?? false} | |
| onChange={(e) => onChange('startAtLogin', e.target.checked)} | |
| /> | |
| <span className="toggle-slider" /> | |
| </label> | |
| </div> | |
| <div className="setting-row"> | |
| <div> | |
| <div className="setting-label">Appearance</div> | |
| <div className="setting-description">Color theme preference</div> | |
| </div> | |
| <select | |
| className="setting-input" | |
| value={settings.theme || 'system'} | |
| onChange={(e) => onChange('theme', e.target.value)} | |
| > | |
| <option value="system">System</option> | |
| <option value="dark">Dark</option> | |
| <option value="light">Light</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // GeneralSettings component was removed because the settings UI is implemented | |
| // inline elsewhere and this component was not being used (dead code). | |
| // | |
| // This file is intentionally left without exports to avoid maintaining a | |
| // duplicate, unused settings UI. |
| @@ -0,0 +1,458 @@ | |||
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); | |||
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS imports Google Fonts via CDN which will fail when the app is offline and may violate the Content Security Policy. Consider bundling the Inter font locally or using system fonts as a fallback. The current CSP doesn't allow loading from external domains.
| * - Total sessions and messages | ||
| */ | ||
|
|
||
| import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage } from '../BaseProvider'; |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import calculatePercentage.
| import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage } from '../BaseProvider'; | |
| import { BaseProvider, ProviderUsage, ProviderStatus } from '../BaseProvider'; |
| // Get month start | ||
| const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); | ||
| const monthStartStr = monthStart.toISOString().split('T')[0]; | ||
|
|
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable monthStartStr.
| // Get month start | |
| const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); | |
| const monthStartStr = monthStart.toISOString().split('T')[0]; | |
| logger.info('Claude usage by model:'); | ||
| for (const [model, modelStats] of Object.entries(stats.modelUsage)) { | ||
| const modelTokens = modelStats.inputTokens + modelStats.outputTokens; | ||
| logger.info(` ${model}: ${formatTokenCount(modelStats.inputTokens)} in, ${formatTokenCount(modelStats.outputTokens)} out`); |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable modelTokens.
| logger.info(` ${model}: ${formatTokenCount(modelStats.inputTokens)} in, ${formatTokenCount(modelStats.outputTokens)} out`); | |
| logger.info( | |
| ` ${model}: ${formatTokenCount(modelStats.inputTokens)} in, ` + | |
| `${formatTokenCount(modelStats.outputTokens)} out, ` + | |
| `${formatTokenCount(modelTokens)} total` | |
| ); |
| * Each session contains messages with token counts that we aggregate. | ||
| */ | ||
|
|
||
| import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider'; |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused imports calculatePercentage, formatUsage.
| import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider'; | |
| import { BaseProvider, ProviderUsage, ProviderStatus } from '../BaseProvider'; |
| */ | ||
|
|
||
| import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider'; | ||
| import { runCommand } from '../../utils/subprocess'; |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import runCommand.
| import { runCommand } from '../../utils/subprocess'; |
|
🙏 |
Summary
This PR adds a cross-platform Electron desktop app that brings CodexBar functionality to Windows and Linux users. The app monitors AI provider API usage limits via the system tray, reading usage data directly from local CLI configuration files.
Features
~/.gemini/tmp/*/chats/session-*.json(token counts per model)~/.claude/stats-cache.json(daily/weekly/monthly usage stats)~/.gemini/antigravity/conversations/*.pb(conversation counts)Tech Stack
Screenshots
The app displays usage stats in a frameless window with provider cards showing:
Testing
Verified on Windows 11 with:
Notes
Stop-Process -Name electron -Forcein PowerShell to cleanly kill the app (Git Bash taskkill has argument parsing issues)