docs: add session analysis report implementation plan

7 tasks with acceptance criteria covering types, analyzer engine,
tests, tab integration, UI components, routing, and polish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Holstein 2026-02-21 16:24:33 -05:00
parent 29401916a8
commit feb0b66df8

View file

@ -0,0 +1,875 @@
# Session Analysis Report Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a toolbar button that runs a full session analysis and displays results in a beautifully formatted report tab.
**Architecture:** Pure renderer-side analysis engine (`sessionAnalyzer.ts`) processes `SessionDetail.messages` in a single pass. Report opens in a new tab type (`'report'`) rendered by `SessionReportTab`. No new IPC needed — all data is already available from `tabSessionData`.
**Tech Stack:** React 18, TypeScript, Zustand, Tailwind CSS, lucide-react icons
---
### Task 1: Add report types
**Acceptance Criteria:**
- [ ] File exists at `src/renderer/types/sessionReport.ts`
- [ ] `SessionReport` interface is exported from `src/renderer/types/sessionReport.ts`
- [ ] `pnpm typecheck` passes with no new errors
**Files:**
- Create: `src/renderer/types/sessionReport.ts`
**Step 1: Create the SessionReport type file**
This file defines all the report section types. The analyzer will return a `SessionReport` object.
```typescript
/**
* Session analysis report types.
* Output of analyzeSession() — one interface per report section.
*/
// =============================================================================
// Pricing
// =============================================================================
export interface ModelPricing {
input: number;
output: number;
cache_read: number;
cache_creation: number;
}
// =============================================================================
// Report Sections
// =============================================================================
export interface ReportOverview {
sessionId: string;
projectId: string;
projectPath: string;
firstMessage: string;
messageCount: number;
hasSubagents: boolean;
contextConsumption: number;
contextConsumptionPct: number | null;
contextAssessment: 'critical' | 'high' | 'moderate' | 'healthy' | null;
compactionCount: number;
gitBranch: string;
startTime: Date | null;
endTime: Date | null;
durationSeconds: number;
durationHuman: string;
totalMessages: number;
}
export interface ModelTokenStats {
apiCalls: number;
inputTokens: number;
outputTokens: number;
cacheCreation: number;
cacheRead: number;
costUsd: number;
}
export interface TokenTotals {
inputTokens: number;
outputTokens: number;
cacheCreation: number;
cacheRead: number;
grandTotal: number;
cacheReadPct: number;
}
export interface ReportTokenUsage {
byModel: Record<string, ModelTokenStats>;
totals: TokenTotals;
}
export interface ReportCostAnalysis {
parentCostUsd: number;
subagentCostUsd: number;
totalSessionCostUsd: number;
costByModel: Record<string, number>;
costPerCommit: number | null;
costPerLineChanged: number | null;
}
export interface ReportCacheEconomics {
cacheCreation5m: number;
cacheCreation1h: number;
cacheRead: number;
cacheEfficiencyPct: number;
coldStartDetected: boolean;
cacheReadToWriteRatio: number;
}
export interface ToolSuccessRate {
totalCalls: number;
errors: number;
successRatePct: number;
}
export interface ReportToolUsage {
counts: Record<string, number>;
totalCalls: number;
successRates: Record<string, ToolSuccessRate>;
}
export interface SubagentEntry {
description: string;
subagentType: string;
model: string;
totalTokens: number;
totalDurationMs: number;
totalToolUseCount: number;
costUsd: number;
costNote?: string;
}
export interface ReportSubagentMetrics {
count: number;
totalTokens: number;
totalDurationMs: number;
totalToolUseCount: number;
totalCostUsd: number;
byAgent: SubagentEntry[];
}
export interface ToolError {
tool: string;
inputPreview: string;
error: string;
messageIndex: number;
isPermissionDenial: boolean;
}
export interface ReportErrors {
errors: ToolError[];
permissionDenials: {
count: number;
denials: ToolError[];
affectedTools: string[];
};
}
export interface GitCommit {
messagePreview: string;
messageIndex: number;
}
export interface ReportGitActivity {
commitCount: number;
commits: GitCommit[];
pushCount: number;
branchCreations: string[];
linesAdded: number;
linesRemoved: number;
linesChanged: number;
}
export interface FrictionCorrection {
messageIndex: number;
keyword: string;
preview: string;
}
export interface ReportFrictionSignals {
correctionCount: number;
corrections: FrictionCorrection[];
frictionRate: number;
}
export interface ReportThrashingSignals {
bashNearDuplicates: { prefix: string; count: number }[];
editReworkFiles: { filePath: string; editIndices: number[] }[];
}
export interface ReportConversationTree {
totalNodes: number;
maxDepth: number;
sidechainCount: number;
branchPoints: number;
branchDetails: {
parentUuid: string;
childCount: number;
parentMessageIndex: number | undefined;
}[];
}
export interface IdleGap {
gapSeconds: number;
gapHuman: string;
afterMessageIndex: number;
}
export interface ReportIdleAnalysis {
idleThresholdSeconds: number;
idleGapCount: number;
totalIdleSeconds: number;
totalIdleHuman: string;
wallClockSeconds: number;
activeWorkingSeconds: number;
activeWorkingHuman: string;
idlePct: number;
longestGaps: IdleGap[];
}
export interface ModelSwitch {
from: string;
to: string;
messageIndex: number;
timestamp: Date | null;
}
export interface ReportModelSwitches {
count: number;
switches: ModelSwitch[];
modelsUsed: string[];
}
export interface ReportWorkingDirectories {
uniqueDirectories: string[];
directoryCount: number;
changes: { from: string; to: string; messageIndex: number }[];
changeCount: number;
isMultiDirectory: boolean;
}
export interface TestSnapshot {
messageIndex: number;
passed: number;
failed: number;
total: number;
raw: string;
}
export interface ReportTestProgression {
snapshotCount: number;
snapshots: TestSnapshot[];
trajectory: 'improving' | 'regressing' | 'stable' | 'insufficient_data';
firstSnapshot: TestSnapshot | null;
lastSnapshot: TestSnapshot | null;
}
export interface ReportStartupOverhead {
messagesBeforeFirstWork: number;
tokensBeforeFirstWork: number;
pctOfTotal: number;
}
export interface ReportTokenDensityTimeline {
quartiles: { q: number; avgTokens: number; messageCount: number }[];
}
export interface ReportPromptQuality {
firstMessageLengthChars: number;
userMessageCount: number;
correctionCount: number;
frictionRate: number;
assessment: 'underspecified' | 'verbose_but_unclear' | 'well_specified' | 'moderate_friction';
note: string;
}
export interface ThinkingBlockAnalysis {
messageIndex: number;
preview: string;
charLength: number;
signals: Record<string, boolean>;
}
export interface ReportThinkingBlocks {
count: number;
analyzedCount: number;
signalSummary: Record<string, number>;
notableBlocks: ThinkingBlockAnalysis[];
}
export interface KeyEvent {
timestamp: Date;
label: string;
deltaSeconds?: number;
deltaHuman?: string;
}
export interface ReportFileReadRedundancy {
totalReads: number;
uniqueFiles: number;
readsPerUniqueFile: number;
redundantFiles: Record<string, number>;
}
// =============================================================================
// Combined Report
// =============================================================================
export interface SessionReport {
overview: ReportOverview;
tokenUsage: ReportTokenUsage;
costAnalysis: ReportCostAnalysis;
cacheEconomics: ReportCacheEconomics;
toolUsage: ReportToolUsage;
subagentMetrics: ReportSubagentMetrics;
errors: ReportErrors;
gitActivity: ReportGitActivity;
frictionSignals: ReportFrictionSignals;
thrashingSignals: ReportThrashingSignals;
conversationTree: ReportConversationTree;
idleAnalysis: ReportIdleAnalysis;
modelSwitches: ReportModelSwitches;
workingDirectories: ReportWorkingDirectories;
testProgression: ReportTestProgression;
startupOverhead: ReportStartupOverhead;
tokenDensityTimeline: ReportTokenDensityTimeline;
promptQuality: ReportPromptQuality;
thinkingBlocks: ReportThinkingBlocks;
keyEvents: KeyEvent[];
messageTypes: Record<string, number>;
serviceTiers: Record<string, number>;
fileReadRedundancy: ReportFileReadRedundancy;
compactionCount: number;
gitBranches: string[];
}
```
**Step 2: Verify types compile**
Run: `pnpm typecheck`
Expected: No errors related to sessionReport.ts (file is only types, no imports yet)
**Step 3: Commit**
```bash
git add src/renderer/types/sessionReport.ts
git commit -m "feat(report): add session analysis report type definitions"
```
---
### Task 2: Build the session analyzer
**Acceptance Criteria:**
- [ ] File exists at `src/renderer/utils/sessionAnalyzer.ts`
- [ ] `analyzeSession` function is exported from `src/renderer/utils/sessionAnalyzer.ts`
- [ ] `pnpm typecheck` passes with no new errors
**Files:**
- Create: `src/renderer/utils/sessionAnalyzer.ts`
**Docs to reference:**
- `scripts/analyze-session.py` — the Python script being ported (all logic)
- `src/main/types/messages.ts``ParsedMessage`, `ToolCall`, `ToolResult`
- `src/main/types/domain.ts``Session`, `SessionMetrics`, `TokenUsage` (= `UsageMetadata`)
- `src/main/types/chunks.ts``SessionDetail`, `Process`
- `src/main/types/jsonl.ts``UsageMetadata` (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens)
**Step 1: Create the analyzer**
Port all logic from `scripts/analyze-session.py` to TypeScript. The analyzer takes a `SessionDetail` (which has `session`, `messages`, `processes`, `metrics`) and returns a `SessionReport`.
Key mapping from Python to TS:
- Python `data["messages"]``detail.messages: ParsedMessage[]`
- Python `data["session"]``detail.session: Session`
- Python `m.get("toolCalls", [])``msg.toolCalls: ToolCall[]`
- Python `m.get("toolResults", [])``msg.toolResults: ToolResult[]`
- Python `m.get("usage")``msg.usage?: TokenUsage` (fields: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`)
- Python `m.get("model")``msg.model?: string`
- Python `m.get("timestamp")``msg.timestamp: Date` (already parsed)
- Python `m.get("isMeta")``msg.isMeta: boolean`
- Python `m.get("uuid")``msg.uuid: string`
- Python `m.get("parentUuid")``msg.parentUuid: string | null`
- Python `m.get("cwd")``msg.cwd?: string`
- Python `m.get("gitBranch")``msg.gitBranch?: string`
- Python `m.get("isSidechain")``msg.isSidechain: boolean`
- Python `m.get("isCompactSummary")``msg.isCompactSummary?: boolean`
- Python `m.get("agentId")``msg.agentId?: string`
- For subagent data, use `detail.processes: Process[]` (already resolved with metrics, duration, description, subagentType)
The function signature:
```typescript
import type { SessionDetail } from '@renderer/types/data';
import type { SessionReport } from '@renderer/types/sessionReport';
export function analyzeSession(detail: SessionDetail): SessionReport { ... }
```
Follow the Python script's single-pass pattern:
1. Initialize accumulators
2. Loop over `detail.messages` once, extracting all data
3. Post-pass aggregation
4. Return typed `SessionReport`
For content text extraction, use this helper (mirrors Python's `extract_text_content`):
```typescript
function extractTextContent(msg: ParsedMessage): string {
const { content } = msg;
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter((block) => block.type === 'text')
.map((block) => block.text)
.join(' ');
}
return '';
}
```
For pricing, port the `MODEL_PRICING` table and `costUsd()` function directly.
For subagent metrics, use `detail.processes` instead of parsing `<usage>` tags — the data is already resolved:
```typescript
const subagentEntries: SubagentEntry[] = detail.processes.map((proc) => ({
description: proc.description ?? 'unknown',
subagentType: proc.subagentType ?? 'unknown',
model: 'default (inherits parent)',
totalTokens: proc.metrics.totalTokens,
totalDurationMs: proc.durationMs,
totalToolUseCount: proc.messages.reduce((sum, m) => sum + m.toolCalls.length, 0),
costUsd: proc.metrics.costUsd ?? 0,
}));
```
Port ALL regex patterns from Python:
- `FRICTION_PATTERNS` — friction keyword detection
- `PERMISSION_PATTERNS` — permission denial detection
- `TEST_PASS_PATTERNS`, `TEST_FAIL_PATTERNS`, `TEST_SUMMARY_PATTERN` — test output parsing
- `THINKING_SIGNALS` — thinking block content analysis
**Step 2: Verify it compiles**
Run: `pnpm typecheck`
Expected: PASS
**Step 3: Commit**
```bash
git add src/renderer/utils/sessionAnalyzer.ts
git commit -m "feat(report): add session analyzer engine (TS port of analyze-session.py)"
```
---
### Task 3: Write analyzer tests
**Acceptance Criteria:**
- [ ] File exists at `test/renderer/utils/sessionAnalyzer.test.ts`
- [ ] `pnpm test test/renderer/utils/sessionAnalyzer.test.ts` passes with all tests green
**Files:**
- Create: `test/renderer/utils/sessionAnalyzer.test.ts`
**Step 1: Write tests**
Test the analyzer with mock `SessionDetail` objects. At minimum:
1. **Empty session** — no messages, returns zeroed report
2. **Basic session** — a few user + assistant messages with usage data, verify overview, token counts, cost
3. **Tool usage** — messages with toolCalls and toolResults, verify tool counts and success rates
4. **Error detection** — toolResults with `isError: true`, verify error list and permission denial detection
5. **Friction detection** — user messages with "no,", "wrong", "actually" keywords
6. **Git activity** — Bash toolCalls containing "git commit", "git push"
7. **Idle gaps** — messages with timestamps >60s apart
8. **Model switches** — assistant messages with different model fields
9. **Conversation tree** — messages with uuid/parentUuid, verify depth and branching
Create a `createMockMessage()` helper for building `ParsedMessage` objects easily:
```typescript
function createMockMessage(overrides: Partial<ParsedMessage> = {}): ParsedMessage {
return {
uuid: crypto.randomUUID(),
parentUuid: null,
type: 'assistant',
timestamp: new Date(),
content: '',
isSidechain: false,
isMeta: false,
toolCalls: [],
toolResults: [],
...overrides,
};
}
```
And a `createMockDetail()` helper:
```typescript
function createMockDetail(overrides: Partial<SessionDetail> = {}): SessionDetail {
return {
session: { id: 'test', projectId: 'test', projectPath: '/test', createdAt: Date.now(), hasSubagents: false, messageCount: 0 } as Session,
messages: [],
chunks: [],
processes: [],
metrics: { durationMs: 0, totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, messageCount: 0 },
...overrides,
};
}
```
**Step 2: Run tests**
Run: `pnpm test test/renderer/utils/sessionAnalyzer.test.ts`
Expected: All tests pass
**Step 3: Commit**
```bash
git add test/renderer/utils/sessionAnalyzer.test.ts
git commit -m "test(report): add session analyzer tests"
```
---
### Task 4: Add 'report' tab type and store action
**Acceptance Criteria:**
- [ ] `src/renderer/types/tabs.ts` contains `'report'` in the Tab type union
- [ ] `src/renderer/components/layout/SortableTab.tsx` contains `report: Activity` in TAB_ICONS
- [ ] `openSessionReport` is declared in TabSlice interface in `src/renderer/store/slices/tabSlice.ts`
- [ ] `pnpm typecheck` passes with no new errors
**Files:**
- Modify: `src/renderer/types/tabs.ts:79` — add `'report'` to Tab type union
- Modify: `src/renderer/components/layout/SortableTab.tsx:28-33` — add report icon to TAB_ICONS
- Modify: `src/renderer/store/slices/tabSlice.ts` — add `openSessionReport` action
- Modify: `src/renderer/store/types.ts` (if needed for new slice, but likely just extend tabSlice)
**Step 1: Add 'report' to Tab type**
In `src/renderer/types/tabs.ts`, line 79, change:
```typescript
type: 'session' | 'dashboard' | 'notifications' | 'settings';
```
to:
```typescript
type: 'session' | 'dashboard' | 'notifications' | 'settings' | 'report';
```
**Step 2: Add report icon to SortableTab**
In `src/renderer/components/layout/SortableTab.tsx`, add `Activity` to the lucide-react import and to `TAB_ICONS`:
```typescript
import { Activity, Bell, FileText, LayoutDashboard, Pin, Search, Settings, X } from 'lucide-react';
const TAB_ICONS = {
dashboard: LayoutDashboard,
notifications: Bell,
settings: Settings,
session: FileText,
report: Activity,
} as const;
```
**Step 3: Add openSessionReport action to tabSlice**
In `src/renderer/store/slices/tabSlice.ts`, add to the `TabSlice` interface:
```typescript
openSessionReport: (sourceTabId: string) => void;
```
Implement it following the `openNotificationsTab` pattern. It needs to:
1. Get `tabSessionData[sourceTabId]` to find the sessionDetail
2. Extract the session's firstMessage for the tab label
3. Open a new tab with `type: 'report'`, the same `projectId` and `sessionId` as the source tab
```typescript
openSessionReport: (sourceTabId: string) => {
const state = get();
const sourceTab = getAllTabs(state.paneLayout).find((t) => t.id === sourceTabId);
if (!sourceTab || sourceTab.type !== 'session') return;
const tabData = state.tabSessionData[sourceTabId];
const sessionDetail = tabData?.sessionDetail;
const label = sessionDetail?.session.firstMessage
? `Report: ${truncateLabel(sessionDetail.session.firstMessage, 30)}`
: 'Session Report';
state.openTab({
type: 'report',
label,
projectId: sourceTab.projectId,
sessionId: sourceTab.sessionId,
});
},
```
**Step 4: Verify types compile**
Run: `pnpm typecheck`
Expected: PASS (PaneContent.tsx will have a gap for the `report` type — we'll add it in Task 6)
**Step 5: Commit**
```bash
git add src/renderer/types/tabs.ts src/renderer/components/layout/SortableTab.tsx src/renderer/store/slices/tabSlice.ts
git commit -m "feat(report): add 'report' tab type and openSessionReport store action"
```
---
### Task 5: Build the report UI components
**Acceptance Criteria:**
- [ ] File exists at `src/renderer/components/report/SessionReportTab.tsx`
- [ ] File exists at `src/renderer/components/report/ReportSection.tsx`
- [ ] Files exist at `src/renderer/components/report/sections/OverviewSection.tsx`, `CostSection.tsx`, `TokenSection.tsx`, `ToolSection.tsx`, `SubagentSection.tsx`, `ErrorSection.tsx`, `GitSection.tsx`, `FrictionSection.tsx`, `TimelineSection.tsx`, `QualitySection.tsx`
- [ ] `pnpm typecheck` passes with no new errors
**Files:**
- Create: `src/renderer/components/report/SessionReportTab.tsx`
- Create: `src/renderer/components/report/ReportSection.tsx` — reusable section card wrapper
- Create: `src/renderer/components/report/sections/OverviewSection.tsx`
- Create: `src/renderer/components/report/sections/CostSection.tsx`
- Create: `src/renderer/components/report/sections/TokenSection.tsx`
- Create: `src/renderer/components/report/sections/ToolSection.tsx`
- Create: `src/renderer/components/report/sections/SubagentSection.tsx`
- Create: `src/renderer/components/report/sections/ErrorSection.tsx`
- Create: `src/renderer/components/report/sections/GitSection.tsx`
- Create: `src/renderer/components/report/sections/FrictionSection.tsx`
- Create: `src/renderer/components/report/sections/TimelineSection.tsx`
- Create: `src/renderer/components/report/sections/QualitySection.tsx`
**Docs to reference:**
- `src/renderer/index.css` — CSS variables for theming
- `.claude/rules/tailwind.md` — Theme architecture (use `bg-surface-raised`, `text-text`, `border-border`, etc.)
- `src/renderer/components/common/TokenUsageDisplay.tsx` — Example of formatted token display
- `src/renderer/utils/formatters.ts` — Existing formatting utilities
**Step 1: Create ReportSection wrapper**
A reusable card component for each report section:
```tsx
interface ReportSectionProps {
title: string;
icon: React.ComponentType<{ className?: string }>;
children: React.ReactNode;
defaultCollapsed?: boolean;
}
```
Uses `bg-surface-raised`, `border-border`, collapsible with ChevronDown/ChevronRight toggle.
**Step 2: Create section components**
Each section receives its typed data from the `SessionReport` and renders it. Design guidelines:
- **Stat grids**: 2-4 columns of key metrics with label + value
- **Tables**: For lists of items (tools, errors, subagents) using `<table>` with `text-xs`
- **Color coding**: Use inline styles with CSS variables — green for good, amber for warning, red for critical
- **Collapsible details**: For verbose lists (errors, thinking blocks), show count in header and expand for details
Key section designs:
**OverviewSection**: Grid of 6-8 stat cards (duration, messages, context %, compaction, branch, cost)
**CostSection**: Cost by model table + stat cards for total, per-commit, per-line
**TokenSection**: By-model table (input/output/cache-read/cache-create/cost per model) + totals row + cache economics stats
**ToolSection**: Sorted table (tool name, calls, errors, success %) — highlight tools with <90% success rate
**SubagentSection**: Table of subagents (description, type, tokens, duration, cost) + summary stats
**ErrorSection**: Grouped by tool, expandable error details with input preview
**GitSection**: Commits list + stat cards (pushes, branches, lines added/removed)
**FrictionSection**: Friction rate badge + corrections list with message previews + thrashing signals
**TimelineSection**: Idle gaps table + model switches list + key events timeline
**QualitySection**: Prompt quality assessment badge + startup overhead stats + test progression
**Step 3: Create SessionReportTab**
Main component that:
1. Gets `sessionDetail` from `tabSessionData` using the tab's `sessionId` (find the source session tab's data)
2. Calls `analyzeSession(sessionDetail)` with `useMemo`
3. Renders a scrollable container with all section components
4. Shows loading/error states if session data isn't loaded
```tsx
import { useMemo } from 'react';
import { useStore } from '@renderer/store';
import { analyzeSession } from '@renderer/utils/sessionAnalyzer';
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>No session data available. Open the session 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" style={{ color: 'var(--color-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} />
<SubagentSection data={report.subagentMetrics} />
<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>
);
};
```
**Step 4: Verify it compiles**
Run: `pnpm typecheck`
Expected: PASS
**Step 5: Commit**
```bash
git add src/renderer/components/report/
git commit -m "feat(report): add session report tab and all section components"
```
---
### Task 6: Wire up routing and toolbar button
**Acceptance Criteria:**
- [ ] `src/renderer/components/layout/PaneContent.tsx` imports and renders `SessionReportTab` for `tab.type === 'report'`
- [ ] `src/renderer/components/layout/TabBar.tsx` contains an Activity button with `onClick` calling `openSessionReport`
- [ ] `pnpm typecheck` passes with no new errors
- [ ] `pnpm test` passes with all existing tests green
**Files:**
- Modify: `src/renderer/components/layout/PaneContent.tsx:42-49` — add report tab routing
- Modify: `src/renderer/components/layout/TabBar.tsx:17,56,102-107,384-387` — add analyze button
**Step 1: Add report routing in PaneContent**
In `src/renderer/components/layout/PaneContent.tsx`, import `SessionReportTab` and add the route:
```tsx
import { SessionReportTab } from '../report/SessionReportTab';
```
In the tab rendering map (around line 42), add before or after the session case:
```tsx
{tab.type === 'report' && <SessionReportTab tab={tab} />}
```
**Step 2: Add analyze button in TabBar**
In `src/renderer/components/layout/TabBar.tsx`:
1. Add `Activity` to the lucide-react import (line 17)
2. Add `openSessionReport` to the store destructure (line 56 area)
3. Add a hover state: `const [analyzeHover, setAnalyzeHover] = useState(false);`
4. Add the button next to ExportDropdown (after line 387):
```tsx
{/* Analyze button - show only for session tabs with loaded data */}
{activeTab?.type === 'session' && activeTabSessionDetail && activeTabId && (
<button
onClick={() => openSessionReport(activeTabId)}
onMouseEnter={() => setAnalyzeHover(true)}
onMouseLeave={() => setAnalyzeHover(false)}
className="rounded-md p-2 transition-colors"
style={{
color: analyzeHover ? 'var(--color-text)' : 'var(--color-text-muted)',
backgroundColor: analyzeHover ? 'var(--color-surface-raised)' : 'transparent',
}}
title="Analyze Session"
>
<Activity className="size-4" />
</button>
)}
```
**Step 3: Verify it compiles**
Run: `pnpm typecheck`
Expected: PASS
**Step 4: Run existing tests to verify nothing broke**
Run: `pnpm test`
Expected: All existing tests still pass
**Step 5: Commit**
```bash
git add src/renderer/components/layout/PaneContent.tsx src/renderer/components/layout/TabBar.tsx
git commit -m "feat(report): wire up toolbar button and report tab routing"
```
---
### Task 7: Manual verification and polish
**Acceptance Criteria:**
- [ ] [MANUAL] App launches with `pnpm dev` and report tab opens when Activity button is clicked
- [ ] [MANUAL] All report sections render with data from the active session
- [ ] `pnpm test` passes
- [ ] `pnpm typecheck` passes
- [ ] `pnpm lint:fix && pnpm format` passes with no remaining issues
**Step 1: Run the app**
Run: `pnpm dev`
1. Open a session tab
2. Click the Activity (analyze) icon in the toolbar
3. Verify a new "Report: ..." tab opens
4. Verify all sections render with data
5. Check that section cards use correct theme colors
6. Verify collapsible sections work
7. Verify the tab icon shows the Activity icon in the tab bar
**Step 2: Run full test suite**
Run: `pnpm test`
Expected: All tests pass
**Step 3: Run typecheck**
Run: `pnpm typecheck`
Expected: No errors
**Step 4: Run lint and format**
Run: `pnpm lint:fix && pnpm format`
**Step 5: Final commit if any polish changes**
```bash
git add -A
git commit -m "feat(report): polish and fix lint issues"
```