feat(report): add session report tab and all section components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Holstein 2026-02-21 16:53:01 -05:00
parent 0e3e20c990
commit d54e36f6fa
12 changed files with 972 additions and 0 deletions

View file

@ -0,0 +1,37 @@
import { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
interface ReportSectionProps {
title: string;
icon: React.ComponentType<{ className?: string }>;
children: React.ReactNode;
defaultCollapsed?: boolean;
}
export const ReportSection = ({
title,
icon: Icon,
children,
defaultCollapsed = false,
}: ReportSectionProps) => {
const [collapsed, setCollapsed] = useState(defaultCollapsed);
return (
<div className="rounded-lg border border-border bg-surface-raised">
<button
onClick={() => setCollapsed(!collapsed)}
className="flex w-full items-center gap-2 p-4 text-left"
>
{collapsed ? (
<ChevronRight className="size-4 text-text-muted" />
) : (
<ChevronDown className="size-4 text-text-muted" />
)}
<Icon className="size-4 text-text-secondary" />
<span className="text-sm font-semibold text-text">{title}</span>
</button>
{!collapsed && <div className="border-t border-border px-4 pb-4 pt-3">{children}</div>}
</div>
);
};

View file

@ -0,0 +1,69 @@
import { useMemo } from 'react';
import { useStore } from '@renderer/store';
import { analyzeSession } from '@renderer/utils/sessionAnalyzer';
import { CostSection } from './sections/CostSection';
import { ErrorSection } from './sections/ErrorSection';
import { FrictionSection } from './sections/FrictionSection';
import { GitSection } from './sections/GitSection';
import { OverviewSection } from './sections/OverviewSection';
import { QualitySection } from './sections/QualitySection';
import { SubagentSection } from './sections/SubagentSection';
import { TimelineSection } from './sections/TimelineSection';
import { TokenSection } from './sections/TokenSection';
import { ToolSection } from './sections/ToolSection';
import type { Tab } from '@renderer/types/tabs';
interface SessionReportTabProps {
tab: Tab;
}
export const SessionReportTab = ({ tab }: SessionReportTabProps) => {
// Find session data from any session tab with matching sessionId
const sessionDetail = useStore((s) => {
const allTabs = s.paneLayout.panes.flatMap((p) => p.tabs);
const sourceTab = allTabs.find((t) => t.type === 'session' && t.sessionId === tab.sessionId);
return sourceTab ? s.tabSessionData[sourceTab.id]?.sessionDetail : null;
});
const report = useMemo(
() => (sessionDetail ? analyzeSession(sessionDetail) : null),
[sessionDetail]
);
if (!report) {
return (
<div className="flex h-full items-center justify-center text-text-muted">
No session data available. Open the session tab first.
</div>
);
}
return (
<div className="h-full overflow-y-auto p-6" style={{ backgroundColor: 'var(--color-surface)' }}>
<h1 className="mb-6 text-lg font-semibold text-text">Session Analysis Report</h1>
<div className="flex flex-col gap-4">
<OverviewSection data={report.overview} />
<CostSection data={report.costAnalysis} />
<TokenSection data={report.tokenUsage} cacheEconomics={report.cacheEconomics} />
<ToolSection data={report.toolUsage} />
{report.subagentMetrics.count > 0 && <SubagentSection data={report.subagentMetrics} />}
{report.errors.errors.length > 0 && <ErrorSection data={report.errors} />}
<GitSection data={report.gitActivity} />
<FrictionSection data={report.frictionSignals} thrashing={report.thrashingSignals} />
<TimelineSection
idle={report.idleAnalysis}
modelSwitches={report.modelSwitches}
keyEvents={report.keyEvents}
/>
<QualitySection
prompt={report.promptQuality}
startup={report.startupOverhead}
testProgression={report.testProgression}
/>
</div>
</div>
);
};

View file

@ -0,0 +1,63 @@
import { DollarSign } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportCostAnalysis } from '@renderer/types/sessionReport';
const fmt = (v: number) => `$${v.toFixed(4)}`;
interface CostSectionProps {
data: ReportCostAnalysis;
}
export const CostSection = ({ data }: CostSectionProps) => {
const modelEntries = Object.entries(data.costByModel).sort((a, b) => b[1] - a[1]);
return (
<ReportSection title="Cost Analysis" icon={DollarSign}>
<div className="mb-4 text-2xl font-bold text-text">{fmt(data.totalSessionCostUsd)}</div>
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Parent Cost</div>
<div className="text-sm font-medium text-text">{fmt(data.parentCostUsd)}</div>
</div>
<div>
<div className="text-xs text-text-muted">Subagent Cost</div>
<div className="text-sm font-medium text-text">{fmt(data.subagentCostUsd)}</div>
</div>
<div>
<div className="text-xs text-text-muted">Per Commit</div>
<div className="text-sm font-medium text-text">
{data.costPerCommit != null ? fmt(data.costPerCommit) : 'N/A'}
</div>
</div>
<div>
<div className="text-xs text-text-muted">Per Line Changed</div>
<div className="text-sm font-medium text-text">
{data.costPerLineChanged != null ? `$${data.costPerLineChanged.toFixed(6)}` : 'N/A'}
</div>
</div>
</div>
{modelEntries.length > 0 && (
<table className="w-full text-xs">
<thead>
<tr className="border-b border-border text-left text-text-muted">
<th className="pb-2 pr-4">Model</th>
<th className="pb-2 pr-4 text-right">Cost</th>
</tr>
</thead>
<tbody>
{modelEntries.map(([model, cost]) => (
<tr key={model} className="border-border/50 border-b">
<td className="py-1.5 pr-4 text-text">{model}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(cost)}</td>
</tr>
))}
</tbody>
</table>
)}
</ReportSection>
);
};

View file

@ -0,0 +1,76 @@
import { useState } from 'react';
import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportErrors, ToolError } from '@renderer/types/sessionReport';
interface ErrorItemProps {
error: ToolError;
}
const ErrorItem = ({ error }: ErrorItemProps) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="border-border/50 rounded border bg-surface p-2">
<button
onClick={() => setExpanded(!expanded)}
className="flex w-full items-center gap-2 text-left text-xs"
>
{expanded ? (
<ChevronDown className="size-3 text-text-muted" />
) : (
<ChevronRight className="size-3 text-text-muted" />
)}
<span className="font-medium text-text">{error.tool}</span>
{error.isPermissionDenial && (
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium"
style={{ backgroundColor: 'rgba(248, 113, 113, 0.15)', color: '#f87171' }}
>
Permission Denied
</span>
)}
<span className="ml-auto text-text-muted">msg #{error.messageIndex}</span>
</button>
{expanded && (
<div className="mt-2 whitespace-pre-wrap break-words rounded bg-surface-raised p-2 text-xs text-text-secondary">
{error.error}
</div>
)}
</div>
);
};
interface ErrorSectionProps {
data: ReportErrors;
}
export const ErrorSection = ({ data }: ErrorSectionProps) => {
return (
<ReportSection title="Errors" icon={AlertTriangle}>
<div className="mb-3 flex items-center gap-3">
<span
className="rounded px-2 py-0.5 text-xs font-medium"
style={{ backgroundColor: 'rgba(248, 113, 113, 0.15)', color: '#f87171' }}
>
{data.errors.length} error{data.errors.length !== 1 ? 's' : ''}
</span>
{data.permissionDenials.count > 0 && (
<span className="text-xs text-text-muted">
{data.permissionDenials.count} permission denial
{data.permissionDenials.count !== 1 ? 's' : ''}
</span>
)}
</div>
<div className="flex flex-col gap-2">
{data.errors.map((error, idx) => (
<ErrorItem key={idx} error={error} />
))}
</div>
</ReportSection>
);
};

View file

@ -0,0 +1,86 @@
import { MessageSquareWarning } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportFrictionSignals, ReportThrashingSignals } from '@renderer/types/sessionReport';
const frictionColor = (rate: number): string => {
if (rate <= 0.1) return '#4ade80';
if (rate <= 0.25) return '#fbbf24';
return '#f87171';
};
interface FrictionSectionProps {
data: ReportFrictionSignals;
thrashing: ReportThrashingSignals;
}
export const FrictionSection = ({ data, thrashing }: FrictionSectionProps) => {
return (
<ReportSection title="Friction Signals" icon={MessageSquareWarning}>
<div className="mb-4 flex items-center gap-3">
<span
className="rounded px-2 py-0.5 text-xs font-medium"
style={{
backgroundColor: `${frictionColor(data.frictionRate)}20`,
color: frictionColor(data.frictionRate),
}}
>
Friction Rate: {(data.frictionRate * 100).toFixed(1)}%
</span>
<span className="text-xs text-text-muted">
{data.correctionCount} correction{data.correctionCount !== 1 ? 's' : ''}
</span>
</div>
{data.corrections.length > 0 && (
<div className="mb-4">
<div className="mb-2 text-xs font-medium text-text-muted">Corrections</div>
<div className="flex flex-col gap-1">
{data.corrections.map((corr, idx) => (
<div key={idx} className="flex items-start gap-2 rounded px-2 py-1 text-xs">
<span
className="shrink-0 rounded px-1.5 py-0.5 font-mono text-[10px]"
style={{ backgroundColor: 'rgba(251, 191, 36, 0.15)', color: '#fbbf24' }}
>
{corr.keyword}
</span>
<span className="truncate text-text-secondary">{corr.preview}</span>
</div>
))}
</div>
</div>
)}
{(thrashing.bashNearDuplicates.length > 0 || thrashing.editReworkFiles.length > 0) && (
<div>
<div className="mb-2 text-xs font-medium text-text-muted">Thrashing Signals</div>
{thrashing.bashNearDuplicates.length > 0 && (
<div className="mb-2">
<div className="mb-1 text-xs text-text-muted">Repeated Bash Commands</div>
{thrashing.bashNearDuplicates.map((dup, idx) => (
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
<span className="text-text-muted">{dup.count}x</span>
<code className="truncate text-text-secondary">{dup.prefix}</code>
</div>
))}
</div>
)}
{thrashing.editReworkFiles.length > 0 && (
<div>
<div className="mb-1 text-xs text-text-muted">Reworked Files (3+ edits)</div>
{thrashing.editReworkFiles.map((file, idx) => (
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
<span className="text-text-muted">{file.editIndices.length}x</span>
<span className="truncate text-text-secondary">{file.filePath}</span>
</div>
))}
</div>
)}
</div>
)}
</ReportSection>
);
};

View file

@ -0,0 +1,71 @@
import { GitBranch } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportGitActivity } from '@renderer/types/sessionReport';
interface GitSectionProps {
data: ReportGitActivity;
}
export const GitSection = ({ data }: GitSectionProps) => {
return (
<ReportSection title="Git Activity" icon={GitBranch}>
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Commits</div>
<div className="text-sm font-medium text-text">{data.commitCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Pushes</div>
<div className="text-sm font-medium text-text">{data.pushCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Lines Added</div>
<div className="text-sm font-medium" style={{ color: '#4ade80' }}>
+{data.linesAdded.toLocaleString()}
</div>
</div>
<div>
<div className="text-xs text-text-muted">Lines Removed</div>
<div className="text-sm font-medium" style={{ color: '#f87171' }}>
-{data.linesRemoved.toLocaleString()}
</div>
</div>
</div>
{data.commits.length > 0 && (
<div>
<div className="mb-2 text-xs font-medium text-text-muted">Commits</div>
<div className="flex flex-col gap-1">
{data.commits.map((commit, idx) => (
<div
key={idx}
className="flex items-center gap-2 rounded px-2 py-1 text-xs text-text"
>
<span className="text-text-muted">#{commit.messageIndex}</span>
<span className="truncate">{commit.messagePreview}</span>
</div>
))}
</div>
</div>
)}
{data.branchCreations.length > 0 && (
<div className="mt-3">
<div className="mb-1 text-xs font-medium text-text-muted">Branches Created</div>
<div className="flex flex-wrap gap-1">
{data.branchCreations.map((branch, idx) => (
<span
key={idx}
className="rounded bg-surface px-2 py-0.5 text-xs text-text-secondary"
>
{branch}
</span>
))}
</div>
</div>
)}
</ReportSection>
);
};

View file

@ -0,0 +1,78 @@
import { Activity } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportOverview } from '@renderer/types/sessionReport';
const assessmentColor = (assessment: ReportOverview['contextAssessment']): string => {
switch (assessment) {
case 'healthy':
return '#4ade80';
case 'moderate':
return '#fbbf24';
case 'high':
return '#f87171';
case 'critical':
return '#f87171';
default:
return '#a1a1aa';
}
};
interface OverviewSectionProps {
data: ReportOverview;
}
export const OverviewSection = ({ data }: OverviewSectionProps) => {
return (
<ReportSection title="Overview" icon={Activity}>
<div className="mb-3 truncate text-xs text-text-muted">{data.firstMessage}</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Duration</div>
<div className="text-sm font-medium text-text">{data.durationHuman}</div>
</div>
<div>
<div className="text-xs text-text-muted">Messages</div>
<div className="text-sm font-medium text-text">{data.totalMessages.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-text-muted">Context Usage</div>
<div
className="text-sm font-medium"
style={{ color: assessmentColor(data.contextAssessment) }}
>
{data.contextConsumptionPct != null ? `${data.contextConsumptionPct}%` : 'N/A'}
{data.contextAssessment && (
<span className="ml-1 text-xs">({data.contextAssessment})</span>
)}
</div>
</div>
<div>
<div className="text-xs text-text-muted">Compactions</div>
<div className="text-sm font-medium text-text">{data.compactionCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Branch</div>
<div className="truncate text-sm font-medium text-text">{data.gitBranch}</div>
</div>
<div>
<div className="text-xs text-text-muted">Subagents</div>
<div className="text-sm font-medium text-text">{data.hasSubagents ? 'Yes' : 'No'}</div>
</div>
<div>
<div className="text-xs text-text-muted">Project</div>
<div className="truncate text-sm font-medium text-text" title={data.projectPath}>
{data.projectPath}
</div>
</div>
<div>
<div className="text-xs text-text-muted">Session ID</div>
<div className="truncate text-sm font-medium text-text" title={data.sessionId}>
{data.sessionId.slice(0, 12)}...
</div>
</div>
</div>
</ReportSection>
);
};

View file

@ -0,0 +1,174 @@
import { BarChart3 } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type {
ReportPromptQuality,
ReportStartupOverhead,
ReportTestProgression,
} from '@renderer/types/sessionReport';
const assessmentColor = (assessment: ReportPromptQuality['assessment']): string => {
switch (assessment) {
case 'well_specified':
return '#4ade80';
case 'moderate_friction':
return '#fbbf24';
case 'underspecified':
return '#f87171';
case 'verbose_but_unclear':
return '#f87171';
default:
return '#a1a1aa';
}
};
const assessmentLabel = (assessment: ReportPromptQuality['assessment']): string => {
switch (assessment) {
case 'well_specified':
return 'Well Specified';
case 'moderate_friction':
return 'Moderate Friction';
case 'underspecified':
return 'Underspecified';
case 'verbose_but_unclear':
return 'Verbose but Unclear';
default:
return assessment;
}
};
const trajectoryColor = (trajectory: ReportTestProgression['trajectory']): string => {
switch (trajectory) {
case 'improving':
return '#4ade80';
case 'regressing':
return '#f87171';
case 'stable':
return '#fbbf24';
default:
return '#a1a1aa';
}
};
interface QualitySectionProps {
prompt: ReportPromptQuality;
startup: ReportStartupOverhead;
testProgression: ReportTestProgression;
}
export const QualitySection = ({ prompt, startup, testProgression }: QualitySectionProps) => {
return (
<ReportSection title="Quality Signals" icon={BarChart3}>
{/* Prompt quality */}
<div className="mb-4">
<div className="mb-2 text-xs font-medium text-text-muted">Prompt Quality</div>
<div className="mb-2 flex items-center gap-2">
<span
className="rounded px-2 py-0.5 text-xs font-medium"
style={{
backgroundColor: `${assessmentColor(prompt.assessment)}20`,
color: assessmentColor(prompt.assessment),
}}
>
{assessmentLabel(prompt.assessment)}
</span>
</div>
<div className="text-xs text-text-secondary">{prompt.note}</div>
<div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">First Message</div>
<div className="text-sm font-medium text-text">
{prompt.firstMessageLengthChars.toLocaleString()} chars
</div>
</div>
<div>
<div className="text-xs text-text-muted">User Messages</div>
<div className="text-sm font-medium text-text">{prompt.userMessageCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Corrections</div>
<div className="text-sm font-medium text-text">{prompt.correctionCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Friction Rate</div>
<div className="text-sm font-medium text-text">
{(prompt.frictionRate * 100).toFixed(1)}%
</div>
</div>
</div>
</div>
{/* Startup overhead */}
<div className="mb-4">
<div className="mb-2 text-xs font-medium text-text-muted">Startup Overhead</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<div>
<div className="text-xs text-text-muted">Messages Before Work</div>
<div className="text-sm font-medium text-text">{startup.messagesBeforeFirstWork}</div>
</div>
<div>
<div className="text-xs text-text-muted">Tokens Before Work</div>
<div className="text-sm font-medium text-text">
{startup.tokensBeforeFirstWork.toLocaleString()}
</div>
</div>
<div>
<div className="text-xs text-text-muted">% of Total</div>
<div className="text-sm font-medium text-text">{startup.pctOfTotal}%</div>
</div>
</div>
</div>
{/* Test progression */}
<div>
<div className="mb-2 text-xs font-medium text-text-muted">Test Progression</div>
<div className="mb-2 flex items-center gap-2">
<span
className="rounded px-2 py-0.5 text-xs font-medium"
style={{
backgroundColor: `${trajectoryColor(testProgression.trajectory)}20`,
color: trajectoryColor(testProgression.trajectory),
}}
>
{testProgression.trajectory === 'insufficient_data'
? 'Insufficient Data'
: testProgression.trajectory.charAt(0).toUpperCase() +
testProgression.trajectory.slice(1)}
</span>
<span className="text-xs text-text-muted">
{testProgression.snapshotCount} snapshot{testProgression.snapshotCount !== 1 ? 's' : ''}
</span>
</div>
{testProgression.firstSnapshot && testProgression.lastSnapshot && (
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-xs text-text-muted">First Run</div>
<div className="text-sm text-text">
<span style={{ color: '#4ade80' }}>
{testProgression.firstSnapshot.passed} passed
</span>
{' / '}
<span style={{ color: '#f87171' }}>
{testProgression.firstSnapshot.failed} failed
</span>
</div>
</div>
<div>
<div className="text-xs text-text-muted">Last Run</div>
<div className="text-sm text-text">
<span style={{ color: '#4ade80' }}>
{testProgression.lastSnapshot.passed} passed
</span>
{' / '}
<span style={{ color: '#f87171' }}>
{testProgression.lastSnapshot.failed} failed
</span>
</div>
</div>
</div>
)}
</div>
</ReportSection>
);
};

View file

@ -0,0 +1,75 @@
import { Users } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportSubagentMetrics } from '@renderer/types/sessionReport';
const fmtCost = (v: number) => `$${v.toFixed(4)}`;
const fmtDuration = (ms: number) => {
const s = Math.round(ms / 1000);
const m = Math.floor(s / 60);
const sec = s % 60;
return m > 0 ? `${m}m ${sec}s` : `${sec}s`;
};
interface SubagentSectionProps {
data: ReportSubagentMetrics;
}
export const SubagentSection = ({ data }: SubagentSectionProps) => {
return (
<ReportSection title="Subagents" icon={Users}>
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Count</div>
<div className="text-sm font-medium text-text">{data.count}</div>
</div>
<div>
<div className="text-xs text-text-muted">Total Tokens</div>
<div className="text-sm font-medium text-text">{data.totalTokens.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-text-muted">Total Duration</div>
<div className="text-sm font-medium text-text">{fmtDuration(data.totalDurationMs)}</div>
</div>
<div>
<div className="text-xs text-text-muted">Total Cost</div>
<div className="text-sm font-medium text-text">{fmtCost(data.totalCostUsd)}</div>
</div>
</div>
{data.byAgent.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-border text-left text-text-muted">
<th className="pb-2 pr-4">Description</th>
<th className="pb-2 pr-4">Type</th>
<th className="pb-2 pr-4 text-right">Tokens</th>
<th className="pb-2 pr-4 text-right">Duration</th>
<th className="pb-2 text-right">Cost</th>
</tr>
</thead>
<tbody>
{data.byAgent.map((agent, idx) => (
<tr key={idx} className="border-border/50 border-b">
<td className="max-w-48 truncate py-1.5 pr-4 text-text" title={agent.description}>
{agent.description}
</td>
<td className="py-1.5 pr-4 text-text-secondary">{agent.subagentType}</td>
<td className="py-1.5 pr-4 text-right text-text">
{agent.totalTokens.toLocaleString()}
</td>
<td className="py-1.5 pr-4 text-right text-text">
{fmtDuration(agent.totalDurationMs)}
</td>
<td className="py-1.5 text-right text-text">{fmtCost(agent.costUsd)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</ReportSection>
);
};

View file

@ -0,0 +1,88 @@
import { Clock } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type {
KeyEvent,
ReportIdleAnalysis,
ReportModelSwitches,
} from '@renderer/types/sessionReport';
interface TimelineSectionProps {
idle: ReportIdleAnalysis;
modelSwitches: ReportModelSwitches;
keyEvents: KeyEvent[];
}
export const TimelineSection = ({ idle, modelSwitches, keyEvents }: TimelineSectionProps) => {
return (
<ReportSection title="Timeline & Activity" icon={Clock}>
{/* Idle stats */}
<div className="mb-4">
<div className="mb-2 text-xs font-medium text-text-muted">Idle Analysis</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Idle Gaps</div>
<div className="text-sm font-medium text-text">{idle.idleGapCount}</div>
</div>
<div>
<div className="text-xs text-text-muted">Total Idle</div>
<div className="text-sm font-medium text-text">{idle.totalIdleHuman}</div>
</div>
<div>
<div className="text-xs text-text-muted">Active Time</div>
<div className="text-sm font-medium text-text">{idle.activeWorkingHuman}</div>
</div>
<div>
<div className="text-xs text-text-muted">Idle %</div>
<div
className="text-sm font-medium"
style={{ color: idle.idlePct > 50 ? '#fbbf24' : '#4ade80' }}
>
{idle.idlePct}%
</div>
</div>
</div>
</div>
{/* Model switches */}
{modelSwitches.count > 0 && (
<div className="mb-4">
<div className="mb-2 text-xs font-medium text-text-muted">
Model Switches ({modelSwitches.count})
</div>
<div className="flex flex-col gap-1">
{modelSwitches.switches.map((sw, idx) => (
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
<span className="text-text-secondary">{sw.from}</span>
<span className="text-text-muted">&rarr;</span>
<span className="text-text">{sw.to}</span>
<span className="ml-auto text-text-muted">msg #{sw.messageIndex}</span>
</div>
))}
</div>
</div>
)}
{/* Key events */}
{keyEvents.length > 0 && (
<div>
<div className="mb-2 text-xs font-medium text-text-muted">Key Events</div>
<div className="flex flex-col gap-1">
{keyEvents.map((event, idx) => (
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
<span className="shrink-0 text-text-muted">
{event.timestamp.toLocaleTimeString()}
</span>
<span className="truncate text-text">{event.label}</span>
{event.deltaHuman && (
<span className="ml-auto shrink-0 text-text-muted">+{event.deltaHuman}</span>
)}
</div>
))}
</div>
</div>
)}
</ReportSection>
);
};

View file

@ -0,0 +1,92 @@
import { Coins } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportCacheEconomics, ReportTokenUsage } from '@renderer/types/sessionReport';
const fmt = (v: number) => v.toLocaleString();
const fmtCost = (v: number) => `$${v.toFixed(4)}`;
interface TokenSectionProps {
data: ReportTokenUsage;
cacheEconomics: ReportCacheEconomics;
}
export const TokenSection = ({ data, cacheEconomics }: TokenSectionProps) => {
const modelEntries = Object.entries(data.byModel).sort((a, b) => b[1].costUsd - a[1].costUsd);
return (
<ReportSection title="Token Usage" icon={Coins}>
{/* By-model table */}
<div className="mb-4 overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-border text-left text-text-muted">
<th className="pb-2 pr-4">Model</th>
<th className="pb-2 pr-4 text-right">API Calls</th>
<th className="pb-2 pr-4 text-right">Input</th>
<th className="pb-2 pr-4 text-right">Output</th>
<th className="pb-2 pr-4 text-right">Cache Read</th>
<th className="pb-2 pr-4 text-right">Cache Create</th>
<th className="pb-2 text-right">Cost</th>
</tr>
</thead>
<tbody>
{modelEntries.map(([model, stats]) => (
<tr key={model} className="border-border/50 border-b">
<td className="py-1.5 pr-4 text-text">{model}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(stats.apiCalls)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(stats.inputTokens)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(stats.outputTokens)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(stats.cacheRead)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(stats.cacheCreation)}</td>
<td className="py-1.5 text-right text-text">{fmtCost(stats.costUsd)}</td>
</tr>
))}
{/* Totals row */}
<tr className="border-t border-border font-medium">
<td className="py-1.5 pr-4 text-text">Total</td>
<td className="py-1.5 pr-4 text-right text-text">
{fmt(modelEntries.reduce((s, [, st]) => s + st.apiCalls, 0))}
</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(data.totals.inputTokens)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(data.totals.outputTokens)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(data.totals.cacheRead)}</td>
<td className="py-1.5 pr-4 text-right text-text">{fmt(data.totals.cacheCreation)}</td>
<td className="py-1.5 text-right text-text">
{fmtCost(modelEntries.reduce((s, [, st]) => s + st.costUsd, 0))}
</td>
</tr>
</tbody>
</table>
</div>
{/* Cache economics */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<div className="text-xs text-text-muted">Cache Efficiency</div>
<div className="text-sm font-medium text-text">{cacheEconomics.cacheEfficiencyPct}%</div>
</div>
<div>
<div className="text-xs text-text-muted">R/W Ratio</div>
<div className="text-sm font-medium text-text">
{cacheEconomics.cacheReadToWriteRatio}x
</div>
</div>
<div>
<div className="text-xs text-text-muted">Cache Read %</div>
<div className="text-sm font-medium text-text">{data.totals.cacheReadPct}%</div>
</div>
<div>
<div className="text-xs text-text-muted">Cold Start</div>
<div
className="text-sm font-medium"
style={{ color: cacheEconomics.coldStartDetected ? '#fbbf24' : '#4ade80' }}
>
{cacheEconomics.coldStartDetected ? 'Yes' : 'No'}
</div>
</div>
</div>
</ReportSection>
);
};

View file

@ -0,0 +1,63 @@
import { Wrench } from 'lucide-react';
import { ReportSection } from '../ReportSection';
import type { ReportToolUsage } from '@renderer/types/sessionReport';
interface ToolSectionProps {
data: ReportToolUsage;
}
export const ToolSection = ({ data }: ToolSectionProps) => {
const toolEntries = Object.entries(data.successRates).sort(
(a, b) => b[1].totalCalls - a[1].totalCalls
);
return (
<ReportSection title="Tool Usage" icon={Wrench}>
<div className="mb-2 text-xs text-text-muted">
{data.totalCalls.toLocaleString()} total calls across {toolEntries.length} tools
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-border text-left text-text-muted">
<th className="pb-2 pr-4">Tool</th>
<th className="pb-2 pr-4 text-right">Calls</th>
<th className="pb-2 pr-4 text-right">Errors</th>
<th className="pb-2 text-right">Success %</th>
</tr>
</thead>
<tbody>
{toolEntries.map(([tool, stats]) => {
const rateColor =
stats.successRatePct < 80
? '#f87171'
: stats.successRatePct < 90
? '#fbbf24'
: undefined;
return (
<tr key={tool} className="border-border/50 border-b">
<td className="py-1.5 pr-4 text-text">{tool}</td>
<td className="py-1.5 pr-4 text-right text-text">
{stats.totalCalls.toLocaleString()}
</td>
<td className="py-1.5 pr-4 text-right text-text">
{stats.errors.toLocaleString()}
</td>
<td
className="py-1.5 text-right"
style={rateColor ? { color: rateColor } : undefined}
>
{stats.successRatePct}%
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</ReportSection>
);
};