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>
27 KiB
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
- Feature specification: feature-specification.md
- Specification decision log: artifacts/decision-log.md
- Specification team findings: artifacts/team-findings.md
- Specification decisions this plan inherits: D1–D18
- Specification open items this plan must respect or resolve: None — the spec's only open item (commit identity) was settled at spec time (F3, D12).
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-3–D-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 | T1–T12: 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).autoSelectModeandcanCommitare inline tested helpers, not separate modules (D-12). - New routes in
apps/server/src/routes/projects.ts, besideGET /api/projects/:id/git: readGET /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 viapath_guard.tsresolveProjectRoot, never from the request (D-4). - Frontend — a Files / Git tab in the existing
RightRail.tsxheader (replacing the static "Files" label, fitting one line); a newGitDiffViewin the same slot as the file tree; auseGitDiffhook; the dirty dot fed byuseProjectGit's existingis_dirty(D-15). Per-file expand/collapse is local state inGitDiffView, 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
runGitcalls under a 30s deadline and a 10MBmaxBuffer(D-7);parseNameStatusbuilds the file list,splitDiffByFilesegments the unified diff,classifymarks binary/large bodies,detectInProgressreads.gitsentinels, and the response carries the resolved base label and the in-progress flag. - Mode/base:
autoSelectModepicks Uncommitted (dirty) or Committed (clean) on first open only (D-12);resolveCommittedBaseresolves@{upstream}→origin/HEAD→ null, falling back to a labeled Uncommitted on null (D-6). - Refresh: the client
git_diff_refreshsessionEvent fires on the five triggers, coalesced behind an in-flight ref so a running refresh absorbs later triggers (D-8). - Diff render:
GitDiffViewhighlights a file's diff via Shikilang:'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 T1–T7 |
| 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 T9–T12 |
| 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 1–4 (read + display); Phase 2 = units 5–6 (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 (T1–T12). 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);canCommitgating (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(thepath_guard.test.tsintegration 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 (T1–T7, pure helpers) + integration (T8–T12, 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-); neverhostExec(shell)(D-3). User text (commit message, file targets) is always discrete argv. - Path traversal —
pathGuard(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 viapath_guard.ts, never the request. - Identity spoofing — commit identity is server-derived (
-c user.name/-c user.emailfrom git config, falling back to theproject_bootstrap.tsconstants); 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'sconnect-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). Nosudo 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
sessionEventsevent, not a WS frame, sopackages/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_refreshtriggers absorb into the running refresh — one read at a time, settling to a single final snapshot (D-8). - Graceful degradation:
detectInProgress(.gitMERGE_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 T1–T12 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 T1–T12; needs the helper signatures fromgit_diff.tsand 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 3–4 and 6; needs theRightRail.tsxheader 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
sessionEventseventgit_diff_refresh— no trigger needs a server push, so a WS frame and a@boocode/contractsrebuild 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:
execFilemaxBuffer 10MB covers any realistic single-user working-tree diff; ~40–50 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
- Outcome delivered: A Files / Git tab in the right-side file panel that reads and (whole-file) stages, unstages, commits, and discards the project repository's changes, built entirely in
apps/server. - Team size: 6 specialists — see artifacts/implementation-iteration-history.md
- Rounds of facilitation: 1 — see artifacts/implementation-iteration-history.md
- Decisions committed: 15 — see artifacts/implementation-decision-log.md
- Decisions settled by evidence: 15 — see artifacts/implementation-decision-log.md
- Decisions settled by junior-developer reframing: 0 (the junior-developer's coupling flag seeded the evidence correction on D-1, but the resolution was evidence) — see artifacts/implementation-decision-log.md
- Decisions settled by user input: 0 — see artifacts/implementation-decision-log.md
- Rejected alternatives recorded: 22 — see artifacts/implementation-decision-log.md
- Open items remaining: 0
- Recommendation: Ship as planned — build Phase 1 (units 1–4) first, then Phase 2 (units 5–6).