Use
slop-review # the VCS's own idea of "my unmerged work"
slop-review -r 'trunk()..@' # jj revset
slop-review -r 'master..HEAD' # git range
slop-review -b main # this checkout's work since it left main
slop-review --vcs git -r 'main..HEAD' --repo /path/to/repo
slop-review -o review.json # write the result to a file instead of stdout
slop-review --format json # JSON instead of prose (default: md)
slop-review --appearance light # `system` (default), `dark` or `light`
slop-review --view unified # `auto` (default), `unified` or `split`
slop-review --font-size 15 # diff text, in points
slop-review --ui-scale 1.25 # everything else
slop-review --log run.log # what the window did, and why it stopped
slop-review --log - # the same, on stderr
slop-review --print-default-config # the config that ships, as a starting point
slop-review vcs check # what gets run, and what is made of it
slop-review trust # let this repo's config define commands
-b (or --branch, or --bookmark) names the branch this work grew from,
and opens on everything here since it left that branch: main..HEAD under
git, main..@ under jj, spelled by each profile so a VCS of your own can say
something else. NAME is anything the VCS resolves — a jj workspace as
name@, say. With --repo pointed at a worktree, -b is how that worktree's
work is reviewed from wherever you are standing, which is what an agent
working in one and a reviewer sitting in another both want. -r and -b are
two spellings of the same thing and cannot both be given.
--log is for the run that went wrong. It writes what the window loaded,
what a background worker failed at or panicked over, and — as its last line —
why the run ended: the review filed, the window closed with nothing decided,
or the panic that took it. A file rather than the terminal by default,
because by the time you are asking why a window disappeared, the window has
disappeared. Nothing goes to standard output but the review, so a log on
stderr is still safe to parse around.
Click a line number to point the cursor at that line, drag down the numbers
to pick a run of them, or shift-click to extend the run you have. Picking is
all a click does: a strip under the run says which lines are picked and opens
the draft box when you ask for it, as does a second click on a line you have
already picked. The draft opens inline under the selection; Ctrl+Enter files
it, Escape throws it away. Suggest drops the selected lines into the
draft as a ```suggestion fence, which is how "this is wrong" becomes
"write this instead" without retyping the code. Filed comments stay in place
under the lines they annotate.
Not every remark is about a line. "Comment on this commit" in the left panel
annotates the change itself — where "this belongs in the previous commit" and
"the message says the opposite of the diff" go — and comment on file in the
file header annotates the whole file, which is where "this file should not be
here" and "there is no test beside it" go. That button is a toggle: it opens
the card, and pressing it again closes it, because the way out of a card you
opened by mistake should be the control you opened it with. Reopening it edits
the remark that is already there rather than filing a second one beside it.
Both land in the same list as line comments.
Every filed remark carries a remove button where it stands — under the lines it annotates, at the top of the file it is about, or beside the commit it was written on — as well as in the review panel's list, so taking one back does not mean hunting for it in a list of thirty. In that list a remark is also the way back to itself: click where it stands or the remark's own text and the window selects the commits it was written against, opens the file, and scrolls its card into view.
Shift-click in the commit list to review a run of commits as one accumulated
change, diffed against the parent of the first. The selection is always
contiguous: a diff of commits 1 and 3 without 2 is a change that never
existed. It is also always within one stack. A revset is not a range — master..
means master..HEAD in git but every visible head in jj, which in a
repository with a couple of worktrees is several unrelated branches and a few
abandoned ones. The list says how many heads it really selected, draws a line
where one stack ends and the next begins, and stops a drag at the edge of the
stack it started in, because diffing across the seam would compare two trees
with no history in common. A comment made on a range is reported against that range, and is not
shown while looking at one of its commits alone — its line numbers only mean
anything against the base they were taken from.
Pick a decision, write a summary, submit. The window closing without a decision is a cancellation, and exits non-zero — an agent can tell that apart from an approval.
A hunk taller than the panel takes its own strip off the top of the page with it, and the window sticks that strip back over the page: halfway down a long hunk you can still see which hunk it is, and tick it off from where you are. It is the strip of the hunk the page has been scrolled past — not the one the cursor is in — because the wheel moves the page and leaves the cursor where it was, and a strip naming a hunk that is nowhere near the top of the page is worse than no strip at all. Where each strip ended up is asked of the widgets after every scroll and every key, since the rows, the cards and the folded gaps between hunks are not all one height and counting them would be wrong by everything it passed. Scrolled on past the end of a hunk — into the lines that were folded away — nothing is stuck, because nothing is being read.
Every commit and every file has a tick box beside it, every hunk of a diff has one on the strip above it, a commit message has its own under the message, and both headings count how many are ticked, as does the header of the file being read. One click marks something read and one unticks it, which is all keeping your place should ever cost. The unread keys walk what is left rather than what is next: in the diff they step to the next hunk nobody has ticked off and then on into the next file with something left in it, and in either list to the next row of it still to be read. A run of commits reviewed together gets all reviewed under its summary, so ticking the run off is one click rather than the same work said three times.
Unwrapped and in one column, a line is as long as it is: the page is as wide as its longest line and goes sideways as well as down, so nothing is cut off at an edge that says nothing about there being more. Side by side it is not, because the two columns are each half of whatever the page is wide — a page sized to its longest line would put the second column off the right-hand edge for every row of the file, which is a worse bargain than the truncation it fixes. wrap is the answer there, and in one column too.
The file list of a single commit ends with commit description, which opens
the message as it was written rather than as a diff of it. It is there because
the message is one of the things there is to read — the commit is not reviewed
until it has been — so it belongs among the things being counted, and ] walks
onto it after the last file. Its box and the one under the message in the
details panel write the same mark: there is one message and one answer about
it.
Ticks fill in upwards, and are worked out rather than only written down. Reading every hunk of a file ticks the file, and a file whose every hunk is already ticked is ticked too — which is what puts the mark back when the key it was written under has moved, as it does whenever the file changes anywhere at all: a file is keyed by the whole of both its versions, while each hunk is keyed by its own changed lines and nothing else. That is worked out by the background pass rather than only when you open the file, so a branch rewritten under you — an absorb, a rebase that moves the hunk next to yours — comes back with its ticks where you left them instead of asking you to open every file to find out. Working it out means drawing the file, so the pass only draws one it could change the answer for: never a file already ticked, and never one holding no line you have ever ticked. The same happens as you go: a hunk is what its changed lines say it is, wherever it sits, so ticking one off has read it in every file that holds the same lines — the same rename made in twenty files, a range read after its commits, a commit read after its range — and the window draws those files in the background and ticks the ones that turn out to be read whole, rather than waiting for you to open each and watch it tick itself. Reading a commit's message and every file it changed ticks the commit, and a run whose every commit is read reads as all reviewed. The other direction does not follow, since ticking a commit off is a coarser statement than going through its files, and it leaves them to be gone through. Unticking takes back whatever vouched for the box — otherwise it would come straight back on — but only what actually did: a commit ticked off over unread files gives up its own tick and leaves them exactly as they were.
None of this travels back. A per-file verdict tells an agent nothing the comments do not already say — a file with a problem has a comment on it, and "read it, nothing to add" asks for no action — while costing a copy of the commit list per file to say so. It is a note to self about where you got to.