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.icon}
{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.
onNavigate?.('optimize')}>Optimize →
)
}
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 (
Model
Input tok
Output tok
Cost
Calls
{models.map(model => (
{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) => (
0 ? Math.max(2, day.cost / max * 100) : 2}%` }}
data-date={day.date}
data-cost={day.cost}
data-calls={day.calls}
data-led={day.topModels[0]?.name ?? ''}
onMouseEnter={event => setTip({ day, x: event.clientX, y: event.clientY })}
onMouseMove={event => setTip({ day, x: event.clientX, y: event.clientY })}
onMouseLeave={() => setTip(null)}
/>
))}
{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 => (
0 ? activity.cost / maxCost * 100 : 0}%` }} />
{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.>}
onNavigate?.('optimize')}>Review →
Models this period Sorted by cost
Most expensive sessions onNavigate?.('sessions')}>See all →
{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
)
}