codeburn/app/renderer/sections/Overview.tsx
Resham Joshi 389c14ce5e
fix(ui): mark estimated headline totals with ~ on every hero (#1722)
#1657 put the ~ mark on estimated rows, but the headline total stayed bare on
the desktop hero, the mac hero, the Windows hero and tray text, and GNOME.
Each now applies the same 1% rule to the provider split's estimated portion
(the desktop helper moves from the VS Code summary into lib/format). Combined
device totals carry no estimated split and stay unmarked.
2026-10-10 07:05:58 +02:00

1335 lines
68 KiB
TypeScript

import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import gsap from 'gsap'
import { CliErrorPanel } from '../components/CliErrorPanel'
import { ActivityHeatmap } from '../components/ActivityHeatmap'
import { ChartTip } from '../components/ChartTip'
import { EmptyNote } from '../components/EmptyState'
import { ListRow } from '../components/ListRow'
import { SectionSkeleton } from '../components/Skeleton'
import { StaleBanner } from '../components/StaleBanner'
import { DUR, motionEnabled, useBarGrowIn } from '../lib/motion'
import { useOptimizeSnapshot } from '../hooks/useOptimizeSnapshot'
import { type Polled, usePolled } from '../hooks/usePolled'
import { asOfLabel, estimatedPortion, formatCompact, formatCount, formatUsd, formatUsdWithCurrency, isEstimatedCost } from '../lib/format'
import { Usd, sumTokens, tokensOf, useUsdPop } from '../components/Usd'
import { codeburn } from '../lib/ipc'
import {
categoryFilters,
dayFilters,
modelFilters,
sessionFilters,
type InvestigationFilters,
} from '../lib/investigation'
import { contiguousDailyWindow, dataStartKey, formatChartDate, localDateKey, sliceDailyToPeriod, sliceDailyToRange } from '../lib/period'
import { ACT_SLOW_MS, YIELD_SLOW_MS } from '../lib/refreshCadence'
import { reportMemoKey } from '../lib/reportMemoKey'
import { barBucketDays, barLayout, formatAxisMoney, niceTicks, ticksClearOfPeak } from '../lib/chartAxis'
import { generationHeadline, generationModels, rememberGeneration } from '../lib/generation'
import { rememberStreak } from '../lib/streak'
import { paceDirection, sparkArea, sparkPath, sparkPoints } from '../lib/spark'
import type {
ActReportJson,
CombinedUsage,
DailyHistoryEntry,
DateRange,
MenubarPayload,
OptimizeBlock,
Period,
Scope,
YieldJsonReport,
} from '../lib/types'
import type { OverviewHeadlineSnapshot } from '../lib/overviewSnapshot'
import { formatCombinedSessionCount, formatSessionCount, sessionCountIsExact, combinedSessionCountHelp, sessionCountHelp } from '../lib/session-count-label'
import { Icon } from '../components/icons'
import { CursorSyncLine } from '../components/CursorSyncLine'
import { localeTag, t } from '../i18n'
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'
}
/** Ring gauge geometry: 96px box, 10px stroke, so the arc radius is 43. */
const GAUGE_BOX = 96
const GAUGE_STROKE = 10
const GAUGE_RADIUS = (GAUGE_BOX - GAUGE_STROKE) / 2
const GAUGE_LENGTH = 2 * Math.PI * GAUGE_RADIUS
/**
* Sweeps the arc up from zero once, on the first mount. A later score arriving
* under the 30s poll snaps to its new length: the ring is a reading, not a
* replayed animation.
*/
function RingGauge({ fraction, face, children }: { fraction: number; face: ReactNode; children: ReactNode }) {
const arcRef = useRef<SVGCircleElement>(null)
const swept = useRef(false)
const offset = GAUGE_LENGTH * (1 - clamp(fraction, 0, 1))
useEffect(() => {
const arc = arcRef.current
if (!arc || swept.current) return
swept.current = true
if (!motionEnabled()) return
const tween = gsap.fromTo(arc, { strokeDashoffset: GAUGE_LENGTH }, {
strokeDashoffset: offset,
duration: DUR.slow / 1000,
ease: 'power2.out',
})
return () => { tween.kill() }
}, [offset])
return (
<div className="ov-gauge">
<div className="ov-gauge-ring">
<svg viewBox={`0 0 ${GAUGE_BOX} ${GAUGE_BOX}`} aria-hidden="true">
<circle className="ov-gauge-track" cx={GAUGE_BOX / 2} cy={GAUGE_BOX / 2} r={GAUGE_RADIUS} />
<circle
ref={arcRef}
className="ov-gauge-arc"
cx={GAUGE_BOX / 2}
cy={GAUGE_BOX / 2}
r={GAUGE_RADIUS}
strokeDasharray={GAUGE_LENGTH}
strokeDashoffset={offset}
transform={`rotate(-90 ${GAUGE_BOX / 2} ${GAUGE_BOX / 2})`}
/>
</svg>
<div className="ov-gauge-face">{face}</div>
</div>
{children}
</div>
)
}
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 === 'C' ? 'grade-warn' : grade === 'D' || grade === 'F' ? 'grade-bad' : 'grade-ok'
return (
<div className={`${bare ? '' : 'ov-card '}ov-efficiency`}>
<div className="ov-activity-head">
<span className="ov-label">{t('overview.efficiency.label')}</span>
<button
className="ov-info"
type="button"
aria-label={t('overview.efficiency.howBuiltAria')}
title={`${t('overview.efficiency.tooltip')}${current.oneShotRate === null ? ` ${t('overview.efficiency.partialGrade')}` : ''}`}
>
<Icon name="info" />
</button>
</div>
<div className="ov-efficiency-main">
<RingGauge
fraction={score / 100}
face={<>
<strong className="ov-gauge-score">{Math.round(score)}</strong>
<span className="ov-gauge-cap">/100</span>
</>}
>
<span className={`ov-grade ${gradeTone}`} aria-label={t('overview.efficiency.gradeAria', { grade })}>{grade}</span>
</RingGauge>
<div className="ov-component-list">
<div className="ov-component-row">
<div><span>{t('overview.efficiency.oneShotLabel')}</span><strong>{formatRate(current.oneShotRate)}</strong></div>
<div className="ov-component-track"><span style={{ width: `${oneShot * 100}%` }} /></div>
</div>
<div className="ov-component-row">
<div><span>{t('overview.efficiency.cacheHitLabel')}</span><strong>{Math.round(current.cacheHitPercent)}%</strong></div>
<div className="ov-component-track"><span style={{ width: `${cacheFrac * 100}%` }} /></div>
</div>
<div className="ov-component-row">
<div><span>{t('overview.efficiency.retryTaxLabel')}</span><strong>{formatUsd(current.retryTax.totalUSD)} · {t('overview.efficiency.percentOfSpend', { percent: (retrySpendFraction * 100).toFixed(1) })}</strong></div>
<div className="ov-component-track adverse"><span style={{ width: `${retryPenalty * 100}%` }} /></div>
</div>
</div>
</div>
</div>
)
}
function CostPerOutcome({ outcome }: { outcome: Polled<YieldJsonReport> }) {
const report = outcome.data
let body: React.ReactNode
if (!report) {
body = <EmptyNote>{outcome.error ? t('overview.outcome.yieldUnavailable') : t('overview.outcome.correlating')}</EmptyNote>
} else if (report.summary.total.sessions === 0 && report.details.length === 0) {
body = <EmptyNote>{t('overview.outcome.noOutcomes')}</EmptyNote>
} 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 = (
<>
<div className="ov-outcome-metrics">
<div><span>{t('overview.outcome.costPerCommit')}</span><strong>{costPerCommit === null ? '—' : formatUsd(costPerCommit)}</strong></div>
<div><span>{t('overview.outcome.costPerProductiveSession')}</span><strong>{costPerProductiveSession === null ? '—' : formatUsd(costPerProductiveSession)}</strong></div>
</div>
<div className="ov-outcome-split">
{t('overview.outcome.productive')} {Math.round(productive.costPercent)}% · {t('overview.outcome.reverted')} {Math.round(report.summary.reverted.costPercent)}% · {t('overview.outcome.abandoned')} {Math.round(report.summary.abandoned.costPercent)}%
</div>
</>
)
}
return (
<div className="ov-card ov-panel">
<div className="ov-panel-head"><Icon name="scale" /><h3>{t('overview.outcome.title')}</h3><span className="r">{asOfLabel(outcome.lastSuccessAt) ?? t('overview.outcome.yieldChip')}</span></div>
<div className="ov-panel-body">
{body}
<p className="ov-widget-caption">{t('overview.outcome.caption')}</p>
</div>
</div>
)
}
// Coaching-note thresholds, mirrored from the CLI so the card and the CLI never
// disagree (src/workflow-insights.ts buildCoachingNotes).
const WORKFLOW_CORRECTION_RATE = 0.15
const WORKFLOW_CORRECTION_COUNT = 3
const WORKFLOW_CHURN_SESSIONS = 3
const WORKFLOW_TTFE_SLOW_MS = 5 * 60 * 1000
/** Median time to first edit: `<60s → Ns`, else `Nm` (src/workflow-insights.ts formatDurationShort). */
function formatWorkflowDuration(ms: number): string {
if (ms >= 60_000) return `${Math.round(ms / 60_000)}m`
return `${Math.round(ms / 1000)}s`
}
type WorkflowRollup = NonNullable<MenubarPayload['current']['workflow']>
type ReworkedFile = { path: string; sessions: number; edits: number }
/**
* One coaching line derived with the CLI's thresholds and dry copy voice
* (src/workflow-insights.ts buildCoachingNotes): corrections, then file churn,
* then time-to-first-edit; the first that fires. Null when none clears its bar.
*/
function workflowCoachingNote(workflow: WorkflowRollup, topReworked?: ReworkedFile): string | null {
const { correctionRate, corrections, medianTimeToFirstEditMs } = workflow
if (correctionRate !== null && correctionRate >= WORKFLOW_CORRECTION_RATE && corrections >= WORKFLOW_CORRECTION_COUNT) {
return t('overview.workflow.correctionNote', { percent: Math.round(correctionRate * 100), times: formatCount(corrections, 'time') })
}
if (topReworked && topReworked.sessions >= WORKFLOW_CHURN_SESSIONS) {
return t('overview.workflow.reworkNote', { path: topReworked.path, sessions: formatCount(topReworked.sessions, 'session'), edits: formatCount(topReworked.edits, 'edit') })
}
if (medianTimeToFirstEditMs !== null && medianTimeToFirstEditMs >= WORKFLOW_TTFE_SLOW_MS) {
return t('overview.workflow.ttfeNote', { duration: formatWorkflowDuration(medianTimeToFirstEditMs) })
}
return null
}
function WorkflowCard({ current }: { current: MenubarPayload['current'] }) {
const workflow = current.workflow
const topReworked = current.topReworkedFiles?.[0]
// Hide when there is no real signal: never show a card of zeros.
const hasSignal = !!workflow && (
workflow.correctionRate !== null ||
workflow.medianTimeToFirstEditMs !== null ||
workflow.corrections > 0 ||
!!topReworked
)
if (!workflow || !hasSignal) return null
const coverage = current.pricingCoverage
const showCoverage = typeof coverage === 'number' && coverage < 1
const note = workflowCoachingNote(workflow, topReworked)
const { correctionRate, corrections, medianTimeToFirstEditMs } = workflow
return (
<div className="ov-card ov-panel ov-workflow-widget">
<div className="ov-panel-head">
<Icon name="sliders-horizontal" />
<h3>{t('overview.workflow.title')}</h3>
{showCoverage && <span className="ov-priced-chip">{t('overview.workflow.pricedChip', { percent: Math.min(99, Math.round(coverage * 100)) })}</span>}
</div>
<div className="ov-panel-body">
<div className="ov-outcome-metrics">
<div>
<span>{t('overview.workflow.correctionRateLabel')}</span>
<strong>{correctionRate === null ? '—' : `${Math.round(correctionRate * 100)}%`}</strong>
{correctionRate !== null && <span>{formatCount(corrections, 'correction')}</span>}
</div>
<div>
<span>{t('overview.workflow.timeToFirstEditLabel')}</span>
<strong>{medianTimeToFirstEditMs === null ? '—' : formatWorkflowDuration(medianTimeToFirstEditMs)}</strong>
<span>{t('overview.workflow.median')}</span>
</div>
</div>
{topReworked && (
<div className="ov-workflow-rework">
{t('overview.workflow.topRework')}<strong>{topReworked.path}</strong> · {formatCount(topReworked.sessions, 'session')} · {formatCount(topReworked.edits, 'edit')}
</div>
)}
<p className="ov-widget-caption">{note ?? t('overview.workflow.defaultCaption')}</p>
</div>
</div>
)
}
export type Signal = {
text: string
trailing?: string
/** Set on rows whose figure is NOT live — today only the stored optimize
* findings, which sit beside live signals and must not read as current. */
asOf?: 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,
/** The stored daily scan: its findings and the age to label them with. */
stored: { topFindings: OptimizeBlock['topFindings']; asOf?: string | null } = { topFindings: [] },
): SignalGroups {
const daily = data.history.daily
const current = data.current
const wins: Signal[] = []
const improvements: Signal[] = []
const risks: Signal[] = []
const streak = rememberStreak(data.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: t('overview.signals.win.cacheHit', { percent: Math.round(current.cacheHitPercent) }) })
}
if (current.oneShotRate !== null && current.oneShotRate >= 0.75) {
wins.push({ text: t('overview.signals.win.oneShot', { percent: Math.round(current.oneShotRate * 100) }) })
}
if (!rangeActive && weekDelta !== null && weekDelta < -10) {
wins.push({ text: t('overview.signals.win.spendDown', { percent: Math.round(Math.abs(weekDelta)) }) })
}
if (streak >= 5) {
wins.push({ text: t('overview.signals.win.streak', { count: streak }) })
}
if (current.localModelSavings.totalUSD > 0) {
wins.push({ text: t('overview.signals.win.localSavings', { amount: formatUsd(current.localModelSavings.totalUSD) }) })
}
// ————— Improvements —————
for (const finding of stored.topFindings.slice(0, 3)) {
improvements.push({ text: finding.title, trailing: formatUsd(finding.savingsUSD), ...(stored.asOf ? { asOf: stored.asOf } : {}) })
}
if (current.cacheHitPercent > 0 && current.cacheHitPercent < 50) {
improvements.push({ text: t('overview.signals.improve.lowCacheHit', { percent: Math.round(current.cacheHitPercent) }) })
}
if (current.oneShotRate !== null && current.oneShotRate < 0.5) {
improvements.push({ text: t('overview.signals.improve.lowOneShot', { percent: Math.round(current.oneShotRate * 100) }) })
}
// 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: t('overview.signals.improve.retryTax', { percent: Math.round(retryShare * 100) }) })
}
// ————— Risks —————
if (today && typicalWeekday > 0 && today.cost > typicalWeekday * 1.8) {
const ratio = today.cost / typicalWeekday
const weekday = now.toLocaleString(localeTag(), { weekday: 'long' })
risks.push({ text: t('overview.signals.risk.spikeToday', { ratio: ratio.toFixed(1).replace(/\.0$/, ''), weekday }) })
}
if (!rangeActive && weekDelta !== null && weekDelta > 25) {
risks.push({ text: t('overview.signals.risk.spendUp', { percent: Math.round(weekDelta) }) })
}
if (!rangeActive && prevMonthTotal > 0 && projectedMonth > prevMonthTotal * 1.3) {
const overPct = Math.round((projectedMonth - prevMonthTotal) / prevMonthTotal * 100)
risks.push({ text: t('overview.signals.risk.pace', { amount: formatUsd(projectedMonth), percent: overPct }) })
}
return { wins: wins.slice(0, 3), improvements: improvements.slice(0, 3), risks: risks.slice(0, 3) }
}
const SIGNAL_GROUPS = [
{
key: 'wins' as const,
labelKey: 'overview.signals.wins',
icon: <Icon name="circle-check" />,
},
{
key: 'improvements' as const,
labelKey: 'overview.signals.improvements',
icon: <Icon name="trending-up" />,
},
{
key: 'risks' as const,
labelKey: 'overview.signals.risks',
icon: <Icon name="triangle-alert" />,
},
]
function SignalsCard({ signals }: { signals: SignalGroups }) {
const groups = SIGNAL_GROUPS.filter(group => signals[group.key].length > 0)
if (!groups.length) return null
return (
<div className="ov-card ov-signals" aria-label={t('overview.signals.aria')}>
<div className="ov-card-inner ov-signal-grid">
{groups.map(group => (
<div className={`ov-signal-group ${group.key}`} key={group.key}>
<div className="ov-signal-head">
{group.icon}
<span>{t(group.labelKey)}</span>
</div>
<ul className="ov-signal-list">
{signals[group.key].map((signal, index) => (
<li className="ov-signal" key={`${signal.text}-${index}`}>
<span title={signal.text}>{signal.text}</span>
{signal.asOf && <small className="ov-signal-age">{signal.asOf}</small>}
{signal.trailing && <span className="ov-signal-trailing">{signal.trailing}</span>}
</li>
))}
</ul>
</div>
))}
</div>
</div>
)
}
function RoutingWhatIf({ routing, onNavigate }: {
routing: MenubarPayload['current']['routingWaste']
onNavigate?: (section: 'optimize') => void
}) {
if (routing.totalSavingsUSD <= 0 || !routing.baselineModel) return null
return (
<div className="ov-card ov-routing">
<div className="ov-card-inner ov-routing-body">
<div><span className="ov-label">{t('overview.routing.label')}</span><p>{t('overview.routing.prefix')}<strong>{routing.baselineModel}</strong>{t('overview.routing.middle')}<strong>{formatUsd(routing.totalSavingsUSD)}</strong>{t('overview.routing.suffix')}</p></div>
<button className="ov-link" type="button" onClick={() => onNavigate?.('optimize')}>{t('overview.routing.cta')}</button>
</div>
</div>
)
}
function deriveStats(data: MenubarPayload, now: Date, anchorKey = localDateKey(now)) {
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
// Cumulative spend, one point per calendar day of the month so far, so a
// silent day is a flat step rather than a missing column.
// Anchored to the last day of the selected window, not to today, so a custom
// range reports the days it actually covers.
const [anchorYear, anchorMonth, anchorDay] = anchorKey.split('-').map(Number)
const priorKey = localDateKey(new Date(anchorYear, anchorMonth - 1, anchorDay - 1))
const anchorCost = daily.find(day => day.date === anchorKey)?.cost ?? 0
const priorDayCost = daily.find(day => day.date === priorKey)?.cost ?? null
const upToAnchor = daily.filter(day => day.date <= anchorKey)
const sevenDayAvg = upToAnchor.length ? mean(upToAnchor.slice(-7).map(day => day.cost)) : null
const dayOverDayPct = priorDayCost !== null && priorDayCost > 0
? ((anchorCost - priorDayCost) / priorDayCost) * 100
: null
let running = 0
const mtdSeries = contiguousDailyWindow(daily, `${monthPrefix}-01`, todayKey).map(day => (running += day.cost))
const remainingDays = Math.max(0, daysInMonth - now.getDate())
const projectedTail = Array.from({ length: remainingDays }, (_, index) => mtd + (projected - mtd) * ((index + 1) / remainingDays))
return {
todayEntry,
todayCost: todayEntry?.cost ?? 0,
mtdEntries,
priorDayEntry: daily.find(day => day.date === priorKey),
sevenDayEntries: upToAnchor.slice(-7),
mtd,
projected,
pacePct,
mtdSeries,
projectedTail,
priorDayCost,
sevenDayAvg,
dayOverDayPct,
prevMonthName: prevMonth.toLocaleString(localeTag(), { month: 'long' }),
}
}
const TREND_WIDTH = 160
const TREND_HEIGHT = 64
/**
* The card's corner curve: cumulative spend anchored to the inner surface's
* bottom-right, filled with a soft gradient in the delta's colour, with today
* marked and any projected tail drawn dashed.
*/
function SpendTrend({ values, tone, dashFrom }: { values: number[]; tone: 'good' | 'bad' | 'flat'; dashFrom?: number }) {
const id = useId()
const points = sparkPoints(values, TREND_WIDTH, TREND_HEIGHT, 6)
if (points.length < 2) return null
const solid = dashFrom === undefined ? points : points.slice(0, dashFrom + 1)
const dashed = dashFrom === undefined ? [] : points.slice(dashFrom)
const last = points.at(-1) ?? points[0]
return (
<div className={`ov-trend tone-${tone}`} aria-hidden="true">
<svg viewBox={`0 0 ${TREND_WIDTH} ${TREND_HEIGHT}`} preserveAspectRatio="none">
<defs>
<linearGradient id={`${id}-fill`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.22" />
<stop offset="70%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
<radialGradient id={`${id}-glow`}>
<stop offset="0%" stopColor="currentColor" stopOpacity="0.06" />
<stop offset="55%" stopColor="currentColor" stopOpacity="0.02" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
</defs>
<ellipse cx={last[0]} cy={last[1]} rx={TREND_WIDTH * 0.7} ry={TREND_HEIGHT * 0.8} fill={`url(#${id}-glow)`} />
<path d={sparkArea(solid, TREND_HEIGHT)} fill={`url(#${id}-fill)`} />
<path className="ov-trend-line" d={sparkPath(solid)} vectorEffect="non-scaling-stroke" />
{dashed.length > 1 && <path className="ov-trend-line dashed" d={sparkPath(dashed)} vectorEffect="non-scaling-stroke" />}
</svg>
<span className="ov-trend-dot" style={{ left: `${(last[0] / TREND_WIDTH) * 100}%`, top: `${(last[1] / TREND_HEIGHT) * 100}%` }} />
</div>
)
}
export function sessionModelKey(project: string, date: string, calls: number, cost: number): string {
return `${project}|${date}|${calls}|${cost}`
}
function buildModelIndex(data: MenubarPayload): Map<string, string> {
const index = new Map<string, string>()
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) continue
index.set(sessionModelKey(project.name, session.date, session.calls, session.cost), dominant.name)
// A top session's cost and calls include its subagents, so match it by identity.
if (session.provider && session.sessionId) index.set(`${session.provider}\u0000${session.sessionId}`, dominant.name)
}
}
return index
}
function streakDays(daily: DailyHistoryEntry[], now: Date): number {
const byDate = new Map(daily.map(day => [day.date, day.cost]))
const spent = (offset: number) =>
(byDate.get(localDateKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - offset))) ?? 0) > 0
// A day that has not happened yet does not end a streak: before the first
// session of the day the count runs from yesterday, so a long run of active
// days never reads as 0.
let streak = 0
for (let offset = spent(0) ? 0 : 1; spent(offset); offset++) 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, tokens, animateKey, animate = true, estimated = false }: { value: number; tokens?: ReturnType<typeof tokensOf>; animateKey: string; animate?: boolean; estimated?: boolean }) {
const pop = useUsdPop<HTMLDivElement>(tokens, false, estimated)
const mark = estimated ? '~' : ''
const ref = pop.ref
const keyRef = useRef<string | null>(null)
useEffect(() => {
const element = ref.current
if (!element) return
const keyChanged = keyRef.current !== animateKey
keyRef.current = animateKey
if (!animate || !keyChanged || !motionEnabled()) {
element.textContent = mark + formatUsd(value)
return
}
const counter = { n: 0 }
const tween = gsap.to(counter, {
n: value,
duration: 0.7,
ease: 'power2.out',
onUpdate: () => { element.textContent = mark + formatUsd(counter.n) },
})
return () => { tween.kill() }
}, [value, animateKey, animate, mark])
return (
<>
<div ref={ref} className="ov-hero-num" data-countup={value} data-countup-animation={animate ? 'enabled' : 'suppressed'} {...pop.props}>{mark}{formatUsd(value)}</div>
{pop.pop}
</>
)
}
/** 0 = Sunday, from a local `YYYY-MM-DD` key. */
function dayOfWeek(date: string): number {
const [year, month, day] = date.split('-').map(Number)
return new Date(year, month - 1, day).getDay()
}
function formatShortDay(date: string): string {
const [, month, day] = date.split('-').map(Number)
return `${month}/${day}`
}
type AggregatedModel = {
name: string
cost: number
calls: number
// Absent when the payload carries no count for the row (an older CLI, or a
// row whose contributing legacy data lacked counts): the table shows "—"
// rather than a misleading zero.
inputTokens?: number
outputTokens?: number
cacheReadTokens?: number
// No column of its own; the cost cell's token popover reads it.
cacheWriteTokens?: number
// Absent on the history.daily fallback, which carries no estimated split.
estimatedCostUSD?: 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,
...(model.inputTokens === undefined ? {} : { inputTokens: model.inputTokens }),
...(model.outputTokens === undefined ? {} : { outputTokens: model.outputTokens }),
...(model.cacheReadTokens === undefined ? {} : { cacheReadTokens: model.cacheReadTokens }),
...(model.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: model.cacheWriteTokens }),
...(model.estimatedCostUSD === undefined ? {} : { estimatedCostUSD: model.estimatedCostUSD }),
}))
.sort((a, b) => b.cost - a.cost)
}
function aggregateModels(daily: DailyHistoryEntry[]): AggregatedModel[] {
const byName = new Map<string, AggregatedModel>()
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, onSelectModel }: { models: AggregatedModel[]; onSelectModel?: (name: string) => void }) {
if (!models.length) return <EmptyNote>{t('overview.models.noUsage')}</EmptyNote>
return (
<div className="ov-model-scroll">
<table className="ov-models" aria-label={t('overview.models.periodTitle')}>
<thead>
<tr>
<th>{t('overview.models.model')}</th>
<th className="num">{t('overview.models.inputTok')}</th>
<th className="num">{t('overview.models.outputTok')}</th>
{/* Reused input tokens: prompts the provider served from cache. */}
<th className="num" title={t('overview.models.cacheReadTooltip')}>{t('overview.models.cacheRead')}</th>
<th className="num">{t('overview.models.cost')}</th>
<th className="num">{t('overview.models.calls')}</th>
</tr>
</thead>
<tbody>
{models.map(model => (
<tr key={model.name}>
<td className="ov-model-name">
{onSelectModel ? (
<button type="button" className="ov-link" title={t('overview.models.viewSessionsFor', { name: model.name })} onClick={() => onSelectModel(model.name)}>{model.name}</button>
) : model.name}
</td>
<td className="num mono">{model.inputTokens === undefined ? '—' : formatCompact(model.inputTokens)}</td>
<td className="num mono">{model.outputTokens === undefined ? '—' : formatCompact(model.outputTokens)}</td>
<td className="num mono">{model.cacheReadTokens === undefined ? '—' : formatCompact(model.cacheReadTokens)}</td>
<td className="num mono"><Usd value={model.cost} tokens={tokensOf(model)} estimated={isEstimatedCost(model.cost, model.estimatedCostUSD)} /></td>
<td className="num">{model.calls.toLocaleString('en-US')}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
/** The drill-through payload App passes down: a filter selection plus the
* (optional) session to open the drawer on at the destination. */
export type InvestigateRequest = {
filters: InvestigationFilters
sessionId?: string | null
}
/** A drawn column: a single day, or a bucket that also carries the first day it covers. */
type ChartDay = DailyHistoryEntry & { spanStart?: string }
/** The days a column stands for. A bucket sums a week, so naming only its last
* day presents a weekly figure as a daily one. */
function spanLabel(day: ChartDay, format: (date: string) => string = date => date): string {
return day.spanStart && day.spanStart !== day.date ? t('overview.chart.dateRangeTo', { from: format(day.spanStart), to: format(day.date) }) : format(day.date)
}
/** Fold `size` consecutive days into one column, dated by the last day it covers
* so the axis label, the today highlight and the no-data cutoff stay truthful. */
function bucketDays(daily: DailyHistoryEntry[], size: number): ChartDay[] {
if (size <= 1) return daily
const buckets: ChartDay[] = []
for (let start = 0; start < daily.length; start += size) {
const slice = daily.slice(start, start + size)
const lead = slice.reduce((best, day) => (day.cost > best.cost ? day : best), slice[0])
buckets.push({
...slice[slice.length - 1],
spanStart: slice[0].date,
cost: slice.reduce((total, day) => total + day.cost, 0),
calls: slice.reduce((total, day) => total + day.calls, 0),
inputTokens: slice.reduce((total, day) => total + day.inputTokens, 0),
outputTokens: slice.reduce((total, day) => total + day.outputTokens, 0),
cacheReadTokens: slice.reduce((total, day) => total + day.cacheReadTokens, 0),
cacheWriteTokens: slice.reduce((total, day) => total + day.cacheWriteTokens, 0),
topModels: lead.topModels,
})
}
return buckets
}
/** The token breakdown behind a bar's amount, in the chart tip's own skin. */
const TOKEN_TIP_ROWS = [
['overview.models.input', 'inputTokens'],
['overview.models.output', 'outputTokens'],
['overview.models.cacheRead', 'cacheReadTokens'],
['overview.models.cacheWrite', 'cacheWriteTokens'],
] as const
function DailyChart({ daily, dataStart = null, animateKey = '', onSelectDay, bucketed = false }: { daily: ChartDay[]; dataStart?: string | null; animateKey?: string; onSelectDay?: (date: string) => void; bucketed?: boolean }) {
const bars = barLayout(daily.length)
const isNoData = (day: ChartDay) => dataStart !== null && day.date < dataStart
const max = Math.max(...daily.map(day => day.cost), 0)
// Bars are drawn against the top tick, not the raw peak, so a bar top and a
// gridline mean the same number.
const valueTicks = niceTicks(max)
const axisMax = valueTicks.at(-1) || 1
const peakIndex = daily.reduce((peak, day, index) => day.cost > (daily[peak]?.cost ?? -1) ? index : peak, 0)
const peak = daily[peakIndex]
const todayKey = localDateKey(new Date())
// Weekly labels work for 30 days, but become unreadable at 6M/Life (26-53
// labels). Long ranges use five even intervals plus the newest day.
const tickStride = daily.length <= 45 ? 7 : Math.ceil((daily.length - 1) / 5)
const tickIndexes = daily.map((_, index) => index).filter(index => index % tickStride === 0)
if (daily.length > 45 && tickIndexes.at(-1) !== daily.length - 1) tickIndexes.push(daily.length - 1)
const ticks = tickIndexes.map(index => daily[index])
const [tip, setTip] = useState<{ day: ChartDay; x: number; y: number } | null>(null)
const chartRef = useRef<HTMLDivElement>(null)
useBarGrowIn(chartRef, '.col', [animateKey])
const columnCentre = (index: number) => ((index + 0.5) / Math.max(1, daily.length)) * 100
return (
<>
<div className="chart-frame">
<div className="chart-plot">
<div className="chart-grid" aria-hidden="true">
{valueTicks.map(tick => <span className="chart-gridline" key={tick} style={{ bottom: `${(tick / axisMax) * 100}%` }} />)}
{bucketed ? null : daily.map((day, index) => (dayOfWeek(day.date) === 0 && index > 0
? <span className="chart-weekline" key={day.date} style={{ left: `${columnCentre(index) - (50 / Math.max(1, daily.length))}%` }} />
: null))}
</div>
<div className="chart" ref={chartRef} style={{ gap: `${bars.gap}px` }}>
{daily.map(day => {
const noData = isNoData(day)
// A day with recorded activity is a drill-through entry: clicking it
// opens the sessions that were active that day (sessions started
// earlier included, within the source's day granularity).
const drillable = !noData && (day.cost > 0 || day.calls > 0) && onSelectDay !== undefined
return (
<button
type="button"
aria-label={`${spanLabel(day)}: ${noData ? t('overview.chart.noDataAria') : formatUsd(day.cost)}${drillable ? ` — ${t('overview.chart.viewSessionsAria')}` : ''}`}
className={`col${day.date === todayKey && !noData ? ' hi' : ''}${noData ? ' nodata' : ''}`}
key={day.date}
style={{ height: `${axisMax > 0 ? Math.max(2, (day.cost / axisMax) * 100) : 2}%`, minWidth: `${bars.minWidth}px` }}
data-date={day.date}
data-cost={day.cost}
data-calls={day.calls}
data-led={day.topModels[0]?.name ?? ''}
data-nodata={noData ? 'true' : 'false'}
onMouseEnter={event => setTip({ day, x: event.clientX, y: event.clientY })}
onMouseMove={event => setTip({ day, x: event.clientX, y: event.clientY })}
onMouseLeave={() => setTip(null)}
onClick={drillable ? () => onSelectDay!(day.date) : undefined}
/>
)
})}
</div>
{peak && peak.cost > 0 && (
<span className="chart-peak-guide" aria-hidden="true" style={{ bottom: `${(peak.cost / axisMax) * 100}%`, left: `${columnCentre(peakIndex)}%` }} />
)}
</div>
<div className="chart-axis" aria-hidden="true">
{ticksClearOfPeak(valueTicks, peak && peak.cost > 0 ? peak.cost : 0, axisMax).map(tick => <span className="chart-axis-tick" key={tick} style={{ bottom: `${(tick / axisMax) * 100}%` }}>{formatAxisMoney(tick)}</span>)}
{peak && peak.cost > 0 && (
<span className="chart-axis-peak" style={{ bottom: `${(peak.cost / axisMax) * 100}%` }}>{formatUsd(peak.cost)}</span>
)}
</div>
<div className="ov-xax">
{ticks.map(day => {
const index = daily.indexOf(day)
return <span key={day.date} style={{ left: `${daily.length > 1 ? index / (daily.length - 1) * 100 : 0}%` }}>{formatChartDate(day.date)}</span>
})}
</div>
</div>
{tip && (
<ChartTip x={tip.x} y={tip.y}>
<div className="chart-tip-d">{spanLabel(tip.day, formatChartDate)}</div>
{isNoData(tip.day) ? (
<div className="chart-tip-s">{t('overview.chart.noDataRecorded')}</div>
) : (
<>
<div className="chart-tip-row">
<i className={tip.day.date === todayKey ? 'chart-tip-sw hi' : 'chart-tip-sw'} />
<span>{t('overview.chart.spend')}</span>
<b>{formatUsd(tip.day.cost)}</b>
</div>
<div className="chart-tip-row">
<i className="chart-tip-sw mut" />
<span>{t('overview.chart.modelLed', { name: tip.day.topModels[0]?.name ?? t('overview.chart.noModel') })}</span>
<b>{formatCount(tip.day.calls, 'call')}</b>
</div>
{TOKEN_TIP_ROWS.map(([label, key]) => (
<div className="chart-tip-row" key={key}>
<i className="chart-tip-sw" />
<span>{t(label)}</span>
<b>{formatCompact(tip.day[key])}</b>
</div>
))}
</>
)}
</ChartTip>
)}
</>
)
}
/** The card header's right slot: the menubar's three daily figures, read off the drawn window. */
function DailySummaries({ daily, anchorIsToday, bucketed = false }: { daily: DailyHistoryEntry[]; anchorIsToday: boolean; bucketed?: boolean }) {
const peak = daily.reduce<DailyHistoryEntry | undefined>((best, day) => (best && best.cost >= day.cost ? best : day), undefined)
const yesterday = daily.at(-2)
const average = mean(daily.map(day => day.cost))
return (
<div className="ov-chart-summaries" aria-label={t('overview.chart.dailySpendSummaryAria')}>
<div className="ov-summary-chip"><span>{bucketed ? t('overview.chart.avgPerWeek') : t('overview.chart.avgPerDay')}</span><strong><Usd value={average} tokens={sumTokens(daily, daily.length)} /></strong></div>
<div className="ov-summary-chip"><span>{t('overview.chart.peak')}</span><strong>{peak ? <><Usd value={peak.cost} tokens={tokensOf(peak)} /> · {formatShortDay(peak.date)}</> : '$0.00'}</strong></div>
<div className="ov-summary-chip"><span>{bucketed ? t('overview.day.previousWeek') : anchorIsToday ? t('overview.day.yesterday') : t('overview.day.previousDay')}</span><strong><Usd value={yesterday?.cost ?? 0} tokens={tokensOf(yesterday)} /></strong></div>
</div>
)
}
function formatRate(rate: number | null): string {
return rate === null ? '—' : `${Math.round(rate * 100)}%`
}
function TopActivities({ activities, onSelectCategory }: { activities: MenubarPayload['current']['topActivities']; onSelectCategory?: (rawCategory: string) => void }) {
const rows = [...activities].sort((a, b) => b.cost - a.cost).slice(0, 6)
if (!rows.length) return <EmptyNote>{t('overview.activities.noActivity')}</EmptyNote>
const maxCost = rows[0].cost
return (
<div className="ov-activities">
{rows.map(activity => {
// Only categories the CLI named with their raw key are drill entries:
// an older payload's label cannot round-trip as a filter value.
const drillable = onSelectCategory !== undefined && !!activity.rawCategory
const select = () => onSelectCategory?.(activity.rawCategory!)
return (
<div
className={drillable ? 'ov-activity ov-drill' : 'ov-activity'}
key={activity.name}
{...(drillable ? {
role: 'button',
tabIndex: 0,
title: t('overview.activities.viewSessions', { name: activity.name }),
onClick: select,
onKeyDown: (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
select()
}
},
} : {})}
>
<div className="ov-activity-bar" aria-hidden="true">
<span style={{ width: `${maxCost > 0 ? activity.cost / maxCost * 100 : 0}%` }} />
</div>
<div className="ov-activity-main">
<span className="ov-activity-name">{activity.name}</span>
<strong>{formatUsd(activity.cost)}</strong>
</div>
<div className="ov-activity-meta">
<span>{formatCount(activity.turns, 'turn')}</span>
<span>{formatRate(activity.oneShotRate)} {t('overview.activities.oneShotSuffix')}</span>
</div>
</div>
)
})}
</div>
)
}
export function Overview({ period, provider }: { period: Period; provider: string }) {
const overview = usePolled<MenubarPayload>(() => codeburn.getOverview(period, provider), [period, provider])
return <OverviewContent period={period} provider={provider} overview={overview} />
}
/** Combined-scope hero footer: a per-device cost breakdown plus a reachable/
* total device count, mirroring the menubar's combined view. An unreachable
* device (powered off, off-network) shows its error in place of a cost. */
function CombinedDevices({ usage }: { usage: CombinedUsage }) {
return (
<div className="ov-combined-devices">
<div className="ov-combined-head">{usage.combined.reachableCount.toLocaleString('en-US')} {t('overview.combined.of')} {formatCount(usage.combined.deviceCount, 'device')}</div>
{usage.perDevice.map(device => (
<div className={device.error ? 'ov-combined-row err' : 'ov-combined-row'} key={device.id}>
<span className="ov-combined-name">{device.local ? `${device.name} · ${t('overview.combined.thisDevice')}` : device.name}</span>
<span className="ov-combined-val">{device.error ?? formatUsd(device.cost)}</span>
</div>
))}
</div>
)
}
export function OverviewContent({
period,
provider = 'all',
range = null,
overview,
onNavigate,
onInvestigate,
ready = true,
scope = 'local',
configSource = null,
refreshToken = 0,
headlineSnapshot = null,
}: {
period: Period
provider?: string
range?: DateRange | null
overview: Polled<MenubarPayload>
onNavigate?: (section: 'optimize' | 'sessions' | 'spend' | 'plans') => void
/** Drill-through entries: day bars, expensive sessions, models, categories. */
onInvestigate?: (request: InvestigateRequest) => void
ready?: boolean
scope?: Scope
/** Scoped Claude config, part of the optimize cache key so a scan computed
* under one config is never shown under another. */
configSource?: string | null
/** Bumped by the shell's manual refresh: forces the slow and daily tiers. */
refreshToken?: number
headlineSnapshot?: OverviewHeadlineSnapshot | null
}) {
const { data, error, lastSuccessAt } = overview
const heroSelectionKey = `${period}|${provider}|${range?.from ?? ''}|${range?.to ?? ''}|${scope}`
// Suppress only the single persisted-headline -> live-data handoff. A stored
// headline remains available after that handoff, so testing the snapshot prop
// directly would disable every later user-triggered period/provider animation.
const pendingSnapshotHandoffRef = useRef<string | null>(null)
const suppressHeroReplay = pendingSnapshotHandoffRef.current === heroSelectionKey
useLayoutEffect(() => {
if (!data && headlineSnapshot) {
pendingSnapshotHandoffRef.current = heroSelectionKey
} else if (data && pendingSnapshotHandoffRef.current === heroSelectionKey) {
pendingSnapshotHandoffRef.current = null
}
}, [data, headlineSnapshot, heroSelectionKey])
// Gate secondary spawns on the app-level readiness (first overview resolved),
// so the cold hydration runs once (via overview) rather than 3 parses at once
// on boot. Defaults true so standalone renders/tests poll normally.
// A bounded Overview timeout is not permission to fan out more expensive
// analysis. On a real heavy corpus the timeout released act/yield, and a user
// Refresh then ran another status parse beside yield. Latch that timeout until
// real Overview data arrives: refresh() clears the current error while its new
// request is pending, which must not accidentally re-open the secondary gate.
const [timeoutBlocked, setTimeoutBlocked] = useState(false)
useEffect(() => {
if (overview.error?.kind === 'timeout') setTimeoutBlocked(true)
else if (overview.data != null) setTimeoutBlocked(false)
}, [overview.data, overview.error?.kind])
const detailsReady = ready && !timeoutBlocked && overview.error?.kind !== 'timeout'
const actReport = usePolled<ActReportJson>(() => codeburn.getActReport(), [refreshToken], { enabled: detailsReady, memoKey: 'overview-act', cadence: { slowMs: ACT_SLOW_MS } })
const yieldReport = usePolled<YieldJsonReport>(() => codeburn.getYield(period, provider), [period, provider, refreshToken], { enabled: detailsReady, memoKey: reportMemoKey('yield', period, provider), cadence: { slowMs: YIELD_SLOW_MS } })
// Daily tier: the live poll runs --no-optimize, so the coach figures below
// come from the on-disk scan for this exact scope, with their age on screen.
const optimizeSnapshot = useOptimizeSnapshot(
{ period, provider, range, configSource, scope },
{ enabled: detailsReady, refreshToken },
)
const optimizeBlock = optimizeSnapshot.data?.optimize ?? null
const optimizeAge = asOfLabel(optimizeSnapshot.data?.computedAt ?? null)
const modelIndex = useMemo(() => data ? buildModelIndex(data) : new Map<string, string>(), [data])
if (!data) {
if (error) return <CliErrorPanel error={error} subject={t('common.subject.usage')} />
if (headlineSnapshot) {
// The time THIS app received (and therefore verified) the payload, not
// the payload's own `generated`: since the poll runs --no-optimize it
// takes the CLI's status snapshot fast path, so `generated` can be up to
// a day older than the moment these numbers were confirmed.
const captured = new Date(headlineSnapshot.capturedAt)
const capturedLabel = Number.isNaN(captured.getTime())
? t('overview.snapshot.earlier')
: localDateKey(captured) === localDateKey(new Date())
? t('overview.snapshot.at', { time: captured.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) })
: t('overview.snapshot.dateAt', { date: captured.toLocaleDateString([], { month: 'short', day: 'numeric' }), time: captured.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) })
const headlineCost = headlineSnapshot.currency
? formatUsdWithCurrency(headlineSnapshot.cost, headlineSnapshot.currency)
: formatUsd(headlineSnapshot.cost)
return (
<div className="ov-dashboard" aria-label={t('overview.snapshot.cachedAria')}>
<div className="ov-card">
<div className="ov-panel-head"><Icon name="circle-dollar-sign" /><h3>{headlineSnapshot.label}</h3><span className="r"><span className="ov-streak">{t('overview.snapshot.exact', { label: capturedLabel })}</span></span></div>
<div className="ov-card-inner ov-hero-split snapshot-hero">
<div className="ov-hero-main">
<div className="ov-hero-num" data-countup={headlineSnapshot.cost}>{headlineCost}</div>
<div className="ov-hero-sub">{formatCount(headlineSnapshot.calls, 'call')} · {t('overview.snapshot.sessionsUpdating')}</div>
</div>
</div>
</div>
<SectionSkeleton label={t('overview.snapshot.updatingDrillDowns')} rows={3} chart />
</div>
)
}
return <SectionSkeleton label={t('overview.snapshot.scanningSessions')} rows={3} chart />
}
const now = new Date()
const rangeActive = !!range
// Combined scope shows the paired-device aggregate in the hero KPIs, mirroring
// the menubar. Only the hero totals are aggregated; the detailed panels below
// (daily chart, models) stay local — the combined payload carries totals only.
const combined = scope === 'combined' ? data.combined : undefined
// One generation behind every period the user can switch to. `periodTotals`
// is emitted only for an unscoped, unfiltered request, so its presence on THIS
// payload is the gate: under a provider, project or config filter the machine-
// wide generation must never stand in for the filtered headline.
const unfiltered = !rangeActive && !combined && !!data.periodTotals
rememberGeneration(unfiltered ? data : null, lastSuccessAt, period)
const headline = unfiltered ? generationHeadline(period, lastSuccessAt) : null
// The hero stands in with the generation only when the models table can stand in
// with the same one; a generation from another period carries the wrong models,
// so both fall back to this payload rather than disagreeing for a moment.
const genModels = unfiltered ? generationModels(period, lastSuccessAt) : null
const useGeneration = headline != null && genModels != null
const heroCost = combined ? combined.combined.cost : useGeneration ? headline.cost : data.current.cost
const heroCalls = combined ? combined.combined.calls : useGeneration ? headline.calls : data.current.calls
// The combined aggregate carries no estimated split, so it is never marked.
const heroEstimated = !combined && isEstimatedCost(heroCost, useGeneration
? genModels.reduce((sum, model) => sum + (model.estimatedCostUSD ?? 0), 0)
: estimatedPortion(data.current))
const heroSessions = combined ? combined.combined.sessions : data.current.sessions
const heroSessionLabel = combined
? formatCombinedSessionCount()
: formatSessionCount(heroSessions, data.current.sessionCountBasis)
// The breakdown must belong to the number on screen: the combined aggregate,
// the generation window, or this payload's own period — never a mix.
const heroTokens = combined
? tokensOf({ ...combined.combined, cacheWriteTokens: combined.combined.cacheCreateTokens })
: tokensOf(useGeneration ? headline : data.current)
const heroSessionHelp = combined
? combinedSessionCountHelp()
: (sessionCountIsExact(data.current.sessionCountBasis) ? undefined : sessionCountHelp())
const animateKey = heroSelectionKey
const anchorKey = rangeActive ? range.to : localDateKey(now)
const anchorIsToday = anchorKey === localDateKey(now)
const stats = deriveStats(data, now, anchorKey)
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),
)
// The chips read the same series the chart draws. Past the fit ceiling the
// chart folds days into weeks, and a Peak taken from the raw days then named a
// day the chart has no bar for, so the guide and the chip disagreed.
const chartBucketSize = barBucketDays(chartDaily.length)
const drawnDaily = bucketDays(chartDaily, chartBucketSize)
// Models this period come from `current.topModels` — period/range/provider-
// scoped by the CLI, and (on CLIs that emit per-model counts) carrying input/
// output/cache-read counts for every model in the period, including days
// whose per-day top-5 history list no longer names them. history.daily is
// the fallback for payloads from older CLIs: its rows know input/output but
// not cache read, so the cache column shows "—" there. Provider-filtered
// history.daily has empty topModels, so a provider filter always sources
// from current.topModels.
//
// `current.topModels` is uncapped (#1318), so this table lists every model
// with usage in the period — the union-over-top-5 fallback it replaced was
// itself truncated per day, and a row cap would drop exactly the local and
// free models (qwen, llama, …) whose cost is $0 and therefore rank last.
// Consumers wanting fewer rows slice their own; this table scrolls instead.
const topModelsCarryCounts = data.current.topModels.some(model =>
model.inputTokens !== undefined || model.outputTokens !== undefined,
)
const models = useGeneration
? topModelsToAggregated(genModels)
: provider !== 'all' || topModelsCarryCounts
? 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 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
const actAge = asOfLabel(actReport.lastSuccessAt)
// Until the daily scan has produced a figure for THIS scope the clause is
// omitted entirely: a $0 here would read as "nothing to recover".
const recoverable = optimizeBlock
? <> <span className="num">{formatUsd(optimizeBlock.savingsUSD)}</span>{t('overview.coach.recoverableSuffix')}{optimizeAge ? <small className="ov-coach-age"> ({optimizeAge})</small> : null}</>
: null
// A custom range has no meaningful "vs last week" or month-to-date baseline.
const signals = deriveSignals(data, now, rangeActive, { topFindings: optimizeBlock?.topFindings ?? [], asOf: optimizeAge })
// Drill-through entry points. An expensive-session row can only open the
// exact session when the payload carries its identity (provider + id);
// otherwise the row keeps the plain "See all" navigation, never a guess.
const openSessionRow = (session: MenubarPayload['current']['topSessions'][number]) => {
if (!session.sessionId || !session.provider || !onInvestigate) {
onNavigate?.('sessions')
return
}
// The drawer key uses the RAW row project (projectKey), not the friendly
// display name, so it matches the sessions-list rows exactly.
const projectKey = session.projectKey ?? session.project
onInvestigate({
filters: sessionFilters({ provider: session.provider, sessionId: session.sessionId }),
sessionId: `${session.provider}\u0000${projectKey}\u0000${session.sessionId}`,
})
}
return (
<div className="ov-dashboard">
{error && <StaleBanner error={error} />}
<div className="ov-card">
<div className="ov-panel-head">
<Icon name="circle-dollar-sign" />
<h3>{combined ? `${t('overview.hero.combined')} · ${data.current.label}` : data.current.label}</h3>
<span className="r"><span className="ov-streak">{t('overview.hero.streakPrefix')}<b>{rememberStreak(data.streak) ?? streakDays(data.history.daily, now)}</b>{t('overview.hero.streakSuffix')}</span></span>
</div>
<div className="ov-card-inner ov-hero-split" aria-label={t('overview.hero.kpiAria')}>
<div className="ov-hero-main">
<div className="ov-hero-figures">
{/* A returning launch already showed a truthful persisted headline.
Replaying the live hero from $0 on handoff makes that exact value
appear to collapse and recover; snap to the revalidated total. */}
<CountUp value={heroCost} tokens={heroTokens} animateKey={animateKey} animate={!suppressHeroReplay} estimated={heroEstimated} />
<div className="ov-hero-sub" title={heroSessionHelp}>{formatCount(heroCalls, 'call')} · {heroSessionLabel}</div>
{(provider === 'cursor' || provider === 'cursor-agent') && <CursorSyncLine status={data.cursorSync} />}
{combined
? <CombinedDevices usage={combined} />
: (
<>
{saved > 0 && (
<div className="ov-saved-line"><span>{t('overview.hero.savedByFixes')}</span><strong>{formatUsd(saved)}</strong><small>{t('overview.hero.across')} {formatCount(applied, 'fix', 'fixes')}{actAge ? ` · ${actAge}` : ''}</small></div>
)}
{localSaved > 0 && (
<div className="ov-saved-line"><span>{t('overview.hero.savedViaLocal')}</span><strong>{formatUsd(localSaved)}</strong><small>{t('overview.hero.localModelRouting')}</small></div>
)}
</>
)}
</div>
<div className="ov-hero-foot">
<div><span>{anchorIsToday ? t('overview.day.yesterday') : t('overview.day.previousDay')}</span><strong>{stats.priorDayCost === null ? t('overview.common.na') : <Usd value={stats.priorDayCost} tokens={tokensOf(stats.priorDayEntry)} />}</strong></div>
<div><span>{t('overview.hero.sevenDayAvg')}</span><strong>{stats.sevenDayAvg === null ? t('overview.common.na') : <Usd value={stats.sevenDayAvg} tokens={sumTokens(stats.sevenDayEntries, stats.sevenDayEntries.length)} />}</strong></div>
<div><span>{anchorIsToday ? t('overview.day.vsYesterday') : t('overview.day.vsPreviousDay')}</span><strong className={stats.dayOverDayPct === null ? undefined : `tone-${paceDirection(stats.dayOverDayPct)}`}>{stats.dayOverDayPct === null ? t('overview.common.na') : `${stats.dayOverDayPct >= 0 ? '+' : '-'}${Math.abs(Math.round(stats.dayOverDayPct))}%`}</strong></div>
</div>
</div>
<ActivityHeatmap daily={data.history.daily} bare />
<EfficiencyScorecard current={data.current} bare />
</div>
</div>
{!rangeActive && (
<div className="ov-stats3">
<div className="ov-card">
<div className="ov-panel-head"><Icon name="calendar" /><h3>{t('overview.stats.monthToDate')}</h3></div>
<div className="ov-card-inner ov-stat">
<SpendTrend values={stats.mtdSeries} tone={stats.pacePct === null ? 'flat' : paceDirection(stats.pacePct)} />
<div className="ov-stat-figures">
<div className="v"><Usd value={stats.mtd} tokens={sumTokens(stats.mtdEntries)} /></div>
{stats.pacePct === null ? (
<div className="d">{t('overview.stats.noMonthPaceYet', { month: stats.prevMonthName })}</div>
) : (
<>
<span className={`ov-stat-pill tone-${paceDirection(stats.pacePct)}`}>
<Icon name={stats.pacePct < 0 ? 'arrow-down' : 'arrow-up'} />
{Math.abs(Math.round(stats.pacePct))}%
</span>
<div className="d">{t('overview.stats.vsMonthPace', { month: stats.prevMonthName })}</div>
</>
)}
</div>
<div className="ov-stat-foot">
<button className="ov-link" type="button" onClick={() => onNavigate?.('spend')}>{t('overview.stats.seeSpend')}<Icon name="arrow-right" /></button>
</div>
</div>
</div>
<div className="ov-card">
<div className="ov-panel-head"><Icon name="trending-up" /><h3>{t('overview.stats.projectedMonth')}</h3></div>
<div className="ov-card-inner ov-stat">
<SpendTrend values={[...stats.mtdSeries, ...stats.projectedTail]} tone="flat" dashFrom={Math.max(0, stats.mtdSeries.length - 1)} />
<div className="ov-stat-figures">
<div className="v">{formatUsd(stats.projected)} <small>{t('overview.stats.est')}</small></div>
<span className="ov-stat-pill tone-neutral">
<b>{formatUsd(Math.max(0, stats.projected - stats.mtd))}</b> {t('overview.stats.toGo')}
</span>
</div>
<div className="ov-stat-foot">
<button className="ov-link" type="button" onClick={() => onNavigate?.('plans')}>{t('overview.stats.seePlans')}<Icon name="arrow-right" /></button>
</div>
</div>
</div>
</div>
)}
<div className="ov-card ov-panel ov-chart-widget">
<div className="ov-panel-head"><Icon name="chart-column" /><h3>{t('overview.chartWidget.dailySpend')}</h3>{data.history.daily.length ? <span className="r"><DailySummaries daily={drawnDaily} anchorIsToday={anchorIsToday} bucketed={chartBucketSize > 1} /></span> : null}</div>
<div className="ov-panel-body">{data.history.daily.length ? <DailyChart daily={drawnDaily} bucketed={chartBucketSize > 1} dataStart={dataStartKey(data.history.daily)} animateKey={animateKey} onSelectDay={date => onInvestigate?.({ filters: dayFilters(date) })} /> : <EmptyNote>{t('overview.chartWidget.noSpendYet')}</EmptyNote>}</div>
</div>
<WorkflowCard current={data.current} />
<div className="ov-insight-band">
<div className="ov-coach">
<div className="ov-card-inner ov-coach-inner">
<Icon name="trending-up" />
<div className="ov-coach-tx">
{rangeActive
? <>{topModel ? <><span className="num">{topModel.name}</span>{t('overview.coach.driverSuffixRange')}</> : t('overview.coach.noDriverRange')}.{recoverable}</>
: <>{weeklyPct === null ? <>{t('overview.coach.noBaseline')}</> : <>{t('overview.coach.pacingLead')}<span className="num">{t('overview.coach.pacingClause', { percent: weeklyPct, direction: t(weekNow >= weekPrior ? 'overview.coach.higher' : 'overview.coach.lower') })}</span></>}{topModel ? <>; <span className="num">{topModel.name}</span>{t('overview.coach.alsoDriverSuffix')}</> : ''}.{recoverable}</>}
</div>
<button className="ov-coach-cta" type="button" onClick={() => onNavigate?.('optimize')}>{t('overview.coach.reviewCta')}</button>
</div>
</div>
</div>
<SignalsCard signals={signals} />
<div className="ov-analytics-row">
<CostPerOutcome outcome={yieldReport} />
<RoutingWhatIf routing={data.current.routingWaste} onNavigate={onNavigate} />
</div>
<div className="ov-body-grid">
<div className="ov-main-column">
<div className="ov-card ov-panel ov-models-widget">
<div className="ov-panel-head"><Icon name="box" /><h3>{t('overview.models.periodTitle')}</h3><span className="r">{t('overview.common.sortedByCost')}</span></div>
<div className="ov-panel-body ov-model-panel"><ModelsTable models={models} onSelectModel={onInvestigate ? name => onInvestigate({ filters: modelFilters([name]) }) : undefined} /></div>
</div>
<div className="ov-card ov-panel ov-sessions-widget">
<div className="ov-panel-head"><Icon name="coins" /><h3>{t('overview.sessions.mostExpensive')}</h3><span className="r"><button className="ov-link" type="button" onClick={() => onNavigate?.('sessions')}>{t('overview.sessions.seeAll')}</button></span></div>
<div className="ov-panel-body">
{data.current.topSessions.length ? data.current.topSessions.map((session, index) => {
const model = (session.provider && session.sessionId ? modelIndex.get(`${session.provider}\u0000${session.sessionId}`) : undefined)
?? modelIndex.get(sessionModelKey(session.project, session.date, session.calls, session.cost))
const sub = [formatChartDate(session.date), model, formatCount(session.calls, 'call')].filter(Boolean).join(' · ')
return <ListRow key={`${session.project}-${session.date}-${index}`} no={String(index + 1).padStart(2, '0')} title={session.project} sub={sub} value={formatUsd(session.cost)} onClick={() => openSessionRow(session)} />
}) : <EmptyNote>{t('overview.sessions.noSessions')}</EmptyNote>}
</div>
</div>
</div>
<div className="ov-side-column">
<div className="ov-card ov-panel ov-activities-widget">
<div className="ov-panel-head"><Icon name="list" /><h3>{t('overview.activities.title')}</h3><span className="r">{t('overview.common.sortedByCost')}</span></div>
<div className="ov-panel-body"><TopActivities activities={data.current.topActivities} onSelectCategory={onInvestigate ? raw => onInvestigate({ filters: categoryFilters(raw) }) : undefined} /></div>
</div>
</div>
</div>
</div>
)
}