Skip to content

Repository files navigation

diffr

Local rich diff review with a human↔agent comment loop. A GitHub-PR-style diff viewer that runs against any git repo, lets you leave inline comments across multiple diff views, and (Phase 2) lets Claude Code read and reply to those comments live via MCP.

diffr — side-by-side diff review with a live human↔agent comment thread

Run

One always-on daemon serves every repo, so you start it once — not per repo.

npm install
npm run build                      # tsc (backend) + vite build (frontend → dist/public)
PORT=4100 node dist/cli.js serve   # one daemon for all repos
# open a repo:  http://localhost:4100/?repo=/path/to/your/repo

The frontend (src/public) is bundled by Vite; the backend (cli/server/mcp/git/store) is compiled by tsc. npm run build runs both and emits an unhashed dist/public/app.js + app.css that the daemon serves. The 12 color themes are fully self-contained — no network access. The palette lives in src/public/themes.ts; the per-theme syntax-token colors are hand-built static CSS in src/public/themes.css, transcribed from the bundled highlight.js theme files (highlight.js/styles/**) and git-diff-view's own token groupings. Switching themes retints the app chrome, the diff pane, and comment code blocks by swapping <html data-theme> and 9 CSS palette vars — no stylesheet reload.

Frontend development

For iterating on the frontend, run the daemon and the Vite dev server side by side (the dev server proxies /api and the SSE stream to the daemon):

npm run serve   # daemon on :4100 (git/store/API)
npm run dev     # Vite dev server (HMR) — open the URL it prints, with ?repo=…

Run npm test (Vitest) for the git/store/API and frontend-helper suites.

Pick the repo from the header dropdown, or pass ?repo=<abs-path> in the URL. The active repo (and diff view) live in the URL, so any view is linkable and survives refresh. A repo shows up in the dropdown once it's been opened (via the URL or by the agent).

State lives in ~/.diff/<key>/review.db (SQLite), keyed by the repo's canonical git-root path — not inside your repos, so there's nothing to .gitignore. (DIFF_HOME overrides the ~/.diff location.)

Daemon (lazy auto-start)

The daemon is a plain user process — no login item / launchd agent (those read as persistence to EDR/MDM tooling). Instead it starts on demand and is shared by every repo and session:

  • The agent starts it for you. If the daemon isn't up when an MCP tool runs, the MCP spawns it detached and retries — the agent loop just works.
  • A shell helper for the browser side (note: named diffr, not diff, so it doesn't shadow the diff binary). Add to ~/.zshrc:
diffr() {
  local repo
  repo=$(git -C "${1:-$PWD}" rev-parse --show-toplevel 2>/dev/null) \
    || { echo "not a git repo: ${1:-$PWD}"; return 1; }
  curl -sf -o /dev/null localhost:4100/api/repos 2>/dev/null \
    || ( nohup node /abs/path/to/diff/dist/cli.js serve >>~/.diff/daemon.log 2>&1 & )
  for _ in {1..20}; do curl -sf -o /dev/null localhost:4100/api/repos && break; sleep 0.15; done
  open "http://localhost:4100/?repo=$repo"
}

Then diffr (current repo) or diffr /path/to/repo opens the viewer, starting the daemon first if needed. It does not survive a reboot — the next diffr or agent tool call brings it back. Logs: ~/.diff/daemon.log. Stop it with pkill -f 'dist/cli.js serve'.

Agent loop (MCP)

The MCP server is a thin client of the daemon: each tool is an HTTP call to http://localhost:4100 (override with DIFF_URL) carrying the session's repo, so the daemon stays the sole owner of the git + store logic and the agent's writes push to the browser in-process. Register once, at user scope:

# register ONCE, available in every repo:
claude mcp add -s user diff-review -- node /abs/path/to/diff/dist/cli.js mcp

Auto-scoping: stdio MCP servers are spawned per session, with cwd set to where claude launched. The MCP resolves the git root from there and sends it as repo=…, so a single user-scope registration automatically operates on whichever repo each session is in — run 5 sessions across 5 repos and each one targets its own store. (DIFF_REPO overrides this if you ever need to pin one.) If the daemon isn't running when a tool is called, the MCP starts it on demand (see Daemon above), so no manual startup is needed.

Tools exposed to the agent:

  • get_open_comments — every open thread across views, with the diff hunk and comment history
  • reply_to_comment(thread_id, body) — reply without resolving
  • resolve_thread(thread_id, body?) — resolve, optional closing reply
  • add_comment(file, line, body, view?, side?) — proactively flag a line
  • get_diff_link(base, head?, pin?) — build a shareable URL that opens a specific diff view. head defaults to worktree; pin defaults to true, resolving both refs to SHAs so the link is immutable (pin:false keeps symbolic refs so it tracks the branch). Refs are validated, so a bad ref errors instead of yielding a dead link.

Every tool also takes an optional repo (absolute path to another checkout) to act on a repo other than the session's — omit it for the common case. So one session can read comments or build a link for a different directory without a separate registration.

Views

Switch the active diff at the top:

  • Uncommitted — working tree vs HEAD
  • Last commitHEAD~1 vs HEAD
  • Branch vs basemerge-base(main, HEAD) vs HEAD (only shown when the branch has diverged)
  • Custom… — compare any two refs (basehead, where head may be a ref, sha, or worktree).

Every view is just a base → head pair; the presets are buttons that prefill one. The active diff lives in the URL as ?base=…&head=…, so any view is linkable and survives refresh. A preset highlights when the current pair matches it, otherwise "Custom" is active.

Comments are scoped to the exact (base_sha, head_sha) they were made on, so they only show up in the view they belong to.

Comments

  • Toggle the diff layout (Inline / Side-by-side) in the header; the choice persists in localStorage. Inline commenting works in both.
  • Hover a line and click the + in the gutter to start a thread. Drag across the gutter to select a range of lines (opening the + on the last line of the selection) and the comment applies to the whole span — the thread shows as file:start-end.
  • Unchanged context between hunks is collapsed behind expanders; expand it and you can comment on those lines too. A comment anchored to a collapsed line auto-reveals just its surrounding hunk on load, so it renders in place.
  • Reply within a thread via a collapsed Reply… box that expands on click; each comment is tagged human or agent.
  • Edit or delete any comment inline (hover a comment to reveal the controls; delete uses a two-step confirm). Deleting a thread's last comment removes the now-empty thread.
  • Every composer — new comment, reply, edit — supports Esc to cancel and Cmd/Ctrl+Enter to submit.
  • resolve / reopen a thread, or mark seen.
  • Threads auto-flip to outdated when their anchored line changes (the line is hashed at comment time and re-checked on each view load). Resolved and outdated threads hide by default — toggle "show resolved/outdated" to bring them back, then reopen if needed.

Live updates: the page subscribes to server-sent events (scoped to the active repo) and re-renders threads whenever the store changes — including the agent's comments, which arrive as HTTP writes into the daemon. Because every writer is now the daemon process itself, an in-process wake pushes updates instantly; no SQLite-WAL file watching needed.

Architecture

  • src/git.ts — git plumbing (diffs, ref resolution, view presets, hunk extraction)
  • src/store.ts — SQLite store; central per-repo DBs under ~/.diff/<key>/
  • src/server.ts — always-on Hono daemon: per-repo store cache, JSON + agent API, in-process SSE, static frontend (sole owner of git/store/filesystem)
  • src/mcp.ts — MCP server (stdio); a thin HTTP client of the daemon
  • src/cli.tsserve (daemon) / mcp (agent) entrypoints
  • src/public/ — single-page Vue 3 app; renders diffs with @git-diff-view/vue, bundled by Vite

Status

  • Phase 1 — store + web app (views, comments, lifecycle, live refresh)
  • Phase 2 — MCP server: get_open_comments, reply_to_comment, resolve_thread, add_comment
  • Phase 3 — outdated-comment auto-detection, custom view picker, agent-context quality
  • Phase 4 — one always-on multi-repo daemon, central ~/.diff stores, repo picker, MCP-over-HTTP
  • Phase 5 — Vue 3 SPA frontend (@git-diff-view/vue), 12 color themes, multi-line range comments, commenting on expanded context lines, and an optional repo arg so MCP tools can target any checkout

About

A diff review tool, for agentic communication

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages