Files
automaker/apps/ui/src/components/usage-popover.tsx

1016 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef, useMemo } 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 { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { useSetupStore } from '@/store/setup-store';
import { AnthropicIcon, OpenAIIcon, ZaiIcon, GeminiIcon } from '@/components/ui/provider-icon';
import { useClaudeUsage, useCodexUsage, useZaiUsage, useGeminiUsage } from '@/hooks/queries';
import {
getExpectedWeeklyPacePercentage,
getExpectedCodexPacePercentage,
getPaceStatusLabel,
} from '@/store/utils/usage-utils';
// Error codes for distinguishing failure modes
const ERROR_CODES = {
API_BRIDGE_UNAVAILABLE: 'API_BRIDGE_UNAVAILABLE',
AUTH_ERROR: 'AUTH_ERROR',
NOT_AVAILABLE: 'NOT_AVAILABLE',
TRUST_PROMPT: 'TRUST_PROMPT',
UNKNOWN: 'UNKNOWN',
} as const;
type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
type UsageError = {
code: ErrorCode;
message: string;
};
const CLAUDE_SESSION_WINDOW_HOURS = 5;
// Helper to format reset time for Codex/z.ai (unix timestamp in seconds or milliseconds)
function formatResetTime(unixTimestamp: number, isMilliseconds = false): string {
const date = new Date(isMilliseconds ? unixTimestamp : unixTimestamp * 1000);
const now = new Date();
const diff = date.getTime() - now.getTime();
// Guard against past timestamps: clamp negative diffs to a friendly fallback
if (diff <= 0) {
return 'Resets now';
}
if (diff < 3600000) {
const mins = Math.max(0, Math.ceil(diff / 60000));
return `Resets in ${mins}m`;
}
if (diff < 86400000) {
const hours = Math.max(0, Math.floor(diff / 3600000));
const mins = Math.max(0, 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' };
}
// Helper to format large numbers with K/M suffixes
function formatNumber(num: number): string {
if (num >= 1_000_000_000) {
return `${(num / 1_000_000_000).toFixed(1)}B`;
}
if (num >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}M`;
}
if (num >= 1_000) {
return `${(num / 1_000).toFixed(1)}K`;
}
return num.toLocaleString();
}
export function UsagePopover() {
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
const zaiAuthStatus = useSetupStore((state) => state.zaiAuthStatus);
const geminiAuthStatus = useSetupStore((state) => state.geminiAuthStatus);
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'claude' | 'codex' | 'zai' | 'gemini'>('claude');
// Track whether the user has manually selected a tab so we don't override their choice
const userHasSelected = useRef(false);
// Check authentication status — use explicit boolean coercion so hooks never
// receive undefined for their `enabled` parameter during auth-loading
const isClaudeAuthenticated = !!claudeAuthStatus?.authenticated;
const isCodexAuthenticated = !!codexAuthStatus?.authenticated;
const isZaiAuthenticated = !!zaiAuthStatus?.authenticated;
const isGeminiAuthenticated = !!geminiAuthStatus?.authenticated;
// Use React Query hooks for usage data
// Only enable polling when popover is open AND the tab is active
const {
data: claudeUsage,
isLoading: claudeLoading,
error: claudeQueryError,
dataUpdatedAt: claudeUsageLastUpdated,
refetch: refetchClaude,
} = useClaudeUsage(open && activeTab === 'claude' && isClaudeAuthenticated);
const {
data: codexUsage,
isLoading: codexLoading,
error: codexQueryError,
dataUpdatedAt: codexUsageLastUpdated,
refetch: refetchCodex,
} = useCodexUsage(open && activeTab === 'codex' && isCodexAuthenticated);
const {
data: zaiUsage,
isLoading: zaiLoading,
error: zaiQueryError,
dataUpdatedAt: zaiUsageLastUpdated,
refetch: refetchZai,
} = useZaiUsage(open && activeTab === 'zai' && isZaiAuthenticated);
const {
data: geminiUsage,
isLoading: geminiLoading,
error: geminiQueryError,
dataUpdatedAt: geminiUsageLastUpdated,
refetch: refetchGemini,
} = useGeminiUsage(open && activeTab === 'gemini' && isGeminiAuthenticated);
// Parse errors into structured format
const claudeError = useMemo((): UsageError | null => {
if (!claudeQueryError) return null;
const message =
claudeQueryError instanceof Error ? claudeQueryError.message : String(claudeQueryError);
// Detect trust prompt error
const isTrustPrompt = message.includes('Trust prompt') || message.includes('folder permission');
if (isTrustPrompt) {
return { code: ERROR_CODES.TRUST_PROMPT, message };
}
if (message.includes('API bridge')) {
return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
}
return { code: ERROR_CODES.AUTH_ERROR, message };
}, [claudeQueryError]);
const codexError = useMemo((): UsageError | null => {
if (!codexQueryError) return null;
const message =
codexQueryError instanceof Error ? codexQueryError.message : String(codexQueryError);
if (message.includes('not available') || message.includes('does not provide')) {
return { code: ERROR_CODES.NOT_AVAILABLE, message };
}
if (message.includes('API bridge')) {
return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
}
return { code: ERROR_CODES.AUTH_ERROR, message };
}, [codexQueryError]);
const zaiError = useMemo((): UsageError | null => {
if (!zaiQueryError) return null;
const message = zaiQueryError instanceof Error ? zaiQueryError.message : String(zaiQueryError);
if (message.includes('not configured') || message.includes('API token')) {
return { code: ERROR_CODES.NOT_AVAILABLE, message };
}
if (message.includes('API bridge')) {
return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
}
return { code: ERROR_CODES.AUTH_ERROR, message };
}, [zaiQueryError]);
const geminiError = useMemo((): UsageError | null => {
if (!geminiQueryError) return null;
const message =
geminiQueryError instanceof Error ? geminiQueryError.message : String(geminiQueryError);
if (message.includes('not configured') || message.includes('not authenticated')) {
return { code: ERROR_CODES.NOT_AVAILABLE, message };
}
if (message.includes('API bridge')) {
return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
}
return { code: ERROR_CODES.AUTH_ERROR, message };
}, [geminiQueryError]);
// Determine which tab to show by default.
// Only apply the default when the popover opens (open transitions to true) and the user has
// not yet made a manual selection during this session. This prevents auth-flag re-renders from
// overriding a tab the user explicitly clicked.
useEffect(() => {
if (!open) {
// Reset the user-selection guard each time the popover closes so the next open always gets
// a fresh default.
userHasSelected.current = false;
return;
}
// The user already picked a tab respect their choice.
if (userHasSelected.current) {
return;
}
// Pick the first available provider in priority order.
if (isClaudeAuthenticated) {
setActiveTab('claude');
} else if (isCodexAuthenticated) {
setActiveTab('codex');
} else if (isZaiAuthenticated) {
setActiveTab('zai');
} else if (isGeminiAuthenticated) {
setActiveTab('gemini');
}
}, [
open,
isClaudeAuthenticated,
isCodexAuthenticated,
isZaiAuthenticated,
isGeminiAuthenticated,
]);
// 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 isZaiStale = useMemo(() => {
return !zaiUsageLastUpdated || Date.now() - zaiUsageLastUpdated > 2 * 60 * 1000;
}, [zaiUsageLastUpdated]);
const isGeminiStale = useMemo(() => {
return !geminiUsageLastUpdated || Date.now() - geminiUsageLastUpdated > 2 * 60 * 1000;
}, [geminiUsageLastUpdated]);
// 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 with optional pace indicator
const ProgressBar = ({
percentage,
colorClass,
pacePercentage,
}: {
percentage: number;
colorClass: string;
pacePercentage?: number | null;
}) => (
<div className="relative h-2 w-full bg-secondary/50 rounded-full overflow-hidden">
<div
className={cn('h-full transition-all duration-500', colorClass)}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
{pacePercentage != null && pacePercentage > 0 && pacePercentage < 100 && (
<div
className="absolute top-0 h-full w-0.5 bg-foreground/60"
style={{ left: `${pacePercentage}%` }}
title={`Expected: ${Math.round(pacePercentage)}%`}
/>
)}
</div>
);
const UsageCard = ({
title,
subtitle,
percentage,
resetText,
isPrimary = false,
stale = false,
pacePercentage,
}: {
title: string;
subtitle: string;
percentage: number;
resetText?: string;
isPrimary?: boolean;
stale?: boolean;
pacePercentage?: number | null;
}) => {
const isValidPercentage =
typeof percentage === 'number' && !isNaN(percentage) && isFinite(percentage);
const safePercentage = isValidPercentage ? percentage : 0;
const status = getStatusInfo(safePercentage);
const StatusIcon = status.icon;
const paceLabel =
isValidPercentage && pacePercentage != null
? getPaceStatusLabel(safePercentage, pacePercentage)
: null;
return (
<div
className={cn(
'rounded-xl border bg-card/50 p-4 transition-opacity',
isPrimary ? 'border-border/60 shadow-sm' : 'border-border/40',
(stale || !isValidPercentage) && 'opacity-50'
)}
>
<div className="flex items-start justify-between mb-3">
<div>
<h4 className={cn('font-semibold', isPrimary ? 'text-sm' : 'text-xs')}>{title}</h4>
<p className="text-[10px] text-muted-foreground">{subtitle}</p>
</div>
{isValidPercentage ? (
<div className="flex items-center gap-1.5">
<StatusIcon className={cn('w-3.5 h-3.5', status.color)} />
<span
className={cn(
'font-mono font-bold',
status.color,
isPrimary ? 'text-base' : 'text-sm'
)}
>
{Math.round(safePercentage)}%
</span>
</div>
) : (
<span className="text-xs text-muted-foreground">N/A</span>
)}
</div>
<ProgressBar
percentage={safePercentage}
colorClass={isValidPercentage ? status.bg : 'bg-muted-foreground/30'}
pacePercentage={pacePercentage}
/>
<div className="mt-2 flex items-center justify-between">
{paceLabel ? (
<p
className={cn(
'text-[10px] font-medium',
safePercentage > (pacePercentage ?? 0) ? 'text-orange-500' : 'text-green-500'
)}
>
{paceLabel}
</p>
) : (
<div />
)}
{resetText && (
<p className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
{resetText}
</p>
)}
</div>
</div>
);
};
// Calculate max percentage for header button
const claudeSessionPercentage = claudeUsage?.sessionPercentage || 0;
const zaiMaxPercentage = zaiUsage?.quotaLimits
? Math.max(
zaiUsage.quotaLimits.tokens?.usedPercent || 0,
zaiUsage.quotaLimits.mcp?.usedPercent || 0
)
: 0;
// Gemini quota from Google Cloud API (if available)
// Default to 0 when usedPercent is not available to avoid a misleading full-red indicator
const geminiMaxPercentage = geminiUsage?.usedPercent ?? 0;
const getProgressBarColor = (percentage: number) => {
if (percentage >= 80) return 'bg-red-500';
if (percentage >= 50) return 'bg-yellow-500';
return 'bg-green-500';
};
const codexPrimaryWindowMinutes = codexUsage?.rateLimits?.primary?.windowDurationMins ?? null;
const codexSecondaryWindowMinutes = codexUsage?.rateLimits?.secondary?.windowDurationMins ?? null;
const codexWindowMinutes =
codexSecondaryWindowMinutes && codexPrimaryWindowMinutes
? Math.min(codexPrimaryWindowMinutes, codexSecondaryWindowMinutes)
: (codexSecondaryWindowMinutes ?? codexPrimaryWindowMinutes);
const codexWindowUsage =
codexWindowMinutes === codexSecondaryWindowMinutes
? codexUsage?.rateLimits?.secondary?.usedPercent
: codexUsage?.rateLimits?.primary?.usedPercent;
// Determine which provider icon and percentage to show based on active tab
const indicatorInfo =
activeTab === 'claude'
? {
icon: AnthropicIcon,
percentage: claudeSessionPercentage,
isStale: isClaudeStale,
title: `Session usage (${CLAUDE_SESSION_WINDOW_HOURS}h window)`,
}
: activeTab === 'codex'
? {
icon: OpenAIIcon,
percentage: codexWindowUsage ?? 0,
isStale: isCodexStale,
}
: activeTab === 'zai'
? {
icon: ZaiIcon,
percentage: zaiMaxPercentage,
isStale: isZaiStale,
title: `Usage (z.ai)`,
}
: activeTab === 'gemini'
? {
icon: GeminiIcon,
percentage: geminiMaxPercentage,
isStale: isGeminiStale,
title: `Usage (Gemini)`,
}
: null;
const statusColor = indicatorInfo ? getStatusInfo(indicatorInfo.percentage).color : '';
const ProviderIcon = indicatorInfo?.icon;
const trigger = (
<Button variant="ghost" size="sm" className="h-9 gap-2 bg-secondary border border-border px-3">
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && ProviderIcon && (
<ProviderIcon className={cn('w-4 h-4', statusColor)} />
)}
<span className="text-sm font-medium">Usage</span>
{(claudeUsage || codexUsage || zaiUsage || geminiUsage) && indicatorInfo && (
<div
title={indicatorInfo.title}
className={cn(
'h-1.5 w-16 bg-muted-foreground/20 rounded-full overflow-hidden transition-opacity',
indicatorInfo.isStale && 'opacity-60'
)}
>
<div
className={cn(
'h-full transition-all duration-500',
getProgressBarColor(indicatorInfo.percentage)
)}
style={{ width: `${Math.min(indicatorInfo.percentage, 100)}%` }}
/>
</div>
)}
</Button>
);
// Determine which tabs to show
const showClaudeTab = isClaudeAuthenticated;
const showCodexTab = isCodexAuthenticated;
const showZaiTab = isZaiAuthenticated;
const showGeminiTab = isGeminiAuthenticated;
const tabCount = [showClaudeTab, showCodexTab, showZaiTab, showGeminiTab].filter(Boolean).length;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent
className="w-80 p-0 overflow-hidden bg-background/95 backdrop-blur-xl border-border shadow-2xl"
align="end"
sideOffset={8}
>
<Tabs
value={activeTab}
onValueChange={(v) => {
userHasSelected.current = true;
setActiveTab(v as 'claude' | 'codex' | 'zai' | 'gemini');
}}
>
{/* Tabs Header */}
{tabCount > 1 && (
<TabsList
className={cn(
'grid w-full rounded-none border-b border-border/50',
tabCount === 2 && 'grid-cols-2',
tabCount === 3 && 'grid-cols-3',
tabCount === 4 && 'grid-cols-4'
)}
>
{showClaudeTab && (
<TabsTrigger value="claude" className="gap-2">
<AnthropicIcon className="w-3.5 h-3.5" />
Claude
</TabsTrigger>
)}
{showCodexTab && (
<TabsTrigger value="codex" className="gap-2">
<OpenAIIcon className="w-3.5 h-3.5" />
Codex
</TabsTrigger>
)}
{showZaiTab && (
<TabsTrigger value="zai" className="gap-2">
<ZaiIcon className="w-3.5 h-3.5" />
z.ai
</TabsTrigger>
)}
{showGeminiTab && (
<TabsTrigger value="gemini" className="gap-2">
<GeminiIcon className="w-3.5 h-3.5" />
Gemini
</TabsTrigger>
)}
</TabsList>
)}
{/* Claude Tab Content */}
<TabsContent value="claude" className="m-0">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center gap-2">
<AnthropicIcon className="w-4 h-4" />
<span className="text-sm font-semibold">Claude Usage</span>
</div>
{claudeError && (
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', claudeLoading && 'opacity-80')}
onClick={() => !claudeLoading && refetchClaude()}
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
)}
</div>
{/* Content */}
<div className="p-4 space-y-4">
{claudeError ? (
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">{claudeError.message}</p>
<p className="text-xs text-muted-foreground">
{claudeError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : claudeError.code === ERROR_CODES.TRUST_PROMPT ? (
<>
Run <code className="font-mono bg-muted px-1 rounded">claude</code> in
your terminal and approve access to continue
</>
) : (
<>
Make sure Claude CLI is installed and authenticated via{' '}
<code className="font-mono bg-muted px-1 rounded">claude login</code>
</>
)}
</p>
</div>
</div>
) : !claudeUsage ? (
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : (
<>
<UsageCard
title="Session Usage"
subtitle="5-hour rolling window"
percentage={claudeUsage.sessionPercentage}
resetText={claudeUsage.sessionResetText}
isPrimary={true}
stale={isClaudeStale}
/>
<div className="grid grid-cols-2 gap-3">
<UsageCard
title="Sonnet"
subtitle="Weekly"
percentage={claudeUsage.sonnetWeeklyPercentage}
resetText={claudeUsage.sonnetResetText}
stale={isClaudeStale}
pacePercentage={getExpectedWeeklyPacePercentage(claudeUsage.weeklyResetTime)}
/>
<UsageCard
title="Weekly"
subtitle="All models"
percentage={claudeUsage.weeklyPercentage}
resetText={claudeUsage.weeklyResetText}
stale={isClaudeStale}
pacePercentage={getExpectedWeeklyPacePercentage(claudeUsage.weeklyResetTime)}
/>
</div>
{claudeUsage.costLimit && claudeUsage.costLimit > 0 && (
<UsageCard
title="Extra Usage"
subtitle={`${claudeUsage.costUsed ?? 0} / ${claudeUsage.costLimit} ${claudeUsage.costCurrency ?? ''}`}
percentage={
claudeUsage.costLimit > 0
? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100
: 0
}
stale={isClaudeStale}
/>
)}
</>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-2 bg-secondary/10 border-t border-border/50">
<a
href="https://status.claude.com"
target="_blank"
rel="noreferrer"
className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
>
Claude Status <ExternalLink className="w-2.5 h-2.5" />
</a>
<span className="text-[10px] text-muted-foreground">Updates every minute</span>
</div>
</TabsContent>
{/* Codex Tab Content */}
<TabsContent value="codex" className="m-0">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center gap-2">
<OpenAIIcon className="w-4 h-4" />
<span className="text-sm font-semibold">Codex Usage</span>
</div>
{codexError && codexError.code !== ERROR_CODES.NOT_AVAILABLE && (
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', codexLoading && 'opacity-80')}
onClick={() => !codexLoading && refetchCodex()}
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
)}
</div>
{/* Content */}
<div className="p-4 space-y-4">
{codexError ? (
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">
{codexError.code === ERROR_CODES.NOT_AVAILABLE
? 'Usage not available'
: codexError.message}
</p>
<p className="text-xs text-muted-foreground">
{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{' '}
<a
href="https://platform.openai.com/usage"
target="_blank"
rel="noreferrer"
className="underline hover:text-foreground"
>
OpenAI dashboard
</a>{' '}
for usage details.
</>
) : (
<>
Make sure Codex CLI is installed and authenticated via{' '}
<code className="font-mono bg-muted px-1 rounded">codex login</code>
</>
)}
</p>
</div>
</div>
) : !codexUsage ? (
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : codexUsage.rateLimits ? (
<>
{codexUsage.rateLimits.primary && (
<UsageCard
title={
getCodexWindowLabel(codexUsage.rateLimits.primary.windowDurationMins).title
}
subtitle={
getCodexWindowLabel(codexUsage.rateLimits.primary.windowDurationMins)
.subtitle
}
percentage={codexUsage.rateLimits.primary.usedPercent}
resetText={formatResetTime(codexUsage.rateLimits.primary.resetsAt)}
isPrimary={true}
stale={isCodexStale}
pacePercentage={getExpectedCodexPacePercentage(
codexUsage.rateLimits.primary.resetsAt,
codexUsage.rateLimits.primary.windowDurationMins
)}
/>
)}
{codexUsage.rateLimits.secondary && (
<UsageCard
title={
getCodexWindowLabel(codexUsage.rateLimits.secondary.windowDurationMins)
.title
}
subtitle={
getCodexWindowLabel(codexUsage.rateLimits.secondary.windowDurationMins)
.subtitle
}
percentage={codexUsage.rateLimits.secondary.usedPercent}
resetText={formatResetTime(codexUsage.rateLimits.secondary.resetsAt)}
stale={isCodexStale}
pacePercentage={getExpectedCodexPacePercentage(
codexUsage.rateLimits.secondary.resetsAt,
codexUsage.rateLimits.secondary.windowDurationMins
)}
/>
)}
{codexUsage.rateLimits.planType && (
<div className="rounded-xl border border-border/40 bg-secondary/20 p-3">
<p className="text-xs text-muted-foreground">
Plan:{' '}
<span className="text-foreground font-medium">
{codexUsage.rateLimits.planType.charAt(0).toUpperCase() +
codexUsage.rateLimits.planType.slice(1)}
</span>
</p>
</div>
)}
</>
) : (
<div className="flex flex-col items-center justify-center py-6 text-center">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<p className="text-sm font-medium mt-3">No usage data available</p>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-2 bg-secondary/10 border-t border-border/50">
<a
href="https://platform.openai.com/usage"
target="_blank"
rel="noreferrer"
className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
>
OpenAI Dashboard <ExternalLink className="w-2.5 h-2.5" />
</a>
<span className="text-[10px] text-muted-foreground">Updates every minute</span>
</div>
</TabsContent>
{/* z.ai Tab Content */}
<TabsContent value="zai" className="m-0">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center gap-2">
<ZaiIcon className="w-4 h-4" />
<span className="text-sm font-semibold">z.ai Usage</span>
</div>
{zaiError && zaiError.code !== ERROR_CODES.NOT_AVAILABLE && (
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', zaiLoading && 'opacity-80')}
onClick={() => !zaiLoading && refetchZai()}
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
)}
</div>
{/* Content */}
<div className="p-4 space-y-4">
{zaiError ? (
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">
{zaiError.code === ERROR_CODES.NOT_AVAILABLE
? 'z.ai not configured'
: zaiError.message}
</p>
<p className="text-xs text-muted-foreground">
{zaiError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : zaiError.code === ERROR_CODES.NOT_AVAILABLE ? (
<>
Set <code className="font-mono bg-muted px-1 rounded">Z_AI_API_KEY</code>{' '}
environment variable to enable z.ai usage tracking
</>
) : (
<>Check your z.ai API key configuration</>
)}
</p>
</div>
</div>
) : !zaiUsage ? (
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : zaiUsage.quotaLimits &&
(zaiUsage.quotaLimits.tokens || zaiUsage.quotaLimits.mcp) ? (
<>
{zaiUsage.quotaLimits.tokens && (
<UsageCard
title="Token Quota"
subtitle={`${formatNumber(zaiUsage.quotaLimits.tokens.used)} / ${formatNumber(zaiUsage.quotaLimits.tokens.limit)} tokens`}
percentage={zaiUsage.quotaLimits.tokens.usedPercent}
resetText={
zaiUsage.quotaLimits.tokens.nextResetTime
? formatResetTime(zaiUsage.quotaLimits.tokens.nextResetTime, true)
: undefined
}
isPrimary={true}
stale={isZaiStale}
/>
)}
{zaiUsage.quotaLimits.mcp && (
<UsageCard
title="MCP Quota"
subtitle={`${formatNumber(zaiUsage.quotaLimits.mcp.used)} / ${formatNumber(zaiUsage.quotaLimits.mcp.limit)} calls`}
percentage={zaiUsage.quotaLimits.mcp.usedPercent}
resetText={
zaiUsage.quotaLimits.mcp.nextResetTime
? formatResetTime(zaiUsage.quotaLimits.mcp.nextResetTime, true)
: undefined
}
stale={isZaiStale}
/>
)}
{zaiUsage.quotaLimits.planType && zaiUsage.quotaLimits.planType !== 'unknown' && (
<div className="rounded-xl border border-border/40 bg-secondary/20 p-3">
<p className="text-xs text-muted-foreground">
Plan:{' '}
<span className="text-foreground font-medium">
{zaiUsage.quotaLimits.planType.charAt(0).toUpperCase() +
zaiUsage.quotaLimits.planType.slice(1)}
</span>
</p>
</div>
)}
</>
) : (
<div className="flex flex-col items-center justify-center py-6 text-center">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<p className="text-sm font-medium mt-3">No usage data available</p>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-2 bg-secondary/10 border-t border-border/50">
<a
href="https://z.ai"
target="_blank"
rel="noreferrer"
className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
>
z.ai <ExternalLink className="w-2.5 h-2.5" />
</a>
<span className="text-[10px] text-muted-foreground">Updates every minute</span>
</div>
</TabsContent>
{/* Gemini Tab Content */}
<TabsContent value="gemini" className="m-0">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-secondary/10">
<div className="flex items-center gap-2">
<GeminiIcon className="w-4 h-4" />
<span className="text-sm font-semibold">Gemini Usage</span>
</div>
{geminiError && geminiError.code !== ERROR_CODES.NOT_AVAILABLE && (
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6', geminiLoading && 'opacity-80')}
onClick={() => !geminiLoading && refetchGemini()}
>
<RefreshCw className="w-3.5 h-3.5" />
</Button>
)}
</div>
{/* Content */}
<div className="p-4 space-y-4">
{geminiError ? (
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">
{geminiError.code === ERROR_CODES.NOT_AVAILABLE
? 'Gemini not configured'
: geminiError.message}
</p>
<p className="text-xs text-muted-foreground">
{geminiError.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : geminiError.code === ERROR_CODES.NOT_AVAILABLE ? (
<>
Run{' '}
<code className="font-mono bg-muted px-1 rounded">gemini auth login</code>{' '}
to authenticate with your Google account
</>
) : (
<>Check your Gemini CLI configuration</>
)}
</p>
</div>
</div>
) : !geminiUsage ? (
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : geminiUsage.authenticated ? (
<>
{/* Show Flash and Pro quota tiers */}
{geminiUsage.flashQuota || geminiUsage.proQuota ? (
<div className="grid grid-cols-2 gap-3">
{geminiUsage.flashQuota && (
<UsageCard
title="Flash"
subtitle="Flash models"
percentage={geminiUsage.flashQuota.usedPercent}
resetText={geminiUsage.flashQuota.resetText}
stale={isGeminiStale}
/>
)}
{geminiUsage.proQuota && (
<UsageCard
title="Pro"
subtitle="Pro models"
percentage={geminiUsage.proQuota.usedPercent}
resetText={geminiUsage.proQuota.resetText}
stale={isGeminiStale}
/>
)}
</div>
) : (
<>
{/* No quota data available - show connected status */}
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle className="w-5 h-5 text-emerald-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">Connected</p>
<p className="text-xs text-emerald-400/70 mt-0.5">
Authenticated via{' '}
<span className="font-mono">
{geminiUsage.authMethod === 'cli_login'
? 'CLI Login'
: geminiUsage.authMethod === 'api_key_env'
? 'API Key (Environment)'
: geminiUsage.authMethod === 'api_key'
? 'API Key'
: 'Unknown'}
</span>
</p>
</div>
</div>
<div className="rounded-xl border border-border/40 bg-secondary/20 p-3">
<p className="text-xs text-muted-foreground">
{geminiUsage.error ? (
<>Quota API: {geminiUsage.error}</>
) : (
<>No usage yet or quota data unavailable</>
)}
</p>
</div>
</>
)}
</>
) : (
<div className="flex flex-col items-center justify-center py-6 text-center">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<p className="text-sm font-medium mt-3">Not authenticated</p>
<p className="text-xs text-muted-foreground mt-1">
Run <code className="font-mono bg-muted px-1 rounded">gemini auth login</code>{' '}
to authenticate
</p>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-4 py-2 bg-secondary/10 border-t border-border/50">
<a
href="https://ai.google.dev"
target="_blank"
rel="noreferrer"
className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
>
Google AI <ExternalLink className="w-2.5 h-2.5" />
</a>
<span className="text-[10px] text-muted-foreground">Updates every minute</span>
</div>
</TabsContent>
</Tabs>
</PopoverContent>
</Popover>
);
}