Files
boocode/docs/features/git-diff-panel/feature-implementation-plan.md
indifferentketchup ca028a4024 docs: add git-diff-panel implementation planning artifacts
Implementation decision log, iteration history, synthesis input, the
implementation plan, and discovery notes for the git-diff-panel feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 02:26:04 +00:00

27 KiB
Raw Blame History

Feature Implementation Plan: Git Diff Panel

A Files / Git tab in the right-side file panel that reads the project repository's diff (two modes) and stages, unstages, commits, and discards whole files — built entirely in apps/server (D-1), shipped via a single docker rebuild, in two phases (read/display, then write actions) with no schema change, no migration, and no @boocode/contracts change.

Source Specification

Outcome

When this plan is executed, the right-side file panel gains a Files / Git tab. Selecting Git shows the project repository's changed files (Uncommitted or Committed mode), each expandable to a syntax-highlighted unified diff. The user can stage, unstage, commit (with a server-derived identity), and discard whole files without leaving the session. The work is delivered by a new apps/server/src/services/git_diff.ts (read logic + git-write helpers + pure helpers), new read and write routes in apps/server/src/routes/projects.ts, and new apps/web UI (GitDiffView, a tab in RightRail.tsx, a useGitDiff hook, and a git_diff_refresh sessionEvent). No apps/coder change, no Postgres schema change, no new env var.

Context

  • Driving constraint: A direct user request (2026-06-02) to add a Paseo-style git diff panel shown "instead of the file browser." Not deadline-bound; scoped to ship as a coherent v1.
  • Stakeholders: The single session user (reviews and commits their own repo changes in-session). The project's security posture (the write surface must not become an assistant-reachable path). On-call/operability (the panel must not stall on large or slow repos or fail opaquely under index contention).
  • Future-state concern: The git-write surface now lives in apps/server. Watch for a third consumer of git ops (would trigger a shared-packages/ extraction, D-1) and for working-tree diffs routinely exceeding the 10MB read buffer (D-7).
  • Out-of-scope boundary: No remote operations (push/pull/PR/merge), no side-by-side layout, no per-hunk staging, no continuous file-watch streaming, no rename of the pending-changes panel, no per-line review/re-prompt surface — all deferred in the spec under YAGNI.

Team Composition and Participation

Specialist Status Key Input
project-manager Coordinator Facilitated R1, corrected the service-owner decision against evidence, synthesized the plan.
software-architect Active Recommended a read-server/write-coder split (R1); the split's premise was refuted by evidence and recorded as the rejected alternative on D-1. Owns the read shape, base resolution, refresh wiring, two-phase sequencing.
adversarial-security-analyst Active Argv-safety, path-traversal, server-derived identity, tool-registry exclusion, artifact-sandbox evidence (D-3D-5, D-13).
on-call-engineer Active Read deadline + maxBuffer, index-lock 409, refresh coalescence, in-progress detection, partial-failure honesty (D-7, D-8).
test-engineer Active T1T12: pure-helper units + temp-repo integration; skip Shiki/layout (no web harness).
junior-developer Reframer Flagged the cross-service coupling of the write-in-coder split, which seeded the evidence correction on D-1; confirmed the read-only-session Git tab needs no extra label (F21).

Implementation Approach

Both the git read and the git write operations live in apps/server (D-1) — reusing the existing safe argv runGit and the project_bootstrap.ts server-side git-write precedent, and avoiding a cross-service proxy hop, a second deploy surface, and coder coupling. The feature reuses Shiki (lang:'diff') already in apps/web, the useProjectGit dirty signal, and the existing RightRail.tsx file-panel host; it introduces one new server service, new routes, one new web view + hook, and one new client sessionEvents event.

Architecture and Integration Points

  • New apps/server/src/services/git_diff.ts — read logic, git-write helpers, and TDD-first pure helpers (parseNameStatus, splitDiffByFile, resolveCommittedBase, autoSelectMode, classify, detectInProgress) (D-2). autoSelectMode and canCommit are inline tested helpers, not separate modules (D-12).
  • New routes in apps/server/src/routes/projects.ts, beside GET /api/projects/:id/git: read GET /api/projects/:id/git/diff?mode=<uncommitted|committed> (D-2); writes for stage / unstage / commit / discard (D-3). The repository root is derived server-side via path_guard.ts resolveProjectRoot, never from the request (D-4).
  • Frontend — a Files / Git tab in the existing RightRail.tsx header (replacing the static "Files" label, fitting one line); a new GitDiffView in the same slot as the file tree; a useGitDiff hook; the dirty dot fed by useProjectGit's existing is_dirty (D-15). Per-file expand/collapse is local state in GitDiffView, not a shared hook (D-11).

Data Model and Persistence

None. The panel reads git state at request time and writes the project repository directly via git; nothing is persisted in Postgres — no schema change, no migration, no new env var (D-10).

Runtime Behavior

  • Read: the read route runs argv runGit calls under a 30s deadline and a 10MB maxBuffer (D-7); parseNameStatus builds the file list, splitDiffByFile segments the unified diff, classify marks binary/large bodies, detectInProgress reads .git sentinels, and the response carries the resolved base label and the in-progress flag.
  • Mode/base: autoSelectMode picks Uncommitted (dirty) or Committed (clean) on first open only (D-12); resolveCommittedBase resolves @{upstream}origin/HEAD → null, falling back to a labeled Uncommitted on null (D-6).
  • Refresh: the client git_diff_refresh sessionEvent fires on the five triggers, coalesced behind an in-flight ref so a running refresh absorbs later triggers (D-8).
  • Diff render: GitDiffView highlights a file's diff via Shiki lang:'diff' lazily on expand, with a per-file loading state (D-15).

External Interfaces

New HTTP routes only (read + four writes) under /api/projects/:id/git/…. Refresh is a client-side sessionEvents event (git_diff_refresh), not a WS frame, so @boocode/contracts is not touched or rebuilt; the only parity step is a no-op case 'git_diff_refresh' in useSidebar.ts applyEvent (D-8, D-9).

Decomposition and Sequencing

Two phases on a single deploy surface (D-14); both ship via docker compose up --build -d boocode.

# Work Unit Delivers Depends On Verification
1 git_diff.ts pure helpers (TDD-first) parseNameStatus, splitDiffByFile, resolveCommittedBase, autoSelectMode, classify, detectInProgress (D-2, D-6) Unit tests T1T7
2 Read route GET /api/projects/:id/git/diff?mode= Diff payload (files, counts, base label, in-progress flag) under 30s/10MB bounds (D-2, D-7) 1 Temp-repo integration T8
3 useGitDiff + RightRail Files/Git tab + dirty dot Read-only panel, tab switch, dirty indicator from useProjectGit (D-15) 2 Manual (no web harness)
4 GitDiffView read-only + lazy Shiki + refresh wiring Per-file expand, lazy lang:'diff', git_diff_refresh sessionEvent + useSidebar.ts no-op case, coalescence ref (D-8, D-15) 3 Manual
5 Write helpers + routes (stage/unstage/commit/discard) Argv-safe writes with -- separators, pathGuard, server-derived -c identity, .strict() commit schema, 409 on index-lock (D-3, D-4, D-5, D-7); endpoints kept out of ALL_TOOLS (D-13) 1, 2 Temp-repo integration T9T12
6 Write affordances + in-progress disable Stage/unstage/commit/discard UI, tracked/untracked discard confirmation, in-progress disable (D-7, D-15) 4, 5 Manual

Phase 1 = units 14 (read + display); Phase 2 = units 56 (write actions).

RAID Log

Assumptions

ID Assumption What Changes If Wrong Verifier Status
A1 The boocode container keeps the /opt:/opt read-write mount, so apps/server can write project paths. D-1 reopens — writes would need a host service. docker-compose.yml:16 Verified (R1)
A2 A realistic single-user working-tree diff stays under 10MB. The read truncates/errors; the streaming reader reopens (Deferred YAGNI). on-call-engineer Unverified — bounded by D-7

Dependencies

ID Dependency Owner Status
Dep1 Shiki ^1.29.2 (lang:'diff') already in apps/web. apps/web Present (no install)
Dep2 useProjectGit already returns is_dirty for the dirty dot. apps/web Present (no new fetch)

Testing Strategy

Sourced from test-engineer (T1T12). Server-side only — apps/web has no test harness, so Shiki rendering and layout/tab behavior are verified manually, not in the suite. Vitest conventions: globals:false (import describe/it/expect), .js import suffixes, include glob src/**/__tests__/**/*.test.ts.

  • Observable behaviors to test: porcelain → file-list parse with change types (T1); unified-diff split per file (T2); base resolution @{upstream}origin/HEAD → null (T3); auto-select mode by dirty/clean (T4); binary/large classify (T5); in-progress sentinel detection (T6); canCommit gating (T7); read route over a temp repo (T8); stage/unstage round-trip (T9); commit with server-derived identity (T10); discard tracked vs untracked (T11); path-guard rejection of ../absolute/symlink-escape and repo-root discard (T12).
  • Test doubles posture: pure helpers tested directly with fixture strings (no git spawn); route/write tests use a real temp repo via mkdtemp + git init (the path_guard.test.ts integration pattern).
  • Edge cases requiring coverage: binary file, oversized diff, no resolvable base, in-progress state, index-lock 409, untracked-discard partial failure, path-escape attempts (D-4, D-7).
  • Test levels: unit (T1T7, pure helpers) + integration (T8T12, temp-repo). No end-to-end / web-layer automation.

Security Posture

adversarial-security-analyst contributed the full write-surface posture. Threat vectors and the mitigations this plan commits to:

  • Command/flag injection — every git invocation uses discrete argv with explicit -- separators between options and user-supplied paths and a flag-injection guard (reject path args starting with -); never hostExec(shell) (D-3). User text (commit message, file targets) is always discrete argv.
  • Path traversalpathGuard(repoRoot, file) resolves each per-file argument and rejects absolute paths, .. traversal, and symlink escape outside the server-derived root; discarding the repo root (.) is rejected (D-4). The root is derived from the project record via path_guard.ts, never the request.
  • Identity spoofing — commit identity is server-derived (-c user.name/-c user.email from git config, falling back to the project_bootstrap.ts constants); the commit request schema is Zod .strict() with {message, files?} only — no author/email/date field can be supplied (D-5).
  • Assistant-driven invocation — the write endpoints are never registered in ALL_TOOLS, so no assistant tool surface reaches them; the indirect artifact path is already closed by the artifact iframe's connect-src 'none' sandbox (F1), so no new CSRF mitigation is built (D-13).

Operational Readiness

  • Deploy surface: single — docker compose up --build -d boocode (rebuilds web + server from the working tree). No sudo systemctl restart boocoder, because no apps/coder code changes (D-1).
  • Schema / migration / env: none — no Postgres change, no migration, no new env var (D-10).
  • Contracts package: not touched — refresh is a client sessionEvents event, not a WS frame, so packages/contracts/ is not rebuilt (D-9).
  • Feature flag / rollout: none — the panel is additive and inert until the Git tab is opened; rollback is reverting the build.

On-Call Resilience Posture

on-call-engineer contributed; each commitment maps to a flagged failure mode.

  • Timeouts and deadlines: the read path runs under a 30s execution deadline and a 10MB maxBuffer, both distinct from the D5 per-file display-size cap; a read that exceeds the deadline exits loading, shows an error, and offers Refresh (D-7).
  • Retry strategy: none on the server. An index-lock write failure returns HTTP 409 "repository busy"; retry is user-driven, so no timer state or hidden busy state is introduced (D-7).
  • Concurrency / coalescence: the client holds an in-flight coalescence ref so concurrent git_diff_refresh triggers absorb into the running refresh — one read at a time, settling to a single final snapshot (D-8).
  • Graceful degradation: detectInProgress (.git MERGE_HEAD / rebase / CHERRY_PICK_HEAD / BISECT_LOG sentinels) folds an in-progress flag into the read response; the client disables write affordances when set, so stage/commit/discard never fail with raw git errors mid-operation (D-7).
  • Data integrity: an untracked-file discard that fails partway reports honest partial failure on the next refresh rather than claiming an unenforceable "state unchanged" (D-7).

Definition of Done

  • Opening the Git tab shows the project repo's changed files in the auto-selected mode; switching to Files restores the tree in the same slot (D-15).
  • Committed mode labels its resolved base (@{upstream}origin/HEAD → labeled Uncommitted fallback) (D-6).
  • Stage / unstage / commit / discard operate at whole-file granularity in Uncommitted mode; discard prompts the tracked vs untracked confirmation; Committed mode is read-only (D-3, D-15).
  • Commit identity is server-derived; the request cannot set it; the schema is .strict() (D-5).
  • Path-escape and repo-root discard are rejected; writes use argv + -- separators (D-3, D-4).
  • Write endpoints are absent from ALL_TOOLS (D-13).
  • Read respects the 30s/10MB bounds; index-lock returns 409; in-progress disables writes; refresh coalesces (D-7, D-8).
  • Tests T1T12 pass (pnpm -C apps/server test).
  • Phase 1 ships and is reviewable before Phase 2 lands (D-14).
  • Post-ship owner: the session user (single-user app); no separate on-call rotation.

Specialist Handoffs for Implementation

  • test-engineer — dispatch at the start of unit 1 to own T1T12; needs the helper signatures from git_diff.ts and the temp-repo integration pattern (path_guard.test.ts).
  • adversarial-security-analyst — dispatch to review unit 5 before merge; needs the write-route handlers and the .strict() commit schema to confirm argv-safety, path-guard coverage, and identity derivation.
  • user-experience-designer — dispatch during units 34 and 6; needs the RightRail.tsx header layout to confirm one-line fit (D18), tap-target minimum, and the tracked/untracked discard wording.

Deferred (YAGNI)

Shared packages/ runGit extraction

  • Why deferred: Rule of Three not met — only apps/server consumes the git ops for this feature (coder is untouched); a single-consumer abstraction is premature.
  • Reopen when: a third consumer needs the same git ops.
  • Source: R1, software-architect.

New WS frame for refresh

  • Why deferred: Replaced by the simpler client sessionEvents event git_diff_refresh — no trigger needs a server push, so a WS frame and a @boocode/contracts rebuild are unnecessary (simpler-version test).
  • Reopen when: a refresh trigger genuinely originates server-side (an out-of-band repo mutation the client cannot observe).
  • Source: R1, software-architect / junior-developer.

Server-side retry on index-lock

  • Why deferred: "Try again" is user-driven; a server retry hides the busy state and adds timer state (evidence test — no observed contention).
  • Reopen when: index-lock contention is observed frequent in practice.
  • Source: R1, on-call-engineer (F5).

Streaming diff reader (cap mid-stream)

  • Why deferred: execFile maxBuffer 10MB covers any realistic single-user working-tree diff; ~4050 LoC for a transient memory spike that does not matter at this scale (simpler-version test).
  • Reopen when: working-tree diffs routinely exceed 10MB.
  • Source: R1, on-call-engineer.

CSRF custom-header on the write routes

  • Why deferred: connect-src 'none' on artifacts + the SameSite=Lax Authelia cookie + a same-origin SPA cover it at single-user scale (evidence test — no exposed cross-origin path).
  • Reopen when: the write routes are exposed to a less-controlled origin.
  • Source: R1, adversarial-security-analyst.

Separate per-file expand-state hook

  • Why deferred: Replaced by local state in GitDiffView — one consumer (single-implementation abstraction).
  • Reopen when: a second component needs the same expand state.
  • Source: R1, software-architect.

autoSelectMode / canCommit as separate modules

  • Why deferred: Kept as inline tested pure helpers in git_diff.ts — splitting into modules adds files without a second consumer (simpler-version test).
  • Reopen when: a second module needs to import them independently.
  • Source: R1, software-architect / test-engineer.

Open Items

  • None. Every Round-1 open question resolved from evidence; the spec's only open item (commit identity) was settled at spec time. The plan is shippable as written.

Summary