import { useEffect, useRef, useState } from 'react' import { Hint } from '../components/Hint' import { CliErrorText, cliErrorDisplay } from '../components/CliErrorPanel' import { ConnectAffordance } from '../components/ConnectAffordance' import { Dropdown } from '../components/Dropdown' import { Panel } from '../components/Panel' import { ProviderLogo } from '../components/ProviderLogo' import type { Section } from '../components/Sidebar' import { usePolled } from '../hooks/usePolled' import { updateDownloadUrl, useUpdateStatus } from '../hooks/useUpdateStatus' import { version as appVersion } from '../../package.json' import { readDailyBudget } from '../lib/budget' import { formatConverted, formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' import { motionClass } from '../lib/motion' import { REFRESH_OPTIONS, useRefreshCadence } from '../lib/refreshCadence' import { showToast } from '../lib/toast' import { ToastHost } from '../components/ToastHost' import { rateLimitedNote } from './Plans' import type { ActionResult, AliasRow, ClaudeConfigSelector, CliError, CombinedUsage, DeviceScanResult, Identity, JsonPlanSummary, MenubarPayload, Period, PlanId, PlanProvider, PriceOverrideList, PriceOverrideRow, PriceRates, QuotaProvider, ShareStatus, StatusJson, TelemetryStatus } from '../lib/types' export type SettingsPane = 'general' | 'providers' | 'aliases' | 'pricing' | 'plans' | 'devices' | 'export' | 'privacy' type Pane = SettingsPane type Theme = 'system' | 'light' | 'dark' type PlanPreset = { id: Exclude; label: string; provider: Exclude } const PLAN_PRESETS: PlanPreset[] = [ { id: 'claude-pro', label: 'Claude Pro', provider: 'claude' }, { id: 'claude-max', label: 'Claude Max 20x', provider: 'claude' }, { id: 'claude-max-5x', label: 'Claude Max 5x', provider: 'claude' }, { id: 'cursor-pro', label: 'Cursor Pro', provider: 'cursor' }, { id: 'supergrok', label: 'SuperGrok', provider: 'grok' }, { id: 'supergrok-heavy', label: 'SuperGrok Heavy', provider: 'grok' }, ] // Claude and Codex subscriptions are detected from the CLI login (see the // detected-subscriptions list), so only non-OAuth providers get a manual preset. const MANUAL_PLAN_PRESETS = PLAN_PRESETS.filter(preset => preset.provider !== 'claude') const CURRENCIES = [ 'USD', 'EUR', 'GBP', 'JPY', 'CNY', 'CAD', 'AUD', 'CHF', 'HKD', 'SGD', 'INR', 'NZD', 'SEK', 'NOK', 'DKK', 'KRW', 'BRL', 'MXN', 'ZAR', 'AED', 'SAR', 'TRY', 'PLN', 'THB', 'IDR', 'MYR', 'PHP', 'RUB', 'ILS', 'CZK', ] function readSetting(key: string): string | null { try { return globalThis.localStorage?.getItem(key) ?? null } catch { return null } } function writeSetting(key: string, value: string): void { try { globalThis.localStorage?.setItem(key, value) } catch { /* storage can be unavailable in hardened contexts */ } } const RAIL_ITEMS: Array<{ id: Pane; label: string; icon: React.ReactNode }> = [ { id: 'general', label: 'General', icon: <> }, { id: 'providers', label: 'Providers', icon: <> }, { id: 'aliases', label: 'Model aliases', icon: <> }, { id: 'pricing', label: 'Pricing', icon: <> }, { id: 'plans', label: 'Plans', icon: <> }, { id: 'devices', label: 'Devices', icon: <> }, { id: 'export', label: 'Export', icon: <> }, { id: 'privacy', label: 'Privacy & data', icon: }, ] function periodLabel(period: Period): string { if (period === 'today') return 'today' if (period === 'week') return 'last 7 days' if (period === 'month') return 'this month' if (period === '30days') return 'last 30 days' return 'all time' } function shortFingerprint(fingerprint: string): string { const parts = fingerprint.split(':').filter(Boolean) if (parts.length < 3) return fingerprint return `${parts[0]}:${parts[1]}:…:${parts[parts.length - 1]}` } /** Inline destructive confirm: the button swaps to a prompt + Confirm/Cancel in * place (no OS dialog). Auto-cancels on Escape or when focus leaves the group. */ function ConfirmButton({ label, prompt, onConfirm }: { label: string; prompt: string; onConfirm: () => void }) { const [confirming, setConfirming] = useState(false) if (!confirming) { return } return ( { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setConfirming(false) }} onKeyDown={event => { if (event.key === 'Escape') setConfirming(false) }} > {prompt} ) } export function Settings({ period, refreshToken = 0, onNavigate, initialPane, claudeConfigs, claudeConfigSource = null, onConfigMutated }: { period: Period; refreshToken?: number; onNavigate?: (section: Section) => void; initialPane?: SettingsPane; claudeConfigs?: ClaudeConfigSelector; claudeConfigSource?: string | null; onConfigMutated?: () => void }) { const [pane, setPane] = useState(initialPane ?? 'general') return ( <>
Settings
{pane === 'general' && } {pane === 'providers' && } {pane === 'aliases' && } {pane === 'pricing' && } {pane === 'plans' && } {pane === 'devices' && } {pane === 'export' && } {pane === 'privacy' && }
) } function GeneralPane({ period, refreshToken, claudeConfigs, claudeConfigSource, onConfigMutated }: { period: Period; refreshToken: number; claudeConfigs?: ClaudeConfigSelector; claudeConfigSource: string | null; onConfigMutated?: () => void }) { const [currencyNonce, setCurrencyNonce] = useState(0) const plans = usePolled(() => codeburn.getPlans(period), [period, refreshToken, currencyNonce]) const [theme, setTheme] = useState(() => { const saved = readSetting('codeburn.theme') return saved === 'light' || saved === 'dark' ? saved : 'system' }) const [defaultPeriod, setDefaultPeriod] = useState(() => readSetting('codeburn.defaultPeriod') ?? 'today') const cadence = useRefreshCadence() const [budgetKind, setBudgetKind] = useState<'off' | 'usd' | 'tokens'>(() => readDailyBudget()?.kind ?? 'off') const [budgetInput, setBudgetInput] = useState(() => { const budget = readDailyBudget(); return budget ? String(budget.value) : '' }) const [budgetError, setBudgetError] = useState('') const update = useUpdateStatus() const version = update?.currentVersion || appVersion const updateNote = update?.updateAvailable && update.latestVersion ? `Update available: ${update.latestVersion}` : update?.latestVersion ? 'Up to date' : '' useEffect(() => { if (theme === 'system') document.documentElement.removeAttribute('data-theme') else document.documentElement.setAttribute('data-theme', theme) }, [theme]) // Store on change; a positive finite amount persists, anything else clears the // cap (so the banner turns off) and, when non-empty, flags a validation error. const persistBudget = (kind: 'off' | 'usd' | 'tokens', input: string) => { const trimmed = input.trim() if (kind === 'off' || trimmed === '') { setBudgetError(''); writeSetting('codeburn.dailyBudget', ''); return } const value = Number(trimmed) if (!Number.isFinite(value) || value <= 0) { setBudgetError('Enter a positive number.'); return } setBudgetError('') writeSetting('codeburn.dailyBudget', JSON.stringify({ kind, value })) } const chooseTheme = (next: Theme) => { setTheme(next) writeSetting('codeburn.theme', next) } const finishCurrency = (result: ActionResult) => { showToast(result.ok ? 'Updated' : result.stderr || 'Unable to update currency', result.ok ? 'ok' : 'error') if (result.ok) { setCurrencyNonce(value => value + 1); onConfigMutated?.() } } const currencies = [...CURRENCIES] if (plans.data?.currency && !currencies.includes(plans.data.currency)) currencies.push(plans.data.currency) const hasConfigs = Boolean(claudeConfigs && claudeConfigs.options.length > 0) const activeConfigLabel = claudeConfigSource ? claudeConfigs?.options.find(option => option.id === claudeConfigSource)?.label ?? claudeConfigSource : 'All Claude configs' return (

General

Display and appearance for the whole app.

Appearance
ThemeMatch your system or force a mode {(['system', 'light', 'dark'] as Theme[]).map(value => )}
{hasConfigs && (
Claude config
Active configApplies to the overview data. Manage config folders with the codeburn CLI.{activeConfigLabel}
)}
Display
{plans.data ? ({ value: code, label: code }))} onChange={value => void codeburn.setCurrency(value).then(finishCurrency)} width={92} /> : plans.error ? : Loading…}
{ setDefaultPeriod(value); writeSetting('codeburn.defaultPeriod', value) }} width={92} />
({ value: option.value, label: option.label }))} onChange={cadence.setValue} width={124} />
{ const kind = value as 'off' | 'usd' | 'tokens'; setBudgetKind(kind); persistBudget(kind, budgetInput) }} width={120} />{budgetKind !== 'off' && { setBudgetInput(event.target.value); persistBudget(budgetKind, event.target.value) }} style={{ width: 90 }} />}
{budgetError &&

{budgetError}

}
About
Version {version}{updateNote && {updateNote}}{update?.updateAvailable && update.tag ? : null}
) } function ProvidersPane({ period, refreshToken }: { period: Period; refreshToken: number }) { const overview = usePolled(() => codeburn.getOverview(period, 'all'), [period, refreshToken]) const details = overview.data?.current.providerDetails // Prefer providerDetails (internal id + display label) so ProviderLogo keys on // the internal id. Fall back to the providers map keys (lowercased display // names) for older CLIs that omit providerDetails. const providers = details ? details.filter(entry => entry.cost > 0).map(entry => ({ id: entry.id, label: entry.label, cost: entry.cost })) : Object.entries(overview.data?.current.providers ?? {}).map(([id, cost]) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1), cost })) return

Providers

codeburn auto-detects coding tools from local session files. No setup needed.

{overview.error ? : !overview.data ?

Loading detected providers…

: providers.length === 0 ?

No providers detected.

: providers.map(entry =>
{entry.label}Detected · {formatUsd(entry.cost)}
)}
} function AliasesPane({ refreshToken, onConfigMutated }: { refreshToken: number; onConfigMutated?: () => void }) { const [actionNonce, setActionNonce] = useState(0) const aliases = usePolled(() => codeburn.getAliases(), [refreshToken, actionNonce]) const [from, setFrom] = useState('') const [to, setTo] = useState('') const [error, setError] = useState('') const complete = (result: ActionResult, added = false) => { if (!result.ok) { setError(result.stderr || 'Alias action failed'); return } setError('') if (added) { setFrom(''); setTo('') } setActionNonce(value => value + 1) onConfigMutated?.() } return

Model aliases

Map an unrecognized model name to a priced model so its cost shows up.

{aliases.error ? : !aliases.data ?

Loading aliases…

: aliases.data.length === 0 ?

No aliases configured. Unknown models are priced at $0 until aliased.

: aliases.data.map(alias =>
{alias.from}{alias.to}
)}
setFrom(event.target.value)} /> setTo(event.target.value)} />
{error &&

{error}

}

Unknown models are priced at $0 until aliased. A local model can instead be credited with what it would have cost via model-savings.

} function priceRateSummary(o: PriceOverrideRow): string { const parts = [`in ${o.inputPerM}`, `out ${o.outputPerM}`] if (typeof o.cacheReadPerM === 'number') parts.push(`read ${o.cacheReadPerM}`) if (typeof o.cacheCreationPerM === 'number') parts.push(`create ${o.cacheCreationPerM}`) return parts.join(' · ') } // '' -> not provided; a positive finite number -> a rate; 'invalid' otherwise. function parseRate(raw: string): number | undefined | 'invalid' { const trimmed = raw.trim() if (trimmed === '') return undefined const value = Number(trimmed) if (!Number.isFinite(value) || value <= 0) return 'invalid' return value } function PricingPane({ refreshToken, onConfigMutated }: { refreshToken: number; onConfigMutated?: () => void }) { const [actionNonce, setActionNonce] = useState(0) const overrides = usePolled(() => codeburn.getPriceOverrides(), [refreshToken, actionNonce]) const [model, setModel] = useState('') const [input, setInput] = useState('') const [output, setOutput] = useState('') const [cacheRead, setCacheRead] = useState('') const [cacheCreation, setCacheCreation] = useState('') const [error, setError] = useState('') const complete = (result: ActionResult, added = false) => { if (!result.ok) { setError(result.stderr || 'Price override action failed'); return } setError('') if (added) { setModel(''); setInput(''); setOutput(''); setCacheRead(''); setCacheCreation('') } setActionNonce(value => value + 1) onConfigMutated?.() } const add = () => { const fields: Array<[keyof PriceRates, string]> = [['input', input], ['output', output], ['cacheRead', cacheRead], ['cacheCreation', cacheCreation]] const rates: PriceRates = {} for (const [key, raw] of fields) { const parsed = parseRate(raw) if (parsed === 'invalid') { setError('Rates must be positive numbers (USD per 1M tokens).'); return } if (parsed !== undefined) rates[key] = parsed } if (!model.trim()) { setError('Enter a model name.'); return } if (rates.input === undefined || rates.output === undefined) { setError('Input and output rates are required.'); return } setError('') void codeburn.setPriceOverride(model.trim(), rates).then(result => complete(result, true)) } return

Pricing

Override or add per-model rates so local or self-hosted models are priced. Rates are USD per 1,000,000 tokens.

{overrides.error ? : !overrides.data ?

Loading price overrides…

: overrides.data.overrides.length === 0 ?

No price overrides configured. Add one below to price an unrecognized or local model.

: overrides.data.overrides.map(override =>
{override.model}{priceRateSummary(override)} void codeburn.removePriceOverride(override.model).then(result => complete(result))} />
)}
setModel(event.target.value)} /> setInput(event.target.value)} /> setOutput(event.target.value)} /> setCacheRead(event.target.value)} /> setCacheCreation(event.target.value)} />
{error &&

{error}

}

Rates are USD per 1,000,000 tokens. Input and output are required; cache read and cache creation are optional. A configured model is overridden; an unknown one is added.

} function planSummaries(status: StatusJson): JsonPlanSummary[] { if (status.plans) return Object.values(status.plans).filter((plan): plan is JsonPlanSummary => Boolean(plan)) return status.plan ? [status.plan] : [] } function DetectedRow({ quota, onReconnect }: { quota: QuotaProvider; onReconnect: () => void }) { const name = quota.provider === 'claude' ? 'Claude' : 'Codex' return
{name} {quota.connection === 'disconnected' || quota.connection === 'accessDenied' ?
: quota.rateLimited ? {rateLimitedNote(quota.provider)} : {quota.planLabel ?? 'Connected'}}
} function PlansPane({ period, refreshToken, onNavigate, onConfigMutated }: { period: Period; refreshToken: number; onNavigate?: (section: Section) => void; onConfigMutated?: () => void }) { const [nonce, setNonce] = useState(0) // Steady poll serves cached quota (force=false); the Connect affordance's // Refresh forces a keychain-allowed fetch via the same path as Plans.tsx. 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 plans = usePolled(() => codeburn.getPlans(period), [period, refreshToken, nonce]) const [presetId, setPresetId] = useState(MANUAL_PLAN_PRESETS[0]!.id) const configured = plans.data ? planSummaries(plans.data) : [] const finish = (result: ActionResult) => { showToast(result.ok ? (result.stdout.trim() || 'Plan updated') : (result.stderr || 'Plan action failed'), result.ok ? 'ok' : 'error') if (result.ok) { setNonce(value => value + 1); onConfigMutated?.() } } const remove = (plan: JsonPlanSummary) => { void codeburn.resetPlan(plan.provider).then(finish) } const add = () => { const preset = MANUAL_PLAN_PRESETS.find(item => item.id === presetId)! void codeburn.setPlan(preset.id, preset.provider).then(finish) } return

Plans

Claude and Codex subscriptions connect and auto-detect your tier. Set a manual budget plan for any other provider.

Detected subscriptions
{quota.error && !quota.data ? : !quota.data ?

Detecting subscriptions…

: quota.data.length === 0 ?

No detectable subscriptions.

: quota.data.map(provider => setReconnectNonce(value => value + 1)} />)}
Budget plans (manual)
{plans.error ? : !plans.data ?

Loading plans…

: configured.length === 0 ?

No manual plans configured.

: configured.map(plan =>
{PLAN_PRESETS.find(item => item.id === plan.id)?.label ?? plan.id}{formatConverted(plan.budget)}/month · {plan.provider} · {plan.percentUsed}% used{(plan.provider === 'claude' || plan.provider === 'codex') && superseded by the detected subscription} remove(plan)} />
)}
({ value: preset.id, label: preset.label }))} onChange={value => setPresetId(value as PlanPreset['id'])} width={160} />

Claude and Codex plans are detected automatically from your login.

} function ExportPane({ period, refreshToken }: { period: Period; refreshToken: number }) { const overview = usePolled(() => codeburn.getOverview(period, 'all'), [period, refreshToken]) const [format, setFormat] = useState<'csv' | 'json'>('csv') const [provider, setProvider] = useState('all') const [destination, setDestination] = useState(null) const [exporting, setExporting] = useState(false) const providers = Object.keys(overview.data?.current.providers ?? {}) const chooseDirectory = async () => { const selected = await codeburn.chooseDirectory() if (selected) setDestination(selected) } const exportNow = async () => { if (!destination) return setExporting(true) try { const result = await codeburn.exportData(format, provider, destination) showToast(result.ok ? `Exported to ${destination}` : (result.stderr || 'Export failed'), result.ok ? 'ok' : 'error') } finally { setExporting(false) } } return

Export

Save your usage as CSV or JSON. Everything stays on your machine.

Format
({ value, label: value.charAt(0).toUpperCase() + value.slice(1) }))]} onChange={setProvider} width={150} />
Destination{destination ?? 'Choose a folder…'}

CSV writes a folder (summary, daily, models, projects, sessions, tools, mcp). JSON writes one file (schema codeburn.export.v2).

} function DevicesPane({ period, refreshToken }: { period: Period; refreshToken: number }) { const [nonce, setNonce] = useState(0) const identity = usePolled(() => codeburn.getIdentity(), [refreshToken]) const shareStatus = usePolled(() => codeburn.getShareStatus(), [refreshToken]) const scan = usePolled(() => codeburn.getDevicesScan(), [refreshToken, nonce]) const devices = usePolled(() => codeburn.getDevices(period), [period, refreshToken, nonce]) const refresh = () => setNonce(value => value + 1) return

Devices

Combine usage across your machines.

} function PrivacyPane() { return

Privacy & data

What codeburn does, and does not do, with your data.

} /> } />
} /** The anonymous-telemetry consent toggle, mirroring the onboarding decision. */ function TelemetryClaim() { const [status, setStatus] = useState(null) useEffect(() => { if (typeof codeburn.telemetryStatus !== 'function') return codeburn.telemetryStatus().then(value => setStatus(value)).catch(() => {}) }, []) if (!status) return null const toggle = () => { if (typeof codeburn.setTelemetryEnabled !== 'function') return codeburn.setTelemetryEnabled(!status.enabled).then(value => setStatus(value)).catch(() => {}) } return
Anonymous telemetry
Optional usage statistics: model mix, task success, performance and errors. Never prompts, code or anything identifying.
} function PrivacyClaim({ title, detail, icon }: { title: string; detail: string; icon: React.ReactNode }) { return
{title}
{detail}
} function ThisDevicePanel({ identity, shareStatus }: { identity: ReturnType>; shareStatus: ReturnType> }) { const status = shareStatus.data ? {shareStatus.data.sharing ? 'Visible' : 'Not sharing'} : null return {identity.data ?
{identity.data.name}Local device name: {identity.data.name}{identity.data.fingerprint}
: identity.error ? :

Reading this device identity…

}{shareStatus.error && }
} function DiscoveredPanel({ scan }: { scan: ReturnType> }) { const found = scan.data?.found.filter(device => !device.paired) ?? [] return {!scan.data && scan.error ? : !scan.data ?

listening…

: found.length === 0 ?

No nearby devices found.

: found.map(device =>
{device.name}fingerprint {shortFingerprint(device.fingerprint)}
)}

To pair a device, run codeburn devices add in a terminal. Pairing is interactive (approve on the other device).

} function PairedPanel({ devices, period, onRefresh }: { devices: ReturnType>; period: Period; onRefresh: () => void }) { const [error, setError] = useState('') const paired = devices.data?.perDevice.filter(device => !device.local) ?? [] const remove = (name: string) => { void codeburn.removeDevice(name).then(result => { if (!result.ok) { setError(result.stderr || 'Unable to remove device'); return } setError('') onRefresh() }) } return Refresh}>{!devices.data && devices.error ? : !devices.data ?

Loading paired devices…

: paired.length === 0 ?

No paired devices yet.

: paired.map(device =>
{device.name}{device.sessions.toLocaleString('en-US')} sessions · {formatUsd(device.cost)} {periodLabel(period)}
remove(device.name)} />
)}{devices.data && devices.data.combined.deviceCount > 1 &&
Combined view active · {devices.data.combined.deviceCount} devices
}{error &&

{error}

}
} function SettingsErrorText({ error }: { error: CliError }) { if (error.kind === 'not-found') { const display = cliErrorDisplay(error); return

{display.title}

} return }