diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx
index b77f6dd7..b4f65156 100644
--- a/ui/src/components/account/flow-viz/account-card.tsx
+++ b/ui/src/components/account/flow-viz/account-card.tsx
@@ -3,9 +3,16 @@
*/
import { AccountSurfaceCard } from '@/components/account/shared/account-surface-card';
+import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
-import { formatQuotaPercent, getProviderMinQuota, getQuotaFailureInfo, cn } from '@/lib/utils';
+import {
+ cn,
+ formatQuotaPercent,
+ getProviderMinQuota,
+ getProviderResetTime,
+ getQuotaFailureInfo,
+} from '@/lib/utils';
import { GripVertical, Loader2, Pause, Play } from 'lucide-react';
import {
useAccountQuota,
@@ -207,36 +214,45 @@ export function AccountCard({
hasGroupedVariants && showQuota
? (account.variants ?? []).map((variant, index) => {
const quotaQuery = variantQuotaQueries[index];
- const minQuota = getProviderMinQuota(account.provider, quotaQuery?.data);
+ const quota = quotaQuery?.data;
+ const minQuota = getProviderMinQuota(account.provider, quota);
+ const resetTime = getProviderResetTime(account.provider, quota);
const quotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
const quotaValue = quotaLabel !== null ? Number(quotaLabel) : null;
- const failureInfo = getQuotaFailureInfo(quotaQuery?.data);
+ const failureInfo = getQuotaFailureInfo(quota);
const label = variant.audienceLabel ?? variant.detailLabel ?? cleanEmail(variant.email);
return (
-
-
- {label}
-
- {quotaQuery?.isLoading
- ? t('accountCard.quotaLoading')
- : quotaValue !== null
- ? `${quotaLabel}%`
- : failureInfo?.label || t('accountCard.quotaUnavailable')}
-
-
- {quotaValue !== null && (
-
-
+
+
+
+
+ {label}
+
+ {quotaQuery?.isLoading
+ ? t('accountCard.quotaLoading')
+ : quotaValue !== null
+ ? `${quotaLabel}%`
+ : failureInfo?.label || t('accountCard.quotaUnavailable')}
+
+
+ {quotaValue !== null && (
+
+ )}
- )}
-
+
+
+
+
+
);
})
: null;
diff --git a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx
new file mode 100644
index 00000000..8086b0a9
--- /dev/null
+++ b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx
@@ -0,0 +1,141 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen, userEvent } from '@tests/setup/test-utils';
+import { AccountCard } from '@/components/account/flow-viz/account-card';
+import type { AccountData } from '@/components/account/flow-viz/types';
+import type { CodexQuotaResult } from '@/lib/api-client';
+import { useAccountQuota, useAccountQuotas } from '@/hooks/use-cliproxy-stats';
+
+vi.mock('@/hooks/use-cliproxy-stats', async () => {
+ const actual = await vi.importActual
(
+ '@/hooks/use-cliproxy-stats'
+ );
+
+ return {
+ ...actual,
+ useAccountQuota: vi.fn(),
+ useAccountQuotas: vi.fn(),
+ };
+});
+
+const mockedUseAccountQuota = vi.mocked(useAccountQuota);
+const mockedUseAccountQuotas = vi.mocked(useAccountQuotas);
+
+function makeCodexQuota(planType: 'plus' | 'team', fiveHour: number, weekly: number) {
+ return {
+ success: true,
+ planType,
+ lastUpdated: Date.now(),
+ windows: [
+ {
+ label: 'Primary',
+ usedPercent: 100 - fiveHour,
+ remainingPercent: fiveHour,
+ resetAfterSeconds: 60 * 60,
+ resetAt: '2026-04-04T08:44:00Z',
+ },
+ {
+ label: 'Secondary',
+ usedPercent: 100 - weekly,
+ remainingPercent: weekly,
+ resetAfterSeconds: 7 * 24 * 60 * 60,
+ resetAt: '2026-04-08T10:20:00Z',
+ },
+ ],
+ coreUsage: {
+ fiveHour: {
+ label: 'Primary',
+ remainingPercent: fiveHour,
+ resetAfterSeconds: 60 * 60,
+ resetAt: '2026-04-04T08:44:00Z',
+ },
+ weekly: {
+ label: 'Secondary',
+ remainingPercent: weekly,
+ resetAfterSeconds: 7 * 24 * 60 * 60,
+ resetAt: '2026-04-08T10:20:00Z',
+ },
+ },
+ } satisfies CodexQuotaResult;
+}
+
+const groupedAccount: AccountData = {
+ id: 'codex:user@example.com',
+ email: 'user@example.com',
+ tokenFile: 'codex-user.json',
+ provider: 'codex',
+ successCount: 9,
+ failureCount: 1,
+ color: '#1e6091',
+ variants: [
+ {
+ id: 'business@example.com',
+ email: 'user@example.com',
+ tokenFile: 'codex-business.json',
+ isDefault: false,
+ successCount: 5,
+ failureCount: 0,
+ audience: 'business',
+ audienceLabel: 'Business',
+ detailLabel: null,
+ },
+ {
+ id: 'personal@example.com',
+ email: 'user@example.com',
+ tokenFile: 'codex-personal.json',
+ isDefault: true,
+ successCount: 4,
+ failureCount: 1,
+ audience: 'personal',
+ audienceLabel: 'Personal',
+ detailLabel: null,
+ },
+ ],
+};
+
+describe('AccountCard grouped quota tooltip', () => {
+ beforeEach(() => {
+ mockedUseAccountQuota.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ } as ReturnType);
+
+ mockedUseAccountQuotas.mockReturnValue([
+ {
+ data: makeCodexQuota('team', 95, 81),
+ isLoading: false,
+ },
+ {
+ data: makeCodexQuota('plus', 64, 42),
+ isLoading: false,
+ },
+ ] as ReturnType);
+ });
+
+ it('shows provider quota tooltip content for each grouped personal/business row on hover', async () => {
+ render(
+ undefined}
+ onMouseLeave={() => undefined}
+ onPointerDown={() => undefined}
+ onPointerMove={() => undefined}
+ onPointerUp={() => undefined}
+ />
+ );
+
+ await userEvent.hover(screen.getByText('Business'));
+ expect((await screen.findAllByText('Plan: team')).length).toBeGreaterThan(0);
+ expect(screen.getAllByText('5h usage limit').length).toBeGreaterThan(0);
+
+ await userEvent.hover(screen.getByText('Personal'));
+ expect((await screen.findAllByText('Plan: plus')).length).toBeGreaterThan(0);
+ expect(screen.getAllByText('Weekly usage limit').length).toBeGreaterThan(0);
+ });
+});