---
title: "Reviewing code while an AI agent is still rewriting it"
description: "Keeping review comments attached to the right code while the producer keeps editing."
canonical: "https://agent-manager.dev/writing/live-review-race/"
last-updated: "2026-08-23"
section: "Writing"
source: "agent-manager"
---

# Reviewing code while an AI agent is still rewriting it

A line number is a position, not an identity. Keeping review comments attached while the producer keeps editing requires more than refreshing `git diff`.

Code review normally begins after the author stops editing. A commit, patch or pull request gives the reviewer a stable object. Coding agents break that assumption. I often start reviewing while an agent is still working, leave a comment on line 84, and watch that line become 91 before I have finished the next file.

The first version of my terminal review UI refreshed the diff and kept each comment's file and line number. It looked fine until an agent inserted code above a commented line. The comment stayed at 84; the code it referred to moved. Sending that review back to the agent now attached a precise instruction to the wrong code, which is worse than sending no location at all.

This is the loop I wanted to preserve: read the agent's changes as whole files, comment on lines, then send all of the comments back as one prompt while the agent keeps working.

![A terminal review where line comments are sent back to a coding agent](/assets/demo-diff.gif)

_Fig. 1live review · comment back to the agent_

Making that loop live required treating a review as concurrent state, not a rendered `git diff`.

## A line number is a position, not an identity

Each saved comment started with the obvious fields:

`internal/ui/diffview.go`

```go
type annotation struct {
    file    string
    line    int
    deleted bool
    excerpt string
    text    string
}
```

`line` is the new-file line number, except for a pure deletion, where it is the old-file number. That is enough to render a comment immediately and produce a useful `path:line` instruction. It is not enough to find the same code after a refresh.

The extra identity is `excerpt`: the trimmed source text from the line when the comment was made, capped at 60 runes. After a same-review refresh, I scan the new file for a line with the same excerpt and the same added/deleted side. If there is exactly one match, the annotation moves to its new line number.

`re-anchoring, simplified`

```
matches, target := 0, 0
for _, line := range file.Lines {
    num, deleted := annotationLine(line)
    if deleted == note.deleted && excerptOf(line.Text) == note.excerpt {
        matches++
        target = num
    }
}
if matches == 1 && !annotationOccupies(target) {
    note.line = target
}
```

This deliberately fails closed. Blank lines are not anchors. If the excerpt occurs twice, the old line number stays. If another comment already occupies the target, the comments do not collapse onto one line. If the agent rewrites the line itself, the excerpt disappears and the original location remains as a best guess.

An AST or syntax-aware fingerprint could follow more edits, but the review works across every language Git can show. A conservative text anchor has one important property: when it cannot prove where a comment belongs, it does not silently invent an answer.

## Not every reload is the same review

Re-anchoring by matching text is only valid when the surrounding identity has stayed fixed. Changing from uncommitted changes to the last commit may show the same file and source line, but it is a different patch. Switching worktrees or sessions is more obviously different.

Annotations are therefore keyed by session and repository path, not by display name:

`review identity`

```
func (m *Model) reviewKey() string {
    return m.diff.sessID + "\x00" + m.diff.repoSel
}
```

The separator prevents ambiguous concatenations, and the resolved repository path prevents two same-named repositories under one agent directory from sharing review state.

Loads also carry the session, scope, repository and a monotonically increasing generation. The UI drops a result if any of those no longer match the open review. A slow Git command from the previous scope must not repaint the current one merely because it completed later.

Only a silent refresh of the same session, repository and scope enables re-anchoring. A scope cycle or session switch replaces the file set without moving saved comments against content they were never written for.

This distinction mattered more than the matching algorithm. “The diff changed” and “the user asked for a different diff” can produce identical data shapes but require different state transitions.

## Freeze the target while the user is typing

Generation checks reject obsolete work, but they do not solve one smaller race:

1. a refresh starts;
2. the reviewer opens the comment editor on line 84;
3. the refresh completes and shifts the file;
4. the reviewer presses Enter.

At step four, which line owns the text?

The UI pauses new probes while the comment editor or send-confirmation box is open. More importantly, a load that started before the box opened is discarded if it lands while either box is active. The in-flight flag is cleared, but the old fingerprint is retained, so the next probe loads the current diff after the box closes.

`drop an in-flight reload`

```
if m.diff.annotating || m.diff.sendConfirm {
    m.diff.loading = false
    return nil
}
```

This freezes only the semantic target during the short interaction. The agent is not paused, Git is not locked, and the rest of the manager keeps updating. Once the comment is saved, the next refresh can move it using the excerpt captured from the stable frame the reviewer actually saw.

## Refresh cheaply, hydrate selectively

Rebuilding and syntax-highlighting every changed file on every poll made large reviews unpleasant. The refresh path is now split into two phases.

First, a cheap repository fingerprint answers whether anything relevant changed. If it did, the UI reloads file metadata and immediately hydrates the selected file. Files with durable review state—a saved annotation or a reviewed mark—also need their contents so that state can be checked and re-anchored. Those less urgent loads run serially in one background command instead of spawning a Git process for every file at once.

`reload order, simplified`

```
current := loadCurrentDiffFile()
for each file with comments or reviewed state {
    stateful = append(stateful, loadDiffFile(file))
}
return tea.Batch(current, loadSerially(stateful))
```

Each file result repeats the same identity checks—session, scope, generation, repository, index and path—before it can mutate the model. Index alone is unsafe because a new diff can reorder the file list while a load is running.

## The final prompt carries coordinates and context

When the reviewer sends, the comments become one numbered, single-line prompt:

`prompt sent to the agent`

```
Code review of your uncommitted changes — address each numbered point,
then summarize what you changed per point:
(1) internal/store/store.go:91 (code: `return rows.Err()`) — wrap this error;
(2) internal/ui/list.go:44 (deleted line) — keep this behavior
```

Newlines in comment bodies become separators so text cannot submit halfway through the paste. Each non-deleted note includes both the latest line number and its code excerpt. The coordinates make the instruction quick to locate; the excerpt makes it intelligible if the agent edits again between the last refresh and reading the prompt.

This is still not collaborative editing. There is no operational transform, no CRDT and no claim that arbitrary rewrites preserve identity. The useful guarantee is narrower: background refreshes cannot move the editor underneath the user, stale asynchronous work cannot replace a newer review, and comments only move when the program has unambiguous evidence for their new location.

That was enough to make reviewing and producing code overlap safely in practice.

> **The code** The implementation is in [internal/ui/diffview.go](https://github.com/YoanWai/agent-manager/blob/main/internal/ui/diffview.go), with the race and ambiguity cases in [internal/ui/diffview_test.go](https://github.com/YoanWai/agent-manager/blob/main/internal/ui/diffview_test.go). Both are part of [agent-manager](https://github.com/YoanWai/agent-manager), an Apache-2.0 Go TUI for running coding agents on tmux. In the UI, `ctrl+r` opens the review, `c` comments a line, and `C` sends the review back to that agent.
