# Enhanced File Panel — Implementation Plan ## TL;DR > **Quick Summary**: Add side-by-side diff, hide whitespace, wrap lines, expand all files, inline diff comments, and in-browser file editing to BooCode's right-rail file panel. > > **Deliverables**: > - Enhanced `GitDiffView.tsx` with toolbar (layout/whitespace/wrap/expand-all toggles) > - Split-layout diff renderer (side-by-side) > - `useDiffPreferences` hook (localStorage persistence) > - Inline diff comment components + Zustand store > - File editing mode in file tree + server write endpoint > - Server `git diff -w` support > > **Estimated Effort**: Medium-Large > **Parallel Execution**: YES — 4 waves > **Critical Path**: Wave 1 (server) → Wave 2 (diff preferences + toolbar) → Wave 3 (split layout) → Wave 4 (comments + editing) --- ## Context ### Original Request User wants to implement these features from Paseo into BooCode's file manager: 1. Unified diff ✅ (exists) / Side by side diff ❌ 2. Hide whitespace ❌ 3. Wrap long lines ❌ 4. Expand all files ❌ (only per-file) 5. Refresh ✅ (exists) 6. Comments on specific diffs ❌ 7. File edits (editing in the file browser) ❌ ### Research Findings - **Paseo** (`/opt/forks/paseo`): Best reference for all features. Key files: `diff-pane.tsx`, `diff-layout.ts`, `diff-rendering.ts`, `review/surface.tsx`, `review/store.ts`, `use-changes-preferences/` - **Existing BooCode files**: `GitDiffView.tsx`, `RightRail.tsx`, `useGitDiff.ts`, `git_diff.ts`, `FileViewerOverlay.tsx` - Key insight: None of the web references have true inline file editing in the browser — this is new ground --- ## Work Objectives ### Core Objective Augment the existing file panel with side-by-side diff, whitespace/wrap/expand toggles, inline comments, and inline file editing. ### Definition of Done - [x] `pnpm -C apps/web build` succeeds with no errors - [x] `pnpm -C apps/server build` succeeds with no errors - [ ] Side-by-side diff renders correctly (two aligned columns) - [ ] Hide whitespace toggles and re-fetches diff - [ ] Wrap lines toggles between pre / pre-wrap - [ ] Expand/Collapse all toggles all file diffs - [ ] Inline comments: click gutter → type → save → display thread - [ ] File edit: double-click tree → edit → save → file changes on disk - [ ] All preferences persist across page refresh ### Must Have - Side-by-side diff view - Hide whitespace toggle (server param) - Wrap long lines toggle (CSS) - Expand/Collapse all file diffs - Inline diff comments with thread UI - In-browser file editing with save - Preference persistence ### Must NOT Have (Guardrails) - No DB migration (comments are client-side) - No new WS frames (reuse git_diff_refresh) - No new `@boocode/contracts` types - No multi-user comment sharing - No git push/pull/PR operations - No inline hunk staging --- ## Verification Strategy ### Test Decision - **Infrastructure exists**: YES (vitest for server) - **Automated tests**: Tests-after for new server route + `git_diff.ts` changes - **Agent-Executed QA**: Playwright for diff interactions, curl for API endpoints ### QA Policy Every task includes agent-executed scenarios. Evidence saved to `.omo/evidence/`. --- ## Execution Strategy ### Waves ``` Wave 1 (Server — foundation): ├── Task 1: Server: whitespace param in git_diff.ts ├── Task 2: Server: POST /api/projects/:id/write_file endpoint ├── Task 3: Server tests for whitespace + write └── [tests + typecheck] Wave 2 (Frontend — preferences + toolbar): ├── Task 4: useDiffPreferences hook (localStorage) ├── Task 5: GitDiffView toolbar (layout/whitespace/wrap/expand-all toggles) ├── Task 6: Wrap lines CSS + hide whitespace re-fetch └── [pnpm build] Wave 3 (Frontend — split layout): ├── Task 7: Diff layout utilities (buildSplitDiffRows etc.) ├── Task 8: Side-by-side renderer in GitDiffView ├── Task 9: Line number gutter + alignment └── [pnpm build] Wave 4 (Frontend — comments + file editing): ├── Task 10: InlineComment store (Zustand + localStorage) ├── Task 11: InlineReviewGutterCell + InlineReviewEditor ├── Task 12: InlineReviewThread (comment display) ├── Task 13: File editing mode in RightRail file tree └── [pnpm build + full smoke test] ``` Critical Path: T1 → T2 → T4 → T5 → T7 → T8 → T10 → T11 → T12 → T13 --- ## TODOs - [x] 1. **Server: Add `ignoreWhitespace` param to git diff** **What to do**: - In `apps/server/src/services/git_diff.ts`, add `ignoreWhitespace?: boolean` to the `getGitDiff` function signature - When `ignoreWhitespace` is true, append `'-w'` to the git diff argv call in `getGitDiff` (the main diff command, not name-status) - Update `GET /api/projects/:id/git/diff` route in `routes/projects.ts` to accept optional query param `whitespace=1` - The param should be optional (backward compatible) — default false **Files to modify**: - `apps/server/src/services/git_diff.ts` — update `getGitDiff()` to accept and use `ignoreWhitespace` - `apps/server/src/routes/projects.ts` — add `whitespace` query param **References**: - Paseo: `useCheckoutDiffQuery({ ignoreWhitespace })` passes to server → `git diff -w` - Existing `git_diff.ts:36-48` `runGit` function — argv pattern to follow **QA Scenarios**: ``` Scenario: Diff with whitespace changes respects ignoreWhitespace param Tool: Bash (curl) Preconditions: A file exists with whitespace-only changes (extra spaces) Steps: 1. GET /api/projects/:id/git/diff ⇒ verify diff_body includes whitespace changes 2. GET /api/projects/:id/git/diff?whitespace=1 ⇒ verify diff_body excludes whitespace-only changes Expected: With whitespace=1, files that only had whitespace changes show as unchanged Evidence: .omo/evidence/task-1-whitespace.txt ``` - [x] 2. **Server: Add POST /api/projects/:id/write_file endpoint** **What to do**: - Add `POST /api/projects/:id/write_file` route in `routes/projects.ts` - Accept `{ path: string, content: string }` body - Validate path via existing `pathGuard` helper (same as git discard) - Write file content atomically: write to `.tmp` then `rename` the file - Return `{ ok: boolean }` on success - Reuse the safe file-write pattern from `services/file_ops.ts` **Files to modify**: - `apps/server/src/routes/projects.ts` — add POST route - `apps/web/src/api/client.ts` — add `writeFile` method - `apps/web/src/api/types.ts` — add write types if needed **References**: - `apps/server/src/services/file_ops.ts` — existing file operations pattern - `apps/server/src/routes/projects.ts:544-592` — git write routes (same security pattern) - `apps/server/src/services/path_guard.ts` — path validation **QA Scenarios**: ``` Scenario: Write file content and verify on disk Tool: Bash (curl) Preconditions: A project exists with a writable path Steps: 1. POST /api/projects/:id/write_file { path: "test.txt", content: "hello" } 2. GET /api/projects/:id/view_file?path=test.txt Expected: Status 200, view_file returns "hello" Evidence: .omo/evidence/task-2-write.txt ``` - [x] 3. **Frontend: useDiffPreferences hook** **What to do**: - Create `apps/web/src/hooks/useDiffPreferences.ts` - Define `DiffPreferences` interface: `{ layout: 'unified'|'split', wrapLines: boolean, hideWhitespace: boolean }` - Default: `{ layout: 'unified', wrapLines: false, hideWhitespace: false }` - Read/write to localStorage key `boocode.diff.preferences` - Return `{ preferences, updatePreferences, resetPreferences }` - Zod-validate on read for forward compatibility **Files to create/modify**: - Create `apps/web/src/hooks/useDiffPreferences.ts` **References**: - `/opt/forks/paseo/packages/app/src/hooks/use-changes-preferences/storage.ts` — exact pattern - `apps/web/src/hooks/useProjectGit.ts` — hooks pattern in BooCode **QA Scenarios**: ``` Scenario: Preferences persist across page refresh Tool: Playwright Preconditions: Page loaded Steps: 1. Call updatePreferences({ layout: 'split' }) 2. Read localStorage.getItem('boocode.diff.preferences') 3. Reload page, read preferences again Expected: layout is 'split' after reload Evidence: .omo/evidence/task-3-prefs.txt ``` - [x] 4. **Frontend: GitDiffView toolbar with all toggles** **What to do**: - Add a toolbar row inside `GitDiffView.tsx` between the mode selector and file list - Controls (left to right): - **Layout toggle**: two-segment button (Unified | Split) — uses `AlignJustify` / `Columns2` icons - **Hide whitespace**: toggle button — `Pilcrow` icon, active state highlights - **Wrap lines**: toggle button — `WrapText` icon - **Expand/Collapse all**: toggle button — `ListChevronsUpDown` / `ListChevronsDownUp` icons - **Refresh**: existing button (already present) - Wire each toggle to the `useDiffPreferences` hook - Expand all state: compute `allExpanded = files.every(f => expandedPaths.has(f.path))` - Pass expand state as a new prop or local state **Files to modify**: - `apps/web/src/components/GitDiffView.tsx` — add toolbar section, expand-all logic **References**: - Paseo `diff-pane.tsx:1114-1273` — `DiffLayoutToggleGroup`, `DiffWhitespaceToggle`, `DiffFilesToolbar` - openchamber `DiffViewToggle.tsx` — simple toggle pattern - happy `InlineFileDiff.tsx:196-219` — `DiffStyleToggle` segment control **QA Scenarios**: ``` Scenario: All toolbar controls render and toggle Tool: Playwright Preconditions: Git tab active with changed files Steps: 1. Verify layout toggle shows "Unified" / "Split" buttons 2. Click "Split" — verify visual change 3. Click "Wrap" — verify wrap toggle 4. Click "Expand all" — verify all files expand 5. Click "Collapse all" — verify all files collapse Expected: Each toggle works and updates state Evidence: .omo/evidence/task-4-toolbar.png ``` - [x] 5. **Frontend: Diff layout utilities + side-by-side renderer** **What to do**: - Create `apps/web/src/utils/diff-layout.ts` with pure functions: - `buildNumberedDiffHunks(diffBody: string): NumberedDiffHunk[]` — parse diff text into hunks with old/new line numbers - `buildUnifiedDiffLines(file): UnifiedDiffDisplayLine[]` — existing behavior - `buildSplitDiffRows(file): SplitDiffRow[]` — pair removals/additions into left/right rows - Create `apps/web/src/components/DiffSplitView.tsx` — the side-by-side renderer: - Two columns (left = deletions, right = additions) with a thin divider - Each column has its own gutter (line numbers) + code content - Use Shiki `codeToHtml(language)` for syntax highlighting per side - Handle empty cells (unpaired lines render as blank) - In `GitDiffView.tsx`, when `layout === 'split'`, render `DiffSplitView` instead of the unified diff body **Files to create/modify**: - Create `apps/web/src/utils/diff-layout.ts` - Create `apps/web/src/components/DiffSplitView.tsx` - Modify `apps/web/src/components/GitDiffView.tsx` — add layout branching **References**: - `/opt/forks/paseo/packages/app/src/utils/diff-layout.ts` — full algorithm - `/opt/forks/paseo/packages/app/src/git/diff-pane.tsx:968-989` — split layout rendering - existing `git_diff.ts` `splitDiffByFile` — already splits unified diff per file **QA Scenarios**: ``` Scenario: Side-by-side diff renders correctly Tool: Playwright Preconditions: Git tab active, files with changes Steps: 1. Click "Split" layout toggle 2. Verify two columns appear with a divider 3. Verify deleted lines are on left side (red background) 4. Verify added lines are on right side (green background) 5. Verify context lines appear on both sides, aligned Expected: Layout matches Paseo's split diff Evidence: .omo/evidence/task-5-splitdiff.png ``` - [x] 6. **Frontend: Inline comment store + Zustand** **What to do**: - Create `apps/web/src/stores/useDiffCommentStore.ts` - Define `DiffComment` interface: `{ id, filePath, side, lineNumber, body, createdAt, updatedAt }` - Create Zustand store with: - `commentsByKey: Map` keyed by `${sessionId}:${mode}:${filePath}` - `addComment(key, comment)` / `updateComment(key, id, body)` / `deleteComment(key, id)` - `loadComments(key)` — load from localStorage - `persist()` — subscribe to store changes, write to localStorage key `boocode.diff.comments.[key]` - Export `useDiffCommentStore` **Files to create**: - Create `apps/web/src/stores/useDiffCommentStore.ts` **References**: - `/opt/forks/paseo/packages/app/src/review/store.ts` — zustand store for comments - `/opt/forks/paseo/packages/app/src/review/state.ts` — CRUD operations **QA Scenarios**: ``` Scenario: Comments persist across page refresh Tool: Playwright Preconditions: Diff panel open with changes Steps: 1. Add comment on a diff line 2. Verify comment thread appears 3. Reload page 4. Navigate to same diff Expected: Comment thread still visible after reload Evidence: .omo/evidence/task-6-comment-store.txt ``` - [x] 7. **Frontend: InlineReviewGutterCell + InlineReviewEditor** **What to do**: - Create `apps/web/src/components/InlineReviewGutterCell.tsx`: - Replaces the plain line-number display in diff rows - Shows line number + "+" icon on hover (to start a comment) - Uses `ReviewableDiffTarget { filePath, side, lineNumber }` for tracking - Create `apps/web/src/components/InlineReviewEditor.tsx`: - Textarea with placeholder "Add comment..." - Save (Ctrl+Enter) / Cancel (Escape) buttons - Animates in below the target line - Integrate into `GitDiffView.tsx` — gutter cells render in the diff line view - Wire to `useDiffCommentStore` **Files to create/modify**: - Create `apps/web/src/components/InlineReviewGutterCell.tsx` - Create `apps/web/src/components/InlineReviewEditor.tsx` - Modify `apps/web/src/components/GitDiffView.tsx` — integrate gutter cells **References**: - Paseo `review/surface.tsx:245-309` — `DiffGutterCell` + `InlineReviewGutterCell` - Paseo `InlineReviewEditor` pattern **QA Scenarios**: ``` Scenario: Create inline comment on diff line Tool: Playwright Preconditions: Git tab, file expanded Steps: 1. Hover over a gutter cell 2. Click "+" button 3. Type comment text 4. Click Save (or Ctrl+Enter) Expected: Comment thread appears below the line Evidence: .omo/evidence/task-7-comment-create.png ``` - [x] 8. **Frontend: InlineReviewThread component** **What to do**: - Create `apps/web/src/components/InlineReviewThread.tsx`: - Renders below a diff line when comments exist for that target - Each comment shown as a card: avatar placeholder, body, timestamp, edit/delete actions - Collapsed state shows comment count badge - Expanded state shows full thread - Integrate into `GitDiffView.tsx` below diff line rows **Files to create/modify**: - Create `apps/web/src/components/InlineReviewThread.tsx` - Modify `apps/web/src/components/GitDiffView.tsx` — render thread below lines **Reference**: - Paseo `review/surface.tsx:537-573` — `InlineReviewThreadContent` **QA Scenarios**: ``` Scenario: Comment thread displays and supports edit/delete Tool: Playwright Preconditions: Comments exist on a diff line Steps: 1. Expand comment thread 2. Verify comment body is visible with timestamp 3. Click edit → modify text → save 4. Click delete → verify comment removed Expected: Full CRUD works on comments Evidence: .omo/evidence/task-8-thread.png ``` - [x] 9. **Frontend: File editing in the file tree** **What to do**: - In `RightRail.tsx`, add a file edit mode: - Double-click a file in the tree (or context menu "Edit") enters edit mode - The file row transforms: file name becomes a monospace textarea pre-filled with file content (fetched via existing `api.projects.viewFile`) - The row shows Save / Cancel buttons - Save: calls `api.projects.writeFile(projectId, path, content)` — the new endpoint from Task 2 - Cancel: reverts to the original content and exits edit mode - After save: re-fetch the file tree + emit `git_diff_refresh` - Only one file editable at a time (close any existing editor before opening new) - Visual indicator (highlighted row) when in edit mode **Files to modify**: - `apps/web/src/components/RightRail.tsx` — add edit mode state, edit UI - `apps/web/src/api/client.ts` — add `writeFile` method (from Task 2) - `apps/web/src/components/TreeLevel.tsx` (inline in RightRail) — accept edit mode props **References**: - Existing `RightRail.tsx:170-175` `openFile` function — pattern for file interaction - Existing `FileViewerOverlay.tsx` — Shiki highlighting reference - Paseo `file-explorer-pane.tsx` — context menu actions pattern **QA Scenarios**: ``` Scenario: Edit file in file tree and save Tool: Playwright Preconditions: Project with a text file Steps: 1. Double-click a file in the file tree 2. Verify file enters edit mode (textarea replaces filename) 3. Modify content 4. Ctrl+Enter to save 5. Verify success indicator Expected: File content updated on disk, tree refreshes Evidence: .omo/evidence/task-9-edit-save.png Scenario: Cancel file edit reverts changes Tool: Playwright Preconditions: File in edit mode Steps: 1. Modify content in textarea 2. Click Cancel / press Escape 3. Re-open file Expected: Original content preserved, edit mode exited Evidence: .omo/evidence/task-9-edit-cancel.txt ``` --- ## Final Verification - [ ] F1. **Plan Compliance Audit** — `oracle` Verify all Must Have features are implemented, Must NOT Have are absent. Output: VERDICT - [ ] F2. **Code Quality** — `unspecified-high` Run `pnpm -C apps/web build`, `pnpm -C apps/server build`, check for `as any`/`@ts-ignore`/console.log. Output: VERDICT - [ ] F3. **Real Manual QA** — `unspecified-high` + `playwright` Execute all QA scenarios from every task, capture evidence. Output: Scenarios [N/N pass] - [ ] F4. **Scope Fidelity** — `deep` Verify spec matches implementation, no scope creep. Output: Tasks [N/N compliant] --- ## Commit Strategy - **1**: `feat(server): add whitespace param to git diff + write_file endpoint` - **2**: `feat(web): diff preferences hook, toolbar toggles, split layout` - **3**: `feat(web): inline diff comments with zustand store` - **4**: `feat(web): in-browser file editing in file tree` --- ## Success Criteria ### Verification Commands ```bash pnpm -C apps/web build # Must pass pnpm -C apps/server build # Must pass ``` ### Final Checklist - [ ] Side-by-side diff renders correctly - [ ] Hide whitespace re-fetches with `-w` - [ ] Wrap lines toggles CSS - [ ] Expand/Collapse all toggles - [ ] Inline comments: create, read, update, delete - [ ] File editing: read, modify, save, cancel - [ ] All preferences survive page reload