agent-ecosystem/docs/plans/2026-02-21-session-analysis-report.md
Paul Holstein feb0b66df8 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>
2026-02-21 16:24:33 -05:00

27 KiB

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.

/**
 * 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

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.tsParsedMessage, ToolCall, ToolResult
  • src/main/types/domain.tsSession, SessionMetrics, TokenUsage (= UsageMetadata)
  • src/main/types/chunks.tsSessionDetail, Process
  • src/main/types/jsonl.tsUsageMetadata (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:

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):

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:

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

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:

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:

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

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:

type: 'session' | 'dashboard' | 'notifications' | 'settings';

to:

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:

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:

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
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

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:

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
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

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:

import { SessionReportTab } from '../report/SessionReportTab';

In the tab rendering map (around line 42), add before or after the session case:

{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):
{/* 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

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

git add -A
git commit -m "feat(report): polish and fix lint issues"