import { useRef, useState } from 'react' import { CliErrorPanel } from '../components/CliErrorPanel' import { ConnectAffordance } from '../components/ConnectAffordance' import { Panel } from '../components/Panel' import { SectionSkeleton } from '../components/Skeleton' import type { Section } from '../components/Sidebar' import { StaleBanner } from '../components/StaleBanner' import { usePolled } from '../hooks/usePolled' import { formatConverted } from '../lib/format' import { codeburn } from '../lib/ipc' import { motionClass } from '../lib/motion' import type { JsonPlanSummary, Period, PlanId, PlanProvider, QuotaProvider, QuotaWindow, StatusJson } from '../lib/types' import type { SettingsPane } from './Settings' const PROVIDER_ORDER: PlanProvider[] = ['all', 'claude', 'codex', 'cursor', 'grok'] const PLAN_NAMES: Record = { 'claude-pro': 'Claude Pro', 'claude-max': 'Claude Max', 'claude-max-5x': 'Claude Max 5x', 'cursor-pro': 'Cursor Pro', supergrok: 'SuperGrok', 'supergrok-heavy': 'SuperGrok Heavy', custom: 'Custom plan', none: 'API usage', } function fmtPct(n: number): string { return Number.isInteger(n) ? `${n}%` : `${n.toFixed(1)}%` } /** Honest copy for a 429 backoff window (the upstream quota endpoint rate * limited us), replacing the generic "waiting" note. */ export function rateLimitedNote(provider: QuotaProvider['provider']): string { const owner = provider === 'claude' ? 'Anthropic' : 'OpenAI' return `${owner} rate limited the quota endpoint, retrying in a few minutes` } function cycleEndDate(plan: JsonPlanSummary): Date | null { const date = new Date(plan.periodEnd) if (Number.isNaN(date.getTime())) return null date.setDate(date.getDate() - 1) return date } function formatShortDate(value: string | Date): string { const date = value instanceof Date ? value : new Date(value) if (Number.isNaN(date.getTime())) return 'unknown' return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', }).format(date) } function planSummaries(status: StatusJson): JsonPlanSummary[] { const plans = status.plans if (plans) { const ordered = PROVIDER_ORDER.flatMap(provider => { const plan = plans[provider] return plan ? [plan] : [] }) if (ordered.length > 0) return ordered } return status.plan ? [status.plan] : [] } function manualPlanSummaries(status: StatusJson): JsonPlanSummary[] { return planSummaries(status).filter(plan => plan.provider !== 'claude' && plan.provider !== 'codex') } export function Plans({ period, refreshToken = 0, onNavigate, ready = true }: { period: Period; refreshToken?: number; onNavigate?: (section: Section, pane?: SettingsPane) => void; ready?: boolean }) { // Force a fresh fetch (bypassing QuotaService's 5-min cache, and its keychain // guard) when the user hits ⌘R or clicks Refresh in the Connect affordance; // the steady 30s poll keeps serving cached quota. const [reconnectNonce, setReconnectNonce] = useState(0) const lastForced = useRef(`${refreshToken}:${reconnectNonce}`) const quota = usePolled(() => { const key = `${refreshToken}:${reconnectNonce}` const force = key !== lastForced.current lastForced.current = key return codeburn.getQuota(force) }, [refreshToken, reconnectNonce]) const reconnect = () => setReconnectNonce(value => value + 1) const budgetReport = usePolled(() => codeburn.getPlans(period), [period, refreshToken], { enabled: ready }) const manualPlans = budgetReport.data ? manualPlanSummaries(budgetReport.data) : [] return ( <>
Plans
{budgetReport.data && budgetReport.error && } {renderQuota(quota.data, quota.error, reconnect)} {renderBudgetPlans(budgetReport.data, budgetReport.error, manualPlans)}
) } function renderQuota(data: QuotaProvider[] | null, error: ReturnType>['error'], onReconnect: () => void) { if (!data) { if (error) { return (

Live quota is unavailable.

) } return } if (data.length === 0) { return (

No quota providers available.

) } return data.map(provider => ) } function renderBudgetPlans(data: StatusJson | null, error: ReturnType>['error'], plans: JsonPlanSummary[]) { if (!data && error) { return (

Budget plans

) } if (plans.length === 0) return null return (

Budget plans

{plans.map(plan => )}
) } function QuotaPanel({ quota, onReconnect }: { quota: QuotaProvider; onReconnect: () => void }) { const providerName = quota.provider === 'claude' ? 'Claude' : 'Codex' return ( {providerName}{quota.planLabel ? {quota.planLabel} : null}} right={} > ) } function ConnectionIndicator({ connection }: { connection: QuotaProvider['connection'] }) { const label = connection === 'transientFailure' ? 'waiting' : connection === 'terminalFailure' ? 'error' : connection === 'accessDenied' ? 'locked' : connection return {label} } function QuotaContent({ quota, onReconnect }: { quota: QuotaProvider; onReconnect: () => void }) { if (quota.connection === 'disconnected' || quota.connection === 'accessDenied') { return } if (quota.connection === 'loading') return

Loading quota…

if (quota.connection === 'stale' || quota.connection === 'transientFailure') { if (quota.rateLimited) return

{rateLimitedNote(quota.provider)}

return

waiting on the CLI…

} if (quota.connection === 'terminalFailure') { return

Quota is currently unavailable.

} return ( <>
{quota.details.map((window, index) => )}
{quota.footerLines.length > 0 ? (
{quota.footerLines.map((line, index) => {line})}
) : null} ) } function QuotaMeter({ window }: { window: QuotaWindow }) { const percent = Math.round(window.percent * 100) const severity = window.percent >= 0.9 ? 'bad' : window.percent >= 0.7 ? 'warn' : 'accent' const reset = formatResetTime(window.resetsAt) return (
{window.label} {percent}% used{reset ? ` · resets ${reset}` : ''}
) } function formatResetTime(resetsAt: string | null): string | null { if (!resetsAt) return null const reset = Date.parse(resetsAt) if (!Number.isFinite(reset)) return null const remainingMinutes = Math.floor((reset - Date.now()) / 60_000) if (remainingMinutes <= 0) return 'now' const days = Math.floor(remainingMinutes / (24 * 60)) const hours = Math.floor((remainingMinutes % (24 * 60)) / 60) const minutes = remainingMinutes % 60 if (days > 0) return `in ${days}d${hours > 0 ? ` ${hours}h` : ''}` if (hours > 0) return `in ${hours}h${minutes > 0 ? ` ${minutes}m` : ''}` return `in ${minutes}m` } function PlanPanel({ plan }: { plan: JsonPlanSummary }) { const hasBudget = plan.budget > 0 const displayPercent = Math.min(100, Math.max(0, plan.percentUsed)) const over = plan.status === 'over' || plan.percentUsed > 100 const trackClass = hasBudget ? (over ? 'over' : undefined) : 'mut' const overage = Math.max(0, plan.spent - plan.budget) const right = hasBudget ? `${formatConverted(plan.spent)} · ${fmtPct(plan.percentUsed)}${overage > 0 ? ` · ${formatConverted(overage)} over` : ''}` : `${formatConverted(plan.spent)} this cycle` const detail = hasBudget ? `${formatConverted(plan.budget)} / month · ${plan.provider}` : `${plan.provider} · pay as you go, no plan` return (
{PLAN_NAMES[plan.id]} {detail} {right}
{hasBudget ? : null}
) } function PaceLine({ plan }: { plan: JsonPlanSummary }) { const end = cycleEndDate(plan) const endLabel = end ? formatShortDate(end) : 'unknown' if (plan.status === 'over' || plan.projectedMonthEnd > plan.budget) { return (
On pace to exceed; projected {formatConverted(plan.projectedMonthEnd)} by {endLabel}
) } if (plan.status === 'near') { return (
{fmtPct(plan.percentUsed)} of budget used; projected {formatConverted(plan.projectedMonthEnd)} by {endLabel}
) } return
On track
}