import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; import { useAuthStore, useQuotaStore, useThemeStore } from '@/stores'; import { apiCallApi, authFilesApi, getApiCallErrorMessage } from '@/services/api'; import type { AntigravityQuotaGroup, AntigravityQuotaState, AuthFileItem, CodexQuotaState, CodexQuotaWindow, GeminiCliQuotaBucketState, GeminiCliQuotaState } from '@/types'; import styles from './QuotaPage.module.scss'; type ThemeColors = { bg: string; text: string; border?: string }; type TypeColorSet = { light: ThemeColors; dark?: ThemeColors }; type ResolvedTheme = 'light' | 'dark'; // Match the legacy file-type badge colors from styles.css. const TYPE_COLORS: Record = { qwen: { light: { bg: '#e8f5e9', text: '#2e7d32' }, dark: { bg: '#1b5e20', text: '#81c784' } }, gemini: { light: { bg: '#e3f2fd', text: '#1565c0' }, dark: { bg: '#0d47a1', text: '#64b5f6' } }, 'gemini-cli': { light: { bg: '#e7efff', text: '#1e4fa3' }, dark: { bg: '#1c3f73', text: '#a8c7ff' } }, aistudio: { light: { bg: '#f0f2f5', text: '#2f343c' }, dark: { bg: '#373c42', text: '#cfd3db' } }, claude: { light: { bg: '#fce4ec', text: '#c2185b' }, dark: { bg: '#880e4f', text: '#f48fb1' } }, codex: { light: { bg: '#fff3e0', text: '#ef6c00' }, dark: { bg: '#e65100', text: '#ffb74d' } }, antigravity: { light: { bg: '#e0f7fa', text: '#006064' }, dark: { bg: '#004d40', text: '#80deea' } }, iflow: { light: { bg: '#f3e5f5', text: '#7b1fa2' }, dark: { bg: '#4a148c', text: '#ce93d8' } }, empty: { light: { bg: '#f5f5f5', text: '#616161' }, dark: { bg: '#424242', text: '#bdbdbd' } }, unknown: { light: { bg: '#f0f0f0', text: '#666666', border: '1px dashed #999999' }, dark: { bg: '#3a3a3a', text: '#aaaaaa', border: '1px dashed #666666' } } }; interface GeminiCliQuotaBucket { modelId?: string; model_id?: string; tokenType?: string; token_type?: string; remainingFraction?: number | string; remaining_fraction?: number | string; remainingAmount?: number | string; remaining_amount?: number | string; resetTime?: string; reset_time?: string; } interface GeminiCliQuotaPayload { buckets?: GeminiCliQuotaBucket[]; } interface AntigravityQuotaInfo { displayName?: string; quotaInfo?: { remainingFraction?: number | string; remaining_fraction?: number | string; remaining?: number | string; resetTime?: string; reset_time?: string; }; quota_info?: { remainingFraction?: number | string; remaining_fraction?: number | string; remaining?: number | string; resetTime?: string; reset_time?: string; }; } type AntigravityModelsPayload = Record; interface AntigravityQuotaGroupDefinition { id: string; label: string; identifiers: string[]; labelFromModel?: boolean; } const ANTIGRAVITY_QUOTA_URLS = [ 'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels', 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels' ]; const ANTIGRAVITY_REQUEST_HEADERS = { Authorization: 'Bearer $TOKEN$', 'Content-Type': 'application/json', 'User-Agent': 'antigravity/1.11.5 windows/amd64' }; const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [ { id: 'claude-gpt', label: 'Claude/GPT', identifiers: [ 'claude-sonnet-4-5-thinking', 'claude-opus-4-5-thinking', 'claude-sonnet-4-5', 'gpt-oss-120b-medium' ] }, { id: 'gemini', label: 'Gemini', identifiers: [ 'gemini-3-pro-high', 'gemini-3-pro-low', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'rev19-uic3-1p' ] }, { id: 'gemini-3-flash', label: 'Gemini 3 Flash', identifiers: ['gemini-3-flash'] }, { id: 'gemini-image', label: 'gemini-3-pro-image', identifiers: ['gemini-3-pro-image'], labelFromModel: true } ]; const GEMINI_CLI_QUOTA_URL = 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota'; const GEMINI_CLI_REQUEST_HEADERS = { Authorization: 'Bearer $TOKEN$', 'Content-Type': 'application/json' }; interface CodexUsageWindow { used_percent?: number | string; usedPercent?: number | string; limit_window_seconds?: number | string; limitWindowSeconds?: number | string; reset_after_seconds?: number | string; resetAfterSeconds?: number | string; reset_at?: number | string; resetAt?: number | string; } interface CodexRateLimitInfo { allowed?: boolean; limit_reached?: boolean; limitReached?: boolean; primary_window?: CodexUsageWindow | null; primaryWindow?: CodexUsageWindow | null; secondary_window?: CodexUsageWindow | null; secondaryWindow?: CodexUsageWindow | null; } interface CodexUsagePayload { plan_type?: string; planType?: string; rate_limit?: CodexRateLimitInfo | null; rateLimit?: CodexRateLimitInfo | null; code_review_rate_limit?: CodexRateLimitInfo | null; codeReviewRateLimit?: CodexRateLimitInfo | null; } const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage'; const CODEX_REQUEST_HEADERS = { Authorization: 'Bearer $TOKEN$', 'Content-Type': 'application/json', 'User-Agent': 'codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal' }; const createStatusError = (message: string, status?: number) => { const error = new Error(message) as Error & { status?: number }; if (status !== undefined) { error.status = status; } return error; }; const getStatusFromError = (err: unknown): number | undefined => { if (typeof err === 'object' && err !== null && 'status' in err) { const rawStatus = (err as { status?: unknown }).status; if (typeof rawStatus === 'number' && Number.isFinite(rawStatus)) { return rawStatus; } const asNumber = Number(rawStatus); if (Number.isFinite(asNumber) && asNumber > 0) { return asNumber; } } return undefined; }; // Normalize auth_index (align with usage.ts normalizeAuthIndex). function normalizeAuthIndexValue(value: unknown): string | null { if (typeof value === 'number' && Number.isFinite(value)) { return value.toString(); } if (typeof value === 'string') { const trimmed = value.trim(); return trimmed ? trimmed : null; } return null; } function normalizeStringValue(value: unknown): string | null { if (typeof value === 'string') { const trimmed = value.trim(); return trimmed ? trimmed : null; } if (typeof value === 'number' && Number.isFinite(value)) { return value.toString(); } return null; } function normalizeNumberValue(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) return null; const parsed = Number(trimmed); return Number.isFinite(parsed) ? parsed : null; } return null; } function normalizeQuotaFraction(value: unknown): number | null { const normalized = normalizeNumberValue(value); if (normalized !== null) return normalized; if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) return null; if (trimmed.endsWith('%')) { const parsed = Number(trimmed.slice(0, -1)); return Number.isFinite(parsed) ? parsed / 100 : null; } } return null; } function normalizePlanType(value: unknown): string | null { const normalized = normalizeStringValue(value); return normalized ? normalized.toLowerCase() : null; } function decodeBase64UrlPayload(value: string): string | null { const trimmed = value.trim(); if (!trimmed) return null; try { const normalized = trimmed.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); if (typeof window !== 'undefined' && typeof window.atob === 'function') { return window.atob(padded); } if (typeof atob === 'function') { return atob(padded); } } catch { return null; } return null; } function parseIdTokenPayload(value: unknown): Record | null { if (!value) return null; if (typeof value === 'object') { return Array.isArray(value) ? null : (value as Record); } if (typeof value !== 'string') return null; const trimmed = value.trim(); if (!trimmed) return null; try { const parsed = JSON.parse(trimmed) as Record; if (parsed && typeof parsed === 'object') return parsed; } catch { } const segments = trimmed.split('.'); if (segments.length < 2) return null; const decoded = decodeBase64UrlPayload(segments[1]); if (!decoded) return null; try { const parsed = JSON.parse(decoded) as Record; if (parsed && typeof parsed === 'object') return parsed; } catch { return null; } return null; } function extractCodexChatgptAccountId(value: unknown): string | null { const payload = parseIdTokenPayload(value); if (!payload) return null; return normalizeStringValue(payload.chatgpt_account_id ?? payload.chatgptAccountId); } function resolveCodexChatgptAccountId(file: AuthFileItem): string | null { const metadata = file && typeof file.metadata === 'object' && file.metadata !== null ? (file.metadata as Record) : null; const attributes = file && typeof file.attributes === 'object' && file.attributes !== null ? (file.attributes as Record) : null; const candidates = [file.id_token, metadata?.id_token, attributes?.id_token]; for (const candidate of candidates) { const id = extractCodexChatgptAccountId(candidate); if (id) return id; } return null; } function resolveCodexPlanType(file: AuthFileItem): string | null { const metadata = file && typeof file.metadata === 'object' && file.metadata !== null ? (file.metadata as Record) : null; const attributes = file && typeof file.attributes === 'object' && file.attributes !== null ? (file.attributes as Record) : null; const idToken = file && typeof file.id_token === 'object' && file.id_token !== null ? (file.id_token as Record) : null; const metadataIdToken = metadata && typeof metadata.id_token === 'object' && metadata.id_token !== null ? (metadata.id_token as Record) : null; const candidates = [ file.plan_type, file.planType, file['plan_type'], file['planType'], file.id_token, idToken?.plan_type, idToken?.planType, metadata?.plan_type, metadata?.planType, metadata?.id_token, metadataIdToken?.plan_type, metadataIdToken?.planType, attributes?.plan_type, attributes?.planType, attributes?.id_token ]; for (const candidate of candidates) { const planType = normalizePlanType(candidate); if (planType) return planType; } return null; } function extractGeminiCliProjectId(value: unknown): string | null { if (typeof value !== 'string') return null; const matches = Array.from(value.matchAll(/\(([^()]+)\)/g)); if (matches.length === 0) return null; const candidate = matches[matches.length - 1]?.[1]?.trim(); return candidate ? candidate : null; } function resolveGeminiCliProjectId(file: AuthFileItem): string | null { const metadata = file && typeof file.metadata === 'object' && file.metadata !== null ? (file.metadata as Record) : null; const attributes = file && typeof file.attributes === 'object' && file.attributes !== null ? (file.attributes as Record) : null; const candidates = [ file.account, file['account'], metadata?.account, attributes?.account ]; for (const candidate of candidates) { const projectId = extractGeminiCliProjectId(candidate); if (projectId) return projectId; } return null; } function parseAntigravityPayload(payload: unknown): Record | null { if (payload === undefined || payload === null) return null; if (typeof payload === 'string') { const trimmed = payload.trim(); if (!trimmed) return null; try { return JSON.parse(trimmed) as Record; } catch { return null; } } if (typeof payload === 'object') { return payload as Record; } return null; } function parseCodexUsagePayload(payload: unknown): CodexUsagePayload | null { if (payload === undefined || payload === null) return null; if (typeof payload === 'string') { const trimmed = payload.trim(); if (!trimmed) return null; try { return JSON.parse(trimmed) as CodexUsagePayload; } catch { return null; } } if (typeof payload === 'object') { return payload as CodexUsagePayload; } return null; } function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayload | null { if (payload === undefined || payload === null) return null; if (typeof payload === 'string') { const trimmed = payload.trim(); if (!trimmed) return null; try { return JSON.parse(trimmed) as GeminiCliQuotaPayload; } catch { return null; } } if (typeof payload === 'object') { return payload as GeminiCliQuotaPayload; } return null; } function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): { remainingFraction: number | null; resetTime?: string; displayName?: string; } { if (!entry) { return { remainingFraction: null }; } const quotaInfo = entry.quotaInfo ?? entry.quota_info ?? {}; const remainingValue = quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining; const remainingFraction = normalizeQuotaFraction(remainingValue); const resetValue = quotaInfo.resetTime ?? quotaInfo.reset_time; const resetTime = typeof resetValue === 'string' ? resetValue : undefined; const displayName = typeof entry.displayName === 'string' ? entry.displayName : undefined; return { remainingFraction, resetTime, displayName }; } function findAntigravityModel( models: AntigravityModelsPayload, identifier: string ): { id: string; entry: AntigravityQuotaInfo } | null { const direct = models[identifier]; if (direct) { return { id: identifier, entry: direct }; } const match = Object.entries(models).find(([, entry]) => { const name = typeof entry?.displayName === 'string' ? entry.displayName : ''; return name.toLowerCase() === identifier.toLowerCase(); }); if (match) { return { id: match[0], entry: match[1] }; } return null; } function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): AntigravityQuotaGroup[] { const groups: AntigravityQuotaGroup[] = []; let geminiResetTime: string | undefined; const [claudeDef, geminiDef, flashDef, imageDef] = ANTIGRAVITY_QUOTA_GROUPS; const buildGroup = ( def: AntigravityQuotaGroupDefinition, overrideResetTime?: string ): AntigravityQuotaGroup | null => { const matches = def.identifiers .map((identifier) => findAntigravityModel(models, identifier)) .filter((entry): entry is { id: string; entry: AntigravityQuotaInfo } => Boolean(entry)); const quotaEntries = matches .map(({ id, entry }) => { const info = getAntigravityQuotaInfo(entry); const remainingFraction = info.remainingFraction ?? (info.resetTime ? 0 : null); if (remainingFraction === null) return null; return { id, remainingFraction, resetTime: info.resetTime, displayName: info.displayName }; }) .filter((entry): entry is NonNullable => entry !== null); if (quotaEntries.length === 0) return null; const remainingFraction = Math.min(...quotaEntries.map((entry) => entry.remainingFraction)); const resetTime = overrideResetTime ?? quotaEntries.map((entry) => entry.resetTime).find(Boolean); const displayName = quotaEntries.map((entry) => entry.displayName).find(Boolean); const label = def.labelFromModel && displayName ? displayName : def.label; return { id: def.id, label, models: quotaEntries.map((entry) => entry.id), remainingFraction, resetTime }; }; const claudeGroup = buildGroup(claudeDef); if (claudeGroup) { groups.push(claudeGroup); } const geminiGroup = buildGroup(geminiDef); if (geminiGroup) { geminiResetTime = geminiGroup.resetTime; groups.push(geminiGroup); } const flashGroup = buildGroup(flashDef); if (flashGroup) { groups.push(flashGroup); } const imageGroup = buildGroup(imageDef, geminiResetTime); if (imageGroup) { groups.push(imageGroup); } return groups; } function formatQuotaResetTime(value?: string): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); } function formatUnixSeconds(value: number | null): string { if (!value) return '-'; const date = new Date(value * 1000); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); } function formatCodexResetLabel(window?: CodexUsageWindow | null): string { if (!window) return '-'; const resetAt = normalizeNumberValue(window.reset_at ?? window.resetAt); if (resetAt !== null && resetAt > 0) { return formatUnixSeconds(resetAt); } const resetAfter = normalizeNumberValue(window.reset_after_seconds ?? window.resetAfterSeconds); if (resetAfter !== null && resetAfter > 0) { const targetSeconds = Math.floor(Date.now() / 1000 + resetAfter); return formatUnixSeconds(targetSeconds); } return '-'; } function resolveAuthProvider(file: AuthFileItem): string { const raw = file.provider ?? file.type ?? ''; return String(raw).trim().toLowerCase(); } function isAntigravityFile(file: AuthFileItem): boolean { return resolveAuthProvider(file) === 'antigravity'; } function isCodexFile(file: AuthFileItem): boolean { return resolveAuthProvider(file) === 'codex'; } function isGeminiCliFile(file: AuthFileItem): boolean { return resolveAuthProvider(file) === 'gemini-cli'; } function isRuntimeOnlyAuthFile(file: AuthFileItem): boolean { const raw = file['runtime_only'] ?? file.runtimeOnly; if (typeof raw === 'boolean') return raw; if (typeof raw === 'string') return raw.trim().toLowerCase() === 'true'; return false; } export function QuotaPage() { const { t } = useTranslation(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [antigravityPage, setAntigravityPage] = useState(1); const [antigravityPageSize, setAntigravityPageSize] = useState(6); const [codexPage, setCodexPage] = useState(1); const [codexPageSize, setCodexPageSize] = useState(6); const [geminiCliPage, setGeminiCliPage] = useState(1); const [geminiCliPageSize, setGeminiCliPageSize] = useState(6); const [antigravityLoading, setAntigravityLoading] = useState(false); const [antigravityLoadingScope, setAntigravityLoadingScope] = useState< 'page' | 'all' | null >(null); const [codexLoading, setCodexLoading] = useState(false); const [codexLoadingScope, setCodexLoadingScope] = useState<'page' | 'all' | null>(null); const [geminiCliLoading, setGeminiCliLoading] = useState(false); const [geminiCliLoadingScope, setGeminiCliLoadingScope] = useState< 'page' | 'all' | null >(null); const antigravityQuota = useQuotaStore((state) => state.antigravityQuota); const setAntigravityQuota = useQuotaStore((state) => state.setAntigravityQuota); const codexQuota = useQuotaStore((state) => state.codexQuota); const setCodexQuota = useQuotaStore((state) => state.setCodexQuota); const geminiCliQuota = useQuotaStore((state) => state.geminiCliQuota); const setGeminiCliQuota = useQuotaStore((state) => state.setGeminiCliQuota); const antigravityLoadingRef = useRef(false); const antigravityRequestIdRef = useRef(0); const codexLoadingRef = useRef(false); const codexRequestIdRef = useRef(0); const geminiCliLoadingRef = useRef(false); const geminiCliRequestIdRef = useRef(0); const disableControls = connectionStatus !== 'connected'; const loadFiles = useCallback(async () => { setLoading(true); setError(''); try { const data = await authFilesApi.list(); setFiles(data?.files || []); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed'); setError(errorMessage); } finally { setLoading(false); } }, [t]); const antigravityFiles = useMemo( () => files.filter((file) => isAntigravityFile(file)), [files] ); const antigravityTotalPages = Math.max( 1, Math.ceil(antigravityFiles.length / antigravityPageSize) ); const antigravityCurrentPage = Math.min(antigravityPage, antigravityTotalPages); const antigravityStart = (antigravityCurrentPage - 1) * antigravityPageSize; const antigravityPageItems = antigravityFiles.slice( antigravityStart, antigravityStart + antigravityPageSize ); const codexFiles = useMemo(() => files.filter((file) => isCodexFile(file)), [files]); const codexTotalPages = Math.max(1, Math.ceil(codexFiles.length / codexPageSize)); const codexCurrentPage = Math.min(codexPage, codexTotalPages); const codexStart = (codexCurrentPage - 1) * codexPageSize; const codexPageItems = codexFiles.slice(codexStart, codexStart + codexPageSize); const geminiCliFiles = useMemo( () => files.filter((file) => isGeminiCliFile(file) && !isRuntimeOnlyAuthFile(file)), [files] ); const geminiCliTotalPages = Math.max(1, Math.ceil(geminiCliFiles.length / geminiCliPageSize)); const geminiCliCurrentPage = Math.min(geminiCliPage, geminiCliTotalPages); const geminiCliStart = (geminiCliCurrentPage - 1) * geminiCliPageSize; const geminiCliPageItems = geminiCliFiles.slice( geminiCliStart, geminiCliStart + geminiCliPageSize ); const fetchAntigravityQuota = useCallback( async (authIndex: string): Promise => { let lastError = ''; let lastStatus: number | undefined; let priorityStatus: number | undefined; let hadSuccess = false; for (const url of ANTIGRAVITY_QUOTA_URLS) { try { const result = await apiCallApi.request({ authIndex, method: 'POST', url, header: { ...ANTIGRAVITY_REQUEST_HEADERS }, data: '{}' }); if (result.statusCode < 200 || result.statusCode >= 300) { lastError = getApiCallErrorMessage(result); lastStatus = result.statusCode; if (result.statusCode === 403 || result.statusCode === 404) { priorityStatus ??= result.statusCode; } continue; } hadSuccess = true; const payload = parseAntigravityPayload(result.body ?? result.bodyText); const models = payload?.models; if (!models || typeof models !== 'object' || Array.isArray(models)) { lastError = t('antigravity_quota.empty_models'); continue; } const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload); if (groups.length === 0) { lastError = t('antigravity_quota.empty_models'); continue; } return groups; } catch (err: unknown) { lastError = err instanceof Error ? err.message : t('common.unknown_error'); const status = getStatusFromError(err); if (status) { lastStatus = status; if (status === 403 || status === 404) { priorityStatus ??= status; } } } } if (hadSuccess) { return []; } throw createStatusError(lastError || t('common.unknown_error'), priorityStatus ?? lastStatus); }, [t] ); const loadAntigravityQuota = useCallback( async (targets: AuthFileItem[], scope: 'page' | 'all') => { if (antigravityLoadingRef.current) return; antigravityLoadingRef.current = true; const requestId = ++antigravityRequestIdRef.current; setAntigravityLoading(true); setAntigravityLoadingScope(scope); try { if (targets.length === 0) return; setAntigravityQuota((prev) => { const nextState = { ...prev }; targets.forEach((file) => { nextState[file.name] = { status: 'loading', groups: [] }; }); return nextState; }); const results = await Promise.all( targets.map(async (file) => { const rawAuthIndex = file['auth_index'] ?? file.authIndex; const authIndex = normalizeAuthIndexValue(rawAuthIndex); if (!authIndex) { return { name: file.name, status: 'error' as const, error: t('antigravity_quota.missing_auth_index') }; } try { const groups = await fetchAntigravityQuota(authIndex); return { name: file.name, status: 'success' as const, groups }; } catch (err: unknown) { const message = err instanceof Error ? err.message : t('common.unknown_error'); const errorStatus = getStatusFromError(err); return { name: file.name, status: 'error' as const, error: message, errorStatus }; } }) ); if (requestId !== antigravityRequestIdRef.current) return; setAntigravityQuota((prev) => { const nextState = { ...prev }; results.forEach((result) => { if (result.status === 'success') { nextState[result.name] = { status: 'success', groups: result.groups }; } else { nextState[result.name] = { status: 'error', groups: [], error: result.error, errorStatus: result.errorStatus }; } }); return nextState; }); } finally { if (requestId === antigravityRequestIdRef.current) { setAntigravityLoading(false); setAntigravityLoadingScope(null); antigravityLoadingRef.current = false; } } }, [fetchAntigravityQuota, setAntigravityQuota, t] ); const buildCodexQuotaWindows = useCallback( (payload: CodexUsagePayload): CodexQuotaWindow[] => { const rateLimit = payload.rate_limit ?? payload.rateLimit ?? undefined; const codeReviewLimit = payload.code_review_rate_limit ?? payload.codeReviewRateLimit ?? undefined; const windows: CodexQuotaWindow[] = []; const addWindow = ( id: string, label: string, window?: CodexUsageWindow | null, limitReached?: boolean, allowed?: boolean ) => { if (!window) return; const resetLabel = formatCodexResetLabel(window); const usedPercentRaw = normalizeNumberValue(window.used_percent ?? window.usedPercent); const isLimitReached = Boolean(limitReached) || allowed === false; const usedPercent = usedPercentRaw ?? (isLimitReached && resetLabel !== '-' ? 100 : null); windows.push({ id, label, usedPercent, resetLabel }); }; addWindow( 'primary', t('codex_quota.primary_window'), rateLimit?.primary_window ?? rateLimit?.primaryWindow, rateLimit?.limit_reached ?? rateLimit?.limitReached, rateLimit?.allowed ); addWindow( 'secondary', t('codex_quota.secondary_window'), rateLimit?.secondary_window ?? rateLimit?.secondaryWindow, rateLimit?.limit_reached ?? rateLimit?.limitReached, rateLimit?.allowed ); addWindow( 'code-review', t('codex_quota.code_review_window'), codeReviewLimit?.primary_window ?? codeReviewLimit?.primaryWindow, codeReviewLimit?.limit_reached ?? codeReviewLimit?.limitReached, codeReviewLimit?.allowed ); return windows; }, [t] ); const fetchCodexQuota = useCallback( async ( file: AuthFileItem ): Promise<{ planType: string | null; windows: CodexQuotaWindow[] }> => { const rawAuthIndex = file['auth_index'] ?? file.authIndex; const authIndex = normalizeAuthIndexValue(rawAuthIndex); if (!authIndex) { throw new Error(t('codex_quota.missing_auth_index')); } const planTypeFromFile = resolveCodexPlanType(file); const accountId = resolveCodexChatgptAccountId(file); if (!accountId) { throw new Error(t('codex_quota.missing_account_id')); } const requestUsage = async (requestHeader: Record) => { const result = await apiCallApi.request({ authIndex, method: 'GET', url: CODEX_USAGE_URL, header: requestHeader }); if (result.statusCode < 200 || result.statusCode >= 300) { throw createStatusError(getApiCallErrorMessage(result), result.statusCode); } const payload = parseCodexUsagePayload(result.body ?? result.bodyText); if (!payload) { throw new Error(t('codex_quota.empty_windows')); } return payload; }; const baseHeader: Record = { ...CODEX_REQUEST_HEADERS, 'Chatgpt-Account-Id': accountId }; const payload = await requestUsage(baseHeader); const planTypeFromUsage = normalizePlanType(payload.plan_type ?? payload.planType); const windows = buildCodexQuotaWindows(payload); return { planType: planTypeFromUsage ?? planTypeFromFile, windows }; }, [buildCodexQuotaWindows, t] ); const loadCodexQuota = useCallback( async (targets: AuthFileItem[], scope: 'page' | 'all') => { if (codexLoadingRef.current) return; codexLoadingRef.current = true; const requestId = ++codexRequestIdRef.current; setCodexLoading(true); setCodexLoadingScope(scope); try { if (targets.length === 0) return; setCodexQuota((prev) => { const nextState = { ...prev }; targets.forEach((file) => { nextState[file.name] = { status: 'loading', windows: [] }; }); return nextState; }); const results = await Promise.all( targets.map(async (file) => { try { const { planType, windows } = await fetchCodexQuota(file); return { name: file.name, status: 'success' as const, planType, windows }; } catch (err: unknown) { const message = err instanceof Error ? err.message : t('common.unknown_error'); const errorStatus = getStatusFromError(err); return { name: file.name, status: 'error' as const, error: message, errorStatus }; } }) ); if (requestId !== codexRequestIdRef.current) return; setCodexQuota((prev) => { const nextState = { ...prev }; results.forEach((result) => { if (result.status === 'success') { nextState[result.name] = { status: 'success', windows: result.windows, planType: result.planType }; } else { nextState[result.name] = { status: 'error', windows: [], error: result.error, errorStatus: result.errorStatus }; } }); return nextState; }); } finally { if (requestId === codexRequestIdRef.current) { setCodexLoading(false); setCodexLoadingScope(null); codexLoadingRef.current = false; } } }, [fetchCodexQuota, setCodexQuota, t] ); const fetchGeminiCliQuota = useCallback( async (file: AuthFileItem): Promise => { const rawAuthIndex = file['auth_index'] ?? file.authIndex; const authIndex = normalizeAuthIndexValue(rawAuthIndex); if (!authIndex) { throw new Error(t('gemini_cli_quota.missing_auth_index')); } const projectId = resolveGeminiCliProjectId(file); if (!projectId) { throw new Error(t('gemini_cli_quota.missing_project_id')); } const result = await apiCallApi.request({ authIndex, method: 'POST', url: GEMINI_CLI_QUOTA_URL, header: { ...GEMINI_CLI_REQUEST_HEADERS }, data: JSON.stringify({ project: projectId }) }); if (result.statusCode < 200 || result.statusCode >= 300) { throw createStatusError(getApiCallErrorMessage(result), result.statusCode); } const payload = parseGeminiCliQuotaPayload(result.body ?? result.bodyText); const buckets = Array.isArray(payload?.buckets) ? payload?.buckets : []; if (buckets.length === 0) return []; return buckets .map((bucket, index) => { const modelId = normalizeStringValue(bucket.modelId ?? bucket.model_id); if (!modelId) return null; const tokenType = normalizeStringValue(bucket.tokenType ?? bucket.token_type); const remainingFractionRaw = normalizeQuotaFraction( bucket.remainingFraction ?? bucket.remaining_fraction ); const remainingAmount = normalizeNumberValue( bucket.remainingAmount ?? bucket.remaining_amount ); const resetTime = normalizeStringValue(bucket.resetTime ?? bucket.reset_time) ?? undefined; let fallbackFraction: number | null = null; if (remainingAmount !== null) { fallbackFraction = remainingAmount <= 0 ? 0 : null; } else if (resetTime) { fallbackFraction = 0; } const remainingFraction = remainingFractionRaw ?? fallbackFraction; return { id: `${modelId}-${tokenType ?? index}`, label: modelId, remainingFraction, remainingAmount, resetTime, tokenType }; }) .filter((bucket): bucket is GeminiCliQuotaBucketState => bucket !== null); }, [t] ); const loadGeminiCliQuota = useCallback( async (targets: AuthFileItem[], scope: 'page' | 'all') => { if (geminiCliLoadingRef.current) return; geminiCliLoadingRef.current = true; const requestId = ++geminiCliRequestIdRef.current; setGeminiCliLoading(true); setGeminiCliLoadingScope(scope); try { if (targets.length === 0) return; setGeminiCliQuota((prev) => { const nextState = { ...prev }; targets.forEach((file) => { nextState[file.name] = { status: 'loading', buckets: [] }; }); return nextState; }); const results = await Promise.all( targets.map(async (file) => { try { const buckets = await fetchGeminiCliQuota(file); return { name: file.name, status: 'success' as const, buckets }; } catch (err: unknown) { const message = err instanceof Error ? err.message : t('common.unknown_error'); const errorStatus = getStatusFromError(err); return { name: file.name, status: 'error' as const, error: message, errorStatus }; } }) ); if (requestId !== geminiCliRequestIdRef.current) return; setGeminiCliQuota((prev) => { const nextState = { ...prev }; results.forEach((result) => { if (result.status === 'success') { nextState[result.name] = { status: 'success', buckets: result.buckets }; } else { nextState[result.name] = { status: 'error', buckets: [], error: result.error, errorStatus: result.errorStatus }; } }); return nextState; }); } finally { if (requestId === geminiCliRequestIdRef.current) { setGeminiCliLoading(false); setGeminiCliLoadingScope(null); geminiCliLoadingRef.current = false; } } }, [fetchGeminiCliQuota, setGeminiCliQuota, t] ); useEffect(() => { loadFiles(); }, [loadFiles]); useEffect(() => { if (loading) return; if (antigravityFiles.length === 0) { setAntigravityQuota({}); return; } setAntigravityQuota((prev) => { const nextState: Record = {}; antigravityFiles.forEach((file) => { const cached = prev[file.name]; if (cached) { nextState[file.name] = cached; } }); return nextState; }); }, [antigravityFiles, loading, setAntigravityQuota]); useEffect(() => { if (loading) return; if (codexFiles.length === 0) { setCodexQuota({}); return; } setCodexQuota((prev) => { const nextState: Record = {}; codexFiles.forEach((file) => { const cached = prev[file.name]; if (cached) { nextState[file.name] = cached; } }); return nextState; }); }, [codexFiles, loading, setCodexQuota]); useEffect(() => { if (loading) return; if (geminiCliFiles.length === 0) { setGeminiCliQuota({}); return; } setGeminiCliQuota((prev) => { const nextState: Record = {}; geminiCliFiles.forEach((file) => { const cached = prev[file.name]; if (cached) { nextState[file.name] = cached; } }); return nextState; }); }, [geminiCliFiles, loading, setGeminiCliQuota]); // Resolve type label text for badges. const getTypeLabel = (type: string): string => { const key = `auth_files.filter_${type}`; const translated = t(key); if (translated !== key) return translated; if (type.toLowerCase() === 'iflow') return 'iFlow'; return type.charAt(0).toUpperCase() + type.slice(1); }; // Resolve type colors for badges. const getTypeColor = (type: string): ThemeColors => { const set = TYPE_COLORS[type] || TYPE_COLORS.unknown; return resolvedTheme === 'dark' && set.dark ? set.dark : set.light; }; const getCodexPlanLabel = (planType?: string | null): string | null => { const normalized = normalizePlanType(planType); if (!normalized) return null; if (normalized === 'plus') return t('codex_quota.plan_plus'); if (normalized === 'team') return t('codex_quota.plan_team'); if (normalized === 'free') return t('codex_quota.plan_free'); return planType || normalized; }; const getQuotaErrorMessage = useCallback( (status: number | undefined, fallback: string) => { if (status === 404) return t('common.quota_update_required'); if (status === 403) return t('common.quota_check_credential'); return fallback; }, [t] ); const renderAntigravityCard = (item: AuthFileItem) => { const displayType = item.type || item.provider || 'antigravity'; const typeColor = getTypeColor(displayType); const quotaState = antigravityQuota[item.name]; const quotaStatus = quotaState?.status ?? 'idle'; const quotaGroups = quotaState?.groups ?? []; const quotaErrorMessage = getQuotaErrorMessage( quotaState?.errorStatus, quotaState?.error || t('common.unknown_error') ); return (
{getTypeLabel(displayType)} {item.name}
{quotaStatus === 'loading' ? (
{t('antigravity_quota.loading')}
) : quotaStatus === 'idle' ? (
{t('antigravity_quota.idle')}
) : quotaStatus === 'error' ? (
{t('antigravity_quota.load_failed', { message: quotaErrorMessage })}
) : quotaGroups.length === 0 ? (
{t('antigravity_quota.empty_models')}
) : ( quotaGroups.map((group) => { const clamped = Math.max(0, Math.min(1, group.remainingFraction)); const percent = Math.round(clamped * 100); const resetLabel = formatQuotaResetTime(group.resetTime); const quotaBarClass = percent >= 60 ? styles.quotaBarFillHigh : percent >= 20 ? styles.quotaBarFillMedium : styles.quotaBarFillLow; return (
{group.label}
{percent}% {resetLabel}
); }) )}
); }; const renderCodexCard = (item: AuthFileItem) => { const displayType = item.type || item.provider || 'codex'; const typeColor = getTypeColor(displayType); const quotaState = codexQuota[item.name]; const quotaStatus = quotaState?.status ?? 'idle'; const windows = quotaState?.windows ?? []; const planType = quotaState?.planType ?? null; const planLabel = getCodexPlanLabel(planType); const isFreePlan = normalizePlanType(planType) === 'free'; const quotaErrorMessage = getQuotaErrorMessage( quotaState?.errorStatus, quotaState?.error || t('common.unknown_error') ); return (
{getTypeLabel(displayType)} {item.name}
{quotaStatus === 'loading' ? (
{t('codex_quota.loading')}
) : quotaStatus === 'idle' ? (
{t('codex_quota.idle')}
) : quotaStatus === 'error' ? (
{t('codex_quota.load_failed', { message: quotaErrorMessage })}
) : ( <> {planLabel && (
{t('codex_quota.plan_label')} {planLabel}
)} {isFreePlan ? (
{t('codex_quota.no_access')}
) : windows.length === 0 ? (
{t('codex_quota.empty_windows')}
) : ( windows.map((window) => { const used = window.usedPercent; const clampedUsed = used === null ? null : Math.max(0, Math.min(100, used)); const remaining = clampedUsed === null ? null : Math.max(0, Math.min(100, 100 - clampedUsed)); const percentLabel = remaining === null ? '--' : `${Math.round(remaining)}%`; const quotaBarClass = remaining === null ? styles.quotaBarFillMedium : remaining >= 80 ? styles.quotaBarFillHigh : remaining >= 50 ? styles.quotaBarFillMedium : styles.quotaBarFillLow; return (
{window.label}
{percentLabel} {window.resetLabel}
); }) )} )}
); }; const renderGeminiCliCard = (item: AuthFileItem) => { const displayType = item.type || item.provider || 'gemini-cli'; const typeColor = getTypeColor(displayType); const quotaState = geminiCliQuota[item.name]; const quotaStatus = quotaState?.status ?? 'idle'; const buckets = quotaState?.buckets ?? []; const quotaErrorMessage = getQuotaErrorMessage( quotaState?.errorStatus, quotaState?.error || t('common.unknown_error') ); return (
{getTypeLabel(displayType)} {item.name}
{quotaStatus === 'loading' ? (
{t('gemini_cli_quota.loading')}
) : quotaStatus === 'idle' ? (
{t('gemini_cli_quota.idle')}
) : quotaStatus === 'error' ? (
{t('gemini_cli_quota.load_failed', { message: quotaErrorMessage })}
) : buckets.length === 0 ? (
{t('gemini_cli_quota.empty_buckets')}
) : ( buckets.map((bucket) => { const fraction = bucket.remainingFraction; const clamped = fraction === null ? null : Math.max(0, Math.min(1, fraction)); const percent = clamped === null ? null : Math.round(clamped * 100); const percentLabel = percent === null ? '--' : `${percent}%`; const resetLabel = formatQuotaResetTime(bucket.resetTime); const remainingAmountLabel = bucket.remainingAmount === null || bucket.remainingAmount === undefined ? null : t('gemini_cli_quota.remaining_amount', { count: bucket.remainingAmount }); const quotaBarClass = percent === null ? styles.quotaBarFillMedium : percent >= 60 ? styles.quotaBarFillHigh : percent >= 20 ? styles.quotaBarFillMedium : styles.quotaBarFillLow; return (
{bucket.label}
{percentLabel} {remainingAmountLabel && ( {remainingAmountLabel} )} {resetLabel}
); }) )}
); }; return (

{t('quota_management.title')}

{t('quota_management.description')}

{error &&
{error}
}
} > {antigravityFiles.length === 0 ? ( ) : ( <>
{antigravityFiles.length} {t('auth_files.files_count')}
{antigravityPageItems.map(renderAntigravityCard)}
{antigravityFiles.length > antigravityPageSize && (
{t('auth_files.pagination_info', { current: antigravityCurrentPage, total: antigravityTotalPages, count: antigravityFiles.length })}
)} )}
} > {codexFiles.length === 0 ? ( ) : ( <>
{codexFiles.length} {t('auth_files.files_count')}
{codexPageItems.map(renderCodexCard)}
{codexFiles.length > codexPageSize && (
{t('auth_files.pagination_info', { current: codexCurrentPage, total: codexTotalPages, count: codexFiles.length })}
)} )}
} > {geminiCliFiles.length === 0 ? ( ) : ( <>
{geminiCliFiles.length} {t('auth_files.files_count')}
{geminiCliPageItems.map(renderGeminiCliCard)}
{geminiCliFiles.length > geminiCliPageSize && (
{t('auth_files.pagination_info', { current: geminiCliCurrentPage, total: geminiCliTotalPages, count: geminiCliFiles.length })}
)} )}
); }