From a8b686d643f38bf11efb83f1753442783ebed2c9 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 10:05:19 -0400 Subject: [PATCH 1/2] feat(ui): show split Claude quota bars --- .../account/shared/account-quota-panel.tsx | 96 ++++++--- .../shared/account-quota-panel.test.tsx | 185 +++++++++++++++++- 2 files changed, 250 insertions(+), 31 deletions(-) diff --git a/ui/src/components/account/shared/account-quota-panel.tsx b/ui/src/components/account/shared/account-quota-panel.tsx index 09c23ffb..f35c7bf9 100644 --- a/ui/src/components/account/shared/account-quota-panel.tsx +++ b/ui/src/components/account/shared/account-quota-panel.tsx @@ -26,6 +26,13 @@ import { useTranslation } from 'react-i18next'; type AccountSurfaceMode = 'compact' | 'detailed'; +interface QuotaRow { + id: 'five-hour' | 'weekly'; + label: string; + compactLabel: string; + value: number; +} + interface AccountQuotaPanelProps { provider: string; quota?: UnifiedQuotaResult; @@ -101,18 +108,27 @@ export function AccountQuotaPanel({ isCodexProvider && quota && isCodexQuotaResult(quota) ? getCodexQuotaBreakdown(quota.windows) : null; - const compactQuotaRows = isCodexProvider + const compactQuotaRows: QuotaRow[] = isCodexProvider ? [ - { label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null }, { - label: mode === 'compact' ? 'Wk' : 'Weekly', + id: 'five-hour', + label: t('quotaTooltip.fiveHourLimit'), + compactLabel: '5h', + value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null, + }, + { + id: 'weekly', + label: t('quotaTooltip.weeklyLimit'), + compactLabel: 'Week', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null, }, - ] + ].filter((row): row is QuotaRow => row.value !== null) : isClaudeProvider && quota && isClaudeQuotaResult(quota) ? [ { - label: '5h', + id: 'five-hour', + label: t('quotaTooltip.fiveHourLimit'), + compactLabel: '5h', value: quota.coreUsage?.fiveHour?.remainingPercent ?? quota.windows.find((window) => window.rateLimitType === 'five_hour') @@ -120,7 +136,9 @@ export function AccountQuotaPanel({ null, }, { - label: mode === 'compact' ? 'Wk' : 'Weekly', + id: 'weekly', + label: t('quotaTooltip.weeklyLimit'), + compactLabel: 'Week', value: quota.coreUsage?.weekly?.remainingPercent ?? quota.windows.find((window) => @@ -134,11 +152,9 @@ export function AccountQuotaPanel({ )?.remainingPercent ?? null, }, - ] + ].filter((row): row is QuotaRow => row.value !== null) : []; - const quotaRows = compactQuotaRows.filter( - (row): row is { label: string; value: number } => row.value !== null - ); + const quotaRows = compactQuotaRows; if (quotaLoading) { return ( @@ -176,28 +192,42 @@ export function AccountQuotaPanel({ {minQuotaLabel}% - {quotaRows.length > 0 && ( -
+ {quotaRows.length > 0 ? ( +
{quotaRows.map((row) => ( - - {row.label} {row.value}% - +
+ + {row.compactLabel} + + + + {formatQuotaPercent(row.value)}% + +
))}
- )} -
-
50 ? 'bg-emerald-500' : minQuotaValue > 20 ? 'bg-amber-500' : 'bg-red-500' - )} - style={{ width: `${minQuotaValue}%` }} + } /> -
+ )}
) : (
@@ -228,18 +258,23 @@ export function AccountQuotaPanel({ )}
{quotaRows.length > 0 ? ( -
+
{quotaRows.map((row) => ( -
- - {row.label} - +
+
+ + {row.label} + + + {formatQuotaPercent(row.value)}% + +
- {row.value}%
))}
@@ -247,6 +282,7 @@ export function AccountQuotaPanel({
diff --git a/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx b/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx index 4263945f..93741255 100644 --- a/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx +++ b/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx @@ -1,7 +1,7 @@ import { render, screen, userEvent } from '@tests/setup/test-utils'; import { describe, expect, it } from 'vitest'; import { AccountQuotaPanel } from '@/components/account/shared/account-quota-panel'; -import type { GeminiCliQuotaResult } from '@/lib/api-client'; +import type { ClaudeQuotaResult, CodexQuotaResult, GeminiCliQuotaResult } from '@/lib/api-client'; function createGeminiFailureQuota(): GeminiCliQuotaResult { return { @@ -19,6 +19,189 @@ function createGeminiFailureQuota(): GeminiCliQuotaResult { }; } +function createClaudeQuota(): ClaudeQuotaResult { + return { + success: true, + lastUpdated: Date.now(), + windows: [ + { + rateLimitType: 'five_hour', + label: '5-hour usage', + status: 'allowed', + utilization: 0.57, + usedPercent: 57, + remainingPercent: 43, + resetAt: '2026-05-01T01:00:00.000Z', + }, + { + rateLimitType: 'seven_day_sonnet', + label: 'Weekly usage', + status: 'allowed', + utilization: 0.6, + usedPercent: 60, + remainingPercent: 40, + resetAt: '2026-05-07T01:00:00.000Z', + }, + ], + coreUsage: { + fiveHour: { + rateLimitType: 'five_hour', + label: '5-hour usage', + remainingPercent: 43, + resetAt: '2026-05-01T01:00:00.000Z', + status: 'allowed', + }, + weekly: { + rateLimitType: 'seven_day_sonnet', + label: 'Weekly usage', + remainingPercent: 40, + resetAt: '2026-05-07T01:00:00.000Z', + status: 'allowed', + }, + }, + }; +} + +function createCodexQuota(): CodexQuotaResult { + return { + success: true, + planType: 'pro', + lastUpdated: Date.now(), + windows: [ + { + label: 'Primary', + category: 'usage', + cadence: '5h', + usedPercent: 38, + remainingPercent: 62, + resetAfterSeconds: 60 * 60, + resetAt: '2026-05-01T01:00:00.000Z', + }, + { + label: 'Secondary', + category: 'usage', + cadence: 'weekly', + usedPercent: 61, + remainingPercent: 39, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-05-07T01:00:00.000Z', + }, + ], + coreUsage: { + fiveHour: { + label: 'Primary', + remainingPercent: 62, + resetAfterSeconds: 60 * 60, + resetAt: '2026-05-01T01:00:00.000Z', + }, + weekly: { + label: 'Secondary', + remainingPercent: 39, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-05-07T01:00:00.000Z', + }, + }, + }; +} + +function createGeminiSuccessQuota(): GeminiCliQuotaResult { + return { + success: true, + buckets: [ + { + id: 'flash::input', + label: 'Gemini Flash', + tokenType: 'input', + remainingFraction: 0.6, + remainingPercent: 60, + resetTime: '2026-05-01T01:00:00.000Z', + modelIds: ['gemini-2.5-flash'], + }, + ], + projectId: 'test-project', + lastUpdated: Date.now(), + }; +} + +describe('AccountQuotaPanel quota bars', () => { + it('shows separate compact Claude 5h and weekly quota bars', () => { + render( + + ); + + expect(screen.getByText('5h')).toBeInTheDocument(); + expect(screen.getByText('Week')).toBeInTheDocument(); + expect(screen.getByText('43%')).toBeInTheDocument(); + expect(screen.getAllByText('40%').length).toBeGreaterThan(0); + expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( + 'aria-valuenow', + '43' + ); + expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( + 'aria-valuenow', + '40' + ); + }); + + it('shows separate compact Codex 5h and weekly quota bars', () => { + render( + + ); + + expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( + 'aria-valuenow', + '62' + ); + expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( + 'aria-valuenow', + '39' + ); + }); + + it('keeps the compact single-bar fallback for providers without split quota rows', () => { + render( + + ); + + expect(screen.getByRole('progressbar', { name: 'Quota' })).toBeInTheDocument(); + expect(screen.queryByRole('progressbar', { name: '5h quota' })).not.toBeInTheDocument(); + expect(screen.queryByRole('progressbar', { name: 'Week quota' })).not.toBeInTheDocument(); + }); + + it('shows explicit detailed Claude quota labels', () => { + render( + + ); + + expect(screen.getByText('5h usage limit')).toBeInTheDocument(); + expect(screen.getByText('Weekly usage limit')).toBeInTheDocument(); + expect(screen.getByRole('progressbar', { name: '5h usage limit quota' })).toBeInTheDocument(); + expect( + screen.getByRole('progressbar', { name: 'Weekly usage limit quota' }) + ).toBeInTheDocument(); + }); +}); + describe('AccountQuotaPanel failure tooltip', () => { it('renders the shared failure tooltip content with viewport-safe shell classes', async () => { render( From 476c6d827fbdbca8d8aa8535c9b6c894bb84dd1c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 10:32:43 -0400 Subject: [PATCH 2/2] fix(ui): improve split quota bar readability --- .../account/shared/account-quota-panel.tsx | 99 ++++++++++++------- .../shared/account-quota-panel.test.tsx | 15 +-- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/ui/src/components/account/shared/account-quota-panel.tsx b/ui/src/components/account/shared/account-quota-panel.tsx index f35c7bf9..244fc649 100644 --- a/ui/src/components/account/shared/account-quota-panel.tsx +++ b/ui/src/components/account/shared/account-quota-panel.tsx @@ -49,6 +49,17 @@ function getQuotaColor(percentage: number): string { return 'bg-green-500'; } +function getQuotaTextColor(percentage: number): string { + const clamped = Math.max(0, Math.min(100, percentage)); + if (clamped <= 20) return 'text-red-600 dark:text-red-400'; + if (clamped <= 50) return 'text-amber-600 dark:text-amber-400'; + return 'text-emerald-600 dark:text-emerald-400'; +} + +function getDisplayQuotaValue(value: number): number { + return Number(formatQuotaPercent(value)); +} + function formatRelativeTime(dateStr: string | undefined): string { if (!dateStr) return ''; @@ -174,59 +185,66 @@ export function AccountQuotaPanel({ {mode === 'compact' ? ( -
-
- - {t('accountCard.quota')} - - 50 - ? 'text-emerald-600 dark:text-emerald-400' - : minQuotaValue > 20 - ? 'text-amber-500' - : 'text-red-500' - )} - > - {minQuotaLabel}% - -
+
{quotaRows.length > 0 ? ( -
+
{quotaRows.map((row) => (
- + {row.compactLabel} - + {formatQuotaPercent(row.value)}%
))}
) : ( - 50 - ? 'bg-emerald-500' - : minQuotaValue > 20 - ? 'bg-amber-500' - : 'bg-red-500' - } - /> +
+
+ + {t('accountCard.quota')} + + 50 + ? 'text-emerald-600 dark:text-emerald-400' + : minQuotaValue > 20 + ? 'text-amber-500' + : 'text-red-500' + )} + > + {minQuotaLabel}% + +
+ 50 + ? 'bg-emerald-500' + : minQuotaValue > 20 + ? 'bg-amber-500' + : 'bg-red-500' + } + /> +
)}
) : ( @@ -265,12 +283,17 @@ export function AccountQuotaPanel({ {row.label} - + {formatQuotaPercent(row.value)}%
{ expect(screen.getByText('5h')).toBeInTheDocument(); expect(screen.getByText('Week')).toBeInTheDocument(); expect(screen.getByText('43%')).toBeInTheDocument(); - expect(screen.getAllByText('40%').length).toBeGreaterThan(0); + expect(screen.getAllByText('45%').length).toBeGreaterThan(0); expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( 'aria-valuenow', '43' ); expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( 'aria-valuenow', - '40' + '45' ); });