Skip to content

Conversation

@bhadraagada
Copy link

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

  • System tray app with modern dark UI (Vercel-inspired design)
  • 12 AI provider integrations: Gemini, Claude, Codex, GitHub Copilot, Cursor, Antigravity, Factory, z.ai, Kiro, Vertex AI, Augment, MiniMax
  • Local usage parsing - reads data directly from CLI config files instead of making API calls:
    • Gemini: ~/.gemini/tmp/*/chats/session-*.json (token counts per model)
    • Claude: ~/.claude/stats-cache.json (daily/weekly/monthly usage stats)
    • Antigravity: ~/.gemini/antigravity/conversations/*.pb (conversation counts)
    • Copilot: VS Code extension detection + GitHub CLI auth status
  • Configurable polling with 5-minute default refresh interval
  • Persistent settings using electron-store
  • Auto-updater support via electron-updater

Tech Stack

  • Electron 28
  • React 18 + TypeScript 5.3
  • Vite for renderer bundling
  • Winston for logging
  • electron-store for persistence

Screenshots
The app displays usage stats in a frameless window with provider cards showing:

  • Token usage (input/output)
  • Request counts
  • Session/weekly/monthly breakdowns
  • CLI version info for installed tools
image image image image

Testing
Verified on Windows 11 with:

  • ✅ Gemini CLI - parses session files, shows token usage by model
  • ✅ Claude CLI - reads stats-cache.json, shows message/session/token counts
  • ✅ Codex CLI - detects CLI version
  • ✅ GitHub Copilot - detects VS Code extension version

Notes

  • On Windows, use Stop-Process -Name electron -Force in PowerShell to cleanly kill the app (Git Bash taskkill has argument parsing issues)
  • Providers that aren't installed gracefully skip detection
  • Usage limits are informational (most AI CLIs don't expose hard limits)

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
Copilot AI review requested due to automatic review settings January 10, 2026 06:22
@bhadraagada
Copy link
Author

#151

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a 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".

Comment on lines +152 to +155
// Update settings
ipcMain.handle('set-setting', async (_event, key: string, value: unknown) => {
settingsStore?.setAny(key, value);
await updateTrayMenu();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +132 to +136
<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)} />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link

Copilot AI left a 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.

Comment on lines +61 to +69

/**
* 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)}`);
}
Copy link

Copilot AI Jan 10, 2026

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.

Suggested change
/**
* 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)}`);

Copilot uses AI. Check for mistakes.
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"node-fetch": "^3.3.2",
"keytar": "^7.9.0",
Copy link

Copilot AI Jan 10, 2026

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.

Suggested change
"keytar": "^7.9.0",

Copilot uses AI. Check for mistakes.
"tough-cookie": "^4.1.3",
"winston": "^3.11.0",
"commander": "^11.1.0",
"node-pty": "^1.0.0"
Copy link

Copilot AI Jan 10, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +96
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>
);
}
Copy link

Copilot AI Jan 10, 2026

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,458 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
Copy link

Copilot AI Jan 10, 2026

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.

Copilot uses AI. Check for mistakes.
* - Total sessions and messages
*/

import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage } from '../BaseProvider';
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import calculatePercentage.

Suggested change
import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage } from '../BaseProvider';
import { BaseProvider, ProviderUsage, ProviderStatus } from '../BaseProvider';

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +123
// Get month start
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthStartStr = monthStart.toISOString().split('T')[0];

Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable monthStartStr.

Suggested change
// Get month start
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthStartStr = monthStart.toISOString().split('T')[0];

Copilot uses AI. Check for mistakes.
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`);
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable modelTokens.

Suggested change
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`
);

Copilot uses AI. Check for mistakes.
* Each session contains messages with token counts that we aggregate.
*/

import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider';
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports calculatePercentage, formatUsage.

Suggested change
import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider';
import { BaseProvider, ProviderUsage, ProviderStatus } from '../BaseProvider';

Copilot uses AI. Check for mistakes.
*/

import { BaseProvider, ProviderUsage, ProviderStatus, calculatePercentage, formatUsage } from '../BaseProvider';
import { runCommand } from '../../utils/subprocess';
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import runCommand.

Suggested change
import { runCommand } from '../../utils/subprocess';

Copilot uses AI. Check for mistakes.
@mynameistito
Copy link

🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants