import { useState, useEffect, useMemo, useCallback } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react'; import { cn } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; import { AnthropicIcon, OpenAIIcon } from '@/components/ui/provider-icon'; // Error codes for distinguishing failure modes const ERROR_CODES = { API_BRIDGE_UNAVAILABLE: 'API_BRIDGE_UNAVAILABLE', AUTH_ERROR: 'AUTH_ERROR', NOT_AVAILABLE: 'NOT_AVAILABLE', UNKNOWN: 'UNKNOWN', } as const; type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; type UsageError = { code: ErrorCode; message: string; }; // Fixed refresh interval (45 seconds) const REFRESH_INTERVAL_SECONDS = 45; // Helper to format reset time for Codex function formatCodexResetTime(unixTimestamp: number): string { const date = new Date(unixTimestamp * 1000); const now = new Date(); const diff = date.getTime() - now.getTime(); if (diff < 3600000) { const mins = Math.ceil(diff / 60000); return `Resets in ${mins}m`; } if (diff < 86400000) { const hours = Math.floor(diff / 3600000); const mins = Math.ceil((diff % 3600000) / 60000); return `Resets in ${hours}h ${mins > 0 ? `${mins}m` : ''}`; } return `Resets ${date.toLocaleDateString()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; } // Helper to format window duration for Codex function getCodexWindowLabel(durationMins: number): { title: string; subtitle: string } { if (durationMins < 60) { return { title: `${durationMins}min Window`, subtitle: 'Rate limit' }; } if (durationMins < 1440) { const hours = Math.round(durationMins / 60); return { title: `${hours}h Window`, subtitle: 'Rate limit' }; } const days = Math.round(durationMins / 1440); return { title: `${days}d Window`, subtitle: 'Rate limit' }; } export function UsagePopover() { const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore(); const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore(); const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus); const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus); const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState<'claude' | 'codex'>('claude'); const [claudeLoading, setClaudeLoading] = useState(false); const [codexLoading, setCodexLoading] = useState(false); const [claudeError, setClaudeError] = useState(null); const [codexError, setCodexError] = useState(null); // Check authentication status const isClaudeAuthenticated = !!claudeAuthStatus?.authenticated; const isCodexAuthenticated = codexAuthStatus?.authenticated; // Determine which tab to show by default useEffect(() => { if (isClaudeAuthenticated) { setActiveTab('claude'); } else if (isCodexAuthenticated) { setActiveTab('codex'); } }, [isClaudeAuthenticated, isCodexAuthenticated]); // Check if data is stale (older than 2 minutes) const isClaudeStale = useMemo(() => { return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000; }, [claudeUsageLastUpdated]); const isCodexStale = useMemo(() => { return !codexUsageLastUpdated || Date.now() - codexUsageLastUpdated > 2 * 60 * 1000; }, [codexUsageLastUpdated]); const fetchClaudeUsage = useCallback( async (isAutoRefresh = false) => { if (!isAutoRefresh) setClaudeLoading(true); setClaudeError(null); try { const api = getElectronAPI(); if (!api.claude) { setClaudeError({ code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message: 'Claude API bridge not available', }); return; } const data = await api.claude.getUsage(); if ('error' in data) { setClaudeError({ code: ERROR_CODES.AUTH_ERROR, message: data.message || data.error, }); return; } setClaudeUsage(data); } catch (err) { setClaudeError({ code: ERROR_CODES.UNKNOWN, message: err instanceof Error ? err.message : 'Failed to fetch usage', }); } finally { if (!isAutoRefresh) setClaudeLoading(false); } }, [setClaudeUsage] ); const fetchCodexUsage = useCallback( async (isAutoRefresh = false) => { if (!isAutoRefresh) setCodexLoading(true); setCodexError(null); try { const api = getElectronAPI(); if (!api.codex) { setCodexError({ code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message: 'Codex API bridge not available', }); return; } const data = await api.codex.getUsage(); if ('error' in data) { if ( data.message?.includes('not available') || data.message?.includes('does not provide') ) { setCodexError({ code: ERROR_CODES.NOT_AVAILABLE, message: data.message || data.error, }); } else { setCodexError({ code: ERROR_CODES.AUTH_ERROR, message: data.message || data.error, }); } return; } setCodexUsage(data); } catch (err) { setCodexError({ code: ERROR_CODES.UNKNOWN, message: err instanceof Error ? err.message : 'Failed to fetch usage', }); } finally { if (!isAutoRefresh) setCodexLoading(false); } }, [setCodexUsage] ); // Auto-fetch on mount if data is stale useEffect(() => { if (isClaudeStale && isClaudeAuthenticated) { fetchClaudeUsage(true); } }, [isClaudeStale, isClaudeAuthenticated, fetchClaudeUsage]); useEffect(() => { if (isCodexStale && isCodexAuthenticated) { fetchCodexUsage(true); } }, [isCodexStale, isCodexAuthenticated, fetchCodexUsage]); // Auto-refresh when popover is open useEffect(() => { if (!open) return; // Fetch based on active tab if (activeTab === 'claude' && isClaudeAuthenticated) { if (!claudeUsage || isClaudeStale) { fetchClaudeUsage(); } const intervalId = setInterval(() => { fetchClaudeUsage(true); }, REFRESH_INTERVAL_SECONDS * 1000); return () => clearInterval(intervalId); } if (activeTab === 'codex' && isCodexAuthenticated) { if (!codexUsage || isCodexStale) { fetchCodexUsage(); } const intervalId = setInterval(() => { fetchCodexUsage(true); }, REFRESH_INTERVAL_SECONDS * 1000); return () => clearInterval(intervalId); } }, [ open, activeTab, claudeUsage, isClaudeStale, isClaudeAuthenticated, codexUsage, isCodexStale, isCodexAuthenticated, fetchClaudeUsage, fetchCodexUsage, ]); // Derived status color/icon helper const getStatusInfo = (percentage: number) => { if (percentage >= 75) return { color: 'text-red-500', icon: XCircle, bg: 'bg-red-500' }; if (percentage >= 50) return { color: 'text-orange-500', icon: AlertTriangle, bg: 'bg-orange-500' }; return { color: 'text-green-500', icon: CheckCircle, bg: 'bg-green-500' }; }; // Helper component for the progress bar const ProgressBar = ({ percentage, colorClass }: { percentage: number; colorClass: string }) => (
); const UsageCard = ({ title, subtitle, percentage, resetText, isPrimary = false, stale = false, }: { title: string; subtitle: string; percentage: number; resetText?: string; isPrimary?: boolean; stale?: boolean; }) => { const isValidPercentage = typeof percentage === 'number' && !isNaN(percentage) && isFinite(percentage); const safePercentage = isValidPercentage ? percentage : 0; const status = getStatusInfo(safePercentage); const StatusIcon = status.icon; return (

{title}

{subtitle}

{isValidPercentage ? (
{Math.round(safePercentage)}%
) : ( N/A )}
{resetText && (

{resetText}

)}
); }; // Calculate max percentage for header button const claudeMaxPercentage = claudeUsage ? Math.max(claudeUsage.sessionPercentage || 0, claudeUsage.weeklyPercentage || 0) : 0; const codexMaxPercentage = codexUsage?.rateLimits ? Math.max( codexUsage.rateLimits.primary?.usedPercent || 0, codexUsage.rateLimits.secondary?.usedPercent || 0 ) : 0; const maxPercentage = Math.max(claudeMaxPercentage, codexMaxPercentage); const isStale = activeTab === 'claude' ? isClaudeStale : isCodexStale; const getProgressBarColor = (percentage: number) => { if (percentage >= 80) return 'bg-red-500'; if (percentage >= 50) return 'bg-yellow-500'; return 'bg-green-500'; }; const trigger = ( ); // Determine which tabs to show const showClaudeTab = isClaudeAuthenticated; const showCodexTab = isCodexAuthenticated; return ( {trigger} setActiveTab(v as 'claude' | 'codex')}> {/* Tabs Header */} {showClaudeTab && showCodexTab && ( Claude Codex )} {/* Claude Tab Content */} {/* Header */}
Claude Usage
{claudeError && ( )}
{/* Content */}
{claudeError ? (

{claudeError.message}

{claudeError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? ( 'Ensure the Electron bridge is running or restart the app' ) : ( <> Make sure Claude CLI is installed and authenticated via{' '} claude login )}

) : !claudeUsage ? (

Loading usage data...

) : ( <>
{claudeUsage.costLimit && claudeUsage.costLimit > 0 && ( 0 ? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100 : 0 } stale={isClaudeStale} /> )} )}
{/* Footer */}
Claude Status Updates every minute
{/* Codex Tab Content */} {/* Header */}
Codex Usage
{codexError && codexError.code !== ERROR_CODES.NOT_AVAILABLE && ( )}
{/* Content */}
{codexError ? (

{codexError.code === ERROR_CODES.NOT_AVAILABLE ? 'Usage not available' : codexError.message}

{codexError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? ( 'Ensure the Electron bridge is running or restart the app' ) : codexError.code === ERROR_CODES.NOT_AVAILABLE ? ( <> Codex CLI doesn't provide usage statistics. Check{' '} OpenAI dashboard {' '} for usage details. ) : ( <> Make sure Codex CLI is installed and authenticated via{' '} codex login )}

) : !codexUsage ? (

Loading usage data...

) : codexUsage.rateLimits ? ( <> {codexUsage.rateLimits.primary && ( )} {codexUsage.rateLimits.secondary && ( )} {codexUsage.rateLimits.planType && (

Plan:{' '} {codexUsage.rateLimits.planType.charAt(0).toUpperCase() + codexUsage.rateLimits.planType.slice(1)}

)} ) : (

No usage data available

)}
{/* Footer */}
OpenAI Dashboard Updates every minute
); }