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:
1 parent
2b81856521
commit
a3f5d2e54e
17 files changed
+729
-34
No files matched your search
@@ -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));
|
||||
Reference in new issue
Block a user