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:
parent
29401916a8
commit
feb0b66df8
1 changed files with 875 additions and 0 deletions
875
docs/plans/2026-02-21-session-analysis-report.md
Normal file
875
docs/plans/2026-02-21-session-analysis-report.md
Normal 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"
|
||||
```
|
||||
Loading…
Reference in a new issue