Keeping your place
Ticks survive between runs, which is the point: an agent is asked for changes, makes them, and asks for a second review, and nobody wants to be asked to read the eleven files it did not touch.
Everything is remembered under a hash of the thing itself — a hunk under its changed lines, a file under its two versions, a message under the prose of it, a commit under its message and its files. Not under a name, because every name available is wrong in a different direction. A jj change ID survives a rewrite by design, so a mark hung on one would hide code nobody has read. A git SHA changes when a rebase touches nothing worth re-reading, so a mark hung on that would ask for the whole branch again. Line numbers move when a hunk above them grows.
Content keys have neither failure. A hunk stays read after a rebase moves it,
after an amend renumbers it, and when the same change is opened from a
different worktree or through the other VCS — the same commit read through jj
and through git keys identically, because the hash is over file bytes and
nothing the VCS names. It stops being read the moment its lines change, which
is exactly when reading it again is the point. Rewording a commit asks for the
message to be read again and leaves the verdict on its diff standing —
rewording, not a trailer: a Co-Authored-By: or Signed-off-by: line added or
dropped at the foot of a message changes nothing anybody has to re-read, so the
key is taken from the message above the trailer block.
A hunk is recognised a second way, because hunks merge. Reviewing a run of
commits as one accumulated change diffs them against the parent of the first,
and two changes that were separate hunks in their own commits fall into one
hunk if they land within six lines of each other — the same thing happens to
anyone who reads a branch commit by commit and then meets it squash-merged. So
each hunk also stores a key per changed line, and a hunk counts as read when
every line of it worth recognising was read as part of some hunk that was
ticked. Lines of punctuation are left out of that: a } is shared with half of
every file ever written, so having read one says nothing about having read
another, and those ride along with the substantial lines around them. A hunk
made of nothing else needs its own tick.
The fallback never marks anything on a partial match — every line has to be accounted for — so it errs towards asking again. On this repository's own history, reading five commits one at a time and then opening them as one range recognises 55 of the 72 hunks. The rest are lines whose changed-ness only appears at the range's own alignment, and they are asked about again, which is the right way for it to be wrong.
The same rule explains why a file read as part of commit 3 alone is not read as part of the 2–4 range: against a different base it is a different diff, so it hashes differently. Commit ticks carry across, because a rebased commit is the same commit.
Comments come back the same way. A remark is kept against the content key of the commits it was written over, so closing the window mid-review and opening it again gives them all back, in place, with the line numbers still meaning what they meant — the commits are byte-for-byte what they were, so the diff is the same diff. Once the agent changes that code the key changes with it, and the remark stays away: it was about a line that no longer exists, and the agent has already been told. A remark made over a run of commits waits for that run to be selected again, because its line numbers were taken against that run's base and mean nothing anywhere else.
Marks live in one SQLite database under $XDG_STATE_HOME/slop-review — state
rather than data, since it is history nobody would miss off a backup, and
never $XDG_CACHE_HOME, since losing a tick is not the same kind of loss as
losing a rendered diff — that one has a cache of its own, in its own file, and
throwing it away costs only time. It is opened in WAL mode and written on the click rather than at
exit, because two worktrees of one repository is the normal case and not the
exotic one: two windows both keep their work, and closing one the rude way
loses nothing.
There are still no migration scripts — no ordered chain of edits to write, keep
and get wrong. Instead the shape wanted is declared in store.rs and the file
on disk is brought up to it on open: missing tables are created, missing columns
are added, and anything else already there is left alone. It is idempotent and
derived from the shape wanted rather than from a history of how it got there.
The one rule it asks for is that a column added later be nullable or carry a
default, since that is all ALTER TABLE ADD COLUMN can do.
Two things reconciling cannot do, and does not have to. A key formula that
changes carries its own version in the key (v2:…), so old rows simply stop
matching and get pruned — nothing needs dropping. And for data whose meaning
has changed, each table has a version of its own, and raising it empties that
table alone: a change to how hunks are split cannot cost you your comments.
Marks not seen for ninety days are pruned.
Keying a file is two VCS reads and a hash — no difftastic, no syntax pass — which is cheap enough to do for every file of every commit in the background while you read the first one. Until that reaches a commit its box is not yet something you can tick, and says so.
The window follows the desktop's light/dark setting, and the menu in the top
right offers the same three choices as --appearance. On Linux the toolkit reports no
system theme at all, so "system" would quietly mean "dark"; the desktop is
asked directly instead, over the XDG settings portal and then GNOME's theme
name. --theme-dark and --theme-light name the syntect themes used in each
(base16-ocean.dark and InspiredGitHub by default). The diff text sizes
independently of the rest of the window: A−/A+ in the header move the
code, Ctrl+-/Ctrl++ move everything else. They are separate settings
because a font size that suits a wall of code rarely suits a row of buttons.