import { useState, useEffect, useMemo, useCallback } 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 { cn } from "@/lib/utils"; import { getElectronAPI } from "@/lib/electron"; import { useAppStore } from "@/store/app-store"; export function ClaudeUsagePopover() { const { claudeRefreshInterval, claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes const isStale = useMemo(() => { return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000; }, [claudeUsageLastUpdated]); const fetchUsage = useCallback(async (isAutoRefresh = false) => { if (!isAutoRefresh) setLoading(true); setError(null); try { const api = getElectronAPI(); if (!api.claude) { throw new Error("Claude API not available"); } const data = await api.claude.getUsage(); if ("error" in data) { throw new Error(data.message || data.error); } setClaudeUsage(data); } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch usage"); } finally { if (!isAutoRefresh) setLoading(false); } }, [setClaudeUsage]); // Auto-fetch on mount if data is stale useEffect(() => { if (isStale) { fetchUsage(true); } }, [isStale, fetchUsage]); useEffect(() => { // Initial fetch when opened if (open) { if (!claudeUsage || isStale) { fetchUsage(); } } // Auto-refresh interval (only when open) let intervalId: NodeJS.Timeout | null = null; if (open && claudeRefreshInterval > 0) { intervalId = setInterval(() => { fetchUsage(true); }, claudeRefreshInterval * 1000); } return () => { if (intervalId) clearInterval(intervalId); }; }, [open, claudeUsage, isStale, claudeRefreshInterval, fetchUsage]); // Derived status color/icon helper const getStatusInfo = (percentage: number) => { if (percentage >= 80) 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; }) => { // Check if percentage is valid (not NaN, not undefined, is a finite number) 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 && (

{title === "Session Usage" && } {resetText}

)}
); }; // Header Button const maxPercentage = claudeUsage ? Math.max(claudeUsage.sessionPercentage || 0, claudeUsage.weeklyPercentage || 0) : 0; const getProgressBarColor = (percentage: number) => { if (percentage >= 100) return "bg-red-500"; if (percentage >= 75) return "bg-yellow-500"; return "bg-green-500"; }; const trigger = ( ); return ( {trigger} {/* Header */}
Claude Usage
{/* Content */}
{error ? (

{error}

Make sure Claude CLI is installed and authenticated via claude login

) : !claudeUsage ? ( // Loading state

Loading usage data...

) : ( <> {/* Primary Card */} {/* Secondary Cards Grid */}
{/* Extra Usage / Cost */} {claudeUsage.costLimit && claudeUsage.costLimit > 0 && ( 0 ? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100 : 0 } stale={isStale} /> )} )}
{/* Footer */}
Claude Status
{/* Could add quick settings link here */}
); }