feat(app): actionable Optimize — wire the CLI's rich findings + copy-paste fixes

- new getOptimizeReport bridge (main + preload + renderer types) running
  `codeburn optimize --format json` (needs a full relaunch)
- restore the Waste / Reverts / Abandoned / Fixes tabs
- Waste findings expand inline to show the explanation (why you're losing
  tokens) + a copy-paste fix block with a Copy button, like the TUI
- summary header: findings count / potential savings / health score

typecheck clean; 133/133 tests pass.
This commit is contained in:
iamtoruk 2026-07-12 16:28:47 -07:00
parent 82f0402abd
commit 905d94253a
9 changed files with 324 additions and 113 deletions

View file

@ -39,6 +39,7 @@ const CHANNELS = [
'codeburn:getCompare',
'codeburn:getYield',
'codeburn:getSpendFlow',
'codeburn:getOptimizeReport',
'codeburn:getDevices',
'codeburn:getDevicesScan',
'codeburn:getShareStatus',
@ -69,10 +70,12 @@ const ARGV_CASES: Array<{ channel: string; args: unknown[]; argv: string[] }> =
{ channel: 'codeburn:getCompare', args: ['month', 'all', 'model-a', 'model-b'], argv: ['compare', '--format', 'json', '--period', 'month', '--model-a', 'model-a', '--model-b', 'model-b'] },
{ channel: 'codeburn:getYield', args: ['today'], argv: ['yield', '--format', 'json', '--period', 'today'] },
{ channel: 'codeburn:getSpendFlow', args: ['month', 'openai'], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--provider', 'openai'] },
{ channel: 'codeburn:getOptimizeReport', args: ['month', 'openai'], argv: ['optimize', '--format', 'json', '--period', 'month', '--provider', 'openai'] },
{ channel: 'codeburn:getOverview', args: ['30days', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['status', '--format', 'menubar-json', '--period', '30days', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getModels', args: ['week', 'claude', true, { from: '2026-07-01', to: '2026-07-11' }], argv: ['models', '--format', 'json', '--period', 'week', '--provider', 'claude', '--by-task', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getYield', args: ['today', { from: '2026-07-01', to: '2026-07-11' }], argv: ['yield', '--format', 'json', '--period', 'today', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getSpendFlow', args: ['month', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['spend', '--format', 'flow-json', '--period', 'month', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getOptimizeReport', args: ['month', 'all', { from: '2026-07-01', to: '2026-07-11' }], argv: ['optimize', '--format', 'json', '--period', 'month', '--from', '2026-07-01', '--to', '2026-07-11'] },
{ channel: 'codeburn:getDevices', args: ['week'], argv: ['devices', '--format', 'json', '--period', 'week'] },
{ channel: 'codeburn:getDevicesScan', args: [], argv: ['devices', 'scan', '--format', 'json'] },
{ channel: 'codeburn:getShareStatus', args: [], argv: ['share', 'status', '--format', 'json'] },

View file

@ -75,6 +75,9 @@ export function createBridgeHandlers(deps: Deps = { spawnCli, spawnCliAction, re
'codeburn:getSpendFlow': run((period: string, provider: string, range?: DateRange) => [
'spend', '--format', 'flow-json', '--period', period, ...providerArgs(provider), ...rangeArgs(range),
]),
'codeburn:getOptimizeReport': run((period: string, provider: string, range?: DateRange) => [
'optimize', '--format', 'json', '--period', period, ...providerArgs(provider), ...rangeArgs(range),
]),
'codeburn:getDevices': run((period: string) => ['devices', '--format', 'json', '--period', period]),
'codeburn:getDevicesScan': run(() => ['devices', 'scan', '--format', 'json']),
'codeburn:getShareStatus': run(() => ['share', 'status', '--format', 'json']),

View file

@ -27,6 +27,7 @@ const bridge = {
getCompare: (period: string, provider: string, modelA: string, modelB: string) => invoke('codeburn:getCompare', period, provider, modelA, modelB),
getYield: (period: string, range?: DateRange) => invoke('codeburn:getYield', period, range),
getSpendFlow: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getSpendFlow', period, provider, range),
getOptimizeReport: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getOptimizeReport', period, provider, range),
getDevices: (period: string) => invoke('codeburn:getDevices', period),
getDevicesScan: () => invoke('codeburn:getDevicesScan'),
getShareStatus: () => invoke('codeburn:getShareStatus'),

View file

@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { App } from './App'
import type { DateRange, MenubarPayload, SpendFlow } from './lib/types'
import type { DateRange, MenubarPayload, OptimizeJsonReport, SpendFlow } from './lib/types'
const stored = new Map<string, string>()
vi.stubGlobal('localStorage', {
@ -15,6 +15,7 @@ vi.stubGlobal('localStorage', {
const mocks = vi.hoisted(() => ({
getOverview: vi.fn<(period: string, provider: string, range?: DateRange) => Promise<MenubarPayload>>(),
getSpendFlow: vi.fn<(period: string, provider: string, range?: DateRange) => Promise<SpendFlow>>(),
getOptimizeReport: vi.fn<(period: string, provider: string, range?: DateRange) => Promise<OptimizeJsonReport>>(),
getModels: vi.fn(),
getSessions: vi.fn(),
getCompareModels: vi.fn(),
@ -91,6 +92,15 @@ describe('App shortcuts', () => {
for (const mock of Object.values(mocks)) mock.mockReset()
mocks.getOverview.mockResolvedValue(overviewPayload())
mocks.getSpendFlow.mockResolvedValue({ period: { label: 'Last 30 days', start: '', end: '' }, models: [], projects: [], links: [] })
mocks.getOptimizeReport.mockResolvedValue({
period: { label: 'Last 30 days', start: null, end: null },
summary: {
healthScore: 100, healthGrade: 'A', findingCount: 0, periodCostUSD: 0,
sessions: 0, calls: 0, potentialSavingsTokens: 0, potentialSavingsCostUSD: 0,
potentialSavingsPercent: 0, costRateUSD: 0,
},
findings: [],
})
mocks.getModels.mockResolvedValue([])
mocks.getSessions.mockResolvedValue([])
mocks.getCompareModels.mockResolvedValue([])

View file

@ -179,7 +179,7 @@ export function App() {
) : section === 'spend' ? (
<SpendContent period={period} provider={provider} range={customRange} overview={overview} refreshToken={refreshToken} />
) : section === 'optimize' ? (
<OptimizeContent period={period} range={customRange} overview={overview} refreshToken={refreshToken} />
<OptimizeContent period={period} provider={provider} range={customRange} overview={overview} refreshToken={refreshToken} />
) : section === 'models' ? (
<Models period={period} provider={provider} range={customRange} refreshToken={refreshToken} onNavigate={navigate} />
) : section === 'compare' ? (

View file

@ -325,6 +325,39 @@ export type SpendFlow = {
links: SpendFlowLink[]
}
// ————— src/optimize.ts —————
export type WasteAction =
| { type: 'paste'; label: string; text: string; destination?: 'claude-md' | 'session-opener' | 'prompt' | 'shell-config' }
| { type: 'command'; label: string; text: string }
| { type: 'file-content'; label: string; path: string; content: string }
export type OptimizeJsonReport = {
period: { label: string; start: string | null; end: string | null }
summary: {
healthScore: number
healthGrade: 'A' | 'B' | 'C' | 'D' | 'F'
findingCount: number
periodCostUSD: number
sessions: number
calls: number
potentialSavingsTokens: number
potentialSavingsCostUSD: number
potentialSavingsPercent: number | null
costRateUSD: number
}
findings: Array<{
id: string
title: string
explanation: string
severity: 'high' | 'medium' | 'low'
trend: 'active' | 'improving' | null
tokensSaved: number
estimatedSavingsUSD: number
fix: WasteAction
}>
}
// ————— T1b: src/sharing/* (defined by the shared contract) —————
export type PendingPairing = { id: string; name: string; code: string }
@ -445,6 +478,7 @@ export interface CodeburnBridge {
getCompare(period: Period, provider: string, modelA: string, modelB: string): Promise<CompareJsonReport>
getYield(period: Period, range?: DateRange): Promise<YieldJsonReport>
getSpendFlow(period: Period, provider: string, range?: DateRange): Promise<SpendFlow>
getOptimizeReport(period: Period, provider: string, range?: DateRange): Promise<OptimizeJsonReport>
getDevices(period: Period): Promise<CombinedUsage>
getDevicesScan(): Promise<DeviceScanResult>
getShareStatus(): Promise<ShareStatus>

View file

@ -1,47 +1,32 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { MenubarPayload, YieldJsonReport } from '../lib/types'
import type { MenubarPayload, OptimizeJsonReport, YieldJsonReport } from '../lib/types'
import { Optimize, OptimizeContent } from './Optimize'
const { getOverview, getYield } = vi.hoisted(() => ({
getOverview: vi.fn<(period: string, provider: string) => Promise<MenubarPayload>>(),
getYield: vi.fn<(period: string) => Promise<YieldJsonReport>>(),
const { getOverview, getOptimizeReport, getYield } = vi.hoisted(() => ({
getOverview: vi.fn(),
getOptimizeReport: vi.fn(),
getYield: vi.fn(),
}))
vi.mock('../lib/ipc', async orig => {
const actual = await orig<typeof import('../lib/ipc')>()
return { ...actual, codeburn: { getOverview, getYield } }
return { ...actual, codeburn: { getOverview, getOptimizeReport, getYield } }
})
function makePayload(): MenubarPayload {
return {
generated: '2026-07-10T19:00:00.000Z',
current: {
label: 'Last 30 days',
cost: 612.48,
calls: 1220,
sessions: 88,
oneShotRate: null,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheHitPercent: 0,
codexCredits: 0,
topActivities: [],
topModels: [],
label: 'Last 30 days', cost: 612.48, calls: 1220, sessions: 88, oneShotRate: null,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
cacheHitPercent: 0, codexCredits: 0, topActivities: [], topModels: [],
localModelSavings: { totalUSD: 0, calls: 0, byModel: [], byProvider: [] },
providers: {},
topProjects: [],
modelEfficiency: [],
topSessions: [],
providers: {}, topProjects: [], modelEfficiency: [], topSessions: [],
retryTax: { totalUSD: 0, retries: 0, editTurns: 0, byModel: [] },
routingWaste: { totalSavingsUSD: 0, baselineModel: '', baselineCostPerEdit: 0, byModel: [] },
tools: [],
skills: [],
subagents: [],
mcpServers: [],
tools: [], skills: [], subagents: [], mcpServers: [],
},
optimize: {
findingCount: 3,
@ -56,6 +41,36 @@ function makePayload(): MenubarPayload {
}
}
function makeOptimizeReport(): OptimizeJsonReport {
return {
period: { label: 'Last 30 days', start: '2026-06-11', end: '2026-07-10' },
summary: {
healthScore: 72, healthGrade: 'C', findingCount: 3, periodCostUSD: 612.48,
sessions: 88, calls: 1220, potentialSavingsTokens: 184_000,
potentialSavingsCostUSD: 94.4, potentialSavingsPercent: 15.4, costRateUSD: 0.0005,
},
findings: [
{
id: 'cost-outliers', title: 'Opus is doing your small talk',
explanation: 'Small conversational requests are running on an expensive model.',
severity: 'high', trend: 'active', tokensSaved: 18_200, estimatedSavingsUSD: 9.1,
fix: { type: 'paste', label: 'Paste into CLAUDE.md', text: 'Use Sonnet for routine questions.', destination: 'claude-md' },
},
{
id: 'context-heavy-sessions', title: 'Cache hit is low in agentseal-dash',
explanation: 'Repeated context is not being served from cache.', severity: 'medium',
trend: null, tokensSaved: 17_400, estimatedSavingsUSD: 8.7,
fix: { type: 'command', label: 'Run this command', text: 'codeburn cache inspect' },
},
{
id: 'warmup-heavy', title: 'Batch tiny requests', explanation: 'Many short sessions repeat setup work.',
severity: 'low', trend: 'improving', tokensSaved: 4_800, estimatedSavingsUSD: 2.4,
fix: { type: 'file-content', label: 'Create configuration', path: '~/.codeburn/config.json', content: '{"batch":true}' },
},
],
}
}
function makeYield(): YieldJsonReport {
return {
period: { label: 'Last 30 days', start: '2026-06-11', end: '2026-07-10' },
@ -63,8 +78,7 @@ function makeYield(): YieldJsonReport {
productive: { costUSD: 440, sessions: 19, costPercent: 72, sessionPercent: 70 },
reverted: { costUSD: 107, sessions: 4, costPercent: 17, sessionPercent: 15 },
abandoned: { costUSD: 65.4, sessions: 3, costPercent: 11, sessionPercent: 15 },
total: { costUSD: 612.4, sessions: 26 },
productiveToRevertedCostRatio: 4.1,
total: { costUSD: 612.4, sessions: 26 }, productiveToRevertedCostRatio: 4.1,
},
details: [
{ sessionId: 'rev-1', project: 'codeburn', category: 'reverted', commitCount: 2, costUSD: 55 },
@ -81,6 +95,13 @@ function emptyPayload(): MenubarPayload {
return payload
}
function emptyOptimizeReport(): OptimizeJsonReport {
const report = makeOptimizeReport()
report.summary = { ...report.summary, findingCount: 0, potentialSavingsTokens: 0, potentialSavingsCostUSD: 0, potentialSavingsPercent: 0 }
report.findings = []
return report
}
function emptyYield(): YieldJsonReport {
const report = makeYield()
report.summary.reverted = { costUSD: 0, sessions: 0, costPercent: 0, sessionPercent: 0 }
@ -90,94 +111,120 @@ function emptyYield(): YieldJsonReport {
}
describe('Optimize', () => {
const writeText = vi.fn<(text: string) => Promise<void>>()
beforeEach(() => {
getOverview.mockReset()
getYield.mockReset()
getOverview.mockReset().mockResolvedValue(makePayload())
getOptimizeReport.mockReset().mockResolvedValue(makeOptimizeReport())
getYield.mockReset().mockResolvedValue(makeYield())
writeText.mockReset().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
})
it('renders findings, reverted sessions, and abandoned sessions together with section totals', async () => {
getOverview.mockResolvedValue(makePayload())
getYield.mockResolvedValue(makeYield())
it('renders tabs and actionable Waste findings with impact, savings, explanation, and copy-paste fix', async () => {
render(<Optimize period="30days" provider="all" />)
expect(await screen.findByText('Opus is doing your small talk')).toBeInTheDocument()
expect(screen.getByText('3 findings · $94.40 potential · health 72/100')).toBeInTheDocument()
expect(screen.getByText('High')).toHaveClass('opt-impact-high')
expect(screen.getByText('Medium')).toHaveClass('opt-impact-medium')
expect(screen.getByText('Low')).toHaveClass('opt-impact-low')
expect(screen.getByText('$9.10')).toBeInTheDocument()
expect(screen.getByText('Cache hit is low in agentseal-dash')).toBeInTheDocument()
expect(screen.getByText('Optimization findings · 3 findings · $94.40 potential')).toBeInTheDocument()
expect(screen.getByText('Reverted sessions · $107.00')).toBeInTheDocument()
expect(screen.getByText('Abandoned sessions · $65.40')).toBeInTheDocument()
expect(screen.getByText('$9.10')).toHaveClass('opt-finding-savings')
expect(screen.getByText('18.2K tokens')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Waste $94.40' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Reverts $107.00' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Abandoned $65.40' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Fixes 3' })).toBeInTheDocument()
const row = screen.getByRole('button', { name: /Opus is doing your small talk/ })
expect(row).toHaveAttribute('aria-expanded', 'false')
fireEvent.click(row)
expect(row).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByText('Small conversational requests are running on an expensive model.')).toBeInTheDocument()
expect(screen.getByText('Use Sonnet for routine questions.')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Copy' }))
await waitFor(() => expect(writeText).toHaveBeenCalledWith('Use Sonnet for routine questions.'))
expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument()
})
it('keeps only one finding expanded and renders file-content path and content', async () => {
render(<Optimize period="30days" provider="all" />)
const first = await screen.findByRole('button', { name: /Opus is doing your small talk/ })
fireEvent.click(first)
fireEvent.click(screen.getByRole('button', { name: /Batch tiny requests/ }))
expect(first).toHaveAttribute('aria-expanded', 'false')
expect(screen.queryByText('Small conversational requests are running on an expensive model.')).not.toBeInTheDocument()
expect(screen.getByText('~/.codeburn/config.json')).toBeInTheDocument()
expect(screen.getByText('{"batch":true}')).toBeInTheDocument()
})
it('switches to Reverts and Abandoned and shows only the matching yield details', async () => {
render(<Optimize period="30days" provider="all" />)
await screen.findByText('Opus is doing your small talk')
fireEvent.click(screen.getByRole('button', { name: 'Reverts $107.00' }))
expect(screen.getByText('codeburn')).toBeInTheDocument()
expect(screen.getByText('2 commits · rev-1')).toBeInTheDocument()
expect(screen.getByText('$55.00')).toBeInTheDocument()
expect(screen.getByText('agentseal-dash')).toBeInTheDocument()
expect(screen.queryByText('sandbox-spike')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Abandoned $65.40' }))
expect(screen.getByText('sandbox-spike')).toBeInTheDocument()
expect(screen.getByText('0 commits · abn-1')).toBeInTheDocument()
expect(screen.getByText('$65.40')).toHaveClass('val')
expect(screen.getByText('$65.40')).not.toHaveClass('ok')
expect(screen.queryByText('codeburn')).not.toBeInTheDocument()
expect(screen.queryByText('desktop-app')).not.toBeInTheDocument()
})
it('renders an honest placeholder for unavailable yield totals and list bodies', async () => {
getOverview.mockResolvedValue(makePayload())
getYield.mockRejectedValue(new Error('yield failed'))
it('renders honest placeholders for unavailable yield totals and tab bodies', async () => {
getYield.mockReset().mockRejectedValue(new Error('yield failed'))
render(<Optimize period="30days" provider="all" />)
expect(await screen.findByText('Reverted sessions · —')).toBeInTheDocument()
expect(screen.getByText('Abandoned sessions · —')).toBeInTheDocument()
expect(screen.getAllByText('—')).toHaveLength(2)
expect(await screen.findByRole('button', { name: 'Reverts —' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Abandoned —' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Reverts —' }))
expect(screen.getByText('—')).toBeInTheDocument()
})
it('shows one compact empty note and hides all empty sections', async () => {
it('keeps the Fixes tab populated and preserves all four empty tab states', async () => {
const { rerender } = render(<Optimize period="30days" provider="all" />)
await screen.findByText('Opus is doing your small talk')
fireEvent.click(screen.getByRole('button', { name: 'Fixes 3' }))
expect(screen.getByText('Opus is doing your small talk')).toBeInTheDocument()
getOverview.mockResolvedValue(emptyPayload())
getOptimizeReport.mockResolvedValue(emptyOptimizeReport())
getYield.mockResolvedValue(emptyYield())
rerender(<Optimize period="week" provider="all" />)
render(<Optimize period="30days" provider="all" />)
expect(await screen.findByText('No waste findings in this range yet.')).toBeInTheDocument()
expect(screen.queryByText(/Optimization findings ·/)).not.toBeInTheDocument()
expect(screen.queryByText(/Reverted sessions ·/)).not.toBeInTheDocument()
expect(screen.queryByText(/Abandoned sessions ·/)).not.toBeInTheDocument()
expect(await screen.findByText('No fixes in this range yet.')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Fixes 0' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Waste $0.00' }))
expect(screen.getByText('No waste findings in this range yet.')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Reverts $0.00' }))
expect(screen.getByText('No reverted sessions in this range yet.')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Abandoned $0.00' }))
expect(screen.getByText('No abandoned sessions in this range yet.')).toBeInTheDocument()
})
it('hides only yield sections whose category has no rows', async () => {
const report = makeYield()
report.summary.abandoned = { costUSD: 0, sessions: 0, costPercent: 0, sessionPercent: 0 }
report.details = report.details.filter(row => row.category !== 'abandoned')
getOverview.mockResolvedValue(makePayload())
getYield.mockResolvedValue(report)
render(<Optimize period="30days" provider="all" />)
expect(await screen.findByText('Reverted sessions · $107.00')).toBeInTheDocument()
expect(screen.queryByText(/Abandoned sessions ·/)).not.toBeInTheDocument()
expect(screen.getByText('Optimization findings · 3 findings · $94.40 potential')).toBeInTheDocument()
it('passes provider and custom range to the optimize report bridge', async () => {
render(<Optimize period="30days" provider="claude" range={{ from: '2026-07-01', to: '2026-07-11' }} />)
await screen.findByText('Opus is doing your small talk')
expect(getOptimizeReport).toHaveBeenCalledWith('30days', 'claude', { from: '2026-07-01', to: '2026-07-11' })
})
it('keeps last-good yield totals and rows visible during revalidation', async () => {
getYield.mockResolvedValueOnce(makeYield()).mockImplementation(() => new Promise<YieldJsonReport>(() => {}))
const overview = {
data: makePayload(),
error: null,
loading: false,
lastSuccessAt: Date.now(),
refresh: vi.fn(),
}
getYield.mockReset().mockResolvedValueOnce(makeYield()).mockImplementation(() => new Promise<YieldJsonReport>(() => {}))
const overview = { data: makePayload(), error: null, loading: false, lastSuccessAt: Date.now(), refresh: vi.fn() }
const { rerender } = render(<OptimizeContent period="30days" overview={overview} refreshToken={0} />)
expect(await screen.findByText('Reverted sessions · $107.00')).toBeInTheDocument()
expect(await screen.findByRole('button', { name: 'Reverts $107.00' })).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Reverts $107.00' }))
expect(screen.getByText('codeburn')).toBeInTheDocument()
rerender(<OptimizeContent period="30days" overview={overview} refreshToken={1} />)
await waitFor(() => expect(getYield).toHaveBeenCalledTimes(2))
expect(screen.getByText('Reverted sessions · $107.00')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Reverts $107.00' })).toBeInTheDocument()
expect(screen.getByText('codeburn')).toBeInTheDocument()
expect(screen.queryByText('—')).not.toBeInTheDocument()
})
})

View file

@ -1,9 +1,14 @@
import { Fragment, useState } from 'react'
import { CliErrorPanel } from '../components/CliErrorPanel'
import { Panel } from '../components/Panel'
import { SegTabs } from '../components/SegTabs'
import { type Polled, usePolled } from '../hooks/usePolled'
import { formatUsd } from '../lib/format'
import { formatCompact, formatUsd } from '../lib/format'
import { codeburn } from '../lib/ipc'
import type { DateRange, MenubarPayload, Period, SessionYieldJson, YieldJsonReport } from '../lib/types'
import type { DateRange, MenubarPayload, OptimizeJsonReport, Period, SessionYieldJson, WasteAction, YieldJsonReport } from '../lib/types'
type OptimizeTab = 'waste' | 'reverts' | 'abandoned' | 'fixes'
function EmptyNote({ children }: { children: React.ReactNode }) {
return <p style={{ color: 'var(--t3)', margin: 0, fontSize: 12 }}>{children}</p>
@ -14,24 +19,31 @@ export function Optimize({ period, provider, range = null }: { period: Period; p
() => range ? codeburn.getOverview(period, provider, range) : codeburn.getOverview(period, provider),
[period, provider, range?.from, range?.to],
)
return <OptimizeContent period={period} range={range} overview={overview} />
return <OptimizeContent period={period} provider={provider} range={range} overview={overview} />
}
export function OptimizeContent({
period,
provider = 'all',
range = null,
overview,
refreshToken = 0,
}: {
period: Period
provider?: string
range?: DateRange | null
overview: Polled<MenubarPayload>
refreshToken?: number
}) {
const optimizeReport = usePolled<OptimizeJsonReport>(
() => range ? codeburn.getOptimizeReport(period, provider, range) : codeburn.getOptimizeReport(period, provider),
[period, provider, range?.from, range?.to, refreshToken],
)
const yieldReport = usePolled<YieldJsonReport>(
() => range ? codeburn.getYield(period, range) : codeburn.getYield(period),
[period, range?.from, range?.to, refreshToken],
)
const [tab, setTab] = useState<OptimizeTab>('waste')
if (!overview.data) {
if (overview.error) return <CliErrorPanel error={overview.error} subject="optimize findings" />
@ -45,52 +57,130 @@ export function OptimizeContent({
const yieldData = yieldReport.error ? null : yieldReport.data
const revertedTotal = yieldData ? formatUsd(yieldData.summary.reverted.costUSD) : '—'
const abandonedTotal = yieldData ? formatUsd(yieldData.summary.abandoned.costUSD) : '—'
const revertedRows = !yieldData || yieldData.details.some(row => row.category === 'reverted')
const abandonedRows = !yieldData || yieldData.details.some(row => row.category === 'abandoned')
const hasFindings = overview.data.optimize.topFindings.length > 0
const isEmpty = !hasFindings && !revertedRows && !abandonedRows
const options = [
{ value: 'waste', label: `Waste ${formatUsd(overview.data.optimize.savingsUSD)}` },
{ value: 'reverts', label: `Reverts ${revertedTotal}` },
{ value: 'abandoned', label: `Abandoned ${abandonedTotal}` },
{ value: 'fixes', label: `Fixes ${overview.data.optimize.findingCount.toLocaleString('en-US')}` },
]
return (
<>
{hasFindings && (
<Panel title={`Optimization findings · ${overview.data.optimize.findingCount.toLocaleString('en-US')} findings · ${formatUsd(overview.data.optimize.savingsUSD)} potential`}>
<WasteRows data={overview.data} />
</Panel>
)}
{revertedRows && (
<Panel title={`Reverted sessions · ${revertedTotal}`}>
<SegTabs
options={options}
value={tab}
onChange={value => setTab(value as OptimizeTab)}
style={{ alignSelf: 'flex-start' }}
/>
<Panel>
{tab === 'waste' ? (
<WasteRows report={optimizeReport} />
) : tab === 'reverts' ? (
<YieldRows report={yieldReport} category="reverted" empty="No reverted sessions in this range yet." />
</Panel>
)}
{abandonedRows && (
<Panel title={`Abandoned sessions · ${abandonedTotal}`}>
) : tab === 'abandoned' ? (
<YieldRows report={yieldReport} category="abandoned" empty="No abandoned sessions in this range yet." />
</Panel>
)}
{isEmpty && <EmptyNote>No waste findings in this range yet.</EmptyNote>}
) : (
<FixesRows data={overview.data} />
)}
</Panel>
</>
)
}
function WasteRows({ data }: { data: MenubarPayload }) {
return <FindingRows findings={data.optimize.topFindings} empty="No waste findings in this range yet." />
function WasteRows({ report }: { report: Polled<OptimizeJsonReport> }) {
if (!report.data) {
if (report.error) return <CliErrorPanel error={report.error} subject="optimize findings" />
return <EmptyNote>Scanning optimize findings</EmptyNote>
}
return (
<div className="opt-waste">
<div className="opt-summary">
{report.data.summary.findingCount.toLocaleString('en-US')} findings · {formatUsd(report.data.summary.potentialSavingsCostUSD)} potential · health {report.data.summary.healthScore}/100
</div>
<ActionableFindingRows findings={report.data.findings} />
</div>
)
}
type Finding = MenubarPayload['optimize']['topFindings'][number]
type OptimizeFinding = OptimizeJsonReport['findings'][number]
const IMPACT_ICON: Record<Finding['impact'], string> = {
const IMPACT_ICON: Record<'high' | 'medium' | 'low', string> = {
high: '↑',
medium: '→',
low: '↓',
}
function actionText(fix: WasteAction): string {
return fix.type === 'file-content' ? fix.content : fix.text
}
function ActionableFindingRows({ findings }: { findings: OptimizeFinding[] }) {
const [expandedId, setExpandedId] = useState<string | null>(null)
const [copiedId, setCopiedId] = useState<string | null>(null)
if (!findings.length) return <EmptyNote>No waste findings in this range yet.</EmptyNote>
const copyFix = async (finding: OptimizeFinding) => {
await navigator.clipboard.writeText(actionText(finding.fix))
setCopiedId(finding.id)
window.setTimeout(() => setCopiedId(current => current === finding.id ? null : current), 1_500)
}
return (
<div className="opt-findings">
{findings.map(finding => {
const expanded = expandedId === finding.id
return (
<Fragment key={finding.id}>
<button
className="opt-finding opt-finding-toggle"
type="button"
aria-expanded={expanded}
onClick={() => setExpandedId(current => current === finding.id ? null : finding.id)}
>
<span className={`opt-impact opt-impact-${finding.severity}`}>
<span aria-hidden="true">{IMPACT_ICON[finding.severity]}</span>
{finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1)}
</span>
<b className="opt-finding-title">{finding.title}</b>
<span className="opt-finding-savings">{formatUsd(finding.estimatedSavingsUSD)}</span>
<span className="opt-finding-tokens">{formatCompact(finding.tokensSaved)} tokens</span>
<span className="opt-finding-chevron" aria-hidden="true"></span>
</button>
{expanded && (
<div className="opt-finding-detail" role="region" aria-label={`${finding.title} details`}>
<p className="opt-explanation">{finding.explanation}</p>
<div className={`opt-fix opt-fix-${finding.fix.type}`}>
<div className="opt-fix-head">
<div>
<b>{finding.fix.label}</b>
{finding.fix.type === 'file-content' && <span className="opt-fix-path">{finding.fix.path}</span>}
</div>
<button className="opt-copy" type="button" onClick={() => void copyFix(finding)}>
{copiedId === finding.id ? 'Copied' : 'Copy'}
</button>
</div>
<pre className="opt-fix-code"><code>{actionText(finding.fix)}</code></pre>
</div>
</div>
)}
</Fragment>
)
})}
</div>
)
}
type Finding = MenubarPayload['optimize']['topFindings'][number]
function FindingRows({ findings, empty }: { findings: Finding[]; empty: string }) {
if (!findings.length) return <EmptyNote>{empty}</EmptyNote>
return (
<div className="opt-findings">
{findings.map((finding, i) => (
<div className="opt-finding" key={`${finding.title}-${i}`}>
<div className="opt-finding opt-finding-legacy" key={`${finding.title}-${i}`}>
<span className="opt-finding-rank">{String(i + 1).padStart(2, '0')}</span>
<b className="opt-finding-title">{finding.title}</b>
<span className={`opt-impact opt-impact-${finding.impact}`}>
@ -135,3 +225,7 @@ function YieldRows({
</>
)
}
function FixesRows({ data }: { data: MenubarPayload }) {
return <FindingRows findings={data.optimize.topFindings} empty="No fixes in this range yet." />
}

View file

@ -504,9 +504,15 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); }
.ov-models th:first-child, .ov-models td:first-child { width: 100%; text-align: left; }
.ov-models .ov-model-name { overflow: hidden; color: var(--ink); font-weight: 550; text-overflow: ellipsis; }
.ov-models td.mono { font-family: var(--mono); color: var(--ink); }
.opt-findings { display: grid; }
.opt-finding { display: grid; grid-template-columns: 28px minmax(0, 1fr) 104px 86px; align-items: center; column-gap: 12px; min-height: 43px; border-top: 1px solid var(--line2); }
.opt-waste { min-width: 0; }
.opt-summary { padding: 0 0 10px; color: var(--mut); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.opt-findings { display: grid; min-width: 0; }
.opt-finding { display: grid; align-items: center; column-gap: 12px; min-height: 43px; border-top: 1px solid var(--line2); }
.opt-finding:first-child { border-top: 0; }
.opt-finding-legacy { grid-template-columns: 28px minmax(0, 1fr) 104px 86px; }
.opt-finding-toggle { width: 100%; grid-template-columns: 104px minmax(0, 1fr) 86px 92px 18px; padding: 0; border-right: 0; border-bottom: 0; border-left: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.opt-finding-toggle:hover, .opt-finding-toggle:focus-visible { background: var(--hover); outline: none; }
.opt-finding-toggle:focus-visible { box-shadow: inset 0 0 0 1px var(--accent); }
.opt-finding-rank { color: var(--mut2); font-family: var(--mono); font-size: 10px; font-variant-numeric: tabular-nums; }
.opt-finding-title { overflow: hidden; color: var(--ink); font-size: 12px; font-weight: 570; text-overflow: ellipsis; white-space: nowrap; }
.opt-impact { --impact: var(--mut2); display: inline-flex; width: fit-content; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 7px; background: color-mix(in srgb, var(--impact) 12%, transparent); color: var(--impact); font-size: 10px; font-weight: 650; line-height: 1; }
@ -514,6 +520,19 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); }
.opt-impact-medium { --impact: var(--warn); }
.opt-impact-low { --impact: var(--mut2); }
.opt-finding-savings { color: var(--ok); font-family: var(--mono); font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; }
.opt-finding-tokens { color: var(--mut); font-family: var(--mono); font-size: 10.5px; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.opt-finding-chevron { color: var(--mut2); font-family: system-ui, sans-serif; font-size: 17px; line-height: 1; transition: transform 140ms ease; }
.opt-finding-toggle[aria-expanded="true"] .opt-finding-chevron { transform: rotate(90deg); }
.opt-finding-detail { min-width: 0; padding: 12px; border-top: 1px solid var(--line2); background: color-mix(in srgb, var(--panel) 84%, var(--hover)); }
.opt-explanation { margin: 0 0 12px; color: var(--mut); font-size: 12px; line-height: 1.55; }
.opt-fix { min-width: 0; }
.opt-fix-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 7px; }
.opt-fix-head b { display: block; color: var(--ink); font-size: 10.5px; font-weight: 620; }
.opt-fix-path { display: block; margin-top: 3px; color: var(--mut2); font-family: var(--mono); font-size: 10px; }
.opt-fix-code { max-width: 100%; overflow-x: auto; margin: 0; padding: 10px 11px; border: 1px solid var(--line); border-radius: 6px; background: var(--phead); color: var(--ink); font-family: var(--mono); font-size: 11px; line-height: 1.5; white-space: pre; }
.opt-fix-command .opt-fix-code code::before { content: '$ '; color: var(--mut2); user-select: none; }
.opt-copy { flex: 0 0 auto; padding: 4px 9px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--mut); font: inherit; font-size: 10.5px; cursor: pointer; }
.opt-copy:hover, .opt-copy:focus-visible { border-color: var(--accent); color: var(--ink); outline: none; }
.ov-analytics-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: stretch; }
.ov-analytics-row > :only-child { grid-column: 1 / -1; }
.ov-body-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 12px; }