import { useState, useMemo } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { useSetupStore } from '@/store/setup-store'; import { useCodexUsage } from '@/hooks/queries'; // 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; }; // Helper to format reset time function formatResetTime(unixTimestamp: number): string { const date = new Date(unixTimestamp * 1000); const now = new Date(); const diff = date.getTime() - now.getTime(); // If less than 1 hour, show minutes if (diff < 3600000) { const mins = Math.ceil(diff / 60000); return `Resets in ${mins}m`; } // If less than 24 hours, show hours and minutes 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` : ''}`; } // Otherwise show date return `Resets ${date.toLocaleDateString()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; } // Helper to format window duration function getWindowLabel(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 CodexUsagePopover() { const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus); const [open, setOpen] = useState(false); // Check if Codex is authenticated const isCodexAuthenticated = codexAuthStatus?.authenticated; // Use React Query for data fetching with automatic polling const { data: codexUsage, isFetching, error: queryError, dataUpdatedAt, refetch, } = useCodexUsage(isCodexAuthenticated); // Check if data is stale (older than 2 minutes) const isStale = useMemo(() => { return !dataUpdatedAt || Date.now() - dataUpdatedAt > 2 * 60 * 1000; }, [dataUpdatedAt]); // Convert query error to UsageError format for backward compatibility const error = useMemo((): UsageError | null => { if (!queryError) return null; const message = queryError instanceof Error ? queryError.message : String(queryError); if (message.includes('not available') || message.includes('does not provide')) { return { code: ERROR_CODES.NOT_AVAILABLE, message }; } if (message.includes('bridge') || message.includes('API')) { return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message }; } return { code: ERROR_CODES.AUTH_ERROR, message }; }, [queryError]); // 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 }) => (
{subtitle}
{error.code === ERROR_CODES.NOT_AVAILABLE ? 'Usage not available' : error.message}
{error.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : error.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
>
)}
Loading usage data...
Plan:{' '} {codexUsage.rateLimits.planType.charAt(0).toUpperCase() + codexUsage.rateLimits.planType.slice(1)}
No usage data available