diff --git a/src/commands/cliproxy/quota-subcommand.ts b/src/commands/cliproxy/quota-subcommand.ts index 683e863a..f426e490 100644 --- a/src/commands/cliproxy/quota-subcommand.ts +++ b/src/commands/cliproxy/quota-subcommand.ts @@ -103,44 +103,59 @@ function getCodexWindowKind(label: string): CodexWindowKind { return 'unknown'; } -function getCodexWindowCadence( - resetAfterSeconds: number | null | undefined -): '5h' | 'weekly' | null { - if ( - typeof resetAfterSeconds !== 'number' || - !isFinite(resetAfterSeconds) || - resetAfterSeconds <= 0 - ) { - return null; - } +type CodexWindowSummary = Pick; - if (resetAfterSeconds <= 6 * 60 * 60) return '5h'; - if (resetAfterSeconds >= 24 * 60 * 60) return 'weekly'; - return null; +function inferCodeReviewCadence( + window: CodexWindowSummary, + allWindows: CodexWindowSummary[] +): '5h' | 'weekly' | null { + const kind = getCodexWindowKind(window.label); + if (kind === 'code-review-weekly') return 'weekly'; + + const reset = window.resetAfterSeconds; + if (typeof reset !== 'number' || !isFinite(reset) || reset <= 0) return null; + + const usage5h = allWindows.find( + (w) => + getCodexWindowKind(w.label) === 'usage-5h' && + typeof w.resetAfterSeconds === 'number' && + isFinite(w.resetAfterSeconds) && + w.resetAfterSeconds > 0 + ); + const usageWeekly = allWindows.find( + (w) => + getCodexWindowKind(w.label) === 'usage-weekly' && + typeof w.resetAfterSeconds === 'number' && + isFinite(w.resetAfterSeconds) && + w.resetAfterSeconds > 0 + ); + + if (!usage5h || !usageWeekly) return null; + + const diffTo5h = Math.abs(reset - (usage5h.resetAfterSeconds as number)); + const diffToWeekly = Math.abs(reset - (usageWeekly.resetAfterSeconds as number)); + return diffToWeekly <= diffTo5h ? 'weekly' : '5h'; } function getCodexWindowDisplayLabel( - window: Pick + window: CodexWindowSummary, + allWindows: CodexWindowSummary[] = [] ): string { - const cadence = getCodexWindowCadence(window.resetAfterSeconds); + const context = allWindows.length > 0 ? allWindows : [window]; switch (getCodexWindowKind(window.label)) { case 'usage-5h': - if (cadence === 'weekly') return 'Weekly usage limit'; return '5h usage limit'; case 'usage-weekly': - if (cadence === '5h') return '5h usage limit'; return 'Weekly usage limit'; case 'code-review-5h': - if (cadence === 'weekly') return 'Code review (weekly)'; - return 'Code review (5h)'; case 'code-review-weekly': - if (cadence === '5h') return 'Code review (5h)'; - return 'Code review (weekly)'; - case 'code-review': - if (cadence === '5h') return 'Code review (5h)'; - if (cadence === 'weekly') return 'Code review (weekly)'; + case 'code-review': { + const inferred = inferCodeReviewCadence(window, context); + if (inferred === '5h') return 'Code review (5h)'; + if (inferred === 'weekly') return 'Code review (weekly)'; return 'Code review'; + } case 'unknown': return window.label; } @@ -281,7 +296,7 @@ function displayCodexQuotaSection(results: { account: string; quota: CodexQuotaR ? dim(` Resets ${formatResetTime(window.resetAfterSeconds)}`) : ''; console.log( - ` ${getCodexWindowDisplayLabel(window).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}` + ` ${getCodexWindowDisplayLabel(window, orderedWindows).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}` ); } console.log(''); diff --git a/ui/src/components/shared/quota-tooltip-content.tsx b/ui/src/components/shared/quota-tooltip-content.tsx index 27ff1c3a..492924c3 100644 --- a/ui/src/components/shared/quota-tooltip-content.tsx +++ b/ui/src/components/shared/quota-tooltip-content.tsx @@ -87,7 +87,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro className="flex justify-between gap-4" > - {getCodexWindowDisplayLabel(w)} + {getCodexWindowDisplayLabel(w, orderedWindows)} {w.remainingPercent}% diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index 81387cea..2c7edc70 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -334,51 +334,68 @@ export function getCodexWindowKind(label: string): CodexWindowKind { return 'unknown'; } -/** - * Convert raw Codex window labels into user-facing labels. - */ -function getCodexWindowCadence( - resetAfterSeconds: number | null | undefined -): '5h' | 'weekly' | null { - if ( - typeof resetAfterSeconds !== 'number' || - !isFinite(resetAfterSeconds) || - resetAfterSeconds <= 0 - ) { - return null; - } +type CodexWindowSummary = Pick; - if (resetAfterSeconds <= 6 * 60 * 60) return '5h'; - if (resetAfterSeconds >= 24 * 60 * 60) return 'weekly'; - return null; +/** + * Infer code-review window cadence by comparing against usage windows. + * This keeps labels stable as countdown values decrease over time. + */ +function inferCodeReviewCadence( + window: CodexWindowSummary, + allWindows: CodexWindowSummary[] +): '5h' | 'weekly' | null { + const kind = getCodexWindowKind(window.label); + if (kind === 'code-review-weekly') return 'weekly'; + + const reset = window.resetAfterSeconds; + if (typeof reset !== 'number' || !isFinite(reset) || reset <= 0) return null; + + const usage5h = allWindows.find( + (w) => + getCodexWindowKind(w.label) === 'usage-5h' && + typeof w.resetAfterSeconds === 'number' && + isFinite(w.resetAfterSeconds) && + w.resetAfterSeconds > 0 + ); + const usageWeekly = allWindows.find( + (w) => + getCodexWindowKind(w.label) === 'usage-weekly' && + typeof w.resetAfterSeconds === 'number' && + isFinite(w.resetAfterSeconds) && + w.resetAfterSeconds > 0 + ); + + if (!usage5h || !usageWeekly) return null; + + const diffTo5h = Math.abs(reset - (usage5h.resetAfterSeconds as number)); + const diffToWeekly = Math.abs(reset - (usageWeekly.resetAfterSeconds as number)); + return diffToWeekly <= diffTo5h ? 'weekly' : '5h'; } export function getCodexWindowDisplayLabel( - labelOrWindow: string | Pick, - resetAfterSecondsOverride?: number | null + labelOrWindow: string | CodexWindowSummary, + allWindows: CodexWindowSummary[] = [] ): string { const label = typeof labelOrWindow === 'string' ? labelOrWindow : labelOrWindow.label; - const cadence = getCodexWindowCadence( - typeof labelOrWindow === 'string' ? resetAfterSecondsOverride : labelOrWindow.resetAfterSeconds - ); + const currentWindow: CodexWindowSummary = + typeof labelOrWindow === 'string' + ? { label, resetAfterSeconds: null } + : { label, resetAfterSeconds: labelOrWindow.resetAfterSeconds }; + const context = allWindows.length > 0 ? allWindows : [currentWindow]; switch (getCodexWindowKind(label)) { case 'usage-5h': - if (cadence === 'weekly') return 'Weekly usage limit'; return '5h usage limit'; case 'usage-weekly': - if (cadence === '5h') return '5h usage limit'; return 'Weekly usage limit'; case 'code-review-5h': - if (cadence === 'weekly') return 'Code review (weekly)'; - return 'Code review (5h)'; case 'code-review-weekly': - if (cadence === '5h') return 'Code review (5h)'; - return 'Code review (weekly)'; - case 'code-review': - if (cadence === '5h') return 'Code review (5h)'; - if (cadence === 'weekly') return 'Code review (weekly)'; + case 'code-review': { + const inferred = inferCodeReviewCadence(currentWindow, context); + if (inferred === '5h') return 'Code review (5h)'; + if (inferred === 'weekly') return 'Code review (weekly)'; return 'Code review'; + } case 'unknown': return label; } diff --git a/ui/tests/unit/ui/lib/quota-utils.test.ts b/ui/tests/unit/ui/lib/quota-utils.test.ts index 338d5c17..8efdf579 100644 --- a/ui/tests/unit/ui/lib/quota-utils.test.ts +++ b/ui/tests/unit/ui/lib/quota-utils.test.ts @@ -528,21 +528,37 @@ describe('getCodexResetTime', () => { }); describe('getCodexWindowDisplayLabel', () => { - it('labels code review primary window as weekly when reset cadence is weekly', () => { + it('labels code review primary as weekly when it matches usage weekly window', () => { + const windows: Array<{ label: string; resetAfterSeconds: number | null }> = [ + { label: 'Primary', resetAfterSeconds: 18000 }, + { label: 'Secondary', resetAfterSeconds: 604800 }, + { label: 'Code Review (Primary)', resetAfterSeconds: 600000 }, + ]; expect( - getCodexWindowDisplayLabel({ - label: 'Code Review (Primary)', - resetAfterSeconds: 604800, - }) + getCodexWindowDisplayLabel( + { + label: 'Code Review (Primary)', + resetAfterSeconds: 600000, + }, + windows + ) ).toBe('Code review (weekly)'); }); - it('labels code review primary window as 5h when reset cadence is short', () => { + it('labels code review primary as 5h when it matches usage 5h window', () => { + const windows: Array<{ label: string; resetAfterSeconds: number | null }> = [ + { label: 'Primary', resetAfterSeconds: 18000 }, + { label: 'Secondary', resetAfterSeconds: 604800 }, + { label: 'Code Review (Primary)', resetAfterSeconds: 17000 }, + ]; expect( - getCodexWindowDisplayLabel({ - label: 'Code Review (Primary)', - resetAfterSeconds: 18000, - }) + getCodexWindowDisplayLabel( + { + label: 'Code Review (Primary)', + resetAfterSeconds: 17000, + }, + windows + ) ).toBe('Code review (5h)'); }); @@ -550,8 +566,13 @@ describe('getCodexWindowDisplayLabel', () => { expect(getCodexWindowDisplayLabel('Secondary')).toBe('Weekly usage limit'); }); - it('uses cadence override when provided with string label', () => { - expect(getCodexWindowDisplayLabel('Primary', 604800)).toBe('Weekly usage limit'); + it('falls back to generic code review label when cadence cannot be inferred', () => { + expect( + getCodexWindowDisplayLabel({ + label: 'Code Review (Primary)', + resetAfterSeconds: 604800, + }) + ).toBe('Code review'); }); });