feat(quota): add Claude quota reset support

- Add reset counts and actions to auth file and quota pages
- Reuse the Codex confirmation flow with Claude eligibility and cooldown checks
- Preserve request IDs on retries to prevent duplicate credit consumption
- Add translations, regression tests, and layout checks
This commit is contained in:
lzt404 committed 2026-10-05 00:45:17 +08:00
1 parent 2b81856521
commit a3f5d2e54e
17 files changed
+729 -34

No files matched your search

+4 -4
View File
@@ -3,7 +3,7 @@ import { useI18n } from '../i18n';
import { MessageNotice } from '../appNotice';
import { readBoolean } from '../services/managementApi';
import { fileName, formatQuotaTimestamp, type AuthFile, type QuotaState } from '../services/quotaService';
import { canResetCodexQuota } from '../services/quotaActions';
import { canResetQuota, hasPendingClaudeReset } from '../services/quotaActions';
import { formatQuotaReset, useQuotaClock } from '../services/quotaTime';
import './AuthFileQuotaPanel.css';
@@ -22,7 +22,7 @@ export function AuthFileQuotaPanel({ quota, file, disabled, onRefresh, onReset,
const name = fileName(file);
const compactLayout = compact || dense;
const fileDisabled = readBoolean(file, 'disabled');
const showReset = Boolean(onReset && (quota.resetCredits ?? 0) > 0);
const showReset = Boolean(onReset && ((quota.resetCredits ?? 0) > 0 || hasPendingClaudeReset(file)));
const resetCreditsLabel = quota.resetCredits !== undefined ? t('authFiles.quota.resets', { count: quota.resetCredits }) : '';
const accountCreditsLabel = quota.creditsUnlimited
? t('quota.creditUnlimited')
@@ -56,8 +56,8 @@ export function AuthFileQuotaPanel({ quota, file, disabled, onRefresh, onReset,
{!compactLayout ? <strong>{t('authFiles.settings.quotaRemaining')}</strong> : null}
{!compactLayout && quota.plan ? <span className="credential-quota-plan">{quota.plan}</span> : null}
{dense && resetCreditsLabel ? <small className="credential-quota-reset-credits">{resetCreditsLabel}</small> : null}
{showReset ? <button type="button" className="secondary-button compact-button credential-quota-reset" onClick={onReset} disabled={disabled || !canResetCodexQuota(file, quota)}
title={t('quota.reset')}>{t('quota.reset')}</button> : null}
{showReset ? <button type="button" className="secondary-button compact-button credential-quota-reset" onClick={onReset} disabled={disabled || !canResetQuota(file, quota)}
title={t('quota.reset')}>{t(hasPendingClaudeReset(file) ? 'quota.claude.retry' : 'quota.reset')}</button> : null}
{!compactLayout ? <button type="button" className="credential-quota-refresh" disabled={disabled || loading} onClick={onRefresh}
title={disabled ? t('quota.fileDisabled') : t('authFiles.quota.refresh')}>
{loading ? <LoaderCircle size={13} className="spin" /> : <RefreshCw size={13} />}
@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { useI18n } from '../i18n';
import { resetCodexQuotaWithConfirmation } from '../services/quotaActions';
import { fileName, formatQuotaTimestamp, type AuthFile, type QuotaState } from '../services/quotaService';
import { hasPendingClaudeReset, resetQuotaWithConfirmation } from '../services/quotaActions';
import { fileName, formatQuotaTimestamp, providerForFile, type AuthFile, type QuotaState } from '../services/quotaService';
import type { ConfirmationOptions } from './ConfirmationDialog';
export function useCodexQuotaReset(
export function useQuotaReset(
askConfirmation: (options: ConfirmationOptions) => Promise<boolean>,
setError: (message: string) => void,
) {
@@ -12,15 +12,15 @@ export function useCodexQuotaReset(
return useCallback(async (file: AuthFile, quota: QuotaState) => {
setError('');
try {
await resetCodexQuotaWithConfirmation(file, () => askConfirmation({
await resetQuotaWithConfirmation(file, () => askConfirmation({
title: t('quota.reset'),
message: t('quota.confirm.title', { name: fileName(file) }),
confirmText: t('quota.confirm.button'),
message: t(providerForFile(file) === 'claude' ? 'quota.claude.confirm' : 'quota.confirm.title', { name: fileName(file) }),
confirmText: t(hasPendingClaudeReset(file) ? 'quota.claude.retry' : 'quota.confirm.button'),
details: [
{ label: t('quota.resetCredits'), value: String(quota.resetCredits ?? '—') },
{ label: t('quota.earliestExpiry'), value: formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale) },
],
warning: t('quota.confirm.warning'),
warning: t(providerForFile(file) === 'claude' ? hasPendingClaudeReset(file) ? 'quota.claude.unknown' : 'quota.claude.warning' : 'quota.confirm.warning'),
}));
} catch (requestError) {
setError(requestError instanceof Error ? requestError.message : String(requestError));