diff --git a/app/renderer/sections/Plans.test.tsx b/app/renderer/sections/Plans.test.tsx
index 2ce7cfa7..18db5665 100644
--- a/app/renderer/sections/Plans.test.tsx
+++ b/app/renderer/sections/Plans.test.tsx
@@ -119,7 +119,7 @@ describe('Plans', () => {
expect(screen.getByRole('heading', { name: 'Budget plans' })).toBeInTheDocument()
expect(screen.getByText('Cursor Pro')).toBeInTheDocument()
- expect(screen.getByText('$20.00 / calendar month · API-equivalent budget, not live quota · cursor')).toBeInTheDocument()
+ expect(screen.getByText('$20.00 / month budget · API-equivalent, not a live provider window · cursor')).toBeInTheDocument()
expect(screen.getByText('$8.20 · 41%')).toBeInTheDocument()
const cursorFill = container.querySelector('[data-testid="plan-track-cursor"] i')
expect(cursorFill).toHaveStyle({ width: '41%' })
@@ -214,7 +214,7 @@ describe('Plans', () => {
render()
- expect(await screen.findByText('€20.00 / calendar month · API-equivalent budget, not live quota · cursor')).toBeInTheDocument()
+ expect(await screen.findByText('€20.00 / month budget · API-equivalent, not a live provider window · cursor')).toBeInTheDocument()
expect(screen.getByText('€8.20 · 41%')).toBeInTheDocument()
})
diff --git a/app/renderer/sections/Plans.tsx b/app/renderer/sections/Plans.tsx
index e51e2fa4..127944d2 100644
--- a/app/renderer/sections/Plans.tsx
+++ b/app/renderer/sections/Plans.tsx
@@ -232,7 +232,7 @@ function PlanPanel({ plan }: { plan: JsonPlanSummary }) {
? `${formatConverted(plan.spent)} · ${fmtPct(plan.percentUsed)}${overage > 0 ? ` · ${formatConverted(overage)} over` : ''}`
: `${formatConverted(plan.spent)} this cycle`
const detail = hasBudget
- ? `${formatConverted(plan.budget)} / calendar month · API-equivalent budget, not live quota · ${plan.provider}`
+ ? `${formatConverted(plan.budget)} / month budget · API-equivalent, not a live provider window · ${plan.provider}`
: `${plan.provider} · pay as you go, no plan`
return (
diff --git a/src/dashboard.tsx b/src/dashboard.tsx
index 0e5b1f0e..86ba2145 100644
--- a/src/dashboard.tsx
+++ b/src/dashboard.tsx
@@ -344,19 +344,25 @@ function planColor(planUsage: PlanUsage): string {
: '#5BF58C'
}
+// Headline and status share one line's worth of terminal each, both truncated
+// end-first, so the headline stays short enough to keep the percentage visible
+// at 80 columns and the status leads with the disclaimer, not the arithmetic.
export function planBudgetHeadline(planUsage: PlanUsage): string {
- return `${planLabel(planUsage)}: ${formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs ${formatCost(planUsage.budgetUsd)}/mo budget`
+ return `${planLabel(planUsage)}: ${formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent / ${formatCost(planUsage.budgetUsd)} budget`
}
export function planStatusText(planUsage: PlanUsage): string {
- const calendar = `Calendar-month budget, not a live provider window. Projected: ${formatCost(planUsage.projectedMonthUsd)}. Next calendar reset in ${planUsage.daysUntilReset} days.`
+ // The period is anniversary-based (plan.resetDay, 1-28, settable per plan via
+ // `codeburn plan set --reset-day`), so this is a monthly budget window, not a
+ // calendar month. The headline already says "budget"; do not repeat it here.
+ const detail = `Not a live provider window. Projected: ${formatCost(planUsage.projectedMonthUsd)}. Next budget reset in ${planUsage.daysUntilReset} days.`
if (planUsage.status === 'under') {
- return `Well within budget. ${calendar}`
+ return `Well within budget. ${detail}`
}
if (planUsage.status === 'near') {
- return `Approaching budget. ${calendar}`
+ return `Approaching budget. ${detail}`
}
- return `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x the sticker price. ${calendar}`
+ return `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x the sticker price. ${detail}`
}
function Overview({ projects, label, width, planUsages, durable }: { projects: ProjectSummary[]; label: string; width: number; planUsages?: PlanUsage[]; durable?: DurableOverview }) {
diff --git a/tests/plan-budget-copy.test.ts b/tests/plan-budget-copy.test.ts
index 7ab7991f..787df94f 100644
--- a/tests/plan-budget-copy.test.ts
+++ b/tests/plan-budget-copy.test.ts
@@ -24,15 +24,18 @@ function usage(overrides: Partial = {}): PlanUsage {
}
}
+// The TUI truncates both lines end-first at the terminal width, so the row is
+// headline + two spaces + the 10-cell bar + a space + the percentage.
+const PLAN_ROW_TAIL = ' ' + '#'.repeat(10) + ' '
+function planRow(planUsage: PlanUsage): string {
+ return planBudgetHeadline(planUsage) + PLAN_ROW_TAIL + planUsage.percentUsed.toFixed(1) + '%'
+}
+
describe('plan budget copy', () => {
- it('labels SuperGrok as a calendar-month budget, not a live window', () => {
- const text = planBudgetHeadline(usage())
- expect(text).toContain('/mo budget')
- expect(text).not.toMatch(/\bplan\b/)
+ it('labels SuperGrok as a budget, not a live window', () => {
+ expect(planBudgetHeadline(usage())).toBe('SuperGrok Heavy: $33.82 API-equivalent / $300.00 budget')
const status = planStatusText(usage())
- expect(status).toContain('not a live provider window')
- expect(status).toContain('Calendar-month budget')
- expect(status).not.toMatch(/Well within plan/)
+ expect(status).toBe('Well within budget. Not a live provider window. Projected: $40.00. Next budget reset in 13 days.')
})
it('uses the same budget language for every preset, not only Grok', () => {
@@ -48,7 +51,36 @@ describe('plan budget copy', () => {
spentApiEquivalentUsd: 8.2,
status: 'near',
})
- expect(planBudgetHeadline(cursor)).toContain('/mo budget')
+ expect(planBudgetHeadline(cursor)).toBe('Cursor Pro: $8.20 API-equivalent / $20.00 budget')
expect(planStatusText(cursor)).toContain('Approaching budget')
})
+
+ // computePeriodFromResetDay builds an anniversary window from plan.resetDay
+ // (1-28, settable with `codeburn plan set --reset-day`), so a plan that resets
+ // on the 15th is NOT on a calendar month and must never be called one.
+ it('never calls an anniversary reset window a calendar month', () => {
+ for (const resetDay of [1, 15, 28]) {
+ const status = planStatusText(usage({
+ plan: { id: 'supergrok-heavy', monthlyUsd: 300, provider: 'grok', resetDay, setAt: '2026-08-01T00:00:00.000Z' },
+ daysUntilReset: 4,
+ }))
+ expect(status).not.toMatch(/calendar/i)
+ expect(status).toContain('Next budget reset in 4 days.')
+ }
+ })
+
+ // The row and the status each get one truncate-end line. At 80 columns the
+ // percentage must survive on the row and the projection on the status line.
+ it('keeps the percentage and the projection readable at 80 columns', () => {
+ for (const planUsage of [
+ usage(),
+ usage({ status: 'over', spentApiEquivalentUsd: 640, percentUsed: 213.3 }),
+ usage({ plan: { id: 'custom', monthlyUsd: 300, provider: 'openrouter', resetDay: 1, setAt: '2026-08-01T00:00:00.000Z' } }),
+ ]) {
+ const row = planRow(planUsage)
+ expect(row.length).toBeLessThanOrEqual(80)
+ expect(row.slice(0, 80)).toContain(`${planUsage.percentUsed.toFixed(1)}%`)
+ expect(planStatusText(planUsage).slice(0, 80)).toContain('Projected: $40.00.')
+ }
+ })
})