|
| 1 | +# Agent Review — IntelliJ Plugin |
| 2 | + |
| 3 | +A Kotlin/IntelliJ plugin that renders file-based code review annotations written by a human |
| 4 | +or a coding agent. No server. No network. The protocol is a directory of JSON files. The |
| 5 | +plugin reads and writes them. The agent reads and writes them. Both parties see each other's |
| 6 | +comments in real time. |
| 7 | + |
| 8 | +## Requirements |
| 9 | + |
| 10 | +- **JDK 17+** (required for IntelliJ Platform 2024.1) |
| 11 | +- **IntelliJ IDEA 2024.1+** (Community or Ultimate), or any JetBrains IDE based on the |
| 12 | + same platform (CLion, WebStorm, PyCharm, etc.) |
| 13 | +- **Git** on PATH (for anchor resolution via `git diff`) |
| 14 | + |
| 15 | +## Building |
| 16 | + |
| 17 | +```bash |
| 18 | +cd review-plugin |
| 19 | + |
| 20 | +# Build the plugin distribution zip |
| 21 | +./gradlew buildPlugin |
| 22 | + |
| 23 | +# The installable zip is at: |
| 24 | +# build/distributions/review-plugin-0.1.0.zip |
| 25 | +``` |
| 26 | + |
| 27 | +To also run the tests: |
| 28 | + |
| 29 | +```bash |
| 30 | +./gradlew test |
| 31 | +``` |
| 32 | + |
| 33 | +To verify plugin descriptor compatibility: |
| 34 | + |
| 35 | +```bash |
| 36 | +./gradlew verifyPluginConfiguration |
| 37 | +``` |
| 38 | + |
| 39 | +## Installing in IntelliJ |
| 40 | + |
| 41 | +### Option A: Install from disk (built zip) |
| 42 | + |
| 43 | +1. Build the plugin: `./gradlew buildPlugin` |
| 44 | +2. Open IntelliJ IDEA |
| 45 | +3. Go to **Settings** → **Plugins** → gear icon (⚙) → **Install Plugin from Disk...** |
| 46 | +4. Select `review-plugin/build/distributions/review-plugin-0.1.0.zip` |
| 47 | +5. Restart the IDE |
| 48 | + |
| 49 | +### Option B: Run a sandboxed IDE instance (for development) |
| 50 | + |
| 51 | +This launches a fresh IntelliJ instance with the plugin pre-installed — your main IDE |
| 52 | +settings are not affected: |
| 53 | + |
| 54 | +```bash |
| 55 | +cd review-plugin |
| 56 | +./gradlew runIde |
| 57 | +``` |
| 58 | + |
| 59 | +This is the best way to test during development. It downloads IntelliJ Community 2024.1 |
| 60 | +automatically on first run. |
| 61 | + |
| 62 | +### Option C: Run in a specific IDE |
| 63 | + |
| 64 | +```bash |
| 65 | +# Run in a specific IntelliJ installation |
| 66 | +./gradlew runIde -PalternativeIdePath="/path/to/IntelliJIDEA" |
| 67 | +``` |
| 68 | + |
| 69 | +## Using the Plugin |
| 70 | + |
| 71 | +### Creating a comment (human → agent) |
| 72 | + |
| 73 | +1. Open any file in the editor |
| 74 | +2. Select the lines you want to comment on (or place your cursor on a single line) |
| 75 | +3. Right-click → **Add Review Comment** (or press **Ctrl+Alt+R**) |
| 76 | +4. Type your comment and click OK |
| 77 | + |
| 78 | +This writes a JSON file to `.review/comments/<id>.json` in the project root. |
| 79 | + |
| 80 | +### Viewing comments |
| 81 | + |
| 82 | +- **Gutter icons**: Red dot = open, green check = resolved, grey dash = won't fix. |
| 83 | + Hover for a tooltip with the comment body. |
| 84 | +- **Tool window**: Click **Code Review** in the bottom tool window bar. Use the |
| 85 | + filter buttons (All / Open / Resolved) to narrow the list. |
| 86 | +- **Navigation**: Click a gutter icon or a comment in the tool window to jump to |
| 87 | + the annotated code. |
| 88 | + |
| 89 | +### Replying / resolving |
| 90 | + |
| 91 | +In the Code Review tool window, select a comment, then: |
| 92 | +- **Reply**: Type in the reply box and click "Reply" |
| 93 | +- **Resolve**: Click "Resolve" (optionally add a reply message) |
| 94 | +- **Won't Fix**: Click "Won't Fix" |
| 95 | + |
| 96 | +### Ghost hunks |
| 97 | + |
| 98 | +When a comment is resolved and the code has changed, the plugin renders the |
| 99 | +original code as a greyed-out block inlay above the current position, so you can |
| 100 | +see what was there before. |
| 101 | + |
| 102 | +## Agent Integration |
| 103 | + |
| 104 | +An agent (Claude Code, Cursor, or any script) participates by reading and writing |
| 105 | +JSON files in `.review/comments/`. No special library required. |
| 106 | + |
| 107 | +### Reading open comments (Python example) |
| 108 | + |
| 109 | +```python |
| 110 | +import json, glob, os |
| 111 | + |
| 112 | +def get_open_comments(project_root): |
| 113 | + pattern = os.path.join(project_root, ".review", "comments", "*.json") |
| 114 | + comments = [] |
| 115 | + for path in glob.glob(pattern): |
| 116 | + with open(path) as f: |
| 117 | + c = json.load(f) |
| 118 | + if c["status"] == "open": |
| 119 | + comments.append(c) |
| 120 | + return comments |
| 121 | +``` |
| 122 | + |
| 123 | +### Writing a new comment |
| 124 | + |
| 125 | +```python |
| 126 | +import json, os, random, subprocess |
| 127 | +from datetime import datetime, timezone |
| 128 | + |
| 129 | +def post_comment(project_root, file_rel, line, target_lines, |
| 130 | + context_before, context_after, body): |
| 131 | + cid = random.randbytes(4).hex() |
| 132 | + commit = subprocess.check_output( |
| 133 | + ["git", "rev-parse", "HEAD"], |
| 134 | + cwd=project_root).decode().strip() |
| 135 | + |
| 136 | + comment = { |
| 137 | + "id": cid, |
| 138 | + "schema_version": 1, |
| 139 | + "author": "agent", |
| 140 | + "created": datetime.now(timezone.utc).isoformat(), |
| 141 | + "status": "open", |
| 142 | + "anchor": { |
| 143 | + "file": file_rel, |
| 144 | + "commit": commit, |
| 145 | + "line_hint": line, |
| 146 | + "hunk": { |
| 147 | + "context_before": context_before, |
| 148 | + "target": target_lines, |
| 149 | + "context_after": context_after |
| 150 | + } |
| 151 | + }, |
| 152 | + "resolved_anchor": None, |
| 153 | + "body": body, |
| 154 | + "thread": [] |
| 155 | + } |
| 156 | + |
| 157 | + dir_path = os.path.join(project_root, ".review", "comments") |
| 158 | + os.makedirs(dir_path, exist_ok=True) |
| 159 | + tmp = os.path.join(dir_path, f"{cid}.json.tmp") |
| 160 | + final = os.path.join(dir_path, f"{cid}.json") |
| 161 | + with open(tmp, "w") as f: |
| 162 | + json.dump(comment, f, indent=2) |
| 163 | + os.rename(tmp, final) # atomic write |
| 164 | +``` |
| 165 | + |
| 166 | +### Resolving a comment |
| 167 | + |
| 168 | +```python |
| 169 | +def resolve_comment(project_root, comment_id, reply_body): |
| 170 | + path = os.path.join(project_root, ".review", "comments", f"{comment_id}.json") |
| 171 | + with open(path) as f: |
| 172 | + comment = json.load(f) |
| 173 | + |
| 174 | + comment["status"] = "resolved" |
| 175 | + comment["thread"].append({ |
| 176 | + "id": random.randbytes(4).hex(), |
| 177 | + "author": "agent", |
| 178 | + "created": datetime.now(timezone.utc).isoformat(), |
| 179 | + "body": reply_body, |
| 180 | + "status": "resolved" |
| 181 | + }) |
| 182 | + |
| 183 | + tmp = path + ".tmp" |
| 184 | + with open(tmp, "w") as f: |
| 185 | + json.dump(comment, f, indent=2) |
| 186 | + os.rename(tmp, path) |
| 187 | +``` |
| 188 | + |
| 189 | +## File Format |
| 190 | + |
| 191 | +Each comment is a single JSON file in `.review/comments/`: |
| 192 | + |
| 193 | +``` |
| 194 | +<project-root>/ |
| 195 | +└── .review/ |
| 196 | + └── comments/ |
| 197 | + ├── a3f1c2d4.json |
| 198 | + └── b7e902f1.json |
| 199 | +``` |
| 200 | + |
| 201 | +| Field | Type | Description | |
| 202 | +|---|---|---| |
| 203 | +| `id` | string | 8-char hex, unique per file | |
| 204 | +| `schema_version` | int | Always `1`. Plugin skips unknown versions | |
| 205 | +| `author` | string | Free string — conventionally `"human"` or `"agent"` | |
| 206 | +| `status` | enum | `"open"` / `"resolved"` / `"wontfix"` | |
| 207 | +| `anchor.file` | string | Project-relative path, forward slashes | |
| 208 | +| `anchor.commit` | string | Git SHA at comment creation time | |
| 209 | +| `anchor.line_hint` | int | 1-based line number (hint, not authoritative) | |
| 210 | +| `anchor.hunk` | object | `context_before`, `target`, `context_after` (string arrays) | |
| 211 | +| `resolved_anchor` | object? | Same shape as `anchor`, written when code changes during resolution | |
| 212 | +| `body` | string | The comment text | |
| 213 | +| `thread` | array | Append-only reply list | |
| 214 | + |
| 215 | +### Rules for writers |
| 216 | + |
| 217 | +- Never modify `id`, `created`, `anchor`, or `author` after initial write |
| 218 | +- To resolve: set root `status` to `"resolved"` and append a thread entry |
| 219 | +- To reply: append to `thread[]` — never rewrite existing entries |
| 220 | +- Write atomically: write to `<id>.json.tmp` then rename to `<id>.json` |
| 221 | +- One comment per file, filename is always `<id>.json` |
| 222 | + |
| 223 | +## Anchor Resolution |
| 224 | + |
| 225 | +When the file has changed since the comment was created, the plugin resolves |
| 226 | +the comment position through a 3-step algorithm: |
| 227 | + |
| 228 | +1. **Exact match** — Check if lines at `line_hint` still match `hunk.target` (trimmed) |
| 229 | +2. **Git diff remap** — Parse `git diff <commit>` to compute line offset |
| 230 | +3. **Fuzzy search** — Sliding-window LCS over the full file (threshold: 0.75) |
| 231 | +4. **Drifted** — If all steps fail, the comment is shown only in the tool window |
| 232 | + |
| 233 | +## Project Structure |
| 234 | + |
| 235 | +``` |
| 236 | +review-plugin/ |
| 237 | +├── build.gradle.kts |
| 238 | +├── settings.gradle.kts |
| 239 | +├── gradle.properties |
| 240 | +└── src/ |
| 241 | + ├── main/kotlin/com/reviewplugin/ |
| 242 | + │ ├── model/ # ReviewComment, Anchor, Hunk, ThreadEntry |
| 243 | + │ ├── store/ # CommentStore (project service), ReviewDirWatcher |
| 244 | + │ ├── anchor/ # HunkMatcher (pure algorithms), AnchorResolver, GitRunner |
| 245 | + │ ├── ui/ |
| 246 | + │ │ ├── gutter/ # ReviewGutterIconProvider (line markers) |
| 247 | + │ │ ├── inlay/ # GhostHunkRenderer, GhostHunkInlayManager |
| 248 | + │ │ └── toolwindow/ # ReviewToolWindowFactory, ReviewToolWindowPanel |
| 249 | + │ └── actions/ # NewReviewCommentAction |
| 250 | + ├── main/resources/ |
| 251 | + │ ├── META-INF/plugin.xml |
| 252 | + │ └── icons/review.svg |
| 253 | + └── test/kotlin/com/reviewplugin/ |
| 254 | + ├── model/ # ReviewCommentTest (18 tests) |
| 255 | + ├── anchor/ # HunkMatcherTest (22 tests) |
| 256 | + └── store/ # CommentStoreFileIOTest (13 tests) |
| 257 | +``` |
| 258 | + |
| 259 | +## CI |
| 260 | + |
| 261 | +GitHub Actions runs on every push to `main` and `claude/**` branches: |
| 262 | + |
| 263 | +- **build-and-test**: Compiles the plugin, runs all 53 unit tests |
| 264 | +- **verify-plugin**: Validates the plugin descriptor, produces the installable zip as an artifact |
| 265 | + |
| 266 | +## Sharing Review State |
| 267 | + |
| 268 | +The `.review/` directory is designed to be committed to git. This way: |
| 269 | +- Comments persist across IDE restarts |
| 270 | +- Agent and human share annotation state across sessions |
| 271 | +- Review history is preserved in version control |
| 272 | + |
| 273 | +Add `.review/` to `.gitignore` only if you want annotations to be local-only. |
0 commit comments