# declutter-diff
A code-review tool for AI-generated changes that lets you toggle whole categories of code —
comments, tests, imports — in and out of the diff, so you can review the logic first and the
rest second.
## The problem
AI coding agents produce large diffs, and a big share of every diff is not logic:
- **Comments and docstrings** — often several lines per function, restating what the code does.
- **Tests** — frequently as long as the change itself.
- **Boilerplate** — imports, logging, formatting churn.
All of it lands in one interleaved stream. A reviewer who wants to answer "is the logic
right?" has to read past the rest to find it, and the attention spent skimming noise is
attention not spent on the code that can break production. Today's tools can hide whitespace
and filter by file path, but nothing understands that a line is a comment or that a block is a
test.
## The idea
Treat a diff as layers that can be switched on and off:
| `c` | Comments | Line and block comments, doc comments, Python docstrings |
| `t` | Tests | Test files (by path convention or test-framework import); later, test blocks inside source files |
| `i` | Imports | `import` / `use` / `#include` statements |
| `l` | Logging | Statements that only call a logging API (`print`, `console.*`, `logger.*`, …) |
| `f` | Formatting | Changes that only move whitespace: re-indent, re-space, re-wrap |
Toggle comments off and the diff shows only code changes: comment-only hunks disappear,
and a line that changed code *and* a trailing comment shows only the code change. A status
line always says what is hidden — `14 comment-only hunks, 3 test files hidden` — so nothing
is skipped silently.
The inverse is just as useful: **comments only**. AI-written comments are often stale or
describe what the code was meant to do rather than what it does. Reviewing them on their own,
as a separate pass, is fast and catches a class of error that is easy to miss inline.
## How it works
1. **Parse both sides** of each changed file with [tree-sitter](https://tree-sitter.github.io/),
which has mature grammars for nearly every language.
2. **Classify** nodes into layers using a small tree-sitter query per language. Comments are
near-trivial (most grammars name them `comment`, `line_comment`, `block_comment`). Tests
combine path conventions (`*Tests/`, `*_test.go`, `*.spec.ts`, `test_*.py`) with in-file
queries (Swift `@Test` / `XCTestCase`, Rust `#[cfg(test)] mod tests`, JS `describe`/`it`).
3. **Project** each side: produce a version of the file with the hidden layers removed, while
keeping a map from every projected line back to its original line.
4. **Diff the projections**, not the originals. This is the important step — hiding lines in a
normal diff after the fact gets mixed lines and comment-only hunks wrong. Re-diffing the
projected text makes those cases fall out naturally.
5. **Render** using the line map, so line numbers, "open in editor" and review comments always
refer to the real file.
The line map (steps 3–5) is where the real difficulty lives; everything else is assembly of
well-understood parts.
## Shape of the product
A **Rust core library** with front-ends on top:
- **TUI (first)** — `declutter main..HEAD`, `declutter --staged`, a file tree plus diff view,
single-key layer toggles. Built with [ratatui](https://ratatui.rs/). Fits the terminal
workflow where AI agents already run.
- **`git difftool` mode** — drop-in for existing git workflows.
- **Machine output** — `--hide comments,tests --format json|patch`, so an agent or script can
consume a decluttered diff.
- **Later:** a browser extension for GitHub / Azure DevOps PR pages, where most review actually
happens; possibly an editor extension.
Rust because tree-sitter's primary bindings are Rust, the TUI ecosystem is strong, and a
single static binary is easy to install.
## Prototype scope (v0)
The prototype proves the core idea end to end and nothing more:
- Input: a git revision range or the working tree.
- Layers: **comments** only (including docstrings).
- Languages: **Swift, TypeScript, Python** (JavaScript rides along on the TSX grammar).
- Views: unified diff in a TUI with a comments on / off / only toggle and the hidden-count
status line.
- Correctness tests for the hard cases: comment-only hunks, code + trailing comment on one
line, multi-line block comments spanning a hunk boundary, docstrings, files with syntax
errors.
Done when: reviewing a real AI-generated branch with comments toggled off shows only code
changes, with correct line numbers, and toggling back restores the full diff.
### Decisions made while building v0
- **Syntax errors don't disable a file.** tree-sitter recovers from errors, and real-world
files often contain constructs a grammar doesn't know. Comments are still collected from
the recovered tree, and the file is marked *partial parse* so the reviewer knows a comment
near the error might be missed.
- **Blank lines are layout when comments are hidden.** An added docstring or comment usually
brings a blank line with it; showing that blank line as a change is noise. With comments
hidden, added or removed blank lines are not shown as changes (context blank lines stay).
- **Swift the grammar can't parse is stood in for.** tree-sitter-swift 0.7 rejects the empty
tuple as a value (`.success(())`), `await` opening a condition (`if await a != b`) and
`nonisolated(unsafe)`. Before parsing, these are swapped for same-length text that parses
(`[]`, blanks), so every byte offset still points into the real file. On a 878-file Swift
codebase this halved the files with parse errors (20 → 10); the rest report the line of
the first error ("partial parse near line 80").
- **Unsupported file types hide nothing** and are counted in the status line
("comments not detected in N files").
- **Default mode is comments hidden** — that is the point of opening the tool.
### Decisions made while adding the tests layer
- **Tests are file-granular.** A file is test code when its path says so (a `test`/`tests`/
`spec`/`__tests__`/`__mocks__`/`__snapshots__` directory, a directory ending in `Tests`, or a
`FooTests`/`FooTest`/`FooSpec`, `.test.`/`.spec.`, `test_`/`_test`/`_spec` file name), or when a
Swift, Python or TypeScript file imports a test framework (`@testable import`, `import XCTest`,
`import Testing`, `pytest`, `unittest`, `vitest`, `@jest/globals`, `node:test`,
`@testing-library/`). Hidden test files leave the file list entirely; *only* lists nothing else.
- **Tests default to shown.** Unlike comments, tests are code a reviewer must read; hiding them
is a choice for ordering the review, not the starting point.
- **The two layers are independent.** Comments mode applies inside test files too, so
"tests only, comments hidden" reviews just the test logic.
- **In-file test blocks are not detected yet** (Rust `#[cfg(test)]`, a stray `@Test` in a
source file). In Swift, Python and TypeScript projects tests live in their own files, so path
rules cover the common case.
## Built since v0
- `declutter pr <url | number>` for GitHub and Azure DevOps pull requests.
- Review marks (`r`), remembered per change, so a re-pushed file comes back unreviewed.
- Word-level highlighting inside edited lines, and syntax colouring.
- Line notes (`m`) exported as one prompt for a coding agent (`E`, `declutter notes`).
- Imports (`i`), logging (`l`) and formatting-only (`f`) layers; Shift + a layer key shows that layer alone.
- Moved-code detection across files, collapsible with `M`.
- Fewer partial Swift parses, and the line of the first error when one remains.
- Open in editor at the cursor's line (`o`).
- Go, Rust and Kotlin, with Rust's in-file `#[cfg(test)]` blocks as part of the tests layer.
- `--print -n`: every row with its old and new line numbers, for agents citing `file:line`.
- Notes from outside the viewer (`declutter notes add`), as drafts; a note on the change as
a whole (`P`); multi-line notes; posting without the viewer (`declutter notes post`); one
GitHub review instead of a comment per note; a log of what was posted, with links.
### Decisions made while opening notes to coding agents
- **A coding agent may write notes; only the reviewer posts them.** Notes added from the
command line are drafts, and drafts are never posted — not even with `--yes`. Opening a
draft (`m`, then Enter) is the act of making it the reviewer's. This keeps the non-goal
intact: declutter carries an agent's text, it never speaks for the reviewer.
- **A note must be on a line of the diff.** The viewer only draws notes on diff rows, so a
note elsewhere could never be opened, and so never posted. Refusals name the lines that
are in the diff, so an agent can correct itself.
- **No pick-list of notes to post.** Opening a draft accepts it and emptying a note deletes
it; a second mechanism for the same decision would only add keys.
- **Existing PR threads are not drawn** (for now). Their anchors drift as the PR moves —
Azure DevOps anchors to an iteration, GitHub marks comments outdated — so markers would
land on the wrong lines. An agent can check the threads itself before adding notes.
## Roadmap
1. In-file test blocks beyond Rust (a stray `@Test` in a Swift or Kotlin source file).
2. More languages (Java, C#, C/C++, Ruby).
3. Side-by-side view.
4. `git difftool` integration and JSON output.
5. Per-repo config (`.declutter.toml`): custom test paths, logging APIs, extra layers defined
as tree-sitter queries.
6. Browser extension for GitHub and Azure DevOps PRs.
## Non-goals
- **Not an AI reviewer.** It does not summarise, judge or comment on code; it changes what a
human sees.
- **Not a formatter or linter.** It never modifies source files.
- **Not a replacement for reading the hidden layers.** Hiding is a way to order the review,
not to skip parts of it — which is why hidden counts are always visible.
## Prior art
- [diffsitter](https://github.com/afnanenayet/diffsitter) — tree-sitter AST diff; can exclude
node kinds via config, but only leaf nodes, with no live toggle and no notion of tests.
- [difftastic](https://github.com/Wilfred/difftastic) — structural diff; excellent at
formatting noise, no category filtering.
- [tuicr](https://github.com/agavra/tuicr), turboreview — terminal review tools for AI diffs;
their "comments" are reviewer notes, not code comments.
None combine a structural parse with reviewer-controlled layer toggles.
## Open questions
- Should "comments off" keep doc comments on public API (they are part of the contract)?
Possibly a separate `d` layer.
- How to present a hunk where the code change is only meaningful with its comment (e.g. a
`// SAFETY:` justification)? Likely: hide by default, flag specially-marked comments.