diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx
index babef8dc..7f678b3a 100644
--- a/ui/src/components/account/flow-viz/account-card.tsx
+++ b/ui/src/components/account/flow-viz/account-card.tsx
@@ -2,24 +2,14 @@
* Account Card Component for Flow Visualization
*/
-import {
- cn,
- formatResetTime,
- getModelsWithTiers,
- groupModelsByTier,
- getProviderMinQuota,
- getProviderResetTime,
- isAgyQuotaResult,
- isCodexQuotaResult,
- isGeminiQuotaResult,
- type ModelTier,
-} from '@/lib/utils';
+import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
-import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react';
+import { GripVertical, Loader2, Pause, Play } from 'lucide-react';
import { useAccountQuota, QUOTA_SUPPORTED_PROVIDERS } from '@/hooks/use-cliproxy-stats';
import type { QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
+import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
import type { AccountData, DragOffset } from './types';
import { cleanEmail } from './utils';
@@ -105,6 +95,7 @@ export function AccountCard({
// Use shared helper for provider-specific minimum quota
const minQuota = getProviderMinQuota(account.provider, quota);
+ const resetTime = getProviderResetTime(account.provider, quota);
// Tier badge (AGY only) - show P for Pro, U for Ultra
const showTierBadge =
@@ -254,95 +245,7 @@ export function AccountCard({
- {quota && isAgyQuotaResult(quota) ? (
-
-
Model Quotas:
- {(() => {
- const tiered = getModelsWithTiers(quota.models || []);
- const groups = groupModelsByTier(tiered);
- const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
- return tierOrder.map((tier, idx) => {
- const models = groups.get(tier);
- if (!models || models.length === 0) return null;
- const isFirst = tierOrder
- .slice(0, idx)
- .every((t) => !groups.get(t)?.length);
- return (
-
- {!isFirst &&
}
- {models.map((m) => (
-
-
- {m.displayName}
-
-
- {m.percentage}%
-
-
- ))}
-
- );
- });
- })()}
- {(() => {
- const resetTime = getProviderResetTime('agy', quota);
- return resetTime ? (
-
-
-
- Resets {formatResetTime(resetTime)}
-
-
- ) : null;
- })()}
-
- ) : quota && isCodexQuotaResult(quota) ? (
-
-
Rate Limits:
- {quota.windows.map((w) => (
-
-
- {w.label}
-
- {w.remainingPercent}%
-
- ))}
- {(() => {
- const resetTime = getProviderResetTime('codex', quota);
- return resetTime ? (
-
-
-
- Resets {formatResetTime(resetTime)}
-
-
- ) : null;
- })()}
-
- ) : quota && isGeminiQuotaResult(quota) ? (
-
-
Buckets:
- {quota.buckets.map((b) => (
-
-
- {b.label}
-
- {b.remainingPercent}%
-
- ))}
- {(() => {
- const resetTime = getProviderResetTime('gemini', quota);
- return resetTime ? (
-
-
-
- Resets {formatResetTime(resetTime)}
-
-
- ) : null;
- })()}
-
- ) : null}
+ {quota && }
diff --git a/ui/src/components/cliproxy/provider-editor/account-item.tsx b/ui/src/components/cliproxy/provider-editor/account-item.tsx
index 474d4db4..a7e58558 100644
--- a/ui/src/components/cliproxy/provider-editor/account-item.tsx
+++ b/ui/src/components/cliproxy/provider-editor/account-item.tsx
@@ -29,20 +29,10 @@ import {
FolderCode,
Check,
} from 'lucide-react';
-import {
- cn,
- formatResetTime,
- getModelsWithTiers,
- groupModelsByTier,
- getProviderMinQuota,
- getProviderResetTime,
- isAgyQuotaResult,
- isCodexQuotaResult,
- isGeminiQuotaResult,
- type ModelTier,
-} from '@/lib/utils';
+import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
+import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
import type { AccountItemProps } from './types';
/**
@@ -125,97 +115,6 @@ export function AccountItem({
const minQuota = getProviderMinQuota(account.provider, quota);
const nextReset = getProviderResetTime(account.provider, quota);
- // Provider-specific tooltip content renderer using type guards
- const renderQuotaTooltip = () => {
- if (!quota?.success) return null;
-
- // Antigravity (agy) provider tooltip
- if (isAgyQuotaResult(quota)) {
- const tiered = getModelsWithTiers(quota.models || []);
- const groups = groupModelsByTier(tiered);
- const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
- return (
-
-
Model Quotas:
- {tierOrder.map((tier, idx) => {
- const models = groups.get(tier);
- if (!models || models.length === 0) return null;
- const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length);
- return (
-
- {!isFirst &&
}
- {models.map((m) => (
-
-
- {m.displayName}
-
-
- {m.percentage}%
-
-
- ))}
-
- );
- })}
- {nextReset && (
-
-
- Resets {formatResetTime(nextReset)}
-
- )}
-
- );
- }
-
- // Codex provider tooltip
- if (isCodexQuotaResult(quota)) {
- return (
-
-
Rate Limit Windows:
- {quota.planType &&
Plan: {quota.planType}
}
- {quota.windows.map((w) => (
-
- {w.label}
- {w.remainingPercent}%
-
- ))}
- {nextReset && (
-
-
- Resets {formatResetTime(nextReset)}
-
- )}
-
- );
- }
-
- // Gemini provider tooltip
- if (isGeminiQuotaResult(quota)) {
- return (
-
-
Model Buckets:
- {quota.buckets.map((b) => (
-
-
- {b.label}
- {b.tokenType ? ` (${b.tokenType})` : ''}
-
- {b.remainingPercent}%
-
- ))}
- {nextReset && (
-
-
- Resets {formatResetTime(nextReset)}
-
- )}
-
- );
- }
-
- return null;
- };
-
return (
- {renderQuotaTooltip()}
+ {quota && }
diff --git a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx
index 464e30b4..ed5ff819 100644
--- a/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx
+++ b/ui/src/components/cliproxy/provider-editor/model-config-tab.tsx
@@ -11,6 +11,7 @@ import { AccountsSection } from './accounts-section';
import { api } from '@/lib/api-client';
import type { ProviderCatalog } from '../provider-model-selector';
import type { OAuthAccount } from '@/lib/api-client';
+import { QUOTA_SUPPORTED_PROVIDERS, type QuotaSupportedProvider } from '@/hooks/use-cliproxy-stats';
interface ModelConfigTabProps {
provider: string;
@@ -167,7 +168,9 @@ export function ModelConfigTab({
isBulkPausing={isBulkPausing}
isBulkResuming={isBulkResuming}
privacyMode={privacyMode}
- showQuota={['agy', 'codex', 'gemini'].includes(provider) && !isRemoteMode}
+ showQuota={
+ QUOTA_SUPPORTED_PROVIDERS.includes(provider as QuotaSupportedProvider) && !isRemoteMode
+ }
isKiro={isKiro}
kiroNoIncognito={kiroNoIncognito}
onKiroNoIncognitoChange={saveKiroNoIncognito}
diff --git a/ui/src/components/shared/index.ts b/ui/src/components/shared/index.ts
index 602a9712..ae67a7e4 100644
--- a/ui/src/components/shared/index.ts
+++ b/ui/src/components/shared/index.ts
@@ -17,6 +17,7 @@ export { PrivacyToggle } from './privacy-toggle';
export { ProjectSelectionDialog } from './project-selection-dialog';
export { ProviderIcon } from './provider-icon';
export { QuickCommands } from './quick-commands';
+export { QuotaTooltipContent } from './quota-tooltip-content';
export { SettingsDialog } from './settings-dialog';
export { SponsorButton } from './sponsor-button';
export { StatCard } from './stat-card';
diff --git a/ui/src/components/shared/quota-tooltip-content.tsx b/ui/src/components/shared/quota-tooltip-content.tsx
new file mode 100644
index 00000000..177394e8
--- /dev/null
+++ b/ui/src/components/shared/quota-tooltip-content.tsx
@@ -0,0 +1,116 @@
+/**
+ * Shared Quota Tooltip Content Component
+ * Displays provider-specific quota information in tooltips
+ */
+
+import { Clock } from 'lucide-react';
+import {
+ cn,
+ formatResetTime,
+ getModelsWithTiers,
+ groupModelsByTier,
+ isAgyQuotaResult,
+ isCodexQuotaResult,
+ isGeminiQuotaResult,
+ type ModelTier,
+ type UnifiedQuotaResult,
+} from '@/lib/utils';
+
+interface QuotaTooltipContentProps {
+ quota: UnifiedQuotaResult;
+ resetTime: string | null;
+}
+
+/**
+ * Renders provider-specific quota tooltip content
+ * Uses type guards for proper TypeScript narrowing
+ */
+export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentProps) {
+ if (!quota?.success) return null;
+
+ // Antigravity (agy) provider tooltip
+ if (isAgyQuotaResult(quota)) {
+ const tiered = getModelsWithTiers(quota.models || []);
+ const groups = groupModelsByTier(tiered);
+ const tierOrder: ModelTier[] = ['primary', 'gemini-3', 'gemini-2', 'other'];
+
+ return (
+
+
Model Quotas:
+ {tierOrder.map((tier, idx) => {
+ const models = groups.get(tier);
+ if (!models || models.length === 0) return null;
+ const isFirst = tierOrder.slice(0, idx).every((t) => !groups.get(t)?.length);
+ return (
+
+ {!isFirst &&
}
+ {models.map((m) => (
+
+
+ {m.displayName}
+
+
+ {m.percentage}%
+
+
+ ))}
+
+ );
+ })}
+
+
+ );
+ }
+
+ // Codex provider tooltip
+ if (isCodexQuotaResult(quota)) {
+ return (
+
+
Rate Limits:
+ {quota.planType &&
Plan: {quota.planType}
}
+ {quota.windows.map((w) => (
+
+ {w.label}
+ {w.remainingPercent}%
+
+ ))}
+
+
+ );
+ }
+
+ // Gemini provider tooltip
+ if (isGeminiQuotaResult(quota)) {
+ return (
+
+
Buckets:
+ {quota.buckets.map((b) => (
+
+
+ {b.label}
+ {b.tokenType ? ` (${b.tokenType})` : ''}
+
+ {b.remainingPercent}%
+
+ ))}
+
+
+ );
+ }
+
+ return null;
+}
+
+/**
+ * Reset time indicator shown at bottom of tooltip
+ */
+function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) {
+ if (!resetTime) return null;
+
+ return (
+
+
+ Resets {formatResetTime(resetTime)}
+
+ );
+}