Skip to content

feat: gx review — TUI diff reviewer with line comments for coding agents - #28

Merged
reckerp merged 17 commits into
mainfrom
feat-review
Jun 30, 2026
Merged

feat: gx review — TUI diff reviewer with line comments for coding agents#28
reckerp merged 17 commits into
mainfrom
feat-review

Conversation

@reckerp

@reckerp reckerp commented Jun 30, 2026

Copy link
Copy Markdown
Owner

gx review — a TUI diff reviewer with line comments for coding agents

Adds a new top-level command, gx review (alias rev, 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

  • Range modes — branch-vs-base by default (merge-base(base, HEAD) → HEAD), switchable in-TUI (s) to a single commit, an A..B range, uncommitted (working tree), or branch + working tree. gx review, gx review <ref>, gx review A..B, gx review --base <ref>.
  • Side-by-side, syntax-highlighted diff with word-level inline change emphasis (git2 enumerates files + blobs → similar computes line + intra-line diffs → syntect highlights). Width-adaptive fallback to a unified single-column view on narrow terminals.
  • Light/dark adaptation — detects the terminal background (OSC 11) and picks a light or dark theme + diff palette; [review] appearance = auto|light|dark overrides.
  • File-tree sidebar — collapsible, status icons, comment-count badges, / fuzzy filter.
  • Comments — single-line (c) and multi-line (V then c) via a small inline popup, with a $EDITOR/$VISUAL pop-out (Ctrl-e); edit (Enter), delete (D), gutter markers.
  • Persist & resume — review state is keyed to (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 with o) and resets with X.
  • Finish (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.). See docs/review.md.

New dependencies

Crate Why
similar (inline) line + word-level diff
syntect (default-features = false, default-fancy) syntax highlighting, pure-Rust regex (no onig C dep — toolchain-free builds)
terminal-light detect light/dark terminal background; reuses the existing crossterm 0.29 (no duplicate)
tempfile exclusive, randomly-named temp file for the $EDITOR draft

syntect-tui was intentionally not used (it pins ratatui 0.29 vs this repo's 0.30); a ~15-line syntect → ratatui span bridge is used instead.

Testing & quality

  • 254 unit tests + 16 integration tests pass; cargo clippy clean; cargo build warning-free.
  • Logic is unit-tested (range resolution, diff model — counts verified against git numstat, re-anchoring incl. duplicate-text tiebreak, FNV-1a key stability, blob serialization); rendering is covered with ratatui::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, $EDITOR robustness, and an idle-redraw performance fix). Two non-behavioral refactors remain as optional follow-ups: splitting the ~1.2k-line src/ui/review/mod.rs into submodules, and replacing five #[allow(clippy::too_many_arguments)] with a context struct.

Notes for reviewers

  • It's a large change (~4k lines). The commits are staged cleanly by implementation unit if you'd prefer to review commit-by-commit.
  • The interactive TUI was exercised manually (light + dark terminals); the rest is covered by tests.
  • Also includes a small unrelated fix silencing a pre-existing unused_assignments false positive in src/commands/workspace/mod.rs (surfaced by a recent rustc).

reckerp added 17 commits June 30, 2026 15:09
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).
@reckerp
reckerp merged commit f8bff5b into main Jun 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant