diff --git a/app/electron/main.test.ts b/app/electron/main.test.ts
index 07b6daa1..fac0a6f6 100644
--- a/app/electron/main.test.ts
+++ b/app/electron/main.test.ts
@@ -69,12 +69,13 @@ const ARGV_CASES: Array<{ channel: string; args: unknown[]; argv: string[] }> =
{ channel: 'codeburn:getSessions', args: ['30days', 'claude', { from: '2026-07-01', to: '2026-07-11' }], argv: ['sessions', '--format', 'json', '--period', '30days', '--provider', 'claude', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getCompareModels', args: ['month', 'codex'], argv: ['compare', '--format', 'json', '--period', 'month', '--provider', 'codex'] },
{ channel: 'codeburn:getCompare', args: ['month', 'all', 'model-a', 'model-b'], argv: ['compare', '--format', 'json', '--period', 'month', '--model-a', 'model-a', '--model-b', 'model-b'] },
- { channel: 'codeburn:getYield', args: ['today'], argv: ['yield', '--format', 'json', '--period', 'today'] },
+ { channel: 'codeburn:getYield', args: ['today', 'all'], argv: ['yield', '--format', 'json', '--period', 'today'] },
+ { channel: 'codeburn:getYield', args: ['today', 'claude'], argv: ['yield', '--format', 'json', '--period', 'today', '--provider', 'claude'] },
{ channel: 'codeburn:getSpendFlow', args: ['month', 'openai'], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--provider', 'openai'] },
{ channel: 'codeburn:getOptimizeReport', args: ['month', 'openai'], argv: ['optimize', '--format', 'json', '--period', 'month', '--provider', 'openai'] },
{ channel: 'codeburn:getOverview', args: ['30days', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['status', '--format', 'menubar-json', '--period', '30days', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getModels', args: ['week', 'claude', true, { from: '2026-07-01', to: '2026-07-11' }], argv: ['models', '--format', 'json', '--period', 'week', '--provider', 'claude', '--by-task', '--from', '2026-07-01', '--to', '2026-07-11'] },
- { channel: 'codeburn:getYield', args: ['today', { from: '2026-07-01', to: '2026-07-11' }], argv: ['yield', '--format', 'json', '--period', 'today', '--from', '2026-07-01', '--to', '2026-07-11'] },
+ { channel: 'codeburn:getYield', args: ['today', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['yield', '--format', 'json', '--period', 'today', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getSpendFlow', args: ['month', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getOptimizeReport', args: ['month', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['optimize', '--format', 'json', '--period', 'month', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getDevices', args: ['week'], argv: ['devices', '--format', 'json', '--period', 'week'] },
@@ -157,7 +158,7 @@ describe('createBridgeHandlers (IPC wiring)', () => {
throw new CliError('nonzero', 'boom')
})
const handlers = createBridgeHandlers(withQuota({ spawnCli, spawnCliAction: vi.fn(), resolveCodeburnPath: () => '/bin/codeburn' }))
- const res = await handlers['codeburn:getYield']!('today')
+ const res = await handlers['codeburn:getYield']!('today', 'all')
expect(res).toEqual({ ok: false, error: { kind: 'nonzero', message: 'boom' } })
})
diff --git a/app/electron/main.ts b/app/electron/main.ts
index 6b9ebbb5..57a2ad67 100644
--- a/app/electron/main.ts
+++ b/app/electron/main.ts
@@ -75,8 +75,8 @@ export function createBridgeHandlers(deps: Deps = { spawnCli, spawnCliAction, re
'codeburn:getCompare': run((period: string, provider: string, modelA: string, modelB: string) => [
'compare', '--format', 'json', '--period', period, ...providerArgs(provider), '--model-a', modelA, '--model-b', modelB,
]),
- 'codeburn:getYield': run((period: string, range?: DateRange) => [
- 'yield', '--format', 'json', '--period', period, ...rangeArgs(range),
+ 'codeburn:getYield': run((period: string, provider: string, range?: DateRange) => [
+ 'yield', '--format', 'json', '--period', period, ...providerArgs(provider), ...rangeArgs(range),
]),
'codeburn:getSpendFlow': run((period: string, provider: string, range?: DateRange) => [
'spend', '--format', 'flow-json', '--period', period, ...providerArgs(provider), ...rangeArgs(range),
diff --git a/app/electron/preload.ts b/app/electron/preload.ts
index b8af3628..98e154aa 100644
--- a/app/electron/preload.ts
+++ b/app/electron/preload.ts
@@ -26,7 +26,7 @@ const bridge = {
getSessions: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getSessions', period, provider, range),
getCompareModels: (period: string, provider: string) => invoke('codeburn:getCompareModels', period, provider),
getCompare: (period: string, provider: string, modelA: string, modelB: string) => invoke('codeburn:getCompare', period, provider, modelA, modelB),
- getYield: (period: string, range?: DateRange) => invoke('codeburn:getYield', period, range),
+ getYield: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getYield', period, provider, range),
getSpendFlow: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getSpendFlow', period, provider, range),
getOptimizeReport: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getOptimizeReport', period, provider, range),
getDevices: (period: string) => invoke('codeburn:getDevices', period),
diff --git a/app/renderer/App.test.tsx b/app/renderer/App.test.tsx
index bd1a5db4..904b196e 100644
--- a/app/renderer/App.test.tsx
+++ b/app/renderer/App.test.tsx
@@ -218,6 +218,26 @@ describe('App shortcuts', () => {
})
})
+ it('builds the provider picker from providerDetails so display-name providers round-trip their internal id', async () => {
+ // grok's display name is "Grok Build"; the picker must show the label but
+ // send the internal id `grok` as --provider (which assertProvider accepts).
+ const payload = overviewPayload()
+ payload.current.providers = { 'grok build': 5, claude: 10 }
+ payload.current.providerDetails = [
+ { id: 'grok', label: 'Grok Build', cost: 5 },
+ { id: 'claude', label: 'Claude', cost: 10 },
+ ]
+ mocks.getOverview.mockResolvedValue(payload)
+
+ render()
+ expect(await screen.findByText('Most expensive sessions')).toBeInTheDocument()
+
+ fireEvent.click(screen.getByText('All providers'))
+ fireEvent.click(await screen.findByRole('option', { name: 'Grok Build' }))
+
+ await waitFor(() => expect(mocks.getOverview).toHaveBeenCalledWith('30days', 'grok'))
+ })
+
it('applies a calendar range to overview and visible section polls', async () => {
render()
diff --git a/app/renderer/App.tsx b/app/renderer/App.tsx
index 18c2739f..b7d5801e 100644
--- a/app/renderer/App.tsx
+++ b/app/renderer/App.tsx
@@ -7,7 +7,7 @@ import { Sidebar, type Section } from './components/Sidebar'
import { rangeLabel, TopBar } from './components/TopBar'
import { Window } from './components/Window'
import { usePolled } from './hooks/usePolled'
-import { formatUsd } from './lib/format'
+import { formatUsd, setActiveCurrency } from './lib/format'
import { codeburn } from './lib/ipc'
import { OverviewContent } from './sections/Overview'
import { OptimizeContent } from './sections/Optimize'
@@ -68,10 +68,11 @@ export function App() {
const [settingsPane, setSettingsPane] = useState('general')
const [period, setPeriod] = useState('30days')
const [provider, setProvider] = useState('all')
- const [detectedProviders, setDetectedProviders] = useState([])
+ const [detectedProviders, setDetectedProviders] = useState>([])
const [customRange, setCustomRange] = useState(null)
const [refreshToken, setRefreshToken] = useState(0)
const [now, setNow] = useState(() => Date.now())
+ const [, setCurrencyTick] = useState(0)
const overview = usePolled(
() => customRange
@@ -90,14 +91,26 @@ export function App() {
useEffect(() => {
if (!overview.data) return
- const found = Object.entries(overview.data.current.providers).filter(([, value]) => value > 0).map(([key]) => key)
+ const details = overview.data.current.providerDetails
+ // Prefer providerDetails (internal id + display label); fall back to the
+ // providers map keys (lowercased display names) for older CLIs.
+ const found = details
+ ? details.filter(entry => entry.cost > 0).map(entry => ({ id: entry.id, label: entry.label }))
+ : Object.entries(overview.data.current.providers).filter(([, value]) => value > 0).map(([key]) => ({ id: key, label: providerName(key) }))
setDetectedProviders(current => {
const next = [...current]
- for (const item of found) if (!next.includes(item)) next.push(item)
+ for (const item of found) if (!next.some(entry => entry.id === item.id)) next.push(item)
return next.length === current.length ? current : next
})
}, [overview.data])
+ useEffect(() => {
+ const currency = overview.data?.currency
+ if (!currency) return
+ setActiveCurrency(currency)
+ setCurrencyTick(tick => tick + 1)
+ }, [overview.data?.currency?.code, overview.data?.currency?.rate, overview.data?.currency?.symbol])
+
useEffect(() => {
const id = window.setInterval(() => setNow(Date.now()), 1000)
return () => window.clearInterval(id)
@@ -143,9 +156,9 @@ export function App() {
const providerOptions = [
{ value: 'all', label: 'All providers' },
- ...detectedProviders.map(value => ({ value, label: providerName(value) })),
+ ...detectedProviders.map(entry => ({ value: entry.id, label: entry.label })),
]
- const providerLabel = providerName(provider)
+ const providerLabel = detectedProviders.find(entry => entry.id === provider)?.label ?? providerName(provider)
const scope = `${customRange ? rangeLabel(customRange) : PERIOD_LABELS[period]} · ${providerLabel}`
return (
diff --git a/app/renderer/lib/format.test.ts b/app/renderer/lib/format.test.ts
index ab6490af..f2fff1f1 100644
--- a/app/renderer/lib/format.test.ts
+++ b/app/renderer/lib/format.test.ts
@@ -1,6 +1,29 @@
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
-import { formatCompact, formatDayLong, formatDayShort, formatDuration } from './format'
+import { formatCompact, formatConverted, formatDayLong, formatDayShort, formatDuration, formatUsd, setActiveCurrency } from './format'
+
+describe('currency-aware formatting', () => {
+ afterEach(() => setActiveCurrency({ code: 'USD', symbol: '$', rate: 1 }))
+
+ it('formats raw USD with the default USD currency', () => {
+ expect(formatUsd(12.34)).toBe('$12.34')
+ expect(formatUsd(1_234.5)).toBe('$1,234.50')
+ })
+
+ it('applies the active FX rate and symbol to raw-USD values exactly once', () => {
+ setActiveCurrency({ code: 'EUR', symbol: '€', rate: 0.9 })
+ // 100 USD × 0.9 = 90.00 EUR
+ expect(formatUsd(100)).toBe('€90.00')
+ expect(formatUsd(1_000)).toBe('€900.00')
+ })
+
+ it('formatConverted swaps the symbol without re-applying the rate (CLI-converted values)', () => {
+ setActiveCurrency({ code: 'EUR', symbol: '€', rate: 0.9 })
+ // Already-EUR input renders as-is, never multiplied by the rate again.
+ expect(formatConverted(90)).toBe('€90.00')
+ expect(formatConverted(20)).toBe('€20.00')
+ })
+})
describe('formatCompact', () => {
it('formats zero, plain counts, thousands, and millions compactly', () => {
diff --git a/app/renderer/lib/format.ts b/app/renderer/lib/format.ts
index 528016c2..1fa78a60 100644
--- a/app/renderer/lib/format.ts
+++ b/app/renderer/lib/format.ts
@@ -1,5 +1,25 @@
+type ActiveCurrency = { code: string; symbol: string; rate: number }
+
+// Single source of truth for display currency. App.tsx sets it from the overview
+// payload; every formatUsd/formatConverted call site then converts for free.
+// Defaults to USD so the first render (before the payload arrives) is correct.
+let activeCurrency: ActiveCurrency = { code: 'USD', symbol: '$', rate: 1 }
+
+export function setActiveCurrency(currency: ActiveCurrency): void {
+ activeCurrency = currency
+}
+
+/** Raw-USD input: multiplies by the active FX rate, then prefixes the symbol. */
export function formatUsd(n: number): string {
- return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+ return formatConverted(n * activeCurrency.rate)
+}
+
+/**
+ * Already-converted input (CLI-side convertCost values, e.g. plan budgets): only
+ * prefixes the active symbol and formats the magnitude — never re-applies the rate.
+ */
+export function formatConverted(n: number): string {
+ return `${activeCurrency.symbol}${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
/** Shorten filesystem and CLI-mangled project paths to their useful trailing segments. */
diff --git a/app/renderer/lib/types.ts b/app/renderer/lib/types.ts
index 7c64c1ee..934e23fc 100644
--- a/app/renderer/lib/types.ts
+++ b/app/renderer/lib/types.ts
@@ -145,6 +145,9 @@ export type MenubarPayload = {
unpricedModels?: Array<{ model: string; calls: number; tokens: number }>
localModelSavings: LocalModelSavings
providers: Record
+ // Optional: older CLIs omit it. `id` is the internal provider name (round-trips
+ // as --provider), `label` the display name. Fall back to `providers` when absent.
+ providerDetails?: Array<{ id: string; label: string; cost: number }>
topProjects: Array<{
name: string
cost: number
@@ -214,6 +217,9 @@ export type MenubarPayload = {
history: {
daily: DailyHistoryEntry[]
}
+ // Active display currency. Payload costs are raw USD; the renderer multiplies by
+ // `rate` and prefixes `symbol` at display time. Optional: older CLIs omit it.
+ currency?: { code: string; symbol: string; rate: number }
combined?: CombinedUsage
claudeConfigs?: ClaudeConfigSelector
}
@@ -492,7 +498,7 @@ export interface CodeburnBridge {
getSessions(period: Period, provider: string, range?: DateRange): Promise
getCompareModels(period: Period, provider: string): Promise
getCompare(period: Period, provider: string, modelA: string, modelB: string): Promise
- getYield(period: Period, range?: DateRange): Promise
+ getYield(period: Period, provider: string, range?: DateRange): Promise
getSpendFlow(period: Period, provider: string, range?: DateRange): Promise
getOptimizeReport(period: Period, provider: string, range?: DateRange): Promise
getDevices(period: Period): Promise
diff --git a/app/renderer/sections/Optimize.test.tsx b/app/renderer/sections/Optimize.test.tsx
index 592a4b20..f646a036 100644
--- a/app/renderer/sections/Optimize.test.tsx
+++ b/app/renderer/sections/Optimize.test.tsx
@@ -227,10 +227,11 @@ describe('Optimize', () => {
expect(screen.queryByRole('button', { name: 'Fixes 25' })).not.toBeInTheDocument()
})
- it('passes provider and custom range to the optimize report bridge', async () => {
+ it('passes provider and custom range to the optimize report and yield bridges', async () => {
render()
await screen.findByText('Opus is doing your small talk')
expect(getOptimizeReport).toHaveBeenCalledWith('30days', 'claude', { from: '2026-07-01', to: '2026-07-11' })
+ expect(getYield).toHaveBeenCalledWith('30days', 'claude', { from: '2026-07-01', to: '2026-07-11' })
})
it('keeps last-good yield totals and rows visible during revalidation', async () => {
diff --git a/app/renderer/sections/Optimize.tsx b/app/renderer/sections/Optimize.tsx
index e87171f8..517f406b 100644
--- a/app/renderer/sections/Optimize.tsx
+++ b/app/renderer/sections/Optimize.tsx
@@ -41,8 +41,8 @@ export function OptimizeContent({
[period, provider, range?.from, range?.to, refreshToken],
)
const yieldReport = usePolled(
- () => range ? codeburn.getYield(period, range) : codeburn.getYield(period),
- [period, range?.from, range?.to, refreshToken],
+ () => range ? codeburn.getYield(period, provider, range) : codeburn.getYield(period, provider),
+ [period, provider, range?.from, range?.to, refreshToken],
)
const [tab, setTab] = useState('waste')
diff --git a/app/renderer/sections/Overview.test.tsx b/app/renderer/sections/Overview.test.tsx
index 9665182f..c777e15e 100644
--- a/app/renderer/sections/Overview.test.tsx
+++ b/app/renderer/sections/Overview.test.tsx
@@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Polled } from '../hooks/usePolled'
+import { setActiveCurrency } from '../lib/format'
import type { ActReportJson, MenubarPayload, YieldJsonReport } from '../lib/types'
import { Overview, OverviewContent, localDateKey } from './Overview'
@@ -16,7 +17,7 @@ function polled(data: MenubarPayload): Polled {
const { getOverview, getActReport, getYield } = vi.hoisted(() => ({
getOverview: vi.fn<(period: string, provider: string) => Promise>(),
getActReport: vi.fn<() => Promise>(),
- getYield: vi.fn<(period: string) => Promise>(),
+ getYield: vi.fn<(period: string, provider: string) => Promise>(),
}))
vi.mock('../lib/ipc', async orig => {
const actual = await orig()
@@ -148,6 +149,7 @@ function makePayload(now: Date): MenubarPayload {
describe('Overview', () => {
beforeEach(() => {
+ setActiveCurrency({ code: 'USD', symbol: '$', rate: 1 })
getOverview.mockReset()
getActReport.mockReset()
getYield.mockReset()
@@ -481,4 +483,18 @@ describe('Overview', () => {
expect(await screen.findByRole('status')).toHaveTextContent('Refresh failed, showing last good data · codeburn exited 1')
})
+
+ it('renders section costs in the active non-USD currency (rate applied once, symbol swapped)', async () => {
+ setActiveCurrency({ code: 'EUR', symbol: '€', rate: 0.9 })
+ const now = new Date()
+ getOverview.mockResolvedValue(makePayload(now))
+
+ render()
+
+ // Cost per outcome sources raw-USD yield values: $/commit = 150/6 = 25 → €22.50,
+ // $/productive session = 120/3 = 40 → €36.00 (rate applied exactly once).
+ const outcome = (await screen.findByText('Cost per outcome')).closest('.ov-panel') as HTMLElement
+ expect(within(outcome).getByText('€22.50')).toBeInTheDocument()
+ expect(within(outcome).getByText('€36.00')).toBeInTheDocument()
+ })
})
diff --git a/app/renderer/sections/Overview.tsx b/app/renderer/sections/Overview.tsx
index b5a08b79..679e008a 100644
--- a/app/renderer/sections/Overview.tsx
+++ b/app/renderer/sections/Overview.tsx
@@ -468,7 +468,7 @@ export function OverviewContent({
onNavigate?: (section: 'optimize') => void
}) {
const actReport = usePolled(() => codeburn.getActReport(), [])
- const yieldReport = usePolled(() => codeburn.getYield(period), [period])
+ const yieldReport = usePolled(() => codeburn.getYield(period, provider), [period, provider])
const { data, error } = overview
const modelIndex = useMemo(() => data ? buildModelIndex(data) : new Map(), [data])
diff --git a/app/renderer/sections/Plans.test.tsx b/app/renderer/sections/Plans.test.tsx
index 3a5933f1..b1588d09 100644
--- a/app/renderer/sections/Plans.test.tsx
+++ b/app/renderer/sections/Plans.test.tsx
@@ -2,6 +2,7 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { setActiveCurrency } from '../lib/format'
import type { JsonPlanSummary, QuotaProvider, StatusJson } from '../lib/types'
import { Plans } from './Plans'
@@ -98,6 +99,7 @@ function quotaProviders(): QuotaProvider[] {
describe('Plans', () => {
beforeEach(() => {
+ setActiveCurrency({ code: 'USD', symbol: '$', rate: 1 })
getPlans.mockReset()
getQuota.mockReset()
getQuota.mockResolvedValue(quotaProviders())
@@ -203,6 +205,19 @@ describe('Plans', () => {
expect(await screen.findByText('Locate the codeburn CLI')).toBeInTheDocument()
})
+ it('does not re-apply the FX rate to CLI-converted plan values (symbol swap only)', async () => {
+ // getPlans values arrive already converted by the CLI (convertCost). With a
+ // EUR rate active, the pane must only swap the symbol — a second ×0.9 here
+ // would render €18.00 / €7.38 instead of the correct €20.00 / €8.20.
+ setActiveCurrency({ code: 'EUR', symbol: '€', rate: 0.9 })
+ getPlans.mockResolvedValue({ ...baseStatus, currency: 'EUR', plans: { cursor: cursorPlan } })
+
+ render()
+
+ expect(await screen.findByText('€20.00 / month · cursor')).toBeInTheDocument()
+ expect(screen.getByText('€8.20 · 41%')).toBeInTheDocument()
+ })
+
it('renders permission-denied CLI failures as the amber Full Disk Access state', async () => {
getPlans.mockRejectedValue({ kind: 'nonzero', message: 'Cursor permission denied: grant Full Disk Access' })
diff --git a/app/renderer/sections/Plans.tsx b/app/renderer/sections/Plans.tsx
index 1e74edec..87ac6692 100644
--- a/app/renderer/sections/Plans.tsx
+++ b/app/renderer/sections/Plans.tsx
@@ -3,7 +3,7 @@ import { Panel } from '../components/Panel'
import type { Section } from '../components/Sidebar'
import { StaleBanner } from '../components/StaleBanner'
import { usePolled } from '../hooks/usePolled'
-import { formatUsd } from '../lib/format'
+import { formatConverted } from '../lib/format'
import { codeburn } from '../lib/ipc'
import type { JsonPlanSummary, Period, PlanId, PlanProvider, QuotaProvider, QuotaWindow, StatusJson } from '../lib/types'
import type { SettingsPane } from './Settings'
@@ -206,9 +206,9 @@ function PlanPanel({ plan }: { plan: JsonPlanSummary }) {
const trackClass = hasBudget ? (over ? 'over' : undefined) : 'mut'
const overage = Math.max(0, plan.spent - plan.budget)
const right = hasBudget
- ? `${formatUsd(plan.spent)} · ${fmtPct(plan.percentUsed)}${overage > 0 ? ` · ${formatUsd(overage)} over` : ''}`
- : `${formatUsd(plan.spent)} this cycle`
- const detail = hasBudget ? `${formatUsd(plan.budget)} / month · ${plan.provider}` : `${plan.provider} · pay as you go, no plan`
+ ? `${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 (
@@ -231,14 +231,14 @@ function PaceLine({ plan }: { plan: JsonPlanSummary }) {
if (plan.status === 'over' || plan.projectedMonthEnd > plan.budget) {
return (
- On pace to exceed; projected {formatUsd(plan.projectedMonthEnd)} by {endLabel}
+ On pace to exceed; projected {formatConverted(plan.projectedMonthEnd)} by {endLabel}
)
}
if (plan.status === 'near') {
return (
- {fmtPct(plan.percentUsed)} of budget used; projected {formatUsd(plan.projectedMonthEnd)} by {endLabel}
+ {fmtPct(plan.percentUsed)} of budget used; projected {formatConverted(plan.projectedMonthEnd)} by {endLabel}
)
}
diff --git a/app/renderer/sections/Settings.test.tsx b/app/renderer/sections/Settings.test.tsx
index 512d9dfe..6c767a82 100644
--- a/app/renderer/sections/Settings.test.tsx
+++ b/app/renderer/sections/Settings.test.tsx
@@ -131,7 +131,7 @@ describe('Settings', () => {
render()
await user.click(screen.getByRole('button', { name: 'Plans' }))
expect((await screen.findAllByText('Claude Max 20x')).length).toBeGreaterThan(0)
- expect(screen.getByText('$200/month · claude · 24% used')).toBeInTheDocument()
+ expect(screen.getByText('$200.00/month · claude · 24% used')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Remove' }))
expect(mocks.resetPlan).toHaveBeenCalledWith('claude')
await user.click(screen.getByLabelText('Add a plan'))
diff --git a/app/renderer/sections/Settings.tsx b/app/renderer/sections/Settings.tsx
index bc4c6f3a..1cc392d0 100644
--- a/app/renderer/sections/Settings.tsx
+++ b/app/renderer/sections/Settings.tsx
@@ -7,7 +7,7 @@ import { Panel } from '../components/Panel'
import { ProviderLogo } from '../components/ProviderLogo'
import type { Section } from '../components/Sidebar'
import { usePolled } from '../hooks/usePolled'
-import { formatUsd } from '../lib/format'
+import { formatConverted, formatUsd } from '../lib/format'
import { codeburn } from '../lib/ipc'
import type { ActionResult, AliasRow, CliError, CombinedUsage, DeviceScanResult, Identity, JsonPlanSummary, MenubarPayload, Period, PlanId, PlanProvider, ShareStatus, StatusJson } from '../lib/types'
@@ -203,7 +203,7 @@ function PlansPane({ period, refreshToken, onNavigate }: { period: Period; refre
Plans
Set a monthly budget plan per provider. codeburn compares it to your API-equivalent spend.