G
YouTube TikTok Games
Skip to content
JanSmrckaPublic

About

Review what your coding agent changed — without leaving the terminal. Comment on lines, send to the agent's pane (tmux/herdr).

Topics

Resources

Contributing

Stars

62 stars

Watchers

2 watching

Forks

Repository files navigation

differ

differ

Review what your coding agent changed — without leaving the terminal.

install · review loop · agents · keys · config · contributing

latest release CI status MIT license Go version works with herdr good first issues


differ: reading an agent's changes, commenting on a line and sending it to the agent's pane

You read the changes file by file, comment on the lines that need it, and send those comments to the agent's pane in one keystroke. differ tracks what you have read and carries the result; it does not review anything for you.

  • comment on the line, send in one key — comments sit inline under the line they are about; S sends them as references the agent can open. review loop →
  • works with herdr and tmux — finds the agents running beside you, sends the review to the one you pick, and under herdr follows it until it answers. Nothing to configure. agents →
  • remembers where you got to — every file is read, commented, sent or changed, and the review survives a crash or a quit.
  • the diff does not move under your comments — it follows the agent live until you comment, then holds your place and tells you it moved.
  • opens your editor at the line — reusing an nvim already open in the same tmux session or herdr workspace. editor →
  • an ordinary diff viewer too — syntax highlighting, split view, staging, committing, branches, commit log. keys →
  • one Go binary — no model reads your diff; nothing leaves your machine except what you send.

Install

brew install jansmrcka/tap/differ
go install github.com/jansmrcka/differ@latest

Or from source: make build (→ bin/differ) or make install (→ $GOPATH/bin).

Usage

differ              # all changes (staged + unstaged + untracked)
differ -s           # staged only
differ -r main      # compare against a branch, tag or commit
differ -c           # open straight into the commit message
differ review       # the same changes, straight into the first diff
differ log          # browse recent commits
differ commit       # review what is staged, then commit

review takes -s and -r too. --no-color, or NO_COLOR set to anything, turns colour off — nothing in differ is distinguished by colour alone.

Exit codes: 0 fine, 1 a runtime problem (not a repository, no such ref), 2 a bad command line.

The review loop

differ review        # the agent's changes, opened on the first diff
j / k                # move down the diff
c                    # comment on this line — type, then ctrl+s
J                    # next file; the one you left is marked read
S                    # send every pending comment to the agent

writing a comment on the line under the cursor the history of what was sent, and where

What that gives you over git diff:

  • It remembers where you got to. Every file is unreviewed, read, commented, sent or changed, and the bar keeps the count: 3/7 reviewed 2 comments 1 pending 1 changed.
  • Comments attach to lines. They show inline under the line they are about and stay pending until you send them.
  • The diff does not move under your comments. A diff with nothing written against it follows the agent live. Once you have commented on a file, an agent rewrite keeps your place and the bar says diff moved (2 more added) — R to reload. Comments follow their line either way; one whose line is gone goes stale rather than pointing at whatever took its place.
  • They survive q. The review is written to .git/differ/review.json on every change, so a crash, a closed tmux window or an accidental quit costs nothing. A comment comes back only if the code it was written about is byte-for-byte unchanged — otherwise the file goes back to unreviewed, because it needs reading again.

Nothing here touches git. Staging and committing stay explicit.

Sending to an agent

A lists the coding agents running in tmux, herdr or zellij and sends the review to the one you pick. Which of them is detected from the environment, nothing to configure; inside none, the review goes to the clipboard or stdout.

Choosing the agent

 agent

 filter…                                                                     4/4

▌ differ:2  claude  /Users/you/git/private/differ
  ELI-panda:2  claude  /Users/you/git/work/ELI-panda
  personal-web:2 %5  opencode  /Users/you/git/private/personal-web
  personal-web:2 %6  claude  /Users/you/git/private/personal-web

 type filters · ↑/↓ · enter chooses · esc closes

Typing narrows the list — by session or workspace, agent, state or title — and esc clears the filter before it closes the picker. The choice applies at once and is written to the config, so it survives a restart.

Under tmux the agent in differ's own tmux session sorts first, then one working in this repository; two in the same window are told apart by pane id. Discovery walks each pane's process tree rather than reading its current command, so an agent started through npx, a shell function or a wrapper script is still found. It knows claude, codex, gemini, copilot, opencode and aider, including forms like npx @anthropic-ai/claude-code and uvx --from aider-chat aider. It never starts an agent, and it will not offer a one-shot such as claude -p. The review is pasted, not submitted: tmux cannot tell an agent from a shell, so differ never presses Enter.

Under herdr herdr is asked (herdr agent list), so each row also shows the agent's state and what it says it is working on, and an agent that is blocked, idle or done sorts above one that is working. The review is submitted with herdr agent prompt, which refuses before sending anything if the agent is blocked on a question — the comments then stay pending and the bar says why. differ remembers the agent by its own session id, so moving its pane does not lose it.

After a send under herdr, differ follows the agent: the comments and H say agent working, then agent answered (or agent waiting for you), and the bar points out a reviewed file that changed in the meantime.

What the agent receives, per comment:

@src/session.ts :L8

this drops the error instead of returning it — the caller cannot tell

The reference carries the file and the line, so nothing repeats them and the agent reads the code itself. It is the form sidekick.nvim emits, and the space before the colon matters: without it the resolver reads @path:9 as one filename and attaches nothing. Claude Code's Neovim integration does not use this form at all — it sends JSON-RPC over a websocket, which is a different channel.

How much of a reference a comment gets depends on what still resolves:

the line resolves @src/cache.ts :L12
only the file does @src/cache.ts
nothing does no reference; the prose still says where it was

The middle case is a line number that would be wrong: the old side of a diff, or the index under -s when the working tree has moved on. Those comments carry the hunk instead, since it is then the only thing that says which code is meant:

@src/api/client.ts
Line: 10 (old)

Changed code:
-  return fetch(url)
+  return await fetch(url)

Comment:
this needs awaiting too

s sends one comment, S all of them, H shows what was sent and whether it arrived. A failed send never discards anything — the comments stay pending. differ pastes into the agent's prompt and does not press Enter, so a misconfigured target cannot execute anything.

Keyboard shortcuts

? lists the current view's keys. The bar along the bottom shows the common ones and hides what would do nothing; a picker or input shows its own. The tables below are checked against the code — a test fails if a key here has no handler, or a handler is missing here.

the ? overlay listing every key for the diff view

Everywhere

Except while typing.

Key Action
? every key for this view
H what has been sent, and whether it got there
! the last failure, in full
t theme picker
A choose the agent reviews go to
ctrl+c quit immediately

File List

 ● M login.ts                +12 -4
   M a/index.ts               +8 -2
   A format.ts               +31 -0
   ? NOTES.md                 +3 -0

● is staged. Once a file has been read, the right-hand column shows review state instead — read, 2 comments, sent, changed.

Key Action
j/k navigate files
enter / l view diff
tab stage/unstage file
a stage all
C commit (AI-generated message via claude)
b open branch picker
v toggle split (side-by-side) diff
e open in editor — differ keeps running
P push (auto --set-upstream if needed)
F pull (fast-forward only)
g/G first/last file
q quit

Diff View

Reading and reviewing are one view: comment on the line you are reading. Nothing here changes git state except tab.

Key Action
j/k move line cursor
} / { next/prev hunk
d/u half page down/up
g/G first/last line
J/K next/prev file
c comment on line (edit existing)
C comment on whole hunk
x delete comment under cursor
s send comment under cursor
S send all pending comments
R reload after the file changed
tab stage/unstage
b open branch picker
v toggle split diff
e open in editor at the cursor's line
esc / h back to file list
q quit

Marks in the gutter: ▌ the line cursor, ● a comment on this line, ! a comment that no longer matches the diff. At the end of the code: › the line was cut to fit, · trailing whitespace.

Split view shades the part of a line that actually differs, so a one-character change does not read as a rewrite. It falls back to unified when the panel is too narrow for two columns.

Commit Mode

Key Action
enter confirm commit
esc cancel

Branch Picker

╭───────────────────────────────────────────────────────╮
│  branch                                               │
│                                                       │
│  > feat/                                        12/34 │
│                                                       │
│ ▌*  feat/roadmap-59                                   │
│     feat/payload-90                                   │
│     feat/agent-picker-84                              │
│                                                       │
│  type filters · ↑/↓ · enter switches · ^n new · esc   │
╰───────────────────────────────────────────────────────╯
Key Action
type filter branches
up/down navigate
ctrl+k/ctrl+j navigate
enter switch branch (asks again if uncommitted changes would move)
ctrl+n create new branch
esc clear filter / close

Log

differ log browses recent commits; enter shows one through the same diff renderer.

Key Action
j/k move through commits
g/G newest/oldest
enter view the commit's diff
esc back to the list, from a diff
q quit

Themes

differ --theme gruvbox

mocha (default, also dark), latte (also light), gruvbox, tokyonight, github. Each is paired with the matching Chroma style so the chrome and the syntax highlighting agree, and all five are held to a contrast floor by a test.

t opens a picker that repaints the screen as you move, so you choose by looking rather than by name. enter keeps it and writes it to the config.

Configuration

Config file: ~/.config/differ/config.json

{
  "theme": "mocha",
  "tab_width": 4,
  "commit_msg_cmd": "claude -p",
  "commit_msg_prompt": "Write a concise git commit message for this diff:",
  "editor_cmd": "",
  "editor_strategy": "auto",
  "editor_panes": [],
  "editor_target": "session",
  "editor_line_args": "",
  "editor_timeout_ms": 0,
  "editor_probe_timeout_ms": 0,
  "split_diff": false,
  "feedback_target": "clipboard",
  "tmux_target": "",
  "herdr_target": "",
  "herdr_pane": "",
  "zellij_target": ""
}

feedback_target is where a review goes: clipboard (default), stdout, tmux, herdr or zellij. A sets it for you, along with tmux_target, zellij_target or the herdr agent's session id (herdr_target) and the pane it was in (herdr_pane). The clipboard target shells out to pbcopy or wl-copy/xclip/xsel, so it does the right thing over SSH.

commit_msg_cmd generates a commit message from the staged diff when you press C; it is pre-filled for you to edit. Any command that reads a diff on stdin works, and differ carries on without one.

Editor

e opens the file under the cursor — at the cursor's line, from the diff or a review. differ keeps running.

Which editor: editor_cmd, else $EDITOR, else vi. editor_cmd supports {file}, {repo} and {line}, substituted inside a single argument so spaces need no quoting. Without {file} differ adds the line itself for editors it knows (+<line> for vi-likes, --goto for code); editor_line_args is the escape hatch for one it does not:

{ "editor_line_args": "{file}:{line}" }     // helix, sublime
{ "editor_line_args": "+{line} {file}" }    // emacs, micro

Where it opens — editor_strategy:

value what happens
auto (default) reuse an nvim already open in this tmux session or herdr workspace → else a new tmux window or a pane beside differ in herdr → else take over differ's terminal
reuse only reuse; say so when there is nothing to reuse
window always a new tmux window (a new pane in herdr)
inline take over differ's terminal, resume when the editor exits
detach run it in the background and carry on

Started from nvim's own :terminal ($NVIM is set, e.g. differ in a float over your editor), auto and reuse open the file in that nvim instead: a float closes, a split hands focus back, and differ keeps running in the hidden terminal buffer.

Outside tmux and herdr, reuse and window are refused with a message rather than silently downgraded. Reuse needs nvim's RPC socket and sends :drop, so nothing unsaved is ever at risk, then focuses that pane. In herdr it never leaves differ's workspace unless editor_target says so; the new pane is quoted for the shell it runs, fish included. editor_panes and editor_target (any, or a session / workspace name) adjust which panes it will consider and how far it reaches.

GUI editors reuse their own window already, so give them detach:

{ "editor_cmd": "code -r --goto {file}:{line}", "editor_strategy": "detach" }
{ "editor_cmd": "zed {file}:{line}",            "editor_strategy": "detach" }

An editor_cmd starting with tmux runs exactly as written and ignores editor_strategy.

tmux and herdr

The layout differ is built for is an agent in one pane and differ in another. It works the same inside tmux or herdr, and detects which from the environment:

tmux herdr
finding agents walks each pane's process tree asks herdr agent list, with each agent's state
sending pasted, never submitted submitted with herdr agent prompt
after a send — follows the agent: working → answered
e reuses nvim in the session, else a new window reuses nvim in the workspace, else a pane beside differ

Zellij is supported for sending reviews: A lists the panes whose foreground command is an agent (zellij action list-panes), and the review is pasted with zellij action paste and never submitted, as in tmux. The choice is stored as zellij_target. e does not know Zellij yet.

To open differ as a tmux popup:

bind g display-popup -E -w 90% -h 90% "cd #{pane_current_path} && differ"

When something fails

One line, in differ's own voice, with what to do:

push failed  ·  ! details  ·  the remote has commits you do not — pull with F first

! shows what git, tmux or the clipboard helper actually said. A failure costs you nothing else — not your place in the diff, not the comments you have written.

Contributing

Contributions are very welcome — bug reports, ideas, docs, a new theme, support for another agent, or code. You do not need to ask first: open an issue or a pull request.

If you are an AI agent working on this repository, read CLAUDE.md first: it has the rules that are not obvious from the code.

About

Review what your coding agent changed — without leaving the terminal. Comment on lines, send to the agent's pane (tmux/herdr).

Topics

Resources

Contributing

Stars

62 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages