docs/internal/review-picker-plan.md
Replace the two existing entry points to the review-diff feature
(start_review_diff and start_review_range) with a single command
Review that opens a dedicated picker screen. The picker covers
the four highest-leverage UX gaps in the current review-diff:
The existing review buffer group (toolbar + sticky header + diff +
comments) is unchanged except for one new 1-row context ribbon
between the toolbar and the sticky header that names what is being
reviewed and exposes a g keybind to re-open the picker.
g → open picker.createBufferGroup primitive, the same way
start_review_branch already is.<data_dir>/audit/<repo>/ directory.The picker is not a left panel attached to the review. It is a
separate buffer group, opened in place of the review (or in place of
the editor when first launched). Same takeover pattern as the command
palette and start_review_branch.
┌────────────┐ Enter ┌────────────┐
│ PICKER │ ──────────────▶ │ REVIEW │
│ │ │ │
│ │ ◀────────────── │ │
└────────────┘ press g └────────────┘
│ │
│ press q │ press q
▼ ▼
back to editor back to editor
Why two screens (rather than a third permanent pane in the review):
j/k means different things in each. Mixing the
two in one layout costs a "which pane am I in?" check on every
keystroke.split v 0.02
fixed header h=1 "Pick what to review Enter: open · Tab: pane · q: cancel"
split h 0.4
scrollable list (presets, commits, branches, custom)
scrollable preview (live diff of the row under the cursor)
List pane content:
★ This PR (main..HEAD) 7 commits +52/−12 ●3
Since I last reviewed 3 new ●1
Working tree 16 files ●2
Last commit (03637f8 feat(util))
─── COMMITS ─────────────────────────────────────────────
○ bca083a feat: farewell ●0
● 9e478d5 feat: f-strings ●1
○ 03637f8 feat(util): sub/mul ●0
─── BRANCHES ────────────────────────────────────────────
✓ main ●0
origin/main ●0
release/v2 ●4
─── CUSTOM ──────────────────────────────────────────────
: type a revspec…
●N is the count of saved comments under the resulting range key.
●0 is rendered dim. ★ marks the auto-detected default. ✓ marks
the current branch.
Layout flips to vertical (list on top, preview below) when
viewport.width < 100.
★ This PROn open, the cursor lands on ★ This PR. The "from" is resolved in
this order, falling back through to the last entry that succeeds:
git rev-parse --abbrev-ref @{u} — upstream of current branchgit merge-base HEAD <default-branch> — main, then master,
then trunkHEAD~1 — last commit onlyThe <default-branch> is whatever git symbolic-ref refs/remotes/origin/HEAD
points at, with main/master/trunk fallbacks if the remote head
isn't set.
If the resolved range is empty (you are sitting on the default branch
with no upstream divergence), the row is shown disabled and the
cursor falls through to Working tree.
Every cursor move in the list pane debounces a git diff <from>..<to>
(100 ms) and re-renders the right pane using the same
buildListLines / parseDiffOutput pipeline the real review uses.
Per-range cache; cleared when the picker closes. Result: scrolling
through commits feels instant on revisits, and what the user sees in
the preview is byte-identical to what they get on Enter.
On review close (q or stop_review_diff), write
<data_dir>/audit/<repo>/watermarks.json:
{
"branches": {
"feature/x": { "tip": "abc123", "updated_at": "2026-04-16T…" }
}
}
When the picker opens, if the current branch has a watermark and
the watermark differs from HEAD, render a Since I last reviewed (N new) row resolving to <watermark>..HEAD. If the watermark
equals HEAD, hide the row (nothing new to review).
This is the unique-value-prop feature. Most reviewers come back to a PR after the author pushes follow-up commits; today they have to find the old SHA themselves.
On picker open, list <data_dir>/audit/<repo>/*.json once, parse the
comments.length from each, and key the counts by review key
(worktree, range-<from>__<to>). Render ●N next to any list row
whose resulting range key has a non-zero count. O(files), tens of ms
even with hundreds of saved reviews.
j / k / Up / Down move list cursor
Enter open the row's range as a review
Tab toggle focus between list and preview pane
PageDown / PageUp scroll preview pane (when focused)
v (commits section) toggle "marked"
V (commits section) extend mark range
: focus the custom-revspec field
q / Esc cancel; close picker, return to where you came from
g refresh the picker (re-scan branches/commits/badges)
Multi-commit non-contiguous selection (v to mark commits, Enter to
flatten the marks into a synthesised range) is in scope for the
picker but can ship in a follow-up — the rest of the picker
delivers >80% of the value without it.
REVIEW_LAYOUT adds one fixed-height node:
split v 0.02
fixed toolbar h=2
split v 0.02
fixed ribbon h=1 ← NEW
split v 0.02
fixed sticky h=1
split h 0.75
scrollable diff
scrollable comments
Ribbon content (mode-aware):
| Mode | Ribbon text |
|---|---|
| worktree | Working tree · 16 files · +40/−77 · 0 comments [g] change range |
| range | ★ main..HEAD · 2 files · +10/−1 · 0 comments [g] change range |
| commit | bca083a feat: farewell · 1 file · +3/−0 · 0 comments [g] change range |
Always visible. The "what am I reviewing?" question never requires a keystroke to answer.
g (mnemonic: go to picker) closes the review group and opens
the picker. Initial picker selection is the row corresponding to the
range you just left, so g-then-Enter is a no-op refresh.
| Concern | Where it lives | New / reused |
|---|---|---|
| Picker buffer group + layout | new audit_picker.ts (sibling to audit_mode.ts) | new |
| List rendering (presets/commits/branches/custom) | audit_picker.ts | new |
| Live preview rendering | audit_picker.ts, calls existing parseDiffOutput + buildListLines | new wrapper, reuses existing |
| Per-range diff cache | audit_picker.ts | new |
★ This PR resolution | audit_picker.ts helper | new |
| Comment-count scan | audit_picker.ts helper, reads getDataDir() / audit / <repo> / *.json | new (tiny) |
| Watermarks read / write | audit_picker.ts (read), audit_mode.ts stop_review_diff (write) | new (tiny) |
| Ribbon row | audit_mode.ts: extend REVIEW_LAYOUT, add buildRibbonEntries() | modified |
[g] change range | audit_mode.ts: add to review-mode keymap; new handler review_open_picker | modified (~3 lines) |
| Open review with picked range | reuses bootstrapRangeReview (audit_mode.ts:3886) and the worktree path of start_review_diff | reused |
Review Diff
and Review Range (Commit or Branch)).j/k. Preview pane debounce-updates.<data_dir>/audit/<repo>/<key>.json
exactly as they do today.g from review → close review group → open picker, with the
current range pre-selected. Comments are persisted continuously
already, so nothing is lost.q from review → close review; write watermarks.json for the
branch's current HEAD.q from picker → close picker; return to the editor (no review
was opened).start_review_range and its single-prompt UI (the picker
replaces it). The cmd.review_range i18n keys also drop.: inside the picker.(file, surrounding-3-line-hash) would survive minor rewrites.
Independently useful; not required for the picker.git blame in the diff: useful in multi-author
branches; orthogonal.git diff main..HEAD on a large monorepo
can take seconds. Mitigations: cache per range, render the preview
pane with a "Loading…" placeholder, and cancel any in-flight fetch
when the cursor moves again.emptyState === 'not_git'.points; the user-facing change is all-or-nothing once Review exists)
★ This PR, Working tree, Last commit, :custom). Live preview wired in. Replaces
start_review_range.Since I last reviewed preset.v/V marking (follow-up).Phases 1–4 are the smallest set that delivers the four headline UX gains. Phases 5–6 round out coverage; phase 7 unlocks the long-tail "these specific commits" use case.