diff --git a/src/renderer/components/report/ReportSection.tsx b/src/renderer/components/report/ReportSection.tsx
new file mode 100644
index 00000000..a9bd20e0
--- /dev/null
+++ b/src/renderer/components/report/ReportSection.tsx
@@ -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 (
+
+
+ {!collapsed &&
{children}
}
+
+ );
+};
diff --git a/src/renderer/components/report/SessionReportTab.tsx b/src/renderer/components/report/SessionReportTab.tsx
new file mode 100644
index 00000000..58958caa
--- /dev/null
+++ b/src/renderer/components/report/SessionReportTab.tsx
@@ -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 (
+
+ No session data available. Open the session tab first.
+
+ );
+ }
+
+ return (
+
+
Session Analysis Report
+
+
+
+
+
+ {report.subagentMetrics.count > 0 && }
+ {report.errors.errors.length > 0 && }
+
+
+
+
+
+
+ );
+};
diff --git a/src/renderer/components/report/sections/CostSection.tsx b/src/renderer/components/report/sections/CostSection.tsx
new file mode 100644
index 00000000..b3400eab
--- /dev/null
+++ b/src/renderer/components/report/sections/CostSection.tsx
@@ -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 (
+
+ {fmt(data.totalSessionCostUsd)}
+
+
+
+
Parent Cost
+
{fmt(data.parentCostUsd)}
+
+
+
Subagent Cost
+
{fmt(data.subagentCostUsd)}
+
+
+
Per Commit
+
+ {data.costPerCommit != null ? fmt(data.costPerCommit) : 'N/A'}
+
+
+
+
Per Line Changed
+
+ {data.costPerLineChanged != null ? `$${data.costPerLineChanged.toFixed(6)}` : 'N/A'}
+
+
+
+
+ {modelEntries.length > 0 && (
+
+
+
+ | Model |
+ Cost |
+
+
+
+ {modelEntries.map(([model, cost]) => (
+
+ | {model} |
+ {fmt(cost)} |
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/src/renderer/components/report/sections/ErrorSection.tsx b/src/renderer/components/report/sections/ErrorSection.tsx
new file mode 100644
index 00000000..1cd04850
--- /dev/null
+++ b/src/renderer/components/report/sections/ErrorSection.tsx
@@ -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 (
+
+
+ {expanded && (
+
+ {error.error}
+
+ )}
+
+ );
+};
+
+interface ErrorSectionProps {
+ data: ReportErrors;
+}
+
+export const ErrorSection = ({ data }: ErrorSectionProps) => {
+ return (
+
+
+
+ {data.errors.length} error{data.errors.length !== 1 ? 's' : ''}
+
+ {data.permissionDenials.count > 0 && (
+
+ {data.permissionDenials.count} permission denial
+ {data.permissionDenials.count !== 1 ? 's' : ''}
+
+ )}
+
+
+
+ {data.errors.map((error, idx) => (
+
+ ))}
+
+
+ );
+};
diff --git a/src/renderer/components/report/sections/FrictionSection.tsx b/src/renderer/components/report/sections/FrictionSection.tsx
new file mode 100644
index 00000000..96ea0032
--- /dev/null
+++ b/src/renderer/components/report/sections/FrictionSection.tsx
@@ -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 (
+
+
+
+ Friction Rate: {(data.frictionRate * 100).toFixed(1)}%
+
+
+ {data.correctionCount} correction{data.correctionCount !== 1 ? 's' : ''}
+
+
+
+ {data.corrections.length > 0 && (
+
+
Corrections
+
+ {data.corrections.map((corr, idx) => (
+
+
+ {corr.keyword}
+
+ {corr.preview}
+
+ ))}
+
+
+ )}
+
+ {(thrashing.bashNearDuplicates.length > 0 || thrashing.editReworkFiles.length > 0) && (
+
+
Thrashing Signals
+
+ {thrashing.bashNearDuplicates.length > 0 && (
+
+
Repeated Bash Commands
+ {thrashing.bashNearDuplicates.map((dup, idx) => (
+
+ {dup.count}x
+ {dup.prefix}
+
+ ))}
+
+ )}
+
+ {thrashing.editReworkFiles.length > 0 && (
+
+
Reworked Files (3+ edits)
+ {thrashing.editReworkFiles.map((file, idx) => (
+
+ {file.editIndices.length}x
+ {file.filePath}
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+};
diff --git a/src/renderer/components/report/sections/GitSection.tsx b/src/renderer/components/report/sections/GitSection.tsx
new file mode 100644
index 00000000..d8d9823e
--- /dev/null
+++ b/src/renderer/components/report/sections/GitSection.tsx
@@ -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 (
+
+
+
+
Commits
+
{data.commitCount}
+
+
+
Pushes
+
{data.pushCount}
+
+
+
Lines Added
+
+ +{data.linesAdded.toLocaleString()}
+
+
+
+
Lines Removed
+
+ -{data.linesRemoved.toLocaleString()}
+
+
+
+
+ {data.commits.length > 0 && (
+
+
Commits
+
+ {data.commits.map((commit, idx) => (
+
+ #{commit.messageIndex}
+ {commit.messagePreview}
+
+ ))}
+
+
+ )}
+
+ {data.branchCreations.length > 0 && (
+
+
Branches Created
+
+ {data.branchCreations.map((branch, idx) => (
+
+ {branch}
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/src/renderer/components/report/sections/OverviewSection.tsx b/src/renderer/components/report/sections/OverviewSection.tsx
new file mode 100644
index 00000000..e4027405
--- /dev/null
+++ b/src/renderer/components/report/sections/OverviewSection.tsx
@@ -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 (
+
+ {data.firstMessage}
+
+
+
Duration
+
{data.durationHuman}
+
+
+
Messages
+
{data.totalMessages.toLocaleString()}
+
+
+
Context Usage
+
+ {data.contextConsumptionPct != null ? `${data.contextConsumptionPct}%` : 'N/A'}
+ {data.contextAssessment && (
+ ({data.contextAssessment})
+ )}
+
+
+
+
Compactions
+
{data.compactionCount}
+
+
+
Branch
+
{data.gitBranch}
+
+
+
Subagents
+
{data.hasSubagents ? 'Yes' : 'No'}
+
+
+
Project
+
+ {data.projectPath}
+
+
+
+
Session ID
+
+ {data.sessionId.slice(0, 12)}...
+
+
+
+
+ );
+};
diff --git a/src/renderer/components/report/sections/QualitySection.tsx b/src/renderer/components/report/sections/QualitySection.tsx
new file mode 100644
index 00000000..c0dea581
--- /dev/null
+++ b/src/renderer/components/report/sections/QualitySection.tsx
@@ -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 (
+
+ {/* Prompt quality */}
+
+
Prompt Quality
+
+
+ {assessmentLabel(prompt.assessment)}
+
+
+
{prompt.note}
+
+
+
First Message
+
+ {prompt.firstMessageLengthChars.toLocaleString()} chars
+
+
+
+
User Messages
+
{prompt.userMessageCount}
+
+
+
Corrections
+
{prompt.correctionCount}
+
+
+
Friction Rate
+
+ {(prompt.frictionRate * 100).toFixed(1)}%
+
+
+
+
+
+ {/* Startup overhead */}
+
+
Startup Overhead
+
+
+
Messages Before Work
+
{startup.messagesBeforeFirstWork}
+
+
+
Tokens Before Work
+
+ {startup.tokensBeforeFirstWork.toLocaleString()}
+
+
+
+
% of Total
+
{startup.pctOfTotal}%
+
+
+
+
+ {/* Test progression */}
+
+
Test Progression
+
+
+ {testProgression.trajectory === 'insufficient_data'
+ ? 'Insufficient Data'
+ : testProgression.trajectory.charAt(0).toUpperCase() +
+ testProgression.trajectory.slice(1)}
+
+
+ {testProgression.snapshotCount} snapshot{testProgression.snapshotCount !== 1 ? 's' : ''}
+
+
+ {testProgression.firstSnapshot && testProgression.lastSnapshot && (
+
+
+
First Run
+
+
+ {testProgression.firstSnapshot.passed} passed
+
+ {' / '}
+
+ {testProgression.firstSnapshot.failed} failed
+
+
+
+
+
Last Run
+
+
+ {testProgression.lastSnapshot.passed} passed
+
+ {' / '}
+
+ {testProgression.lastSnapshot.failed} failed
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/renderer/components/report/sections/SubagentSection.tsx b/src/renderer/components/report/sections/SubagentSection.tsx
new file mode 100644
index 00000000..bfef657d
--- /dev/null
+++ b/src/renderer/components/report/sections/SubagentSection.tsx
@@ -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 (
+
+
+
+
+
Total Tokens
+
{data.totalTokens.toLocaleString()}
+
+
+
Total Duration
+
{fmtDuration(data.totalDurationMs)}
+
+
+
Total Cost
+
{fmtCost(data.totalCostUsd)}
+
+
+
+ {data.byAgent.length > 0 && (
+
+
+
+
+ | Description |
+ Type |
+ Tokens |
+ Duration |
+ Cost |
+
+
+
+ {data.byAgent.map((agent, idx) => (
+
+ |
+ {agent.description}
+ |
+ {agent.subagentType} |
+
+ {agent.totalTokens.toLocaleString()}
+ |
+
+ {fmtDuration(agent.totalDurationMs)}
+ |
+ {fmtCost(agent.costUsd)} |
+
+ ))}
+
+
+
+ )}
+
+ );
+};
diff --git a/src/renderer/components/report/sections/TimelineSection.tsx b/src/renderer/components/report/sections/TimelineSection.tsx
new file mode 100644
index 00000000..e01718dc
--- /dev/null
+++ b/src/renderer/components/report/sections/TimelineSection.tsx
@@ -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 (
+
+ {/* Idle stats */}
+
+
Idle Analysis
+
+
+
Idle Gaps
+
{idle.idleGapCount}
+
+
+
Total Idle
+
{idle.totalIdleHuman}
+
+
+
Active Time
+
{idle.activeWorkingHuman}
+
+
+
Idle %
+
50 ? '#fbbf24' : '#4ade80' }}
+ >
+ {idle.idlePct}%
+
+
+
+
+
+ {/* Model switches */}
+ {modelSwitches.count > 0 && (
+
+
+ Model Switches ({modelSwitches.count})
+
+
+ {modelSwitches.switches.map((sw, idx) => (
+
+ {sw.from}
+ →
+ {sw.to}
+ msg #{sw.messageIndex}
+
+ ))}
+
+
+ )}
+
+ {/* Key events */}
+ {keyEvents.length > 0 && (
+
+
Key Events
+
+ {keyEvents.map((event, idx) => (
+
+
+ {event.timestamp.toLocaleTimeString()}
+
+ {event.label}
+ {event.deltaHuman && (
+ +{event.deltaHuman}
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/src/renderer/components/report/sections/TokenSection.tsx b/src/renderer/components/report/sections/TokenSection.tsx
new file mode 100644
index 00000000..9dd1dd0c
--- /dev/null
+++ b/src/renderer/components/report/sections/TokenSection.tsx
@@ -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 (
+
+ {/* By-model table */}
+
+
+
+
+ | Model |
+ API Calls |
+ Input |
+ Output |
+ Cache Read |
+ Cache Create |
+ Cost |
+
+
+
+ {modelEntries.map(([model, stats]) => (
+
+ | {model} |
+ {fmt(stats.apiCalls)} |
+ {fmt(stats.inputTokens)} |
+ {fmt(stats.outputTokens)} |
+ {fmt(stats.cacheRead)} |
+ {fmt(stats.cacheCreation)} |
+ {fmtCost(stats.costUsd)} |
+
+ ))}
+ {/* Totals row */}
+
+ | Total |
+
+ {fmt(modelEntries.reduce((s, [, st]) => s + st.apiCalls, 0))}
+ |
+ {fmt(data.totals.inputTokens)} |
+ {fmt(data.totals.outputTokens)} |
+ {fmt(data.totals.cacheRead)} |
+ {fmt(data.totals.cacheCreation)} |
+
+ {fmtCost(modelEntries.reduce((s, [, st]) => s + st.costUsd, 0))}
+ |
+
+
+
+
+
+ {/* Cache economics */}
+
+
+
Cache Efficiency
+
{cacheEconomics.cacheEfficiencyPct}%
+
+
+
R/W Ratio
+
+ {cacheEconomics.cacheReadToWriteRatio}x
+
+
+
+
Cache Read %
+
{data.totals.cacheReadPct}%
+
+
+
Cold Start
+
+ {cacheEconomics.coldStartDetected ? 'Yes' : 'No'}
+
+
+
+
+ );
+};
diff --git a/src/renderer/components/report/sections/ToolSection.tsx b/src/renderer/components/report/sections/ToolSection.tsx
new file mode 100644
index 00000000..8f8bce2b
--- /dev/null
+++ b/src/renderer/components/report/sections/ToolSection.tsx
@@ -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 (
+
+
+ {data.totalCalls.toLocaleString()} total calls across {toolEntries.length} tools
+
+
+
+
+
+ | Tool |
+ Calls |
+ Errors |
+ Success % |
+
+
+
+ {toolEntries.map(([tool, stats]) => {
+ const rateColor =
+ stats.successRatePct < 80
+ ? '#f87171'
+ : stats.successRatePct < 90
+ ? '#fbbf24'
+ : undefined;
+
+ return (
+
+ | {tool} |
+
+ {stats.totalCalls.toLocaleString()}
+ |
+
+ {stats.errors.toLocaleString()}
+ |
+
+ {stats.successRatePct}%
+ |
+
+ );
+ })}
+
+
+
+
+ );
+};