import type { KeyboardEvent, MouseEvent } from 'react' import { useEffect, useState } from 'react' import { CliErrorPanel } from '../components/CliErrorPanel' import { EmptyNote } from '../components/EmptyState' import { Panel } from '../components/Panel' import { SectionSkeleton } from '../components/Skeleton' import { StaleBanner } from '../components/StaleBanner' import { type Polled, usePolled } from '../hooks/usePolled' import { formatDayShort, formatUsd } from '../lib/format' import { codeburn } from '../lib/ipc' import type { CliError, DateRange, MenubarPayload, Period } from '../lib/types' type PullRequests = NonNullable type PrRow = PullRequests['rows'][number] // A PR's active window: one day collapses to a single label, otherwise the two // endpoints joined with a hyphen (never an en/em dash, per repo copy rules). function spanLabel(firstStarted: string, lastEnded: string): string { const start = formatDayShort(firstStarted) const end = formatDayShort(lastEnded) if (start === '—' && end === '—') return '—' return start === end ? start : `${start} - ${end}` } function sessionWord(n: number): string { return n === 1 ? 'session' : 'sessions' } function ModelChips({ models }: { models: string[] }) { return (
{models.map(model => {model})}
) } function openPr(event: MouseEvent, url: string): void { event.preventDefault() event.stopPropagation() void codeburn.openExternal(url) } // Keyboard activation for the button-role row, guarded so Enter/Space fired on // the inner link (its own control) never doubles up as a row toggle. function rowKeyDown(event: KeyboardEvent, toggle: () => void): void { if (event.target !== event.currentTarget) return if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() toggle() } } /** Standalone entry: self-fetches the overview payload (used in tests). The App * passes its shared overview poll straight into PullRequestsContent instead. */ export function PullRequests({ period, provider, range = null }: { period: Period; provider: string; range?: DateRange | null }) { const overview = usePolled( () => range ? codeburn.getOverview(period, provider, range) : codeburn.getOverview(period, provider), [period, provider, range?.from, range?.to], ) // The key remounts the content on a period/provider/range switch so row state // (an open expansion) never survives onto the same PR rendered from new data. return } export function PullRequestsContent({ overview }: { overview: Polled }) { if (!overview.data) { if (overview.error) return return } return } function PullRequestsPage({ pullRequests, staleError }: { pullRequests?: PullRequests; staleError: CliError | null }) { return ( <> {staleError && } {pullRequests && pullRequests.rows.length > 0 ? : PR links are captured as sessions are parsed. Once a session references a pull request, it appears here.} ) } function PrTable({ pullRequests }: { pullRequests: PullRequests }) { const { rows, distinctCost, distinctSessions, subagentSessions, attributedCost, unattributedCost } = pullRequests const [expandedUrl, setExpandedUrl] = useState(null) // Reset any open expansion when the PR set changes (a period/provider switch or // a refresh that alters the list): a stale expandedUrl would otherwise linger // pointing at a row that is no longer present. const rowKey = rows.map(row => row.url).join('|') useEffect(() => { setExpandedUrl(null) }, [rowKey]) // A new-attribution payload carries `attributedCost`; an older by-reference // payload omits it, so the rows are not summable and the footer must differ. const summable = attributedCost !== undefined const unattributed = unattributedCost ?? 0 // Reconcile to the visible numbers: every PR is present, so the summary is // exactly the sum of the rounded cards a person can inspect below. const displayedAttributed = rows.reduce((sum, row) => sum + Number(row.cost.toFixed(2)), 0) return ( <>
Attributed spend {formatUsd(summable ? displayedAttributed : distinctCost)}
Pull requests {rows.length.toLocaleString('en-US')}
Linked sessions {distinctSessions.toLocaleString('en-US')}
Folded agent runs {(subagentSessions ?? 0).toLocaleString('en-US')}
Attributed pull requests Sorted by spend, highest first
{rows.length.toLocaleString('en-US')} total
{rows.map(pr => ( setExpandedUrl(current => current === pr.url ? null : pr.url)} /> ))}
{summable ? (

Costs are attributed turn by turn, so every row adds up without double counting. {subagentSessions ? ` ${subagentSessions.toLocaleString('en-US')} subagent ${subagentSessions === 1 ? 'run is' : 'runs are'} included in the PR where the work happened.` : ''}

) : (

{formatUsd(distinctCost)} across {distinctSessions.toLocaleString('en-US')} distinct {sessionWord(distinctSessions)} produced pull requests. {' '}Attribution is by reference: a session referencing several PRs counts toward each, so the rows above are not summed.

)} {unattributed > 0 && (

Not tied to a specific PR: {formatUsd(unattributed)}

)} ) } const APPROX_TITLE = 'Approximate: the transcript expired before per-turn capture, so this PR’s share is an even split of the whole session.' function PrRowView({ pr, expanded, onToggle }: { pr: PrRow; expanded: boolean; onToggle: () => void }) { const models = pr.models ?? [] const categories = pr.categories ?? [] const catMax = categories.length ? Math.max(...categories.map(cat => cat.cost)) : 0 return (
rowKeyDown(event, onToggle)} >
openPr(event, pr.url)}>{pr.label}
{spanLabel(pr.firstStarted, pr.lastEnded)} {pr.sessions.toLocaleString('en-US')} {sessionWord(pr.sessions)} {pr.calls.toLocaleString('en-US')} calls
Models
Spend {pr.approx ? '~' : ''}{formatUsd(pr.cost)}
{expanded && (
{categories.length > 0 ? (
Work breakdown {formatUsd(pr.cost)} total
{categories.map(cat => (
{cat.name} {formatUsd(cat.cost)}
))}
) : (

No per-turn detail (estimated from a whole-session split).

)}
)}
) }