Skip to content

Move Review viewer comments into a main-process slice with anchoring - #336

Merged
frenchie4111 merged 2 commits into
mainfrom
review-comments-slice
Oct 29, 2026
Merged

frenchie4111 merged 2 commits into
mainfrom
review-comments-slice

Conversation

@frenchie4111

Copy link
Copy Markdown
Collaborator

Step 1 of the agent-reviewer work: Review viewer comments move out of `ReviewPane` `useState` and into a persisted `reviews` slice. Nothing agent-facing is built here. The aim is a data model that main-side code (MCP tools later) can write to directly.

Data model (`src/shared/state/reviews.ts`)

`reviews.byPath[worktreePath].comments: ReviewComment[]`, where each comment has:

  • `id`: a UUID generated in main
  • `filePath`, `side: 'old' | 'new'`, and `line`, the current line, valid at `anchoredSha`
  • `anchor: { commitSha, line, snippet }`: fixed at creation. The commit and line the comment was written against, plus the trimmed text of that line
  • `author`: `{kind: 'human'}` or `{kind: 'agent', tabId, label?}`. Replies use the same author shape
  • `status: 'open' | 'resolved' | 'outdated'`, plus `body`, `createdAt`, and optional `replies[]`

Events: `commentAdded`, `commentDeleted`, `commentStatusChanged`, `replyAdded` (reducer only, no UI yet), `anchorsUpdated` (batched), `worktreeRemoved`. Single-item patches use `findIndex` + `slice` (anti-pattern #3). The renderer reads through a per-worktree `useReviewComments(path)` hook (anti-pattern #4).

The slice persists to `config.reviews` the same way `pinned`/`aliases` do, is sanitized on boot, and loses a worktree's entry when that worktree leaves the list.

One correction to the brief: the review diff never shows working-tree content. Whole-branch mode is merge-base..HEAD (`getFileDiffSides(…, 'branch')`), and commit and range modes are commit-to-commit. So a comment always anchors to a real SHA: HEAD or the selected tip for the new side, the diff base for the old side. There's no "working tree vs committed" flag to record.

Main computes the snippet itself (`src/main/review-comments.ts`). `addReviewComment` resolves the SHA for that side of the current view and reads the line with `git show`. A caller only supplies `{file, line, side, body, view?}`, which is the shape step 2's `add_review_comment` MCP tool needs.

Outdated detection (`src/shared/review-anchor.ts`)

There's no poller and no subscriber. `reviews:refreshAnchors(worktreePath, file?)` runs when the Review pane opens and when a file is selected:

  1. One `rev-parse HEAD`. Comments whose `anchoredSha` is already HEAD are skipped, so in the steady state there are no git reads and no dispatches.
  2. For stale new-side comments it reads each file once at HEAD and looks for the snippet: first at the current line, then at the original line, then the nearest exact match anywhere in the file. Snippets shorter than 4 characters (`}`) only match in place, so a comment can't jump to a random brace.
  3. If the snippet is found, the comment moves to that line, and an `outdated` comment goes back to `open`. If it isn't found, the comment becomes `outdated`. Resolved comments are left alone.
  4. All changes go out as one `anchorsUpdated` event. Statuses are recomputed from a fresh snapshot after the git awaits, so resolving a comment while a refresh is in flight isn't undone.

Old-side comments point at the diff base, which doesn't move as the author commits, so they are never outdated.

When drawing, `ReviewDiffPane` places each comment by snippet against the content on screen. That way a commit or range view (whose content differs from HEAD) still shows it on the right code. A comment it can't place goes into a collapsible "N comments not on a line in this diff" list above the diff, greyed out, and is never deleted. That covers outdated comments, old-side comments (inline diff mode has no click target for removed lines yet) and lines that don't exist in the selected revision.

Reviewed checkmarks stay per-viewer renderer state

The "viewed" checkmarks are my progress through this file set. They still reset when the commit selection changes, because the progress is relative to the files in view. Sharing them properly would need content-hash invalidation (GitHub resets "viewed" when a file changes). A review agent has no use for them either, so I left them out of the slice.

UI

  • Each comment shows its author ("You", or the agent label), a Resolved/Outdated tag, Resolve/Reopen and delete. Resolved and outdated comments are dimmed, and outdated ones are struck through. Inline styles stay rem-based (§11) and icons use `icon-*`.
  • The file tree badge shows the open count. If a file has only resolved or outdated comments, it shows a muted total instead.
  • The header count shows open comments, with a tooltip for open comments on files outside the current selection. Changing the selection no longer discards comments.
  • Send to Agent and Copy send open comments only, as `path:line @sha7 — body`, plus replies. Old-side comments are labeled as a removed line. Routing to the active tab is unchanged.
  • Fixed a latent bug: the view-zone effect didn't depend on the editor instance, so after a file switch the zones were drawn into the disposed editor. With persisted comments, nothing would have shown after a reload.

Testing

  • Reducer tests cover every event, including reference identity on no-ops. There are also tests for anchor relocation and outdating, and for `formatReviewComments`.
  • `typecheck`, `electron-vite build` and `vitest` all pass.
  • Manual check against a scratch repo in headless mode with an isolated `HARNESS_DATA_DIR`:
    • added a comment; it reached config.json
    • committed two lines above it, reloaded, and the comment moved from L4 to L6
    • committed a removal of the line; it showed as Outdated above the diff and Send was disabled
    • in the single-commit view of the original commit, it showed inline on the original line
    • Resolve and Reopen round-trip

🤖 Generated with Claude Code

frenchie4111 and others added 2 commits October 29, 2026 18:12
Comments are keyed by worktree path, persisted to config.json, and
pruned when the worktree is removed. Each comment records the commit
and line it was written against plus a snippet of that line, so
refreshReviewAnchors can follow it as new commits land or mark it
outdated when the line is gone. Main generates ids and computes the
snippet itself, so later main-side callers only need file/line/side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReviewPane drops its local comment state and the clear-on-selection
effect; comments now come from useReviewComments and mutate over IPC.
ReviewDiffPane places each comment by snippet so commit/range views
still draw it on the right code, lists unplaceable (outdated / old-side)
comments above the diff, and adds resolve/reopen plus author labels.
Send to Agent and Copy include only open comments, with side and SHA.

Also fixes view zones being drawn into the previous, disposed editor
after a file switch — the effect now depends on the editor instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit fbb33f4 into main Oct 29, 2026
1 check 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