mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
feat(cliproxy): expose codex weekly reset schedule in quota views
This commit is contained in:
@@ -238,6 +238,7 @@ Re-creates symlinks for shared commands, skills, and settings.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
ccs cliproxy doctor # Check quota status for all agy accounts
|
ccs cliproxy doctor # Check quota status for all agy accounts
|
||||||
|
ccs cliproxy quota # Show agy/codex/gemini quotas (Codex: 5h + weekly reset schedule)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Auto-Failover**: When an Antigravity account runs out of quota, CCS automatically switches to another account with remaining capacity. Shared GCP project accounts are excluded (pooled quota).
|
**Auto-Failover**: When an Antigravity account runs out of quota, CCS automatically switches to another account with remaining capacity. Shared GCP project accounts are excluded (pooled quota).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import * as path from 'node:path';
|
|||||||
import { getAuthDir } from './config-generator';
|
import { getAuthDir } from './config-generator';
|
||||||
import { getProviderAccounts, getPausedDir } from './account-manager';
|
import { getProviderAccounts, getPausedDir } from './account-manager';
|
||||||
import { sanitizeEmail, isTokenExpired } from './auth-utils';
|
import { sanitizeEmail, isTokenExpired } from './auth-utils';
|
||||||
import type { CodexQuotaResult, CodexQuotaWindow } from './quota-types';
|
import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types';
|
||||||
|
|
||||||
/** ChatGPT backend API base URL */
|
/** ChatGPT backend API base URL */
|
||||||
const CODEX_API_BASE = 'https://chatgpt.com/backend-api';
|
const CODEX_API_BASE = 'https://chatgpt.com/backend-api';
|
||||||
@@ -57,6 +57,99 @@ interface CodexWindowData {
|
|||||||
resetAfterSeconds?: number | null;
|
resetAfterSeconds?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CodexWindowKind =
|
||||||
|
| 'usage-5h'
|
||||||
|
| 'usage-weekly'
|
||||||
|
| 'code-review-5h'
|
||||||
|
| 'code-review-weekly'
|
||||||
|
| 'code-review'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
|
function getCodexWindowKind(label: string): CodexWindowKind {
|
||||||
|
const lower = (label || '').toLowerCase();
|
||||||
|
const isCodeReview = lower.includes('code review') || lower.includes('code_review');
|
||||||
|
const isPrimary = lower.includes('primary');
|
||||||
|
const isSecondary = lower.includes('secondary');
|
||||||
|
|
||||||
|
if (isCodeReview) {
|
||||||
|
if (isPrimary) return 'code-review-5h';
|
||||||
|
if (isSecondary) return 'code-review-weekly';
|
||||||
|
return 'code-review';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrimary) return 'usage-5h';
|
||||||
|
if (isSecondary) return 'usage-weekly';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build explicit 5h + weekly usage summary from raw Codex windows.
|
||||||
|
* Falls back to shortest/longest reset windows if API labels change.
|
||||||
|
*/
|
||||||
|
export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCoreUsageSummary {
|
||||||
|
if (!windows || windows.length === 0) {
|
||||||
|
return { fiveHour: null, weekly: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let fiveHourWindow: CodexQuotaWindow | null = null;
|
||||||
|
let weeklyWindow: CodexQuotaWindow | null = null;
|
||||||
|
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
|
||||||
|
|
||||||
|
for (const window of windows) {
|
||||||
|
const kind = getCodexWindowKind(window.label);
|
||||||
|
if (kind === 'usage-5h') {
|
||||||
|
if (!fiveHourWindow) fiveHourWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'usage-weekly') {
|
||||||
|
if (!weeklyWindow) weeklyWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'unknown') {
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!fiveHourWindow || !weeklyWindow) && nonCodeReviewWindows.length > 0) {
|
||||||
|
const withReset = nonCodeReviewWindows
|
||||||
|
.filter(
|
||||||
|
(w) =>
|
||||||
|
typeof w.resetAfterSeconds === 'number' &&
|
||||||
|
isFinite(w.resetAfterSeconds) &&
|
||||||
|
w.resetAfterSeconds >= 0
|
||||||
|
)
|
||||||
|
.sort((a, b) => (a.resetAfterSeconds || 0) - (b.resetAfterSeconds || 0));
|
||||||
|
|
||||||
|
if (!fiveHourWindow) {
|
||||||
|
fiveHourWindow = withReset[0] || nonCodeReviewWindows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!weeklyWindow) {
|
||||||
|
weeklyWindow =
|
||||||
|
withReset.length > 1
|
||||||
|
? withReset[withReset.length - 1]
|
||||||
|
: nonCodeReviewWindows.find((w) => w !== fiveHourWindow) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapWindow = (window: CodexQuotaWindow | null): CodexCoreUsageSummary['fiveHour'] => {
|
||||||
|
if (!window) return null;
|
||||||
|
return {
|
||||||
|
label: window.label,
|
||||||
|
remainingPercent: window.remainingPercent,
|
||||||
|
resetAfterSeconds: window.resetAfterSeconds,
|
||||||
|
resetAt: window.resetAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fiveHour: mapWindow(fiveHourWindow),
|
||||||
|
weekly: mapWindow(weeklyWindow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read auth data from Codex auth file
|
* Read auth data from Codex auth file
|
||||||
*/
|
*/
|
||||||
@@ -295,6 +388,7 @@ export async function fetchCodexQuota(
|
|||||||
|
|
||||||
const data = (await response.json()) as CodexUsageResponse;
|
const data = (await response.json()) as CodexUsageResponse;
|
||||||
const windows = buildCodexQuotaWindows(data);
|
const windows = buildCodexQuotaWindows(data);
|
||||||
|
const coreUsage = buildCodexCoreUsageSummary(windows);
|
||||||
|
|
||||||
// Extract plan type
|
// Extract plan type
|
||||||
const planTypeRaw = data.plan_type || data.planType;
|
const planTypeRaw = data.plan_type || data.planType;
|
||||||
@@ -311,6 +405,7 @@ export async function fetchCodexQuota(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
windows,
|
windows,
|
||||||
|
coreUsage,
|
||||||
planType,
|
planType,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
accountId,
|
accountId,
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ export interface CodexQuotaWindow {
|
|||||||
resetAt: string | null;
|
resetAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Core Codex usage window (5h/weekly) extracted from raw windows */
|
||||||
|
export interface CodexCoreUsageWindow {
|
||||||
|
/** Source window label */
|
||||||
|
label: string;
|
||||||
|
/** Percentage remaining (0-100) */
|
||||||
|
remainingPercent: number;
|
||||||
|
/** Seconds until quota resets, null if unknown */
|
||||||
|
resetAfterSeconds: number | null;
|
||||||
|
/** ISO timestamp when quota resets, null if unknown */
|
||||||
|
resetAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Core Codex usage summary with explicit 5h and weekly windows */
|
||||||
|
export interface CodexCoreUsageSummary {
|
||||||
|
/** Short-cycle usage limit window (typically 5h) */
|
||||||
|
fiveHour: CodexCoreUsageWindow | null;
|
||||||
|
/** Long-cycle usage limit window (typically weekly) */
|
||||||
|
weekly: CodexCoreUsageWindow | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex quota fetch result
|
* Codex quota fetch result
|
||||||
*/
|
*/
|
||||||
@@ -35,6 +55,8 @@ export interface CodexQuotaResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
/** Quota windows (primary, secondary, code review) */
|
/** Quota windows (primary, secondary, code review) */
|
||||||
windows: CodexQuotaWindow[];
|
windows: CodexQuotaWindow[];
|
||||||
|
/** Explicit core usage windows (5h + weekly) for easier reset display */
|
||||||
|
coreUsage?: CodexCoreUsageSummary;
|
||||||
/** Plan type: free, plus, team, or null if unknown */
|
/** Plan type: free, plus, team, or null if unknown */
|
||||||
planType: 'free' | 'plus' | 'team' | null;
|
planType: 'free' | 'plus' | 'team' | null;
|
||||||
/** Timestamp of fetch */
|
/** Timestamp of fetch */
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export async function showHelp(): Promise<void> {
|
|||||||
['default <account>', 'Set default account for rotation'],
|
['default <account>', 'Set default account for rotation'],
|
||||||
['pause <account>', 'Pause account (skip in rotation)'],
|
['pause <account>', 'Pause account (skip in rotation)'],
|
||||||
['resume <account>', 'Resume paused account'],
|
['resume <account>', 'Resume paused account'],
|
||||||
['quota', 'Show quota status for all providers'],
|
['quota', 'Show quota status for all providers (Codex includes 5h + weekly reset)'],
|
||||||
['quota --provider <name>', 'Filter by provider (agy|codex|gemini)'],
|
['quota --provider <name>', 'Filter by provider (agy|codex|gemini)'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -67,7 +67,13 @@ function formatResetTime(seconds: number): string {
|
|||||||
if (seconds <= 0) return 'now';
|
if (seconds <= 0) return 'now';
|
||||||
if (seconds < 60) return `in ${seconds}s`;
|
if (seconds < 60) return `in ${seconds}s`;
|
||||||
if (seconds < 3600) return `in ${Math.round(seconds / 60)}m`;
|
if (seconds < 3600) return `in ${Math.round(seconds / 60)}m`;
|
||||||
return `in ${Math.round(seconds / 3600)}h`;
|
if (seconds < 86400) return `in ${Math.round(seconds / 3600)}h`;
|
||||||
|
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.round((seconds % 86400) / 3600);
|
||||||
|
if (hours <= 0) return `in ${days}d`;
|
||||||
|
if (hours >= 24) return `in ${days + 1}d`;
|
||||||
|
return `in ${days}d ${hours}h`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatResetTimeISO(isoTime: string): string {
|
function formatResetTimeISO(isoTime: string): string {
|
||||||
@@ -78,6 +84,40 @@ function formatResetTimeISO(isoTime: string): string {
|
|||||||
return formatResetTime(seconds);
|
return formatResetTime(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAbsoluteResetTime(isoTime: string): string | null {
|
||||||
|
if (!isoTime) return null;
|
||||||
|
const resetDate = new Date(isoTime);
|
||||||
|
if (isNaN(resetDate.getTime())) return null;
|
||||||
|
const date = resetDate.toLocaleDateString(undefined, {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
const time = resetDate.toLocaleTimeString(undefined, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
return `${date} ${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCodexWindowReset(
|
||||||
|
window: Pick<CodexQuotaResult['windows'][number], 'resetAfterSeconds' | 'resetAt'>
|
||||||
|
): string | null {
|
||||||
|
if (typeof window.resetAfterSeconds === 'number' && isFinite(window.resetAfterSeconds)) {
|
||||||
|
const relative = formatResetTime(Math.max(0, window.resetAfterSeconds));
|
||||||
|
if (window.resetAfterSeconds >= 86400 && window.resetAt) {
|
||||||
|
const absolute = formatAbsoluteResetTime(window.resetAt);
|
||||||
|
return absolute ? `${relative} (${absolute})` : relative;
|
||||||
|
}
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.resetAt) {
|
||||||
|
return formatResetTimeISO(window.resetAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
type CodexWindowKind =
|
type CodexWindowKind =
|
||||||
| 'usage-5h'
|
| 'usage-5h'
|
||||||
| 'usage-weekly'
|
| 'usage-weekly'
|
||||||
@@ -286,15 +326,45 @@ function displayCodexQuotaSection(results: { account: string; quota: CodexQuotaR
|
|||||||
|
|
||||||
console.log(` ${statusIcon}${account}${defaultMark}${planBadge}`);
|
console.log(` ${statusIcon}${account}${defaultMark}${planBadge}`);
|
||||||
|
|
||||||
|
const coreUsageSummary = quota.coreUsage ?? {
|
||||||
|
fiveHour: fiveHourWindow
|
||||||
|
? {
|
||||||
|
label: fiveHourWindow.label,
|
||||||
|
remainingPercent: fiveHourWindow.remainingPercent,
|
||||||
|
resetAfterSeconds: fiveHourWindow.resetAfterSeconds,
|
||||||
|
resetAt: fiveHourWindow.resetAt,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
weekly: weeklyWindow
|
||||||
|
? {
|
||||||
|
label: weeklyWindow.label,
|
||||||
|
remainingPercent: weeklyWindow.remainingPercent,
|
||||||
|
resetAfterSeconds: weeklyWindow.resetAfterSeconds,
|
||||||
|
resetAt: weeklyWindow.resetAt,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
const resetParts: string[] = [];
|
||||||
|
const fiveHourReset = coreUsageSummary.fiveHour
|
||||||
|
? formatCodexWindowReset(coreUsageSummary.fiveHour)
|
||||||
|
: null;
|
||||||
|
const weeklyReset = coreUsageSummary.weekly
|
||||||
|
? formatCodexWindowReset(coreUsageSummary.weekly)
|
||||||
|
: null;
|
||||||
|
if (fiveHourReset) resetParts.push(`5h ${fiveHourReset}`);
|
||||||
|
if (weeklyReset) resetParts.push(`weekly ${weeklyReset}`);
|
||||||
|
if (resetParts.length > 0) {
|
||||||
|
console.log(` ${dim(`Reset schedule: ${resetParts.join(' | ')}`)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const orderedWindows = [fiveHourWindow, weeklyWindow, ...quota.windows].filter(
|
const orderedWindows = [fiveHourWindow, weeklyWindow, ...quota.windows].filter(
|
||||||
(w, index, arr): w is NonNullable<typeof w> => !!w && arr.indexOf(w) === index
|
(w, index, arr): w is NonNullable<typeof w> => !!w && arr.indexOf(w) === index
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const window of orderedWindows) {
|
for (const window of orderedWindows) {
|
||||||
const bar = formatQuotaBar(window.remainingPercent);
|
const bar = formatQuotaBar(window.remainingPercent);
|
||||||
const resetLabel = window.resetAfterSeconds
|
const resetValue = formatCodexWindowReset(window);
|
||||||
? dim(` Resets ${formatResetTime(window.resetAfterSeconds)}`)
|
const resetLabel = resetValue ? dim(` Resets ${resetValue}`) : '';
|
||||||
: '';
|
|
||||||
console.log(
|
console.log(
|
||||||
` ${getCodexWindowDisplayLabel(window, orderedWindows).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
|
` ${getCodexWindowDisplayLabel(window, orderedWindows).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'bun:test';
|
import { describe, it, expect } from 'bun:test';
|
||||||
import { buildCodexQuotaWindows } from '../../../src/cliproxy/quota-fetcher-codex';
|
import {
|
||||||
|
buildCodexQuotaWindows,
|
||||||
|
buildCodexCoreUsageSummary,
|
||||||
|
} from '../../../src/cliproxy/quota-fetcher-codex';
|
||||||
|
|
||||||
describe('Codex Quota Fetcher', () => {
|
describe('Codex Quota Fetcher', () => {
|
||||||
describe('buildCodexQuotaWindows', () => {
|
describe('buildCodexQuotaWindows', () => {
|
||||||
@@ -187,4 +190,69 @@ describe('Codex Quota Fetcher', () => {
|
|||||||
expect(windows[0].remainingPercent).toBe(100);
|
expect(windows[0].remainingPercent).toBe(100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildCodexCoreUsageSummary', () => {
|
||||||
|
it('extracts 5h and weekly windows from labeled usage windows', () => {
|
||||||
|
const windows = buildCodexQuotaWindows({
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 35,
|
||||||
|
reset_after_seconds: 18000,
|
||||||
|
},
|
||||||
|
secondary_window: {
|
||||||
|
used_percent: 60,
|
||||||
|
reset_after_seconds: 604800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = buildCodexCoreUsageSummary(windows);
|
||||||
|
|
||||||
|
expect(summary.fiveHour?.label).toBe('Primary');
|
||||||
|
expect(summary.fiveHour?.remainingPercent).toBe(65);
|
||||||
|
expect(summary.fiveHour?.resetAfterSeconds).toBe(18000);
|
||||||
|
expect(summary.weekly?.label).toBe('Secondary');
|
||||||
|
expect(summary.weekly?.remainingPercent).toBe(40);
|
||||||
|
expect(summary.weekly?.resetAfterSeconds).toBe(604800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to shortest and longest reset windows when labels are unknown', () => {
|
||||||
|
const windows = [
|
||||||
|
{
|
||||||
|
label: 'Window A',
|
||||||
|
usedPercent: 20,
|
||||||
|
remainingPercent: 80,
|
||||||
|
resetAfterSeconds: 18000,
|
||||||
|
resetAt: '2026-02-15T15:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Window B',
|
||||||
|
usedPercent: 45,
|
||||||
|
remainingPercent: 55,
|
||||||
|
resetAfterSeconds: 604800,
|
||||||
|
resetAt: '2026-02-21T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Code Review (Primary)',
|
||||||
|
usedPercent: 10,
|
||||||
|
remainingPercent: 90,
|
||||||
|
resetAfterSeconds: 3600,
|
||||||
|
resetAt: '2026-02-15T11:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const summary = buildCodexCoreUsageSummary(windows);
|
||||||
|
|
||||||
|
expect(summary.fiveHour?.label).toBe('Window A');
|
||||||
|
expect(summary.fiveHour?.resetAfterSeconds).toBe(18000);
|
||||||
|
expect(summary.weekly?.label).toBe('Window B');
|
||||||
|
expect(summary.weekly?.resetAfterSeconds).toBe(604800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null summaries when no windows are available', () => {
|
||||||
|
const summary = buildCodexCoreUsageSummary([]);
|
||||||
|
expect(summary.fiveHour).toBeNull();
|
||||||
|
expect(summary.weekly).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
|||||||
if (isCodexQuotaResult(quota)) {
|
if (isCodexQuotaResult(quota)) {
|
||||||
const { fiveHourWindow, weeklyWindow, codeReviewWindows, unknownWindows } =
|
const { fiveHourWindow, weeklyWindow, codeReviewWindows, unknownWindows } =
|
||||||
getCodexQuotaBreakdown(quota.windows);
|
getCodexQuotaBreakdown(quota.windows);
|
||||||
|
const fiveHourResetAt = quota.coreUsage?.fiveHour?.resetAt ?? fiveHourWindow?.resetAt ?? null;
|
||||||
|
const weeklyResetAt = quota.coreUsage?.weekly?.resetAt ?? weeklyWindow?.resetAt ?? null;
|
||||||
const orderedWindows = [fiveHourWindow, weeklyWindow, ...codeReviewWindows, ...unknownWindows]
|
const orderedWindows = [fiveHourWindow, weeklyWindow, ...codeReviewWindows, ...unknownWindows]
|
||||||
.filter((w): w is NonNullable<typeof w> => !!w)
|
.filter((w): w is NonNullable<typeof w> => !!w)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -92,7 +94,11 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
|||||||
<span className="font-mono">{w.remainingPercent}%</span>
|
<span className="font-mono">{w.remainingPercent}%</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<ResetTimeIndicator resetTime={resetTime} />
|
<CodexResetIndicators
|
||||||
|
fiveHourResetTime={fiveHourResetAt}
|
||||||
|
weeklyResetTime={weeklyResetAt}
|
||||||
|
fallbackResetTime={resetTime}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -132,3 +138,40 @@ function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CodexResetIndicators({
|
||||||
|
fiveHourResetTime,
|
||||||
|
weeklyResetTime,
|
||||||
|
fallbackResetTime,
|
||||||
|
}: {
|
||||||
|
fiveHourResetTime: string | null;
|
||||||
|
weeklyResetTime: string | null;
|
||||||
|
fallbackResetTime: string | null;
|
||||||
|
}) {
|
||||||
|
const hasSpecificReset = !!fiveHourResetTime || !!weeklyResetTime;
|
||||||
|
if (!hasSpecificReset && !fallbackResetTime) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-1 border-t border-border/50 space-y-1">
|
||||||
|
{fiveHourResetTime && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Clock className="w-3 h-3 text-blue-400" />
|
||||||
|
<span className="text-blue-400 font-medium">
|
||||||
|
5h resets {formatResetTime(fiveHourResetTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{weeklyResetTime && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Clock className="w-3 h-3 text-indigo-400" />
|
||||||
|
<span className="text-indigo-400 font-medium">
|
||||||
|
Weekly resets {formatResetTime(weeklyResetTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasSpecificReset && fallbackResetTime && (
|
||||||
|
<ResetTimeIndicator resetTime={fallbackResetTime} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,12 +183,34 @@ export interface CodexQuotaWindow {
|
|||||||
resetAt: string | null;
|
resetAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Core Codex usage window (5h/weekly) extracted from raw windows */
|
||||||
|
export interface CodexCoreUsageWindow {
|
||||||
|
/** Source window label */
|
||||||
|
label: string;
|
||||||
|
/** Percentage remaining (0-100) */
|
||||||
|
remainingPercent: number;
|
||||||
|
/** Seconds until quota resets, null if unknown */
|
||||||
|
resetAfterSeconds: number | null;
|
||||||
|
/** ISO timestamp when quota resets, null if unknown */
|
||||||
|
resetAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Core Codex usage summary with explicit 5h and weekly windows */
|
||||||
|
export interface CodexCoreUsageSummary {
|
||||||
|
/** Short-cycle usage limit window (typically 5h) */
|
||||||
|
fiveHour: CodexCoreUsageWindow | null;
|
||||||
|
/** Long-cycle usage limit window (typically weekly) */
|
||||||
|
weekly: CodexCoreUsageWindow | null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Codex quota result */
|
/** Codex quota result */
|
||||||
export interface CodexQuotaResult {
|
export interface CodexQuotaResult {
|
||||||
/** Whether fetch succeeded */
|
/** Whether fetch succeeded */
|
||||||
success: boolean;
|
success: boolean;
|
||||||
/** Quota windows (primary, secondary, code review) */
|
/** Quota windows (primary, secondary, code review) */
|
||||||
windows: CodexQuotaWindow[];
|
windows: CodexQuotaWindow[];
|
||||||
|
/** Explicit core usage windows (5h + weekly) for easier reset display */
|
||||||
|
coreUsage?: CodexCoreUsageSummary;
|
||||||
/** Plan type: free, plus, team, or null if unknown */
|
/** Plan type: free, plus, team, or null if unknown */
|
||||||
planType: 'free' | 'plus' | 'team' | null;
|
planType: 'free' | 'plus' | 'team' | null;
|
||||||
/** Timestamp of fetch */
|
/** Timestamp of fetch */
|
||||||
|
|||||||
Reference in New Issue
Block a user