import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import gsap from 'gsap' import { CliErrorPanel } from '../components/CliErrorPanel' import { ActivityHeatmap } from '../components/ActivityHeatmap' import { EmptyNote } from '../components/EmptyState' import { ListRow } from '../components/ListRow' import { SectionSkeleton } from '../components/Skeleton' import { StaleBanner } from '../components/StaleBanner' import { motionEnabled, useBarGrowIn } from '../lib/motion' import { type Polled, usePolled } from '../hooks/usePolled' import { formatCompact, formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' import { contiguousDailyWindow, formatChartDate, localDateKey, sliceDailyToPeriod, sliceDailyToRange } from '../lib/period' import type { ActReportJson, DailyHistoryEntry, DateRange, MenubarPayload, Period, YieldJsonReport, } from '../lib/types' export { localDateKey } from '../lib/period' function median(values: number[]): number { if (!values.length) return 0 const sorted = [...values].sort((a, b) => a - b) const mid = Math.floor(sorted.length / 2) return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 } function mean(values: number[]): number { return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0 } function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)) } type EfficiencyGrade = 'A+' | 'A' | 'B' | 'C' | 'D' | 'F' function efficiencyGrade(score: number): EfficiencyGrade { if (score >= 93) return 'A+' if (score >= 85) return 'A' if (score >= 75) return 'B' if (score >= 65) return 'C' if (score >= 55) return 'D' return 'F' } function EfficiencyScorecard({ current, bare = false }: { current: MenubarPayload['current']; bare?: boolean }) { const oneShot = current.oneShotRate ?? 0.6 const cacheFrac = clamp(current.cacheHitPercent / 100, 0, 1) const retrySpendFraction = current.retryTax.totalUSD / Math.max(current.cost, 1e-9) const retryPenalty = clamp(retrySpendFraction * 4, 0, 1) // score = 100 * (0.45*oneShot + 0.30*cacheFrac + 0.25*(1-retryPenalty)) // Missing one-shot data uses the specified neutral 0.6 and is disclosed below. const score = 100 * (0.45 * oneShot + 0.30 * cacheFrac + 0.25 * (1 - retryPenalty)) const grade = efficiencyGrade(score) const gradeTone = grade === 'A+' || grade === 'A' ? 'grade-a' : grade === 'D' ? 'grade-d' : grade === 'F' ? 'grade-f' : 'grade-bc' return (
Efficiency
{Math.round(score)} / 100
{grade}
One-shot{formatRate(current.oneShotRate)}
Cache hit{Math.round(current.cacheHitPercent)}%
Retry tax{formatUsd(current.retryTax.totalUSD)} · {(retrySpendFraction * 100).toFixed(1)}% of spend

Composite of one-shot, cache hit, and retry tax.{current.oneShotRate === null ? ' Partial grade: one-shot is unavailable.' : ''}

) } function CostPerOutcome({ outcome }: { outcome: Polled }) { const report = outcome.data let body: React.ReactNode if (!report) { body = {outcome.error ? 'Yield data is unavailable for this period.' : 'Correlating sessions with git…'} } else if (report.summary.total.sessions === 0 && report.details.length === 0) { body = No git-correlated outcomes in this period. } else { const commits = report.details.reduce((sum, detail) => sum + detail.commitCount, 0) const costPerCommit = commits > 0 ? report.summary.total.costUSD / commits : null const productive = report.summary.productive const costPerProductiveSession = productive.sessions > 0 ? productive.costUSD / productive.sessions : null body = ( <>
$ / commit{costPerCommit === null ? '—' : formatUsd(costPerCommit)}
$ / productive session{costPerProductiveSession === null ? '—' : formatUsd(costPerProductiveSession)}
productive {Math.round(productive.costPercent)}% · reverted {Math.round(report.summary.reverted.costPercent)}% · abandoned {Math.round(report.summary.abandoned.costPercent)}%
) } return (

Cost per outcome

Yield
{body}

Git-correlated. Reverted/abandoned = spend that didn't ship.

) } export type Signal = { text: string; trailing?: string } export type SignalGroups = { wins: Signal[]; improvements: Signal[]; risks: Signal[] } /** * Client-side port of the menubar's FindingsSection rule set * (mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift:133-205). Thresholds * mirror the Swift; the desktop-only weekday-spike anomaly is absorbed as a risk. * Week-over-week and month-projection rules are suppressed for a custom range. */ export function deriveSignals(data: MenubarPayload, now: Date, rangeActive: boolean): SignalGroups { const daily = data.history.daily const current = data.current const wins: Signal[] = [] const improvements: Signal[] = [] const risks: Signal[] = [] const streak = streakDays(daily, now) // Week-over-week: mean of the last 7 active entries vs the prior 7 (matches the // coach's pacing line). Needs >= 14 entries for both windows to exist. let weekDelta: number | null = null if (daily.length >= 14) { const recent14 = daily.slice(-14) const weekNow = mean(recent14.slice(-7).map(day => day.cost)) const weekPrior = mean(recent14.slice(0, 7).map(day => day.cost)) if (weekPrior > 0) weekDelta = (weekNow - weekPrior) / weekPrior * 100 } // Month projection vs previous calendar month's total. const todayKey = localDateKey(now) const monthPrefix = todayKey.slice(0, 7) const mtd = daily.filter(day => day.date.startsWith(monthPrefix)).reduce((sum, day) => sum + day.cost, 0) const medianDaily = median(daily.slice(-7).map(day => day.cost)) const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate() const projectedMonth = mtd + medianDaily * Math.max(0, daysInMonth - now.getDate()) const prevPrefix = localDateKey(new Date(now.getFullYear(), now.getMonth() - 1, 1)).slice(0, 7) const prevMonthTotal = daily.filter(day => day.date.startsWith(prevPrefix)).reduce((sum, day) => sum + day.cost, 0) // Weekday spike: today vs the mean of prior same-weekday entries. const today = daily.find(day => day.date === todayKey) const sameWeekdayCosts = daily .filter(day => { if (day.date === todayKey) return false const [year, month, date] = day.date.split('-').map(Number) return new Date(year, month - 1, date).getDay() === now.getDay() }) .map(day => day.cost) const typicalWeekday = mean(sameWeekdayCosts) // ————— Wins ————— if (current.cacheHitPercent >= 80) { wins.push({ text: `Cache hit at ${Math.round(current.cacheHitPercent)}%, most prompts reuse cache` }) } if (current.oneShotRate !== null && current.oneShotRate >= 0.75) { wins.push({ text: `${Math.round(current.oneShotRate * 100)}% one-shot, edits land first try` }) } if (!rangeActive && weekDelta !== null && weekDelta < -10) { wins.push({ text: `Spend down ${Math.round(Math.abs(weekDelta))}% vs last 7 days` }) } if (streak >= 5) { wins.push({ text: `${streak}-day usage streak` }) } if (current.localModelSavings.totalUSD > 0) { wins.push({ text: `${formatUsd(current.localModelSavings.totalUSD)} saved via local models` }) } // ————— Improvements ————— for (const finding of data.optimize.topFindings.slice(0, 3)) { improvements.push({ text: finding.title, trailing: formatUsd(finding.savingsUSD) }) } if (current.cacheHitPercent > 0 && current.cacheHitPercent < 50) { improvements.push({ text: `Cache hit only ${Math.round(current.cacheHitPercent)}%, paying for cold prompts` }) } if (current.oneShotRate !== null && current.oneShotRate < 0.5) { improvements.push({ text: `${Math.round(current.oneShotRate * 100)}% one-shot, lots of iteration` }) } // Retry-tax share is not a menubar rule; the threshold is the point where the // efficiency scorecard's retry penalty saturates (retrySpendFraction * 4 == 1). const retryShare = current.retryTax.totalUSD / Math.max(current.cost, 1e-9) if (retryShare >= 0.25) { improvements.push({ text: `Retry tax is ${Math.round(retryShare * 100)}% of spend` }) } // ————— Risks ————— if (today && typicalWeekday > 0 && today.cost > typicalWeekday * 1.8) { const ratio = today.cost / typicalWeekday const weekday = now.toLocaleString('en-US', { weekday: 'long' }) risks.push({ text: `Today's spend is ${ratio.toFixed(1).replace(/\.0$/, '')}× your typical ${weekday}` }) } if (!rangeActive && weekDelta !== null && weekDelta > 25) { risks.push({ text: `Spend up ${Math.round(weekDelta)}% vs prior 7 days` }) } if (!rangeActive && prevMonthTotal > 0 && projectedMonth > prevMonthTotal * 1.3) { const overPct = Math.round((projectedMonth - prevMonthTotal) / prevMonthTotal * 100) risks.push({ text: `On pace for ${formatUsd(projectedMonth)} this month, +${overPct}% vs last` }) } return { wins: wins.slice(0, 3), improvements: improvements.slice(0, 3), risks: risks.slice(0, 3) } } const SIGNAL_GROUPS = [ { key: 'wins' as const, label: 'Wins', icon: <>, }, { key: 'improvements' as const, label: 'Improvements', icon: <>, }, { key: 'risks' as const, label: 'Risks', icon: <>, }, ] function SignalsCard({ signals }: { signals: SignalGroups }) { const groups = SIGNAL_GROUPS.filter(group => signals[group.key].length > 0) if (!groups.length) return null return (
{groups.map(group => (
{group.label}
    {signals[group.key].map((signal, index) => (
  • {signal.text} {signal.trailing && {signal.trailing}}
  • ))}
))}
) } function RoutingWhatIf({ routing, onNavigate }: { routing: MenubarPayload['current']['routingWaste'] onNavigate?: (section: 'optimize') => void }) { if (routing.totalSavingsUSD <= 0 || !routing.baselineModel) return null return (
Routing what-if

Routing to {routing.baselineModel} could save ~{formatUsd(routing.totalSavingsUSD)} this period.

) } function deriveStats(data: MenubarPayload, now: Date) { const daily = data.history.daily const todayKey = localDateKey(now) const todayEntry = daily.find(day => day.date === todayKey) const monthPrefix = todayKey.slice(0, 7) const mtdEntries = daily.filter(day => day.date.startsWith(monthPrefix)) const mtd = mtdEntries.reduce((sum, day) => sum + day.cost, 0) const medianDaily = median(daily.slice(-7).map(day => day.cost)) const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate() const projected = mtd + medianDaily * Math.max(0, daysInMonth - now.getDate()) const prevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1) const prevPrefix = localDateKey(prevMonth).slice(0, 7) const priorEntries = daily.filter(day => day.date.startsWith(prevPrefix)) const priorAverage = mean(priorEntries.map(day => day.cost)) const currentAverage = mean(mtdEntries.map(day => day.cost)) const pacePct = priorAverage > 0 ? ((currentAverage - priorAverage) / priorAverage) * 100 : null return { todayEntry, todayCost: todayEntry?.cost ?? 0, mtd, projected, pacePct, prevMonthName: prevMonth.toLocaleString('en-US', { month: 'long' }), } } export function sessionModelKey(project: string, date: string, calls: number, cost: number): string { return `${project}|${date}|${calls}|${cost}` } function buildModelIndex(data: MenubarPayload): Map { const index = new Map() for (const project of data.current.topProjects) { for (const session of project.sessionDetails) { const dominant = [...session.models].sort((a, b) => b.cost - a.cost)[0] if (dominant) index.set(sessionModelKey(project.name, session.date, session.calls, session.cost), dominant.name) } } return index } function streakDays(daily: DailyHistoryEntry[], now: Date): number { const byDate = new Map(daily.map(day => [day.date, day.cost])) let streak = 0 for (let offset = 0; ; offset++) { const date = new Date(now.getFullYear(), now.getMonth(), now.getDate() - offset) if ((byDate.get(localDateKey(date)) ?? 0) <= 0) break streak++ } return streak } /** * Hero cost with a count-up that fires on mount and whenever the filter key * changes (a user action), but never on the 30s poll: a value that arrives * under the same `animateKey` snaps in place instead of re-animating. */ function CountUp({ value, animateKey }: { value: number; animateKey: string }) { const ref = useRef(null) const keyRef = useRef(null) useEffect(() => { const element = ref.current if (!element) return const keyChanged = keyRef.current !== animateKey keyRef.current = animateKey if (!keyChanged || !motionEnabled()) { element.textContent = formatUsd(value) return } const counter = { n: 0 } const tween = gsap.to(counter, { n: value, duration: 0.7, ease: 'power2.out', onUpdate: () => { element.textContent = formatUsd(counter.n) }, }) return () => { tween.kill() } }, [value, animateKey]) return
{formatUsd(value)}
} function formatShortDay(date: string): string { const [, month, day] = date.split('-').map(Number) return `${month}/${day}` } type AggregatedModel = { name: string cost: number calls: number // Absent in provider-filtered mode: `current.topModels` carries no per-model // token counts, so the table shows "—" rather than a misleading zero. inputTokens?: number outputTokens?: number } /** Provider-filtered source: `current.topModels` is already period/range/provider-scoped by the CLI. */ function topModelsToAggregated(models: MenubarPayload['current']['topModels']): AggregatedModel[] { return models .map(model => ({ name: model.name, cost: model.cost, calls: model.calls })) .sort((a, b) => b.cost - a.cost) } function aggregateModels(daily: DailyHistoryEntry[]): AggregatedModel[] { const byName = new Map() for (const day of daily) { for (const model of day.topModels) { const row = byName.get(model.name) ?? { name: model.name, cost: 0, calls: 0, inputTokens: 0, outputTokens: 0, } row.cost += model.cost row.calls += model.calls row.inputTokens = (row.inputTokens ?? 0) + model.inputTokens row.outputTokens = (row.outputTokens ?? 0) + model.outputTokens byName.set(model.name, row) } } return [...byName.values()].sort((a, b) => b.cost - a.cost) } function ModelsTable({ models }: { models: AggregatedModel[] }) { if (!models.length) return No model usage in this range yet. return (
{models.map(model => ( ))}
Model Input tok Output tok Cost Calls
{model.name} {model.inputTokens === undefined ? '—' : formatCompact(model.inputTokens)} {model.outputTokens === undefined ? '—' : formatCompact(model.outputTokens)} {formatUsd(model.cost)} {model.calls.toLocaleString('en-US')}
) } function DailyChart({ daily, animateKey = '' }: { daily: DailyHistoryEntry[]; animateKey?: string }) { const max = Math.max(...daily.map(day => day.cost), 0) const peakIndex = daily.reduce((peak, day, index) => day.cost > (daily[peak]?.cost ?? -1) ? index : peak, 0) const peak = daily[peakIndex] const yesterday = daily.at(-2) const average = mean(daily.map(day => day.cost)) const ticks = daily.filter((_, index) => index % 7 === 0) const [tip, setTip] = useState<{ day: DailyHistoryEntry; x: number; y: number } | null>(null) const [tipPosition, setTipPosition] = useState<{ left: number; top: number } | null>(null) const tipRef = useRef(null) const chartRef = useRef(null) useBarGrowIn(chartRef, '.col', [animateKey]) useLayoutEffect(() => { if (!tip) { setTipPosition(null) return } const width = tipRef.current?.offsetWidth ?? 220 const height = tipRef.current?.offsetHeight ?? 62 const gutter = 8 const cursorGap = 12 let left = tip.x + cursorGap if (left + width > window.innerWidth - gutter) left = tip.x - width - cursorGap left = Math.max(gutter, Math.min(left, window.innerWidth - width - gutter)) let top = tip.y - height - cursorGap if (top < gutter) top = tip.y + cursorGap top = Math.max(gutter, Math.min(top, window.innerHeight - height - gutter)) setTipPosition({ left, top }) }, [tip]) return ( <>
{daily.map((day, index) => (
{ticks.map(day => { const index = daily.indexOf(day) return 1 ? index / (daily.length - 1) * 100 : 0}%` }}>{formatChartDate(day.date)} })}
Avg/day{formatUsd(average)}
Peak{peak ? `${formatUsd(peak.cost)} · ${formatShortDay(peak.date)}` : '$0.00'}
Yesterday{formatUsd(yesterday?.cost ?? 0)}
{tip && createPortal(
{formatChartDate(tip.day.date)}
{formatUsd(tip.day.cost)}
{tip.day.calls} calls · {tip.day.topModels[0]?.name ?? 'No model'} led
, document.body, )} ) } function formatRate(rate: number | null): string { return rate === null ? '—' : `${Math.round(rate * 100)}%` } function TopActivities({ activities }: { activities: MenubarPayload['current']['topActivities'] }) { const rows = [...activities].sort((a, b) => b.cost - a.cost).slice(0, 6) if (!rows.length) return No activity in this range yet. const maxCost = rows[0].cost return (
{rows.map(activity => (
{activity.name} {formatUsd(activity.cost)}
{activity.turns.toLocaleString('en-US')} turns {formatRate(activity.oneShotRate)} one-shot
))}
) } export function Overview({ period, provider }: { period: Period; provider: string }) { const overview = usePolled(() => codeburn.getOverview(period, provider), [period, provider]) return } export function OverviewContent({ period, provider = 'all', range = null, overview, onNavigate, }: { period: Period provider?: string range?: DateRange | null overview: Polled onNavigate?: (section: 'optimize' | 'sessions') => void }) { const actReport = usePolled(() => codeburn.getActReport(), []) const yieldReport = usePolled(() => codeburn.getYield(period, provider), [period, provider]) const { data, error } = overview const modelIndex = useMemo(() => data ? buildModelIndex(data) : new Map(), [data]) if (!data) { if (error) return return } const now = new Date() const rangeActive = !!range const animateKey = `${period}|${provider}|${range?.from ?? ''}|${range?.to ?? ''}` const stats = deriveStats(data, now) const periodDaily = sliceDailyToPeriod(data.history.daily, period, now) // Daily chart: contiguous zero-filled calendar window. A custom range spans // [from..to]; otherwise the trend covers at least the last 30 days, extended // back to the earliest active day already in the period window. const defaultChartStart = localDateKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29)) const chartDaily = rangeActive ? contiguousDailyWindow(data.history.daily, range.from, range.to) : contiguousDailyWindow( data.history.daily, periodDaily[0] && periodDaily[0].date < defaultChartStart ? periodDaily[0].date : defaultChartStart, localDateKey(now), ) // Provider-filtered history.daily has empty topModels, so source the models // table from current.topModels (already period/range/provider-scoped) instead. const models = provider !== 'all' ? topModelsToAggregated(data.current.topModels) : aggregateModels(rangeActive ? sliceDailyToRange(data.history.daily, range.from, range.to) : periodDaily) const recent14 = data.history.daily.slice(-14) const weekNow = mean(recent14.slice(-7).map(day => day.cost)) const weekPrior = mean(recent14.slice(-14, -7).map(day => day.cost)) const weeklyPct = weekPrior > 0 ? Math.round(Math.abs((weekNow - weekPrior) / weekPrior * 100)) : null const weeklyDirection = weekNow >= weekPrior ? 'higher' : 'lower' const topModel = data.current.topModels[0] const saved = actReport.data?.totals.realizedCostUSD ?? 0 const applied = saved > 0 ? (actReport.data?.totals.measuredActions ?? 0) : 0 const localSaved = data.current.localModelSavings.totalUSD // A custom range has no meaningful "vs last week" or month-to-date baseline. const signals = deriveSignals(data, now, rangeActive) return (
{error && }
{data.current.label}{streakDays(data.history.daily, now)}-day streak
{data.current.calls.toLocaleString('en-US')} calls · {data.current.sessions.toLocaleString('en-US')} sessions
Saved by applied fixes{formatUsd(saved)}across {applied} {applied === 1 ? 'fix' : 'fixes'}
{localSaved > 0 && (
Saved via local models{formatUsd(localSaved)}local-model routing
)}
{!rangeActive && (
Month to date
{formatUsd(stats.mtd)}
{stats.pacePct === null ? `No ${stats.prevMonthName} pace yet` : `${stats.pacePct >= 0 ? '+' : ''}${Math.round(stats.pacePct)}% vs ${stats.prevMonthName} pace`}
Projected month
{formatUsd(stats.projected)} est
{formatUsd(Math.max(0, stats.projected - stats.mtd))} to go
)}

Daily spend

{topModel ? `Biggest driver: ${topModel.name}` : 'No model driver yet'}
{data.history.daily.length ? : No spend yet.}
{rangeActive ? <>{topModel ? <>{topModel.name} is the biggest driver in this range : 'No single model dominates this range'}. {formatUsd(data.optimize.savingsUSD)} is recoverable. : <>{weeklyPct === null ? <>No prior-week pacing baseline yet : <>You're pacing {weeklyPct}% {weeklyDirection} than last week}{topModel ? <>; {topModel.name} is the biggest driver : ''}. {formatUsd(data.optimize.savingsUSD)} is recoverable.}

Models this period

Sorted by cost

Most expensive sessions

{data.current.topSessions.length ? data.current.topSessions.map((session, index) => { const model = modelIndex.get(sessionModelKey(session.project, session.date, session.calls, session.cost)) const sub = [formatChartDate(session.date), model, `${session.calls} calls`].filter(Boolean).join(' · ') return onNavigate?.('sessions')} /> }) : No sessions in this range.}

Top activities

Sorted by cost
) }