New @boocode/ion package (v0.0.1) for inference optimization network. .codesight/ wiki artifacts for codebase documentation. .omo/ work plans for openspec cleanup and enhanced file panel.
486 lines
19 KiB
Markdown
486 lines
19 KiB
Markdown
# 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<string, DiffComment[]>` 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
|