From 74690b594fd2a48fe3bc7ebc853c376543671645 Mon Sep 17 00:00:00 2001 From: Dillon Sienko Date: Fri, 23 Oct 2026 09:09:13 -0700 Subject: [PATCH 1/3] Rewrite the README against the current code --- README.md | 181 +++++++++++++++++++++++++++++------------------------- 1 file changed, 96 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 60dc51b..3701b8b 100644 --- a/README.md +++ b/README.md @@ -6,116 +6,137 @@ install · quick start · agents · terminal UI · keybindings · development

-A local code review UI tool with optional AI agent integration. +A local code review UI for a git branch, with optional AI agent integration. -Review any branch with syntax-highlighted diffs, inline comments, and risk scoring. If you have a coding agent installed locally, you can leave feedback directly on the diff, watch the agent respond, and review the next round of changes in a tight loop without leaving the browser. There is also an AI-powered explain feature that walks you through what the branch does. +You get syntax-highlighted diffs, inline comment threads and risk scoring. If you have a coding agent installed, you can leave comments on the diff, send them to the agent, watch it respond, and review the next round of changes without leaving the page. -Runs as a local web server with a Preact frontend. No build step and no accounts. Your code stays on your machine, with one exception: a markdown file containing a mermaid or plantuml block is rendered by kroki.io, so that block's source is sent there when you preview the file. Frontend libraries load from a CDN, so the first paint needs a network. +Use it before you push. Review what your agent just wrote while nothing is on a remote yet, or look at your own branch in a real diff UI without opening a draft MR. GitHub or GitLab review still happens after. -## Who is this for +It runs as a local web server with a Preact frontend, or as a terminal UI with `diffui --tui`. There is no build step and no accounts. Your code stays on your machine with one exception: a markdown file with a mermaid or plantuml block is rendered by kroki.io, so that block's source is sent there when you preview the file. The web UI loads its frontend libraries from a CDN, so the first paint needs a network. -If you use Claude Code, Codex, Cursor, or similar tools, diffui beats scrolling terminal diffs or waiting for a pushed MR. Review locally, leave comments, and send feedback back to the agent before anything hits a remote. - -For reviewing your own work before pushing, it saves you opening a draft MR just to see your changes in a proper diff UI. Useful when your IDE's diff view doesn't cut it for larger changes, or when you want risk scoring, comment threads, or a cross-file overview. +## Install -If you spend most of your time in a terminal, diffui fits that flow. It runs locally, prints a URL, and works well in terminal browsers or apps that support inline web content. +diffui needs Python 3.10 or newer. There is no PyPI release, so install from GitHub: -Use diffui *before* pushing, to catch issues early and iterate with agents locally. GitHub/GitLab review still happens after. +```bash +pipx install git+https://github.com/dgsienko/diffui +``` -## Install +To work on diffui, install a clone in editable mode so the `diffui` command follows your checkout: ```bash +git clone https://github.com/dgsienko/diffui +cd diffui pipx install -e . ``` ## Quick start -`cd` into any git repo on a feature branch and run: +`cd` into a git repo on a feature branch and run: ```bash diffui ``` -This starts a local server and prints the URL. Open that URL via an external browser or within your terminal application (if supported). You'll see every file changed on the current branch (vs the merge base with `origin/main` or `origin/master`, falling back to the local branch when there's no remote) in a tabbed, syntax-highlighted diff viewer. +This starts a server on a free local port and prints the URL. Open it in a browser, or in a terminal app that renders web content. You'll see every file changed on the branch, plus uncommitted and untracked files, in a syntax-highlighted diff. + +The branch is compared with its merge base against `main` or `master`. diffui uses the `origin/` copy when it exists and the local branch otherwise. -Sibling repos in the same parent directory are auto-discovered and available via a dropdown for easy repository switching. +When the parent directory holds several git repos, they show up in a dropdown for switching. To review specific repos, pass their paths: + +```bash +diffui ~/code/api ~/code/web +``` + +diffui watches the repo with watchfiles and pushes changes to the page over a WebSocket (SSE as a fallback), so edits from an agent appear without a reload. ### CLI flags ```bash -diffui --open # Automatically open the URL in an external browser -diffui --comments # Dump all comments to stdout -diffui --json # Export review session as structured JSON -diffui --tui # Review in the terminal instead of the browser +diffui --open # Open the URL in your browser +diffui --comments # Print all comments to stdout +diffui --json # Print the review session as JSON +diffui --tui # Review in the terminal (see Terminal UI) ``` ## Reviewing changes -- Navigation: the explorer sidebar lists the changed files, grouped by directory, file type, or change status. Click a file or use the arrow keys to move through them in the order the sidebar shows. The Explorer button in the top bar hides and shows it. `]` jumps to the next unreviewed file. -- Diff modes: toggle between unified diff, side-by-side split, or full file view using the segmented control in the file header above the diff. Whitespace, Hide reviewed, Blame, More context and Mark reviewed sit next to it. -- View filter: a dropdown in the top bar switches between all branch changes, uncommitted working changes, the agent's last turn (see [Last turn](#last-turn)), or individual commits (most recent first). -- Reviewing: press `r` or use the dedicated button to mark a file as reviewed. The top bar shows how many files you've reviewed next to the total added and removed lines. If a reviewed file changes on disk, it's automatically unmarked. When everything is reviewed, a completion screen appears with stats. -- Hunk navigation: `j`/`k` to jump between hunks. Hunk headers show their own +/- counts and can be collapsed by clicking. The raw `diff --git`/`index`/`---`/`+++` lines are hidden. A rename, mode change, new or deleted file, or binary file gets one short line instead. -- Search: `ctrl+f` to search within the current diff with match highlighting and navigation. `ctrl+g` to jump to a specific line. `ctrl+shift+f` (or the "Search files" button) to filter the file list. -- Risk scoring: files are scored by risk factors (migrations, config changes, large deletions, test removal, high churn). Medium and high risk files show colored dots in the file tree, with the level in the tooltip. Sort by risk via the command palette. +- The explorer sidebar lists the changed files, grouped by directory, file type or change status. Click a file or press the left and right arrow keys to move through them in the order the sidebar shows. The Explorer button in the top bar hides and shows it. +- The view dropdown in the top bar switches between all branch changes, uncommitted changes, the agent's last turn (see [Last turn](#last-turn)), and single commits, most recent first. +- A segmented control in the file header switches between unified, split and full file views. Whitespace, Hide reviewed, Blame, More context and Mark reviewed sit next to it. +- Press `r` to mark a file reviewed. The top bar shows reviewed files out of the total, next to the added and removed line counts. If a reviewed file changes on disk, it is unmarked. When everything is reviewed, a completion screen shows your stats. `]` jumps to the next unreviewed file. +- `j` and `k` jump between hunks. Click a hunk header to collapse it. The raw `diff --git`, `index`, `---` and `+++` lines are hidden, and a rename, mode change, new or deleted file, or binary file gets one short line. +- `ctrl+f` searches within the diff and highlights the matches. `ctrl+g` jumps to a line. `ctrl+shift+f`, or the Search files button, filters the file list. +- Files are scored for risk from their path and size: migration, infra, schema and deploy paths, config files, large deletions, high churn and test removal. Medium and high risk files get a colored dot in the file tree, with the level in the tooltip. `s` sorts by risk. +- A `.diffuiignore` file in the repo root hides matching files from the file list and from search. It takes one glob per line, and `#` starts a comment. `diffui --json` still lists them. ## Comments -Right-click a line or press `c` to leave a comment. Comments support: +Right-click a line, or hover it and press `c`, to leave a comment. -- Text selection: highlight text, within a line or across several, and click the floating "Comment" button to leave a comment that references that exact span; the referenced text stays highlighted in the diff (works in unified and split views) -- Categories: tag as bug, suggestion, nit, or question (colored badges) -- Code suggestions: attach a proposed code change with inline before/after preview and an Apply button that patches the file. Apply refuses if the line has changed since you wrote the comment, or if the comment covers more than one line -- Threads: reply to comments; named authors (user vs agent) with color-coded identities -- Resolution: resolve/reopen with a toggle; resolved comments appear dimmed -- Navigation: `n`/`p` to jump between comments across files -- Comments panel: click "Comments" in the top bar to see all open comments grouped by file -- Bulk resolve: resolve all comments in a file or globally from the command palette or comments panel -- Export: `Shift+S` copies a formatted markdown review summary to clipboard with stats and open comments by category +- Select text within a line or across several, then click the floating Comment button. The comment references that exact span, and the span stays highlighted in the diff. This works in unified and split views. +- Tag a comment as bug, suggestion, nit or question. +- A suggestion carries a proposed replacement, with an inline before and after preview and an Apply button that patches the file and resolves the comment. Apply refuses if the line changed since you wrote the comment, or if the comment covers more than one line. +- Replies form a thread. User and agent replies show their author in different colors. +- Resolve or reopen a thread with its toggle. Resolved comments are dimmed. +- `n` and `p` jump between comments across files. +- The Comments button in the top bar opens a panel of open comments grouped by file. Resolve all comments in a file, or everywhere, from the panel or the command palette. +- `S` copies a markdown review summary to the clipboard, with file and comment counts, open comments by category, and the status of each file. ## AI agent integration -Comments are stored at `~/.config/diffui/{repo}/{branch}/comments.json` as structured JSON (file path, line number, diff context, comment text, author, category, code suggestion, and resolution status). AI agents can read and reply to this file directly. - -- Send to agent: click the button in the top bar to spawn your configured agent CLI (Claude Code, Codex, OpenCode, or Cursor Agent) with a context file containing full diffs, review state, and existing comment threads. An interactive terminal panel slides up from the bottom, streaming the agent's full TUI output in real time. You can answer permission prompts, navigate multi-choice menus, and interact with the agent directly. The terminal shows elapsed time and detects when the agent goes idle. diffui auto-refreshes as the agent makes changes. -- Explain changes: click the button in the top bar to generate a self-contained HTML walkthrough of the branch (TL;DR, file-by-file analysis, architecture notes, risk flags). +Comments are stored at `~/.config/diffui/{repo}/{branch}/comments.json`. Each one has the file path, line number, diff context, comment text, author, category, suggestion and status. An agent can read that file and reply to it directly. -Both actions are also available in the command palette (`ctrl+k`). +- Send to agent builds a prompt from your open comments and writes a context file with the full diffs of the commented files, the review state and the existing threads. It then starts your configured agent CLI in a terminal panel that slides up from the bottom of the page. The panel streams the agent's full TUI, so you can answer permission prompts and use its menus. It shows elapsed time and notices when the agent goes idle. The page refreshes as the agent edits. +- Explain changes starts the agent CLI in the background to write a self-contained HTML walkthrough of the branch: TL;DR, motivation, file-by-file analysis, architecture notes, risk flags and where to focus. -Or tell your agent directly: "Go address the diffui comments." +Both are also in the command palette (`ctrl+k`, or `cmd+k` on macOS). Pick the agent in Settings (gear icon): Claude Code, Codex, OpenCode or Cursor Agent. -Configure your preferred agent CLI in Settings (gear icon). +You can also point your own agent at the comments file. ### Running inside herdr -When you start the `diffui` server in a [herdr](https://herdr.dev) pane, Send to agent in the browser pastes the prompt into the agent already running in that workspace, so you keep its session and context. It picks the agent working in the same repo, or the first agent in the workspace if none is. herdr then focuses that pane, and you can add to the prompt before pressing Enter. diffui won't paste while the agent is sitting at a permission prompt, because Claude Code throws the paste away there. +When you start `diffui` in a [herdr](https://herdr.dev) pane, Send to agent pastes the prompt into the agent already running in that workspace, so you keep its session and context. diffui needs `herdr` on your PATH and detects the pane from `HERDR_WORKSPACE_ID`. -Inside herdr, diffui also tracks agent turns for the [Last turn](#last-turn) scope. +It picks the agent working in the same repo, or the first agent in the workspace if none is. herdr focuses that pane, and you can add to the prompt before pressing Enter. diffui won't paste while the agent is at a permission prompt, because Claude Code throws the paste away there. -diffui also reports two values for the workspace's row in herdr's sidebar: `$comments` (like `3 open`) and `$reviewed` (like `☑ 12/20`). Add them to your herdr config to show them: +diffui also reports two values for the workspace's row in herdr's sidebar: `$comments` (like `3 open`) and `$reviewed` (like `☑ 12/20`). Add them to a row in your herdr config: ```toml [ui.sidebar.spaces] rows = [ ["state_icon", "workspace"], - ["branch", "git_status"], ["$comments", "$reviewed"], ] ``` +diffui clears both values when it exits. + +### Last turn + +Last turn shows what the agent changed since its latest turn began. It needs diffui in a herdr pane and an agent working in the same repo in any herdr workspace. + +The server polls `herdr agent list` every second. When an agent in the repo goes from idle or done to working, it saves the worktree as a git tree, untracked files included. It builds that tree in a temporary index, so your index and files stay untouched. The tree is stored under `refs/worktree/diffui/last-turn`, so it survives a restart, and each linked worktree keeps its own. The scope diffs that tree against the worktree as it is now. + +Without herdr, or before an agent has started a turn, the scope says "No agent turn seen yet". The snapshot is taken when the poll first sees `working`, so a change made before that is already in it and does not show. A turn that changes nothing still replaces the previous baseline. + ## Terminal UI -`diffui --tui` runs the review loop in the terminal, so it fits in a herdr split next to the coding agent. It starts the same server on a free port and talks to it over HTTP, which means comments, reviewed state and risk scores are the ones the browser shows. Press `o` to open the web UI at the current file. +`diffui --tui` runs the review loop in the terminal, so it fits in a herdr split next to your coding agent. It starts the same server on a free port and talks to it over HTTP. Comments, reviewed state, risk scores and your theme are the ones the browser shows. Press `o` to open the web UI at the current file. + +The TUI covers the file list, a unified or split diff with syntax highlighting, comment threads, reviewed state, diff scopes, search, find in file and Send to agent. Explain changes, markdown and mermaid preview, blame and the agent terminal stay in the browser. -The TUI covers the file list, a unified or split diff with syntax highlighting, comment threads, reviewed state, diff scopes, search, find in file, and Send to agent. The file list sits on the right of the diff. Inside herdr, Send to agent pastes the prompt into the workspace's agent pane. Anywhere else it shows the prompt so you can copy it with `c`. Explain changes, markdown and mermaid preview, blame and the agent terminal stay in the browser. +The file list sits on the right of the diff. Under 110 columns it stacks below the diff, and `z` hides it. + +Inside herdr, `A` pastes the prompt into the workspace's agent pane. Anywhere else it shows the prompt, and `c` copies it. | Key | Action | | --- | --- | | `left` / `right`, `h` / `l` | Previous / next file | | `k` / `j` | Previous / next hunk | | `p` / `n` | Previous / next comment thread | -| `up` / `down`, `pgup` / `pgdn`, `g` / `G` | Move the cursor | -| `r` | Mark the file reviewed or unreviewed, then open the next unreviewed file | +| `up` / `down`, `pgup` / `pgdn`, `ctrl+u` / `ctrl+d`, `g` / `G` | Move the cursor | +| `r` | Mark the file reviewed or unreviewed. Marking it reviewed opens the next unreviewed file | | `]` | Next unreviewed file | | `s` | Sort files by risk | | `u` / `b` / `t` | Scope: uncommitted changes / everything on the branch / the agent's last turn | @@ -136,18 +157,18 @@ The TUI covers the file list, a unified or split diff with syntax highlighting, | `?` | Help | | `q` | Quit | -The footer's first hints follow the cursor: `c` Comment and `v` Select range on a line, `c` Comment on N lines and `esc` Clear with a range, `c` Reply and `x` Resolve on a thread. +The footer shows the keys that fit the row under the cursor. -Clicking a file opens it, and clicking a diff row selects it. Hovering a row swaps its line number for a `[+]`. Clicking the number column opens the comment box for that line in one click, and dragging down it comments on the range. `ctrl+s` saves the comment. A range comment is stored the way a multi-line text selection is in the browser, so the web UI highlights the same lines. +Clicking a file opens it, and clicking a diff row selects it. Hovering a row swaps its line number for a `[+]`. Click the number column to open the comment box for that line, or drag down it to comment on a range. `ctrl+s` saves the comment. A range comment is stored the way a multi-line text selection is in the browser, so the web UI highlights the same lines. ### Scopes The header shows the current scope. The browser has the same choices in its view dropdown. -- Uncommitted (`u`): staged and unstaged changes plus new files. -- Branch (`b`): everything since the branch left its base, commits included. This is the default. -- Last turn (`t`): changes since the agent's latest turn started. See [Last turn](#last-turn). -- Commit (`C`): the changes of one commit. +- Uncommitted (`u`) is staged and unstaged changes plus new files. +- Branch (`b`) is everything since the branch left its base, commits included. This is the default. +- Last turn (`t`) is the changes since the agent's latest turn started. See [Last turn](#last-turn). +- Commit (`C`) is the changes of one commit. ### Search and find @@ -161,26 +182,20 @@ The header shows the current scope. The browser has the same choices in its view `|` puts the old file on the left and the new one on the right, with their own line numbers and tints. It needs a diff pane 100 columns wide. In a narrower pane the TUI stays unified and says so, and hiding the file list with `z` often makes room. A comment on a row goes against the new line, or the old line when it was removed, which is what the browser's split view stores. On a row that replaces one line with another, click the left or right number column, or press `tab`, to choose the side. -### Last turn - -Last turn needs diffui to run in a herdr pane and an agent to work in the same repo in any herdr workspace. The server polls `herdr agent list` every second. When an agent inside the repo goes from idle or done to working, it saves the worktree as a git tree, untracked files included, built in a temporary index so your index and files are untouched. The tree is stored under `refs/worktree/diffui/last-turn`, so it survives a restart, and each linked worktree keeps its own. The scope diffs that tree against the worktree as it is now. - -Without herdr, or before an agent has started a turn, the scope says "No agent turn seen yet". herdr reports `working` a moment after the agent starts writing, so a change in that gap is already in the saved tree and does not show. A turn that changes nothing still replaces the previous baseline. - ## Display -- 16 color themes, with Graphite as the default: Graphite, Catppuccin Mocha/Latte, GitHub Dark, Dracula, One Dark, Solarized Dark, Gruvbox Dark, Nord, Tokyo Night, Rose Pine, Rose Pine Moon, Monokai Pro, Kanagawa, Everforest, Ayu Dark -- Blame gutter: toggleable git blame showing author and age -- Markdown/image preview: rendered preview for `.md` files, inline display for images. Markdown is sanitized before rendering, and diagram blocks go to kroki.io -- Font size and line wrap: adjustable in settings or via command palette -- Ignore whitespace: toggle via the file header button or `w` key -- Open in editor: `ctrl+click` a line to open it in VS Code, Cursor, Vim, or Neovim (configurable in settings) +- 16 color themes, with Graphite as the default: Graphite, Catppuccin Mocha, Catppuccin Latte, GitHub Dark, Dracula, One Dark, Solarized Dark, Gruvbox Dark, Nord, Tokyo Night, Rosé Pine, Rosé Pine Moon, Monokai Pro, Kanagawa, Everforest and Ayu Dark. +- The blame gutter shows the author and age of each line. +- `.md` files get a rendered preview and images show inline. Markdown is sanitized before rendering, and diagram blocks go to kroki.io. +- Font size and line wrap are in Settings and the command palette. +- `w` or the Whitespace button toggles ignore whitespace. +- `ctrl+click` (`cmd+click` on macOS) a line to open it in VS Code, Cursor, Vim or Neovim. Pick the editor in Settings. ## Keybindings | Key | Action | | --- | --- | -| `ctrl+k` | Command palette | +| `ctrl+k` / `cmd+k` | Command palette | | `?` | Keyboard shortcuts overlay | | `left` / `right` | Previous / next file | | `j` / `k` | Next / previous hunk | @@ -196,34 +211,27 @@ Without herdr, or before an agent has started a turn, the scope says "No agent t | `y` | Copy file path | | `Y` | Copy GitLab link | | `ctrl+f` | Search in diff | -| `ctrl+g` | Go to line | +| `ctrl+g` / `cmd+g` | Go to line | | `ctrl+shift+f` | Search files | -| `ctrl+click` | Open in editor | +| `ctrl+click` / `cmd+click` | Open in editor | | `right-click` | Comment on line (shows the native menu when text is selected, so you can copy) | | select text | Floating "Comment" button to comment on the exact substring | | `escape` | Close panel or dialog | -All shortcuts can be rebound in Settings. +Most shortcuts are ignored while a dialog is open. `r`, `a`, `c`, `y`, `s`, `S`, `b` and `]` can be rebound in Settings. The terminal UI keys are in [Terminal UI](#terminal-ui). ## State -All state lives in `~/.config/diffui/`. Nothing is written to repo directories. +Review state lives in `~/.config/diffui/`: -- `settings.json`: theme, editor, agent CLI, display name (global) -- `{repo}/{branch}/reviewed.json`: review status (per-branch) -- `{repo}/{branch}/comments.json`: comments (per-branch) -- `{repo}/{branch}/session.json`: UI session state (per-branch) +- `settings.json` holds theme, editor, agent CLI, display name, font size, line wrap and rebound keys. It is global. +- `{repo}/{branch}/reviewed.json` holds review status. +- `{repo}/{branch}/comments.json` holds comments. +- `{repo}/{branch}/session.json` holds UI session state: the active file, diff mode, scroll positions and the explorer and reviewed-file toggles. -## Other features +`{repo}` is the repo's full path with `/` replaced by `_`, and `{branch}` is the branch name the same way. -- `.diffuiignore` file to hide files from review (still visible to agents) -- Whole-repo file and text search in the terminal UI, built on the fff index -- `diffui --json` for programmatic agent workflows -- Auto-refresh via WebSocket (SSE fallback) powered by watchfiles; the terminal UI uses the SSE stream -- Virtual scrolling for large files -- Session persistence (active file, scroll positions, UI state) -- Context-aware keybindings (suppressed when dialogs are open) -- Untracked files shown alongside committed changes +Three things live elsewhere. The last-turn baseline is a git ref in the repo's own git directory. The agent context file (`diffui-agent-context-{branch}.md`) and the Explain output (`diffui-explain-{branch}.html`) go to your system temp directory, and diffui deletes them when it exits. The one working tree write is Apply on a suggestion. ## Development @@ -231,8 +239,11 @@ All state lives in `~/.config/diffui/`. Nothing is written to repo directories. pip install -e ".[dev]" pytest ruff check . +ruff format --check . ``` +AGENTS.md has the architecture. + ## License Copyright (c) 2026 Dillon Sienko. All rights reserved. From 2e945038b1ac6e9c3cf51753b69eecceb1b130b4 Mon Sep 17 00:00:00 2001 From: Dillon Sienko Date: Fri, 23 Oct 2026 09:09:13 -0700 Subject: [PATCH 2/3] Tighten AGENTS.md and correct stale claims --- AGENTS.md | 606 +++++++++++++----------------------------------------- 1 file changed, 143 insertions(+), 463 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7a44aea..d5ff875 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,502 +1,182 @@ # diffui -UI for reviewing AI agent diffs. FastAPI backend + Preact frontend, -no build step required, plus a Textual terminal client of the same API. - -## Tech Stack - -- Python 3.10+ -- FastAPI + uvicorn (server) -- Preact + HTM (frontend, no build step) -- Pygments for syntax highlighting -- watchfiles for real-time filesystem watching (Rust-backed FSEvents/inotify) -- xterm.js for interactive agent terminal (loaded via ESM importmap) -- Git operations via `subprocess` (no gitpython dependency) -- PTY-based agent process management (`pty` + `select` stdlib) -- Textual + httpx for the terminal UI (`diffui --tui`) -- fff-search (Rust file finder) for whole-repo search -- Installed globally via `pipx install -e .` +UI for reviewing AI agent diffs. A FastAPI server with a Preact web frontend, plus a Textual terminal client (`diffui --tui`) that talks to the same API. + +## Stack + +- Python 3.10+, FastAPI + uvicorn, Pygments, watchfiles, fff-search (whole-repo search), Textual + httpx (TUI). +- The frontend has no build step. Preact, HTM, marked and xterm.js load through the importmap in `static/index.html`. Don't add a bundler. +- Git runs through `subprocess` (no gitpython). The agent terminal uses `pty` and `select`. +- Install with `pipx install -e .`. ## Architecture ```text diffui/ -├── cli.py # Entry point: starts FastAPI server -├── diff.py # Pure functions: diff parsing, line classification, syntax -│ # highlighting, word-level diff ranges -├── git_utils.py # Git subprocess helpers, blame, state persistence, -│ # repo discovery, session management -├── server/ # FastAPI backend (web UI) -│ ├── app.py # App factory: mounts routes, static files, starts watcher -│ ├── events.py # WebSocket + SSE endpoints, watchfiles-based filesystem -│ │ # watcher with 400ms debounce -│ ├── herdr.py # herdr CLI calls: agent paste, sidebar status, agent list +├── cli.py # Entry point: server, --tui, --comments, --json +├── diff.py # Pure functions: diff parsing, line classification, word-level ranges +├── git_utils.py # Git subprocess helpers, blame, state persistence, repo discovery +├── server/ # FastAPI backend +│ ├── app.py # App factory, _LocalOriginOnly middleware, lifespan (watcher, pollers) +│ ├── events.py # /api/ws and /api/events, watchfiles watcher with 400ms debounce +│ ├── herdr.py # herdr CLI calls: agent list, paste, sidebar status │ ├── highlight.py # Pygments-to-HTML adapter, word highlight overlay -│ ├── routes_comments.py # Comment CRUD, suggestion apply endpoint -│ ├── routes_diff.py # Diff, files, blame, preview, risk scoring -│ ├── routes_repo.py # Repo listing and switching -│ ├── routes_review.py # Review toggle, summary export, agent/explain -│ │ # orchestration (thread-safe process management) -│ ├── routes_agent_terminal.py # PTY-based interactive agent terminal, -│ │ # WebSocket streaming, idle detection -│ ├── fff_index.py # Resident fff index of the active repo behind /api/search -│ ├── routes_search.py # /api/search: file names and content, in a view or the whole repo -│ ├── routes_settings.py # Settings, editor open, session persistence -│ ├── models.py # Pydantic request models for all endpoints -│ ├── state.py # AppState singleton: shared server state -│ ├── turns.py # Last-turn tracking: herdr agent poll, start snapshot -│ └── theme_css.py # generate_css_vars(theme): CSS custom properties -├── tui/ # Textual client (`diffui --tui`), a thin client of server/ -│ ├── app.py # DiffuiApp: key bindings, scopes, review actions, SSE-driven refresh +│ ├── routes_comments.py # Comment CRUD, bulk resolve, suggestion apply +│ ├── routes_diff.py # /api/files, diff, file, blame, preview, risk scoring +│ ├── routes_repo.py # Repo listing and switching, branch, commits +│ ├── routes_review.py # Reviewed toggle, summary, agent prompt/paste/run, explain +│ ├── routes_agent_terminal.py # PTY agent terminal: /api/agent/start, kill, ws +│ ├── routes_search.py # /api/search: file names and content +│ ├── routes_settings.py # Settings, editor open, session +│ ├── fff_index.py # Resident fff index of the active repo behind /api/search +│ ├── turns.py # Last turn: herdr poll, start snapshot, /api/last-turn +│ ├── models.py # Pydantic request models +│ ├── state.py # AppState singleton +│ └── theme_css.py # generate_css_vars(theme) +├── tui/ # Textual client, a thin client of server/ +│ ├── app.py # DiffuiApp: bindings, scopes, review actions, SSE-driven refresh │ ├── client.py # LocalServer (uvicorn in a thread) and ApiClient (httpx) │ ├── diffview.py # DiffView: virtual-scrolled diff with inline threads, cursor, mouse -│ ├── rows.py # Row and build_rows: unified rows and split old/new pairs +│ ├── rows.py # Row, build_rows, split_pairs │ ├── unified.py # Strip for one unified line row │ ├── split.py # Strip for one split line row -│ ├── style.py # Style cache, tints, code segments with find highlights +│ ├── style.py # Style cache, tints, marker and prefix_fg, code segments with find spans │ ├── find.py # Find (matches over rows) and the FindBar widget -│ ├── search.py # SearchScreen: names and content, Changed or All files +│ ├── search.py # SearchScreen │ ├── filelist.py # FileList: tree or risk-ordered files │ └── screens.py # Help, comment composer, agent prompt and commit picker -├── static/ # Preact frontend (no build step, ESM imports via importmap) -│ ├── app.js # Root component, state management, keyboard shortcuts, -│ │ # WebSocket client, session restore -│ ├── lib/ # Shared utilities -│ │ ├── markdown.js # marked.js wrapper with GFM config -│ │ └── utils.js # shortName(), mergeRef(), useResize() helpers -│ ├── components/ # TopBar, FileHeader, DiffViewer, SplitDiffViewer, -│ │ # FullFileViewer, FileTree, CommentBox, -│ │ # CommentDisplay, CommentsPanel, SearchBar, -│ │ # FileFilterBar, LineActions, SettingsPanel, -│ │ # Minimap, ShortcutOverlay, Toast, -│ │ # CommandPalette, CompletionScreen, PreviewViewer, -│ │ # AgentStatusBar, AgentConfirmDialog, -│ │ # AgentTerminal, GoToLineDialog -│ ├── index.html # Shell page with importmap for preact/htm CDN -│ └── style.css # All styles via CSS custom properties (themed) -└── themes/ - ├── __init__.py # Theme state (get/set_current_theme), re-exports - ├── theme.py # Theme dataclass (30+ color fields) - └── definitions.py # 16 theme instances with Pygments token color maps +├── static/ # Preact frontend: app.js, components/, lib/ (markdown.js, utils.js), style.css +└── themes/ # Theme dataclass, 16 themes in definitions.py, current-theme state ``` -### Shared Patterns - -- Multi-repo discovery: `resolve_repos()` in `git_utils.py` scans - the parent directory for sibling git repos. -- Path containment: `repo_path()` in `git_utils.py` joins a path onto the repo - root and raises `PathOutsideRepo` if the result resolves outside it, symlinks - included, which `app.py` maps to a 400. Every read and write driven by a - request goes through it. A route that only hands the path to git calls - `repo_relative()`, which checks the same way and returns the original string. - Joining `get_repo_root()` with a request value by hand reaches any file on the - machine, because pathlib returns an absolute right-hand operand whole. -- Revisions from a client: a `view` that is none of `all`, `working` or `last-turn` - is looked up in `app_state.commits` before it reaches `git diff`. Git reads a - leading `--` as an option, so an unchecked value can write files. -- Branch-scoped state: reviewed status, comments, and session stored - at `~/.config/diffui/{repo}/{branch}/`. `get_repo_root()` and - `_cached_current_branch()` are `lru_cache`'d. `_save_json` writes a temp - file and renames it. `_load_json` moves an unparseable file aside to - `.corrupt`, so the next save cannot overwrite it. -- Comment threads: comments have `id` (UUID), `author`/`author_type` - fields, `category` (bug/suggestion/nit/question), optional `suggestion` - (proposed code), and `status` (open/resolved). Replies are structured - dicts. Edit locking: user content is editable until an agent replies. - Threads are collapsible in the web UI. -- Multi-agent identity: agent authors get unique colors via a name - hash. Multiple agents reviewing the same branch appear as distinct - identities in comment headers and reply threads. -- Word-level diff: `pair_diff_lines` matches consecutive remove/add - pairs and `word_diff_ranges` uses `difflib.SequenceMatcher` to find - changed word spans. -- Worktree support: `get_git_dir()` resolves the actual `.git` - directory for both regular repos and worktrees. - -### Web UI Patterns - -- WebSocket + SSE: `events.py` provides a WebSocket endpoint - (`/api/ws`) for bidirectional communication. Client prefers WebSocket - and falls back to SSE (`/api/events`) if the connection closes. - Both use `watchfiles` (Rust-backed FSEvents/inotify) for real-time - filesystem watching with 400ms debounce. Watches repo dir, `.git` - dir, and comments file. Background tasks use `_bg_tasks` set to - prevent garbage collection. `start_poller` runs from the lifespan handler, - so both it and `_do_restart` can use `asyncio.get_running_loop()`. -- Git dir boundary: `_classify_changes` compares against the git dir plus a - separator. A bare `startswith` also matches `.gitignore` and `.github/`, - which turns a workflow edit into a full `reload_repo_state()`. -- Watcher follows the branch: comments live in a per-branch dir, and - `_watch_loop` snapshots that path when it starts, so a branch switch - (`git_changed`) restarts the watcher. `reload_repo_state()` also re-reads - comments, so `_apply_state_updates` adds `comments_changed` to the broadcast - when they actually differ, so the client does not sit on a stale thread - while the server has the new one. -- Diff caching: server-side `_diff_cache` keyed by - `(repo, merge_base, path, view, mtime)`. Client-side `diffCache` Map - for instant file switching with stale-while-revalidate. Numstat is - cached in `AppState` and refreshed on `reload_repo_state`. -- Virtual scrolling: `FullFileViewer` uses windowed rendering (only - visible lines + buffer in DOM). Hunk blocks in unified/split views - use `content-visibility: auto` for browser-native off-screen culling. -- Scroll persistence: client saves/restores scroll positions per - file in a Map. Comment dropdown navigation scrolls to the exact line - using `data-line-new` attributes for O(1) DOM lookup. -- Risk scoring: `_score_risk()` in `routes_diff.py` assigns - risk_level (low/medium/high) based on file patterns (migrations, - configs, large deletions, test removal, high churn). Shown as colored - dots on tree items, with the level in the tooltip. Sort-by-risk toggle via command - palette. -- Agent orchestration: confirmation dialog shows agent name, comment - count, and behavior summary. `/api/agent/start` spawns the configured - agent CLI in interactive mode via PTY (`pty.openpty()`). The agent's - full TUI streams to a slide-up terminal panel (`AgentTerminal`) via - a dedicated WebSocket (`/api/agent/ws`). Users can answer permission - prompts, navigate multi-choice menus, and interact directly. Output - is buffered (64KB ring buffer) for reconnect replay. Idle detection - (3s of no output after activity) updates status to "Done — waiting - for input". Stop/Close buttons in terminal header. Context built by - `_build_agent_context` with full diffs, review state, and thread - history. Agent CLI configurable in settings (Claude Code, Codex, - OpenCode, Cursor Agent). Legacy polling endpoints (`/api/agent/run`, - `/api/agent/status`) still available for non-terminal use. -- herdr: inside a herdr pane (`HERDR_WORKSPACE_ID` set), `/api/agent/prompt` - returns the workspace's agent as `herdr_agent`, and the confirm dialog - calls `/api/agent/paste` in place of `/api/agent/start`. The prompt goes to - that pane as one bracketed paste. `report_status` pushes the open comment - and reviewed counts to the herdr sidebar after each state change, and - `clear_status` removes them on shutdown. Tests run a fake `herdr` from - `PATH`. -- Last turn: `view=last-turn` diffs the tree saved when an agent's latest turn - started against the worktree as it is now. `turns.py` polls `herdr agent list` - every second, only when `HERDR_WORKSPACE_ID` is set and `herdr` is on `PATH`, - and counts the agents whose `foreground_cwd` (else `cwd`) is inside the repo, - in any workspace. `TurnTracker` fires on the sample where the repo goes from - resting (`idle`, `done`, or no agent) to `working`. A `blocked` or unknown - agent is mid-turn, so working after it starts nothing, and the first sample - after startup never fires. On the edge `snapshot_worktree` copies the real - index to a temp file, runs `git add -A` and `git write-tree` against the copy, - and `write_turn_tree` stores the tree under `refs/worktree/diffui/last-turn`, - which is per linked worktree and survives a restart. `AppState.last_turn` - holds the base tree, a fresh tree of the worktree and `git diff --numstat` - between them, and `reload_last_turn` rebuilds it from `reload_repo_state` and - every `files_changed` event, so the file list and each diff come from one pair - of trees, untracked files included. `GET /api/last-turn` returns the reason an - empty view is empty ("No agent turn seen yet", or that nothing changed). - Limits: herdr flips to `working` a moment after the agent starts writing, and - the poll adds up to a second, so those first writes sit in the base tree and - do not show. A turn that changes nothing still replaces the previous base. -- Search: `GET /api/search?q=&view=&scope=` returns `files` (names matching `q` as a - substring or in-order letters, best first), `hits` (`path`, `line`, `text`, - read from the working tree, case-insensitive, at most `MAX_HITS` = 200) and - `truncated`. It covers the files `view_files(view)` lists, minus - `.diffuiignore` matches, and skips binary and over 1 MB files. Names also match - fuzzily through fff, so a typo still finds its file, in the order fff ranks them. - `scope=all` searches the whole repo through fff, and `files` are then - `{path, changed}` and each hit carries `changed`, whether `view` lists the file. - `scope=changed`, the default, keeps the plain shape. fff skips `.gitignore`d - and binary files and does not apply `.diffuiignore`. -- fff index: `fff_index.finder()` hands out one `FileFinder` for the active repo - under a lock, since sync routes run in a threadpool. It is built on the first - search that needs it, never at startup, with `watch=False`, because `events.py` - already watches the tree. `_apply_state_updates` calls `mark_stale()` on - `files_changed` and `git_changed`, and the next `finder()` runs `scan_files()` - before it hands the index out, so an idle repo costs no rescans. The index is - dropped when the repo changes (`switch_repo`, or a different `get_repo_root()`) - and closed in the lifespan shutdown. A write is missed by a search that arrives - inside the 400 ms debounce before the event, and the next keystroke finds it. - A lowercase query matches case-insensitively under fff's smart case, so - `/api/search` lowercases it, and `multi_grep` takes it as plain text where - `grep` would read a `*.py` word as a path filter. fff's fuzzy name match - returns something for almost any query, so `_fuzzy_names` keeps only scores - within 75% of the best and above 8 per character of the longest word. -- Markdown is sanitized: `renderMd` in `lib/markdown.js` runs marked's output - through an allowlist of tags, attributes and URL schemes before it reaches - `dangerouslySetInnerHTML`. marked emits raw HTML, and comment bodies, agent - replies and previewed `.md` files all come from outside, so without the - allowlist a branch under review can run script in the diffui origin. Diagram - blocks are sent to kroki.io, the one call that leaves the machine. -- Explain changes: `/api/explain` spawns the configured agent CLI - to generate a self-contained HTML walkthrough. `/api/explain/status` - polls. `/api/explain/view` serves the result as a localhost page - (path-traversal guarded to tempdir). Top bar button + status bar link. -- Ignore whitespace: `ignore_whitespace` query param on diff - endpoints passes `-w` to `git diff`. Frontend toggle (`w` key). -- Ignore patterns: `.diffuiignore` file in repo root, fnmatch - syntax. Files marked `ignored: true` in `/api/files`, hidden by - default in UI but always included in agent context. -- Bulk resolve: `/api/comments/bulk-resolve` with optional - `file_path` filter and `action` (resolve/reopen). Used by command - palette, comments panel header, and the file header. -- Font size / word wrap: `font_size` and `word_wrap` persisted in - settings. Font size sets `--code-font-size` CSS variable. Word wrap - adds `.word-wrap-enabled` class to ``, uses `pre-wrap` on - `.diff-code` with flex-shrink-0 gutter to preserve line numbers. -- Custom keybindings: `keybindings` dict in settings maps action - IDs to key strings. SettingsPanel has a collapsible rebinding UI with - key capture. ShortcutOverlay and CommandPalette reflect overrides. -- Blame gutter: toggleable per-file. Fetches `git blame --porcelain` - via `/api/blame/{path}`. Results cached client-side in a `Map`. -- Preview mode: toggleable for `.md` and image files. Backend uses - `mimetypes.guess_type` for image detection with 10MB size cap. Markdown - rendered via `marked.js` through shared `lib/markdown.js`. -- Layout: one top bar (explorer toggle, repo and branch, connection dot, view - filter, review progress, then Search files, Comments, Explain changes, - Shortcuts, Settings and Send to agent). `App` renders a single `FileHeader` - above the active viewer with the path breadcrumb, the Unified/Split/File - control, Whitespace, Hide reviewed, Blame, context, Resolve all and Mark - reviewed. The file tree is the only file navigator, and `treeOrder()` in - `FileTree.js` gives the order that prev/next file and next-unreviewed follow. - Sort by risk turns the tree into one flat list in risk order. -- Diff file headers: `split_into_hunks` returns the lines before the first `@@` - as a pseudo hunk. `parse_diff_to_json` does not emit that hunk or the `@@` - line of each hunk, but both still advance the line `index`, which saved - comments anchor on. `file_note` (from `file_change_note`) is set when the - header holds a rename, mode change, new or deleted file, or binary marker. - The unified view shows each hunk header once, in the collapsible bar. The - split view prints it in the left pane only. -- Theme switching: server generates CSS custom properties from - Theme dataclass fields. Client injects a `