agent-ecosystem/docs/diff-view-implementation-plan.md
iliya e160626c64 feat: implement diff view functionality with read-only and accept/reject capabilities
- Introduced a comprehensive implementation plan for the diff view feature, structured in four phases: MVP (read-only), accept/reject per hunk, per-task scoping, and enhanced features.
- Phase 1 includes a read-only diff view per agent, utilizing JSONL data to display file changes.
- Defined new types for file changes and review data, and established IPC channels for fetching member changes and reading file content.
- Developed backend services for extracting file changes and aggregating review data, alongside frontend components for displaying diffs and managing state.
- Subsequent phases will enhance the diff view with accept/reject functionality, task-specific change scoping, and improved user experience features.
2026-02-24 20:29:41 +02:00

976 lines
34 KiB
Markdown

# Diff View + Accept/Reject -- Plan
## Overview
4-phase plan. Phase 1 -> self-contained MVP (read-only diff view per agent).
Phase 2 -> accept/reject per hunk with disk writes. Phase 3 -> per-task scoping.
Phase 4 -> polish features.
---
## Phase 1: MVP -- Read-Only Diff View Per Agent
**Goal**: show all file changes made by a team member in a diff review panel,
using data from JSONL files. No accept/reject yet.
### 1.1 Packages to Install
```bash
pnpm add diff # jsdiff v8 -- structuredPatch, applyPatch, parsePatch
```
`@codemirror/merge` + `react-codemirror-merge` deferred to Phase 2.
`diff` is needed immediately for programmatic hunk computation from `tool_use.input`.
### 1.2 Types to Define
**File: `src/shared/types/review.ts`** (NEW ~120 LOC)
```typescript
/** Represents one file edit extracted from JSONL */
export interface FileChange {
filePath: string; // Absolute path on disk
toolName: 'Edit' | 'Write' | 'NotebookEdit' | 'Bash';
toolUseId: string; // For linking back to JSONL
timestamp: string; // ISO timestamp of the tool_use
memberName: string; // Agent who made the change
sessionId: string;
subagentId?: string; // null for lead session
// For Edit tool (main session with toolUseResult)
originalFile?: string; // Full file content BEFORE edit (from toolUseResult)
structuredPatch?: Hunk[]; // Ready-made unified diff hunks (from toolUseResult)
// For Edit tool (subagent -- no toolUseResult, only tool_use.input)
oldString?: string; // tool_use.input.old_string
newString?: string; // tool_use.input.new_string
replaceAll?: boolean;
// For Write tool
writeContent?: string; // Full new file content
writeType?: 'create' | 'overwrite';
// Reliability indicator
confidence: 'high' | 'medium' | 'low';
}
export interface Hunk {
oldStart: number;
oldLines: number;
newStart: number;
newLines: number;
lines: string[]; // Each line prefixed with ' ', '+', '-'
}
/** A file with all its changes aggregated */
export interface ReviewFile {
filePath: string;
relativePath: string; // Relative to project root
language: string; // Inferred from extension
changes: FileChange[]; // Ordered by timestamp
stats: { added: number; removed: number };
status: 'added' | 'modified' | 'deleted';
}
/** Complete review data for an agent */
export interface AgentReviewData {
teamName: string;
memberName: string;
files: ReviewFile[];
totalStats: { added: number; removed: number; filesChanged: number };
extractedAt: string;
confidence: 'high' | 'medium' | 'low'; // Lowest confidence of all changes
}
```
Re-export from `src/shared/types/index.ts`.
### 1.3 IPC Channels
**File: `src/preload/constants/ipcChannels.ts`** (MODIFY -- add 2 lines)
```typescript
/** Get file changes for a team member (diff review) */
export const REVIEW_GET_MEMBER_CHANGES = 'review:getMemberChanges';
/** Read current file content from disk (for conflict detection) */
export const REVIEW_READ_FILE = 'review:readFile';
```
### 1.4 Backend Service
**File: `src/main/services/team/FileChangeExtractor.ts`** (NEW ~350 LOC)
Main service that parses JSONL files and extracts `FileChange[]`.
Responsibilities:
- Uses `TeamMemberLogsFinder.findMemberLogPaths()` to get JSONL paths
- For MAIN session JSONL: extracts `toolUseResult` objects with `originalFile` + `structuredPatch` (high confidence)
- For SUBAGENT JSONL: extracts `tool_use.input` (old_string, new_string, file_path) from Edit blocks (medium confidence)
- For Write tools: extracts `tool_use.input.content` + `file_path` (medium confidence for overwrite, high for create)
- Uses `diff` package's `structuredPatch()` to compute hunks when `structuredPatch` is not present in JSONL
- Caches results with 2-minute TTL (like MemberStatsComputer)
- Error filtering: skips entries where `typeof toolUseResult === 'string'` or `is_error: true`
**File: `src/main/services/team/ReviewAggregator.ts`** (NEW ~150 LOC)
Transforms `FileChange[]` into `AgentReviewData`:
- Groups changes by `filePath`
- Computes per-file stats (lines added/removed)
- Infers file status (added/modified/deleted)
- Computes relative paths from project root
- Infers language from file extension (reuse shared utility)
### 1.5 IPC Handler
**File: `src/main/ipc/review.ts`** (NEW ~120 LOC)
Follows exact same pattern as `teams.ts`:
- `let fileChangeExtractor: FileChangeExtractor | null = null;`
- `let reviewAggregator: ReviewAggregator | null = null;`
- `initializeReviewHandlers(extractor, aggregator)`
- `registerReviewHandlers(ipcMain)` / `removeReviewHandlers(ipcMain)`
- `wrapReviewHandler<T>()` -- same as `wrapTeamHandler`
- Handlers:
- `handleGetMemberChanges(event, teamName, memberName)` -> `IpcResult<AgentReviewData>`
- `handleReadFile(event, filePath)` -> `IpcResult<string>` (reads current file from disk, with path validation)
**File: `src/main/ipc/handlers.ts`** (MODIFY)
- Import and register review handlers
**File: `src/main/ipc/guards.ts`** (MODIFY -- if needed for new validations)
### 1.6 Preload Bridge
**File: `src/preload/index.ts`** (MODIFY)
- Add `review` namespace to exposed API:
```typescript
review: {
getMemberChanges: (teamName: string, memberName: string) =>
invokeIpcWithResult(REVIEW_GET_MEMBER_CHANGES, teamName, memberName),
readFile: (filePath: string) =>
invokeIpcWithResult(REVIEW_READ_FILE, filePath),
}
```
**File: `src/shared/types/api.ts`** (MODIFY)
- Add `ReviewAPI` interface
- Add `review: ReviewAPI` to `ElectronAPI`
**File: `src/renderer/api/httpClient.ts`** (MODIFY)
- Add review HTTP fallback stubs
### 1.7 Zustand Store
**File: `src/renderer/store/slices/reviewSlice.ts`** (NEW ~120 LOC)
```typescript
export interface ReviewSlice {
// State
reviewData: AgentReviewData | null;
reviewLoading: boolean;
reviewError: string | null;
selectedReviewFile: string | null; // filePath
// Actions
fetchMemberChanges: (teamName: string, memberName: string) => Promise<void>;
selectReviewFile: (filePath: string | null) => void;
clearReview: () => void;
}
```
**File: `src/renderer/store/index.ts`** (MODIFY -- add slice)
**File: `src/renderer/store/types.ts`** (MODIFY -- add to AppState)
### 1.8 UI Components
**File: `src/renderer/components/team/review/ReviewPanel.tsx`** (NEW ~180 LOC)
Main container component. Layout:
```
+----------------------------------+
| ReviewPanel |
| [member-name] +142 -38 [Close] |
+----------+-----------------------+
| FileTree | DiffContent |
| | |
| src/ | file: auth.ts |
| auth.ts| @@ -1,5 +1,42 @@ |
| +87 -2 | + import jwt ... |
| | |
| test/ | @@ -42,3 +42,8 @@ |
| auth.. | - const OLD = ... |
| +42 -0 | + const NEW = ... |
+----------+-----------------------+
```
Props: `teamName: string, memberName: string, onClose: () => void`
**File: `src/renderer/components/team/review/ReviewFileTree.tsx`** (NEW ~150 LOC)
Left sidebar with file list:
- Grouped by directory
- Per-file stats (+added / -removed)
- Active file highlight
- Click to select file
- Collapsible directory groups
**File: `src/renderer/components/team/review/ReviewDiffContent.tsx`** (NEW ~120 LOC)
Right panel showing the diff for selected file:
- Header with filename, language badge, stats
- Uses improved DiffViewer (Phase 1 keeps the LCS approach but adds useMemo + proper line numbers)
- Handles multiple changes to same file (shows them sequentially)
- Shows confidence indicator for low/medium confidence changes
- Collapse/expand unchanged code regions
**File: `src/renderer/components/team/review/ReviewEmptyState.tsx`** (NEW ~30 LOC)
Empty state when no changes found.
### 1.9 Integration Point
**File: `src/renderer/components/team/members/MemberCard.tsx`** (MODIFY)
Add "Review Changes" button to member card:
```tsx
<button onClick={() => openReviewPanel(memberName)}>
<GitCompareArrows className="size-4" /> Review
</button>
```
**File: `src/renderer/components/team/TeamDetailView.tsx`** (MODIFY)
Add ReviewPanel rendering (slide-in panel or dialog). Wire up state:
```tsx
{reviewMember && (
<ReviewPanel
teamName={teamName}
memberName={reviewMember}
onClose={() => setReviewMember(null)}
/>
)}
```
### 1.10 Existing DiffViewer -- Migration Strategy
The existing `DiffViewer.tsx` in `src/renderer/components/chat/viewers/` is used for
inline Edit tool display in chat history. It stays UNCHANGED in Phase 1.
The new review components are in a separate `team/review/` directory and do NOT modify DiffViewer.
In Phase 2, when CodeMirror is introduced, both surfaces will be migrated.
### 1.11 Service Registration
**File: `src/main/services/team/index.ts`** (MODIFY -- add 2 exports)
**File: `src/main/services/index.ts`** (MODIFY -- re-export)
### 1.12 Language Detection Utility
**File: `src/shared/utils/languageDetection.ts`** (NEW ~50 LOC)
Extract the `EXTENSION_LANGUAGE_MAP` and `inferLanguage()` from `DiffViewer.tsx` into
a shared utility. Both DiffViewer and ReviewDiffContent will import from here.
DiffViewer.tsx gets modified to import instead of duplicating.
### 1.13 Testing Strategy
**File: `test/main/services/team/FileChangeExtractor.test.ts`** (NEW ~250 LOC)
- Test parsing Edit tool_use with toolUseResult (main session)
- Test parsing Edit tool_use without toolUseResult (subagent)
- Test parsing Write create / overwrite
- Test error filtering (failed edits, rejected edits)
- Test caching behavior
**File: `test/main/services/team/ReviewAggregator.test.ts`** (NEW ~100 LOC)
- Test grouping changes by file
- Test stats computation
- Test relative path calculation
- Test file status inference
**File: `test/main/ipc/review.test.ts`** (NEW ~80 LOC)
- Test input validation (teamName, memberName)
- Test error wrapping
**File: `test/shared/utils/languageDetection.test.ts`** (NEW ~40 LOC)
### 1.14 Phase 1 Summary
| Category | New Files | Modified Files | Estimated LOC |
|----------|-----------|----------------|---------------|
| Types | 1 | 2 | ~120 |
| Backend services | 2 | 2 | ~500 |
| IPC handler | 1 | 2 | ~120 |
| Preload/API | 0 | 3 | ~40 |
| Store | 1 | 2 | ~120 |
| UI components | 4 | 2 | ~480 |
| Shared utils | 1 | 1 | ~50 |
| Tests | 4 | 0 | ~470 |
| **Total** | **14** | **14** | **~1,900** |
### 1.15 Risks
| Risk | Probability | Mitigation |
|------|-------------|------------|
| Subagent JSONL lacks toolUseResult -- hunks inaccurate | HIGH (known) | Use `diff.structuredPatch(old_string, new_string)` for subagents; show confidence badge |
| Large files slow LCS diff | MEDIUM | Phase 1 uses `diff` package (Myers algorithm), not hand-rolled LCS; add useMemo |
| Write tool missing originalFile | HIGH (known) | Show "file created" or "file overwritten" without diff; add note |
| Multiple JSONL files per member | LOW | Already handled by `findMemberLogPaths()` |
### 1.16 Dependencies
Phase 1 is self-contained. No dependency on other phases.
---
## Phase 2: Accept/Reject Per Hunk
**Goal**: interactive diff UI with per-hunk Accept/Reject buttons. Reject writes
modified file back to disk.
### 2.1 Packages to Install
```bash
pnpm add @codemirror/merge # Diff UI with acceptChunk/rejectChunk
pnpm add react-codemirror-merge # React wrapper
pnpm add @codemirror/state # Core dependency
pnpm add @codemirror/view # Core dependency
pnpm add @codemirror/lang-javascript # Syntax highlight
pnpm add @codemirror/lang-python
pnpm add @codemirror/lang-css
pnpm add @codemirror/lang-html
pnpm add @codemirror/lang-json
pnpm add @codemirror/lang-markdown
pnpm add @codemirror/lang-rust
pnpm add @codemirror/lang-sql
pnpm add @codemirror/theme-one-dark # Dark theme matching our palette
pnpm add node-diff3 # Three-way merge for conflict detection
```
### 2.2 New Types
**File: `src/shared/types/review.ts`** (MODIFY -- add ~80 LOC)
```typescript
/** Per-hunk review decision */
export type HunkDecision = 'accepted' | 'rejected' | 'pending';
/** Review state for a single file */
export interface FileReviewState {
filePath: string;
hunkDecisions: HunkDecision[]; // One per hunk, indexed
viewed: boolean;
hasConflict: boolean;
conflictDetails?: string;
}
/** Request to apply review decisions to disk */
export interface ApplyReviewRequest {
teamName: string;
memberName: string;
filePath: string;
hunkDecisions: HunkDecision[];
originalFile: string; // Base version for patch computation
currentDiskContent: string; // For conflict detection
}
export interface ApplyReviewResult {
success: boolean;
conflictDetected: boolean;
conflictDetails?: string;
newContent?: string;
}
```
### 2.3 IPC Channels
**File: `src/preload/constants/ipcChannels.ts`** (MODIFY)
```typescript
/** Apply review decisions (write to disk) */
export const REVIEW_APPLY_DECISIONS = 'review:applyDecisions';
/** Get file-history backup content */
export const REVIEW_GET_BACKUP = 'review:getBackup';
```
### 2.4 Backend Services
**File: `src/main/services/team/ReviewApplier.ts`** (NEW ~200 LOC)
Core logic for writing accepted/rejected hunks to disk:
```
Accept hunk: No-op (file already has the change)
Reject hunk: Compute reverse patch for that hunk, apply to current file
Reject all: Write originalFile to disk
Partial: Apply only accepted hunks from originalFile base
```
Implementation:
1. Read current file from disk
2. If current != expected (agent version), run 3-way merge:
- base = originalFile (before agent edit)
- ours = result of applying only accepted hunks to originalFile
- theirs = current disk content
- Use `node-diff3.diff3Merge()` for conflict detection
3. If no conflict: write merged result
4. If conflict: return conflict details to UI, do NOT write
**File: `src/main/services/team/BackupReader.ts`** (NEW ~60 LOC)
Reads `~/.claude/file-history/{sessionId}/{backupFileName}` backup files.
Used as fallback when `originalFile` is not in JSONL (Write tool case).
### 2.5 IPC Handler
**File: `src/main/ipc/review.ts`** (MODIFY -- add 2 handlers)
- `handleApplyDecisions(event, request: ApplyReviewRequest)` -> `IpcResult<ApplyReviewResult>`
- Validates all fields
- Calls ReviewApplier
- Path traversal validation (prevent writing outside project dir)
- `handleGetBackup(event, sessionId, backupFileName)` -> `IpcResult<string>`
- Validates sessionId format
- Reads backup file content
### 2.6 Preload / API
**File: `src/preload/index.ts`** (MODIFY)
**File: `src/shared/types/api.ts`** (MODIFY -- extend ReviewAPI)
**File: `src/renderer/api/httpClient.ts`** (MODIFY)
### 2.7 Zustand Store
**File: `src/renderer/store/slices/reviewSlice.ts`** (MODIFY -- add ~80 LOC)
```typescript
// Additional state
fileReviewStates: Record<string, FileReviewState>;
applyingReview: boolean;
applyError: string | null;
// Additional actions
setHunkDecision: (filePath: string, hunkIndex: number, decision: HunkDecision) => void;
acceptAllHunks: (filePath: string) => void;
rejectAllHunks: (filePath: string) => void;
acceptAllFiles: () => void;
rejectAllFiles: () => void;
applyReviewDecisions: (filePath: string) => Promise<ApplyReviewResult>;
markFileViewed: (filePath: string) => void;
```
### 2.8 UI Components
**File: `src/renderer/components/team/review/CodeMirrorDiffView.tsx`** (NEW ~250 LOC)
Replaces the simple DiffViewer in the review panel with CodeMirror merge view:
- `MergeView` from `@codemirror/merge` with `mergeControls: true`
- Theme integration with CSS variables (dark/light)
- `collapseUnchanged` for hiding unchanged regions
- `allowInlineDiffs` for character-level highlighting
- Custom `mergeControls` renderer for Accept/Reject buttons matching our design system
- `goToNextChunk`/`goToPreviousChunk` wired to keyboard shortcuts
- Read-only mode (user cannot edit the code, only accept/reject)
- Emits `onHunkDecision(hunkIndex, decision)` callback
**File: `src/renderer/components/team/review/ReviewToolbar.tsx`** (NEW ~80 LOC)
Bottom toolbar:
- "Reject All" / "Accept All" buttons
- Unified / Split toggle
- Stats summary (e.g. "3/7 hunks accepted")
- Apply button (writes to disk)
**File: `src/renderer/components/team/review/ConflictDialog.tsx`** (NEW ~80 LOC)
Dialog shown when conflict is detected:
- Shows conflict details
- Options: "Force reject (overwrite)", "Skip this file", "Cancel"
**File: `src/renderer/components/team/review/ReviewFileTree.tsx`** (MODIFY)
Add per-file status indicators:
- Checkmark (all accepted)
- X (all rejected)
- Partial (mixed)
- Warning (conflict detected)
- Eye icon (viewed/unviewed)
**File: `src/renderer/components/team/review/ReviewDiffContent.tsx`** (MODIFY)
Replace inline diff rendering with `CodeMirrorDiffView` component.
### 2.9 CodeMirror Theme
**File: `src/renderer/components/team/review/codemirrorTheme.ts`** (NEW ~80 LOC)
Custom CodeMirror theme that maps to our CSS variables:
- `--diff-added-bg`, `--diff-removed-bg`
- `--code-bg`, `--code-border`
- Font family matching our monospace stack
- Accept/Reject button styling
### 2.10 Existing DiffViewer Migration
At this point, the chat viewer's `DiffViewer.tsx` can optionally be migrated to use
CodeMirror as well. This is NOT required for the review feature but improves consistency.
If done:
- `DiffViewer.tsx` becomes a thin wrapper around CodeMirror (read-only, no accept/reject)
- LCS algorithm removed
- Bundle size increase ~130KB (CodeMirror core) -- acceptable since already loaded for review
Recommended: keep old DiffViewer in chat view for now (it works, it's lightweight).
Only the review panel uses CodeMirror.
### 2.11 Testing Strategy
**File: `test/main/services/team/ReviewApplier.test.ts`** (NEW ~200 LOC)
- Test reject single hunk
- Test reject all hunks
- Test partial accept/reject
- Test conflict detection (file changed after agent edit)
- Test three-way merge resolution
- Test path traversal prevention
**File: `test/main/services/team/BackupReader.test.ts`** (NEW ~60 LOC)
- Test reading backup files
- Test missing backup graceful handling
### 2.12 Phase 2 Summary
| Category | New Files | Modified Files | Estimated LOC |
|----------|-----------|----------------|---------------|
| Types | 0 | 1 | ~80 |
| Backend services | 2 | 0 | ~260 |
| IPC handler | 0 | 1 | ~60 |
| Preload/API | 0 | 3 | ~30 |
| Store | 0 | 1 | ~80 |
| UI components | 4 | 2 | ~490 |
| Theme | 1 | 0 | ~80 |
| Tests | 2 | 0 | ~260 |
| **Total** | **9** | **8** | **~1,340** |
### 2.13 Risks
| Risk | Probability | Mitigation |
|------|-------------|------------|
| CodeMirror bundle size (~130KB) | LOW | Lazy import; only loaded when review panel opens |
| Three-way merge conflicts hard to resolve | MEDIUM | Show clear conflict UI; always offer "force" option |
| originalFile missing for some edits | MEDIUM | Fall back to file-history backups; show warning |
| CodeMirror theme integration complex | LOW | Start with `one-dark` theme, customize incrementally |
| react-codemirror-merge API changes | LOW | Pin version; wrapper is thin |
### 2.14 Dependencies
- Requires Phase 1 (review data extraction)
- Phase 2 review decisions are per-agent only (not per-task)
---
## Phase 3: Per-Task Scoping
**Goal**: show diffs scoped to a specific task, not just an agent.
Integrate review into the kanban board task cards.
### 3.1 No New Packages
All needed packages installed in Phases 1-2.
### 3.2 Types
**File: `src/shared/types/review.ts`** (MODIFY -- add ~50 LOC)
```typescript
/** Time window for task-scoped change extraction */
export interface TaskTimeWindow {
taskId: string;
memberName: string;
startTimestamp: string | null; // First activity related to task
endTimestamp: string | null; // Task completion or latest activity
confidence: 'high' | 'medium' | 'low';
markers: TaskMarker[];
}
export interface TaskMarker {
type: 'task_start' | 'task_complete' | 'task_create' | 'task_update' | 'mention';
timestamp: string;
source: string; // JSONL line info
}
/** Review scoped to a task */
export interface TaskReviewData extends AgentReviewData {
taskId: string;
taskSubject: string;
timeWindow: TaskTimeWindow;
}
```
### 3.3 IPC Channels
**File: `src/preload/constants/ipcChannels.ts`** (MODIFY)
```typescript
/** Get file changes scoped to a task */
export const REVIEW_GET_TASK_CHANGES = 'review:getTaskChanges';
```
### 3.4 Backend Service
**File: `src/main/services/team/TaskTimeWindowResolver.ts`** (NEW ~250 LOC)
Resolves the time window for a task by scanning JSONL files:
1. Use `TeamMemberLogsFinder.findLogsForTask()` to find relevant JSONL files
2. Scan each file for task markers:
- `TaskCreate` with matching task ID -> start marker
- `TaskUpdate` with status `in_progress` -> start marker
- `TaskUpdate` with status `completed` -> end marker
- `SendMessage` referencing task ID -> activity marker
- Comment mentioning `#taskId` -> activity marker
3. Build `TaskTimeWindow` from earliest start to latest end
4. Confidence levels:
- HIGH: both explicit start + end markers found
- MEDIUM: only start OR end found, other inferred from timestamps
- LOW: no explicit markers, only mentions -- wide time window
**File: `src/main/services/team/FileChangeExtractor.ts`** (MODIFY -- add ~80 LOC)
New method: `extractChangesForTask(teamName, taskId, timeWindow)`
- Same JSONL parsing as per-agent
- Filters `tool_use` blocks by timestamp within `timeWindow`
- Additional heuristic: if task owner is known, only include that member's changes
### 3.5 IPC Handler
**File: `src/main/ipc/review.ts`** (MODIFY)
Add `handleGetTaskChanges(event, teamName, taskId)` handler.
### 3.6 Preload / API
Same pattern: extend `ReviewAPI`, update `preload/index.ts`, update `httpClient.ts`.
### 3.7 Zustand Store
**File: `src/renderer/store/slices/reviewSlice.ts`** (MODIFY)
```typescript
// Additional state
taskReviewData: TaskReviewData | null;
taskReviewLoading: boolean;
// Additional action
fetchTaskChanges: (teamName: string, taskId: string) => Promise<void>;
```
### 3.8 UI Components
**File: `src/renderer/components/team/review/TaskReviewPanel.tsx`** (NEW ~100 LOC)
Wraps ReviewPanel with task-specific header:
- Task subject + ID
- Time window visualization (start -> end)
- Confidence badge
- Same file tree + diff content as ReviewPanel (reuses components)
**File: `src/renderer/components/team/kanban/KanbanTaskCard.tsx`** (MODIFY)
Add "Review Changes" button on task cards that are in `review` or `done` columns:
```tsx
{(task.kanbanColumn === 'review' || task.status === 'completed') && (
<button onClick={() => openTaskReview(task.id)}>
<GitCompareArrows className="size-3.5" /> Changes
</button>
)}
```
**File: `src/renderer/components/team/dialogs/TaskDetailDialog.tsx`** (MODIFY)
Add "View Changes" tab/section to task detail dialog.
### 3.9 Testing Strategy
**File: `test/main/services/team/TaskTimeWindowResolver.test.ts`** (NEW ~200 LOC)
- Test finding task markers in JSONL
- Test HIGH confidence (both markers)
- Test MEDIUM confidence (partial markers)
- Test LOW confidence (only mentions)
- Test multiple sessions contributing to same task
**File: `test/main/services/team/FileChangeExtractor.task.test.ts`** (NEW ~120 LOC)
- Test time-window filtering
- Test cross-session task changes
### 3.10 Phase 3 Summary
| Category | New Files | Modified Files | Estimated LOC |
|----------|-----------|----------------|---------------|
| Types | 0 | 1 | ~50 |
| Backend services | 1 | 1 | ~330 |
| IPC handler | 0 | 1 | ~30 |
| Preload/API | 0 | 3 | ~20 |
| Store | 0 | 1 | ~30 |
| UI components | 1 | 2 | ~100 |
| Tests | 2 | 0 | ~320 |
| **Total** | **4** | **9** | **~880** |
### 3.11 Risks
| Risk | Probability | Mitigation |
|------|-------------|------------|
| Time window too wide (catches unrelated changes) | MEDIUM | Use confidence levels; show warning for LOW confidence |
| Task timestamps not in JSONL | HIGH (known) | Rely on tool_use timestamps from JSONL, not task JSON file |
| Multiple agents working on same task | LOW | Show all contributing agents, grouped by member |
| Task markers hard to find in large JSONL | MEDIUM | Reuse `fileMentionsTaskId()` for fast scanning |
### 3.12 Dependencies
- Requires Phase 1 (FileChangeExtractor)
- Optionally uses Phase 2 (accept/reject) but works without it (read-only task review)
---
## Phase 4: Enhanced Features
**Goal**: polish, keyboard navigation, "viewed" tracking, multi-edit timeline,
git fallback for Bash changes.
### 4.1 Packages to Install
```bash
pnpm add simple-git # Git operations for Bash change detection
```
### 4.2 Feature A: Multiple Edits to Same File (Timeline View)
**File: `src/renderer/components/team/review/FileEditTimeline.tsx`** (NEW ~120 LOC)
When a file has multiple `FileChange` entries:
- Show a horizontal timeline of edits
- Each node = one edit (with timestamp, agent name)
- Click node to see that specific diff
- "Final" shows cumulative diff
### 4.3 Feature B: Keyboard Navigation
**File: `src/renderer/hooks/useReviewKeyboardNav.ts`** (NEW ~80 LOC)
Keyboard shortcuts (while review panel is focused):
- `j` / `k` -- next/previous file
- `n` / `p` -- next/previous hunk
- `a` -- accept current hunk
- `r` -- reject current hunk
- `A` (shift+a) -- accept all hunks in file
- `R` (shift+r) -- reject all hunks in file
- `v` -- toggle viewed
- `Escape` -- close review panel
Integrates with CodeMirror's `goToNextChunk` / `goToPreviousChunk`.
### 4.4 Feature C: "Viewed" File Tracking
**File: `src/renderer/store/slices/reviewSlice.ts`** (MODIFY -- ~20 LOC)
Persistent "viewed" state per file (stored in IndexedDB via `idb-keyval`):
- Key: `review:{teamName}:{memberName}:{filePath}`
- Value: `{ viewed: boolean, viewedAt: string }`
- Badge in file tree: eye icon / number of unviewed files
### 4.5 Feature D: Git Fallback for Bash Changes
**File: `src/main/services/team/GitDiffProvider.ts`** (NEW ~150 LOC)
For changes made via Bash (git apply, sed, etc.):
1. Get project's git repo path from team config
2. Use `simple-git` to run `git log --author --since --until --stat` filtered by session timestamps
3. For each changed file: `git diff <before-sha>..<after-sha> -- <file>`
4. Convert to `FileChange[]` with `confidence: 'medium'`
Integration: called by `FileChangeExtractor` when `toolName === 'Bash'` and git is available.
### 4.6 Feature E: Split/Unified View Toggle
**File: `src/renderer/components/team/review/CodeMirrorDiffView.tsx`** (MODIFY)
Add `orientation` prop:
- `'a-b'` (side-by-side / split view)
- Unified view via custom rendering
Store user preference in localStorage.
### 4.7 Testing Strategy
**File: `test/main/services/team/GitDiffProvider.test.ts`** (NEW ~100 LOC)
**File: `test/renderer/hooks/useReviewKeyboardNav.test.ts`** (NEW ~80 LOC)
### 4.8 Phase 4 Summary
| Category | New Files | Modified Files | Estimated LOC |
|----------|-----------|----------------|---------------|
| Backend services | 1 | 1 | ~150 |
| UI components | 1 | 1 | ~120 |
| Hooks | 1 | 0 | ~80 |
| Store | 0 | 1 | ~20 |
| Tests | 2 | 0 | ~180 |
| **Total** | **5** | **3** | **~550** |
### 4.9 Risks
| Risk | Probability | Mitigation |
|------|-------------|------------|
| simple-git not available on all systems | MEDIUM | Feature is optional fallback; graceful degradation |
| Git diff timestamps don't match JSONL exactly | MEDIUM | Use wide time window (+/- 60s) for matching |
| Keyboard navigation conflicts with existing shortcuts | LOW | Scope to review panel focus only |
### 4.10 Dependencies
- Requires Phase 2 (CodeMirror for split/unified toggle, keyboard nav)
- Git fallback can be done independently
---
## Complete File Manifest
### All New Files (32 total)
| Phase | File | LOC |
|-------|------|-----|
| 1 | `src/shared/types/review.ts` | ~120 |
| 1 | `src/shared/utils/languageDetection.ts` | ~50 |
| 1 | `src/main/services/team/FileChangeExtractor.ts` | ~350 |
| 1 | `src/main/services/team/ReviewAggregator.ts` | ~150 |
| 1 | `src/main/ipc/review.ts` | ~120 |
| 1 | `src/renderer/store/slices/reviewSlice.ts` | ~120 |
| 1 | `src/renderer/components/team/review/ReviewPanel.tsx` | ~180 |
| 1 | `src/renderer/components/team/review/ReviewFileTree.tsx` | ~150 |
| 1 | `src/renderer/components/team/review/ReviewDiffContent.tsx` | ~120 |
| 1 | `src/renderer/components/team/review/ReviewEmptyState.tsx` | ~30 |
| 1 | `test/main/services/team/FileChangeExtractor.test.ts` | ~250 |
| 1 | `test/main/services/team/ReviewAggregator.test.ts` | ~100 |
| 1 | `test/main/ipc/review.test.ts` | ~80 |
| 1 | `test/shared/utils/languageDetection.test.ts` | ~40 |
| 2 | `src/main/services/team/ReviewApplier.ts` | ~200 |
| 2 | `src/main/services/team/BackupReader.ts` | ~60 |
| 2 | `src/renderer/components/team/review/CodeMirrorDiffView.tsx` | ~250 |
| 2 | `src/renderer/components/team/review/ReviewToolbar.tsx` | ~80 |
| 2 | `src/renderer/components/team/review/ConflictDialog.tsx` | ~80 |
| 2 | `src/renderer/components/team/review/codemirrorTheme.ts` | ~80 |
| 2 | `test/main/services/team/ReviewApplier.test.ts` | ~200 |
| 2 | `test/main/services/team/BackupReader.test.ts` | ~60 |
| 3 | `src/main/services/team/TaskTimeWindowResolver.ts` | ~250 |
| 3 | `src/renderer/components/team/review/TaskReviewPanel.tsx` | ~100 |
| 3 | `test/main/services/team/TaskTimeWindowResolver.test.ts` | ~200 |
| 3 | `test/main/services/team/FileChangeExtractor.task.test.ts` | ~120 |
| 4 | `src/main/services/team/GitDiffProvider.ts` | ~150 |
| 4 | `src/renderer/components/team/review/FileEditTimeline.tsx` | ~120 |
| 4 | `src/renderer/hooks/useReviewKeyboardNav.ts` | ~80 |
| 4 | `test/main/services/team/GitDiffProvider.test.ts` | ~100 |
| 4 | `test/renderer/hooks/useReviewKeyboardNav.test.ts` | ~80 |
### All Modified Files (across all phases)
| File | Phases | Changes |
|------|--------|---------|
| `src/shared/types/review.ts` | 1,2,3 | Type additions |
| `src/shared/types/index.ts` | 1 | Re-export |
| `src/shared/types/api.ts` | 1,2,3 | ReviewAPI interface |
| `src/preload/constants/ipcChannels.ts` | 1,2,3 | Channel constants |
| `src/preload/index.ts` | 1,2,3 | Bridge methods |
| `src/main/ipc/handlers.ts` | 1 | Register review handlers |
| `src/main/ipc/review.ts` | 2,3 | Additional handlers |
| `src/main/services/team/index.ts` | 1,2,3,4 | Barrel exports |
| `src/main/services/index.ts` | 1 | Re-export |
| `src/renderer/api/httpClient.ts` | 1,2,3 | HTTP fallback |
| `src/renderer/store/index.ts` | 1 | Add slice |
| `src/renderer/store/types.ts` | 1 | AppState type |
| `src/renderer/store/slices/reviewSlice.ts` | 2,3,4 | State extensions |
| `src/renderer/components/team/members/MemberCard.tsx` | 1 | Review button |
| `src/renderer/components/team/TeamDetailView.tsx` | 1 | Panel integration |
| `src/renderer/components/team/review/ReviewFileTree.tsx` | 2 | Status indicators |
| `src/renderer/components/team/review/ReviewDiffContent.tsx` | 2 | CodeMirror swap |
| `src/renderer/components/team/kanban/KanbanTaskCard.tsx` | 3 | Review button |
| `src/renderer/components/team/dialogs/TaskDetailDialog.tsx` | 3 | Changes tab |
| `src/renderer/components/chat/viewers/DiffViewer.tsx` | 1 | Extract languageDetection |
| `src/main/services/team/FileChangeExtractor.ts` | 3,4 | Task scope + git fallback |
| `src/renderer/components/team/review/CodeMirrorDiffView.tsx` | 4 | Split/unified toggle |
---
## Estimated Total
| Phase | New Files | Modified Files | LOC | Packages |
|-------|-----------|----------------|-----|----------|
| 1 MVP | 14 | 14 | ~1,900 | `diff` |
| 2 Accept/Reject | 9 | 8 | ~1,340 | `@codemirror/*`, `node-diff3` |
| 3 Per-Task | 4 | 9 | ~880 | -- |
| 4 Enhanced | 5 | 3 | ~550 | `simple-git` |
| **Total** | **32** | **34** | **~4,670** | 14 packages |
---
## Implementation Order Recommendation
```
Week 1: Phase 1 (MVP read-only diff view)
- Day 1-2: Types + FileChangeExtractor + ReviewAggregator + tests
- Day 3: IPC handler + preload bridge
- Day 4-5: UI components + store + integration
Week 2: Phase 2 (Accept/Reject)
- Day 1-2: CodeMirror integration + theme
- Day 3: ReviewApplier + conflict detection + tests
- Day 4: Toolbar + ConflictDialog
- Day 5: Polish + testing
Week 3: Phase 3 (Per-Task) + Phase 4 start
- Day 1-2: TaskTimeWindowResolver + tests
- Day 3: Task review UI + kanban integration
- Day 4-5: Phase 4 features (keyboard nav, viewed tracking)
Week 4: Phase 4 completion + polish
- Day 1-2: Git fallback
- Day 3: File edit timeline
- Day 4-5: Integration testing, edge cases, performance tuning
```
---
## Architecture Decision Records
### ADR-1: Separate `review:*` IPC namespace vs extending `team:*`
**Decision**: Separate `review:*` namespace.
**Reason**: Review is a distinct concern with its own service lifecycle. Mixing into
`teams.ts` (already 1400+ LOC) would make it harder to maintain. Following the
existing pattern where `team:*` channels are for team CRUD/messaging and new domains
get their own namespace.
### ADR-2: `diff` (jsdiff) for hunk computation vs raw structured patch from JSONL
**Decision**: Use JSONL `structuredPatch` when available (main session Edit), fall back
to `diff.structuredPatch()` for subagents.
**Reason**: JSONL data is most reliable (computed by CLI at edit time). But subagent
JSONL lacks it, so we need programmatic fallback. `diff` v8 has 47M weekly downloads
and proven reliability.
### ADR-3: CodeMirror vs @pierre/diffs
**Decision**: `@codemirror/merge`.
**Reason**: Native `acceptChunk()` / `rejectChunk()` API, mature ecosystem (580K
downloads), MIT license, TypeScript support, active maintenance. `@pierre/diffs` is
newer (Sep 2025), has no explicit license, and Shadow DOM complicates theme integration.
### ADR-4: Keep existing DiffViewer in chat view
**Decision**: Do NOT replace chat DiffViewer with CodeMirror in Phase 2.
**Reason**: Chat DiffViewer is read-only and lightweight (~370 LOC). Adding CodeMirror
bundle to every chat view is unnecessary. Review panel loads CodeMirror lazily only when
opened. Migration can be done later if needed.
### ADR-5: Per-agent first, per-task second
**Decision**: Phase 1-2 are per-agent only. Per-task added in Phase 3.
**Reason**: Per-agent is 100% reliable (each agent has its own JSONL). Per-task
requires time-window inference (~85% reliability). Ship reliable feature first,
add task scoping as enhancement.