feat: gx review — TUI diff reviewer with line comments for coding agents - #28
Merged
Conversation
Adds the gx review (alias rev) command and the diff-range resolver: branch-vs-base (merge-base...HEAD), single commit, A..B range, and an uncommitted resolver. Each range carries a scope_id for branch-keyed persistence. The command currently prints the resolved range; the diff viewer lands in following units.
Enumerate changed files for a range (tree-to-tree and tree-to-workdir, including untracked) and build per-file hunks lazily: load old/new blob contents and run similar (from_lines + iter_inline_changes) into a flat Context/Added/Removed row model with 1-based line numbers and word-level emphasis ranges. Binary (NUL heuristic) and oversized files render a placeholder. Add/remove counts match git numstat.
Add a lazy process-global SyntaxSet/ThemeSet and a Highlighter that highlights whole files into line-indexed ratatui spans via our own syntect Style -> ratatui Style bridge (syntect-tui pins ratatui 0.29, so we cannot use it). syntect runs default-features=false + default-fancy (pure-Rust fancy-regex, no onig C dep). A size guard returns plain text for very large files. Adds ReviewConfig (theme, side_by_side_min_width, default_mode).
Add the review TUI: a stdout event loop (App with Mode/Focus, vim keys
j/k, Ctrl-d/u, gg/G, ]c/[c & }/{ hunk jumps, Tab/S-Tab files, h/l
scroll, v split-toggle, b sidebar, ? help, q quit) and a diff widget
that pairs removed/added runs into side-by-side columns (width-adaptive
unified fallback) with line-number gutters and layered syntax color +
diff background + word-level emphasis. Lazy per-file build+highlight,
empty/placeholder states, flat file sidebar. Rendering verified via
TestBackend.
Add ReviewState (comments anchored to file/side/line-range with an anchor_text snapshot for later re-anchoring) and the comment UX: c comments the current line, V starts a multi-line selection then c, Enter edits and D deletes the comment under the cursor, all via a GitHub-style inline popup (Ctrl-s save, esc cancel). Commented lines get a gutter marker and per-file sidebar badges; non-anchorable rows (hunk headers, gap rows) are no-ops with a hint. Deferred: the $EDITOR pop-out (needs a terminal-suspend helper).
Press F to finish: serialize all comments into a prompt-wrapped Markdown blob (header instruction + per-file sections + a diff snippet of context around each comment + the note) and copy it to the system clipboard via clipboard::copy. Empty reviews are blocked with a hint; clipboard failures fall back to printing the blob to stdout after teardown.
Persist review state to {temp}/gx-review/{key}.json, keyed by an FNV-1a
digest of the clone's common git dir + the range scope (stable across
toolchains; identical across worktrees of one clone; never committed).
Reviews resume on launch and re-anchor per file as it is opened: a
comment whose line still matches stays, one whose anchor_text moved is
re-anchored, and an unresolvable one moves to an orphaned list (viewable
with o). X (pressed twice) discards the saved review.
Add the grev alias (review), a README commands-table row, and a docs page covering range modes, the full keymap, the quit-vs-finish distinction, persistence/reset, and the [review] config keys. Per-mode help bars are already rendered by the TUI.
Press s to switch the review range live: b = branch vs base, t = branch plus working tree, u = uncommitted (working tree vs HEAD) — which makes uncommitted mode reachable for the first time. Switching rebuilds the file list and diffs, and each scope keeps its own branch-keyed review (the current one is saved first). Also drop the unused RangeMode field and surface rename old->new + the status char in the diff title.
Replace the flat sidebar with a nested, collapsible tree built from the changed-file paths, with status icons and per-file comment badges. Add a Focus axis: Tab moves focus to the sidebar where j/k navigate, Enter/l opens a file or toggles a directory, h collapses, and / enters a Filter mode (fuzzy match via SkimMatcherV2) that narrows to matching files. The focused pane is outlined; the selected file stays bold.
Add a terminal::suspend helper (leave the alternate screen + raw mode, run a closure, re-enter and redraw) and wire Ctrl-e in the comment popup to it: the buffer is written to a temp file, $EDITOR/$VISUAL opens on it, and the saved contents replace the popup buffer on return. Falls back with a status hint when the editor is unset or exits without saving. The event loop drives the suspend since it owns the terminal.
WorkspaceError's struct-variant fields (wanted/existing/base) are used
only inside thiserror's #[error("…{field}…")] messages; a recent rustc
misattributes a generated-code assignment to the field spans and warns
they are 'never read'. Add a module-scoped #![allow(unused_assignments)]
so cargo build is warning-free. Pre-existing, unrelated to gx review.
Detect the terminal background (OSC 11 via terminal-light) and pick a light or dark syntax theme + diff palette accordingly, so the diff is readable on light terminals (previously hardcoded dark theme + near-black backgrounds were unreadable there). Add [review] appearance = auto|light|dark (default auto); theme now defaults to empty = auto-pick (InspiredGitHub for light, base16-ocean.dark for dark). Context lines use the terminal's own background; add/remove use pale tints on light.
Apply verified fixes from the multi-agent review: - switch_range now rebuilds the file tree (stale indices could panic on select) [P1] - re-anchor picks the nearest matching line and orphans on ambiguity, not first match [P1, 3 reviewers] - suspend() marks ACTIVE_TTY active before re-entry so the panic hook can restore [P1] - the +worktree range gets a distinct persistence key (was colliding with branch) [P2] - atomic state save (temp + rename) [P2] - honor [review] default_mode = uncommitted [P2] - byte-size cap before decoding/scanning a blob [P2] - marker_span uses a &'static str (no per-line alloc) [P2] - clamp the cursor after a view/width change [P3] Adds a re-anchor duplicate-text test. 254 tests pass, clippy clean.
Address review findings #5,#6,#7,#12,#13,#14,#21: - $EDITOR draft now uses tempfile (exclusive, random, 0600) [#6] and the editor string is tokenized so flags work, e.g. 'code -w' [#7] - blob/workdir loaders propagate git2/IO errors instead of silently rendering an empty diff [#12] - the review is saved before propagating a build error [#13] - review-state dir is 0700 on unix; prior on-disk state is kept as a .bak before overwrite (concurrent-session safety net) [#5,#14] - range switch to an empty file set shows a distinct status [#21]
Addresses findings #15,#16,#17: the event loop drew (and rebuilt the visual model, file-tree rows, and comment marks) ~10x/sec while idle. It now blocks on input and re-renders only after a key or resize event, so those per-frame rebuilds happen on actual change instead of continuously. Also adds explicit Resize handling (previously the layout only updated on the next idle tick).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gx review— a TUI diff reviewer with line comments for coding agentsAdds a new top-level command,
gx review(aliasrev,grev), that opens a full-screen TUI for reviewing a diff, annotating lines GitHub-style, and handing the result to a coding agent via the clipboard.What it does
merge-base(base, HEAD) → HEAD), switchable in-TUI (s) to a single commit, anA..Brange, uncommitted (working tree), or branch + working tree.gx review,gx review <ref>,gx review A..B,gx review --base <ref>.git2enumerates files + blobs →similarcomputes line + intra-line diffs →syntecthighlights). Width-adaptive fallback to a unified single-column view on narrow terminals.[review] appearance = auto|light|darkoverrides./fuzzy filter.c) and multi-line (Vthenc) via a small inline popup, with a$EDITOR/$VISUALpop-out (Ctrl-e); edit (Enter), delete (D), gutter markers.(clone, branch, range-scope)and stored in an ephemeral, never-committed temp location; it re-anchors comments to the current diff on resume (orphaning ones that no longer resolve, viewable witho) and resets withX.F) — serializes all comments plus surrounding diff context into a prompt-wrapped Markdown blob and copies it to the clipboard, ready to paste into a coding agent.Keymap:
?in-app shows the full list (j/k,Ctrl-d/u,]c/[c,Tab→ sidebar, etc.). Seedocs/review.md.New dependencies
similar(inline)syntect(default-features = false,default-fancy)onigC dep — toolchain-free builds)terminal-lightcrossterm 0.29(no duplicate)tempfile$EDITORdraftsyntect-tuiwas intentionally not used (it pinsratatui 0.29vs this repo's0.30); a ~15-linesyntect → ratatuispan bridge is used instead.Testing & quality
cargo clippyclean;cargo buildwarning-free.git numstat, re-anchoring incl. duplicate-text tiebreak, FNV-1a key stability, blob serialization); rendering is covered withratatui::TestBackend.Code review
A multi-agent code review ran over the branch; every finding with functional impact was fixed and committed (the one crash, re-anchoring correctness, terminal-restore safety, persistence atomicity + permissions, error propagation,
$EDITORrobustness, and an idle-redraw performance fix). Two non-behavioral refactors remain as optional follow-ups: splitting the ~1.2k-linesrc/ui/review/mod.rsinto submodules, and replacing five#[allow(clippy::too_many_arguments)]with a context struct.Notes for reviewers
unused_assignmentsfalse positive insrc/commands/workspace/mod.rs(surfaced by a recent rustc).