hewdiff 0.3.0

High-performance review-first terminal diff viewer with PR-style comments
hewdiff-0.3.0 is not a library.

hew

A fast, review-first terminal patch viewer, in Rust.

hew reads a unified diff and opens it in an interactive review UI, displaying GitHub-PR-style threaded comments loaded from a sidecar JSON file. Source selection (working tree, revs, two files) is delegated to git — you just pipe a diff in.

hew = to cut/shape a block with an axe. Same "chunk/block" lineage as a diff hunk. Three letters, fast to type. Inspired by hunk, rebuilt from zero as a native single binary.

Install

cargo install --path .
# or
cargo build --release   # → target/release/hew

Usage

hew consumes a unified patch and nothing else — from a file, or stdin:

hew change.patch                 # review a patch file
git diff HEAD | hew              # review the working tree (piped)
git show <rev> | hew             # review a commit
git diff <a> <b> | hew           # compare two refs
hew - < change.patch             # explicit stdin

Load existing review comments from a sidecar JSON file. This file is an immutable input — hew reads it as the review's starting point and never writes back to it:

hew change.patch --comments review.json

On exit, hew prints a compacted action log (what the session changed) to stdout — it never modifies its inputs:

git diff | hew --comments base.json > actions.json

The log is the minimal set of actions (add_comment, reply, resolve, unresolve, delete) that turn base.json into the reviewed state; a thread created then deleted, or a resolve toggled back, cancels out. An untouched session prints []. A consumer (e.g. a GitHub bridge) replays the log against the same base.

Replay needs stable thread ids. Actions reference threads by id. For the log to be replayable against base.json, that base must carry stable id values (a producer like a GitHub bridge writes them). A handwritten sidecar that omits id gets fresh random ids at load, so its resolve/reply/delete actions won't match the on-disk base — fine for ad-hoc viewing, not for replay.

Options

Flag Meaning
FILE (positional) Patch file to review. Omit or use - for stdin.
--comments <FILE> Sidecar JSON of existing review comments to load (immutable).

Keys

Key Action
j / k (or ↓/↑) Move one line
Ctrl-D / Ctrl-U Half page down / up
Space / b (or Ctrl-F/Ctrl-B, PageDown/PageUp) Page down / up
Ctrl-E / Ctrl-Y Scroll viewport one line (cursor stays in view)
g / G (or Home/End) Jump to top / bottom
[ / ] Jump to previous / next file
n / N Jump to next / previous comment
Enter / o Toggle the comment thread on the current line
v Visual line-select: extend with j/k, then i anchors a comment to the range
i Write a new comment on the current line (or the visual/drag selection)
r Reply to the thread on the current line
R Resolve / unresolve the thread on the current line
D Delete the thread on the current line
/ Focus the file list / the diff pane
Ctrl-B Toggle the file list sidebar
Tab / s Toggle unified ↔ split (side-by-side) layout
y Copy the selected line(s) to the clipboard
Esc Leave visual mode / clear the line selection
Ctrl-L Force a full repaint
q Quit

Mouse: click a file in the sidebar to open it, click a diff line to place the cursor, drag to select a range of lines, and use the wheel to scroll the pane under the pointer. Scrolling the file list just moves the list — it leaves the selected file unchanged. Drag the sidebar/diff divider to resize the sidebar. Both panes show a scrollbar when content overflows. y copies the selection to the clipboard via OSC 52 (works in terminals that support it).

Multi-file diffs show a file list sidebar, grouped by directory (a dim header per directory, files listed by basename with +adds/-dels, current file highlighted), and the diff pane shows only the selected file. Keyboard navigation acts on the focused pane: focus the sidebar with and j/k/g/G move between files (the diff follows); (or Enter) returns to the diff, where j/k/paging scroll within that one file. [/] switch files from either pane. The focused pane's selection is brighter.

Unified stacks -/+ lines; split shows old on the left and new on the right (like git delta --side-by-side), pairing changed lines across a divider. Toggling keeps the cursor on the same line.

Comments are loaded from a sidecar (immutable) and displayed (gutter markers + inline popup). You can compose/reply/resolve/delete threads in-app; on exit hew prints the compacted action log to stdout (the inputs are never written).

Comment sidecar format

{
  "threads": [
    {
      "file": "src/main.rs",
      "side": "new",
      "range": { "start": 18, "end": 22 },
      "resolved": false,
      "comments": [
        { "author": "agent", "body": "This match arm is unreachable." },
        { "author": "you",   "body": "Good catch." }
      ]
    }
  ]
}
  • side: "new" (added/context, RIGHT) or "old" (removed, LEFT)
  • range: a single line uses start == end
  • comments[0] is the thread root; the rest are replies
  • author, resolved, id, created_at are optional (sensible defaults)

A bare [ ...threads... ] array is also accepted.

Action log format (output)

On exit hew prints a JSON array of actions to stdout — the minimal delta that turns the --comments base into the reviewed state. This is the program's result: an agent reads it to drive gh (post comments, resolve threads), feeds it into the next step, or audits it. No action types beyond these five are ever emitted:

[
  { "action": "add_comment", "thread_id": "<uuid>", "file": "src/main.rs",
    "side": "new", "start_line": 18, "line": 22, "body": "This arm is unreachable.", "author": "you" },
  { "action": "reply",     "thread_id": "<uuid>", "body": "Good catch.", "author": "you" },
  { "action": "resolve",   "thread_id": "<uuid>" },
  { "action": "unresolve", "thread_id": "<uuid>" },
  { "action": "delete",    "thread_id": "<uuid>" }
]
  • add_comment is a new thread's root, anchored to (file, side, line). line is the thread's last line (GitHub's anchor); start_line is present only for a multi-line range and omitted for a single line (matching GitHub's start_line/line review-comment shape). Its thread_id is reused by any reply to the same thread within the log.
  • reply / resolve / unresolve / delete reference an existing thread by thread_id (a base thread, or one add_comment-ed earlier in the same log).
  • author is omitted when unset.
  • An untouched session prints [].
  • Compaction is automatic: add-then-delete and resolve-then-unresolve cancel.

There is no GitHub-specific code in hew — the binary only speaks this JSON. The "bridge" to GitHub is whoever consumes the log: an agent that knows this schema can read gh api PR threads into a base sidecar and replay the action log back through gh, with no wrapper binary required. Thin, convenience-only examples of both directions ship in examples/: fetch_pr.sh prepares the base sidecar from a PR, and apply_actions.sh replays the log's add_comments via gh (reply/resolve/delete need the consumer's thread_id→GitHub-id mapping).

Examples

Real {patch + comments} from public PRs live in examples/:

hew examples/misskey-ja.patch   --comments examples/misskey-ja.comments.json
hew examples/rust-long-en.patch --comments examples/rust-long-en.comments.json

See examples/README.md for how to fetch more.

Design & roadmap

hew stays intentionally small and is a pure filter: no GitHub/network integration, no patch apply/edit/merge, no structural (AST) diff, and no "save" — its inputs (the patch and the --comments base) are immutable, and it emits a compacted action log to stdout on exit. It offers unified and split layouts, in-app authoring (compose/reply/resolve/delete), syntax highlighting (syntect + two-face's bat syntax set for broad language coverage, Monokai Extended Bright theme, pure-Rust fancy-regex), and sidecar comment threads.

Because hew is a pure filter with a documented JSON contract (see Comment sidecar format and Action log format above), the GitHub round-trip needs no code in the binary: an agent that understands the schema is the bridge — it prepares the base sidecar from gh api and replays the action log through gh.

Design invariants

These are the rules that keep hew small — they should not be broken:

  • hew never talks to GitHub. It eats a patch + a comment JSON, nothing else. The GitHub round-trip lives entirely outside the binary; any gh wrappers ship only as examples/, never as a dependency.
  • hew is a pure filter — no "save". All inputs are immutable: the patch (stdin) and the --comments base JSON are read, never written. There is no save/flush/autosave/document concept.
  • The TUI is the sole writer of its in-memory store. All edits (compose/reply/resolve/delete) mutate one CommentStore.
  • Output is a compacted action log, not the comment store: on exit hew emits diff(base, final) as a minimal action array to stdout (a thread created then deleted, or a resolve toggled back, cancels out). Replay requires the base to carry stable thread ids.
  • Channels stay separated: stdin = patch, stderr/tty = render, stdout = action-log result.
  • No daemon, no DB, no background services, and a deliberately minimal CLI (just FILE and --comments).

Planned

A tree-sitter highlighting backend and theme selection. Optional gh wrapper examples may ship under examples/, but they are conveniences, not a dependency. Live in-session AI co-review over a socket is deferred and out of scope for v1; the turn-based flow (an agent prepares a base review, opens hew for the human, then drives gh from the emitted action log) covers the v1 workflows without it.

Note: hew parses plain unified diffs, not git format-patch mailbox output (gh pr diff --patch). Use a .diff/git diff stream instead.