fix(plan): scope TUI plan row to billing period, use currency-aware formatting

Address review feedback on #74:

1. TUI plan row previously used the active tab's filtered projects as
   plan spend, so 'Today' showed today's cost as plan spent. Switch
   renderDashboard and reloadData to getPlanUsageOrNull(), which uses
   the plan's own billing period regardless of tab.

2. Plan row rendered via a local formatUsd that hardcoded USD. Replace
   every call with formatCost so 'codeburn currency EUR' flows through.
   Removes the adjacent '$3,425.52' vs '$32.07' style mismatch.

3. renderPlanBar capped filled width at 100%, so 105% and 1700% looked
   identical. Past 100%, render a full bar plus chevron tail sized by
   order of magnitude (log10): 1.05x -> 1 chevron, 17x -> 2, 170x -> 3.

4. 'running on API overage pricing' is wrong for Claude Pro/Max (rate
   limited, not charged overage). Drop that claim; keep the Nx-over
   multiplier and match the under/near projection line structure.

5. Spell out 'equiv' as 'API-equivalent' in the plan label.

Dead code cleanup: getPlanUsageOrNullForProjects is now unused; remove
it. getPlanUsageFromProjects stays (unit tests still use it).
This commit is contained in:
Trevin Chow 2026-04-18 11:12:57 -07:00 committed by iamtoruk
parent 3f7470d29b
commit cb4c3ee305
2 changed files with 15 additions and 20 deletions

View file

@ -11,7 +11,7 @@ import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
import { dateKey } from './day-aggregator.js'
import { CompareView } from './compare.js'
import { getPlanUsageOrNullForProjects, type PlanUsage } from './plan-usage.js'
import { getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
import { planDisplayName } from './plans.js'
import { join } from 'path'
@ -157,14 +157,15 @@ function fit(s: string, n: number): string {
return s.length > n ? s.slice(0, n) : s.padEnd(n)
}
function formatUsd(value: number): string {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }).format(value)
}
function renderPlanBar(percentUsed: number, width: number): string {
const capped = Math.max(0, Math.min(100, percentUsed))
const filled = Math.round((capped / 100) * width)
return `${'▓'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}`
if (percentUsed <= 100) {
const capped = Math.max(0, percentUsed)
const filled = Math.round((capped / 100) * width)
return `${'▓'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}`
}
const factor = percentUsed / 100
const chevrons = Math.min(4, Math.max(1, Math.floor(Math.log10(factor)) + 1))
return `${'▓'.repeat(width)}${'▶'.repeat(chevrons)}`
}
function Overview({ projects, label, width, planUsage }: { projects: ProjectSummary[]; label: string; width: number; planUsage?: PlanUsage }) {
@ -179,7 +180,7 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm
const allInputTokens = totalInput + totalCacheRead + totalCacheWrite
const cacheHit = allInputTokens > 0
? (totalCacheRead / allInputTokens) * 100 : 0
const planLabel = planUsage ? `${planDisplayName(planUsage.plan.id)} plan: ${formatUsd(planUsage.spentApiEquivalentUsd)} equiv / ${formatUsd(planUsage.budgetUsd)} included` : ''
const planLabel = planUsage ? `${planDisplayName(planUsage.plan.id)}: ${formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs ${formatCost(planUsage.budgetUsd)} plan` : ''
const planPct = planUsage ? `${planUsage.percentUsed.toFixed(1)}%` : ''
const planColor = planUsage
? planUsage.status === 'over'
@ -219,10 +220,10 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm
</Text>
<Text dimColor wrap="truncate-end">
{planUsage.status === 'under'
? `Well within plan. Projected month: ${formatUsd(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
? `Well within plan. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
: planUsage.status === 'near'
? `Approaching plan limit. Projected month: ${formatUsd(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
: `You're ${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x over subscription value; running on API overage pricing.`}
? `Approaching plan limit. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`
: `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x your subscription value. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`}
</Text>
</>
)}
@ -703,7 +704,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
if (reloadGenerationRef.current !== generation) return
setProjects(filteredProjects)
const usage = await getPlanUsageOrNullForProjects(filteredProjects)
const usage = await getPlanUsageOrNull()
if (reloadGenerationRef.current !== generation) return
setPlanUsage(usage ?? undefined)
} catch (error) {
@ -802,7 +803,7 @@ export async function renderDashboard(period: Period = 'week', provider: string
await loadPricing()
const range = customRange ?? getDateRange(period)
const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
const planUsage = await getPlanUsageOrNullForProjects(filteredProjects)
const planUsage = await getPlanUsageOrNull()
const isTTY = process.stdin.isTTY && process.stdout.isTTY
if (isTTY) {
const { waitUntilExit } = render(

View file

@ -143,12 +143,6 @@ export async function getPlanUsageOrNull(today = new Date()): Promise<PlanUsage
return getPlanUsage(plan, today)
}
export async function getPlanUsageOrNullForProjects(projects: ProjectSummary[], today = new Date()): Promise<PlanUsage | null> {
const plan = await readPlan()
if (!isActivePlan(plan)) return null
return getPlanUsageFromProjects(plan, projects, today)
}
export function isActivePlan(plan: Plan | undefined): plan is Plan {
return Boolean(plan) && plan.id !== 'none' && Number.isFinite(plan.monthlyUsd) && plan.monthlyUsd > 0
}