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.
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/repoThe 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.
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.)
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, notdiff, so it doesn't shadow thediffbinary). 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'.
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 mcpAuto-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 historyreply_to_comment(thread_id, body)— reply without resolvingresolve_thread(thread_id, body?)— resolve, optional closing replyadd_comment(file, line, body, view?, side?)— proactively flag a lineget_diff_link(base, head?, pin?)— build a shareable URL that opens a specific diff view.headdefaults toworktree;pindefaults totrue, resolving both refs to SHAs so the link is immutable (pin:falsekeeps 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.
Switch the active diff at the top:
- Uncommitted — working tree vs
HEAD - Last commit —
HEAD~1vsHEAD - Branch vs base —
merge-base(main, HEAD)vsHEAD(only shown when the branch has diverged) - Custom… — compare any two refs (
base→head, whereheadmay be a ref, sha, orworktree).
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.
- 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
humanoragent. - 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.
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 daemonsrc/cli.ts—serve(daemon) /mcp(agent) entrypointssrc/public/— single-page Vue 3 app; renders diffs with@git-diff-view/vue, bundled by Vite
- 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
~/.diffstores, 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 optionalrepoarg so MCP tools can target any checkout
