annot: ephemeral space for human-AI thinking
A native annotation tool that opens content in a window, lets humans add structured feedback with line-level precision, and outputs results for AI consumption when closed.
annot is a structured dialogue medium — a space where human and AI take turns refining content. It's designed to:
- Pull humans into the loop at decision points
- Capture rich, located feedback — not just yes/no, but what and where
- Shape content through reaction — propose replacements, sketch alternatives, add context
- Exit cleanly — ephemeral by design, zero persistence burden
Open any source file for annotation. Syntax highlighting adapts to language. Navigate by line numbers.
Review changes from a git or jj repository, or raw unified diffs. Color-coded: additions green, deletions red. Annotations capture both old and new line numbers.
Targets work from the CLI (annot diff, annot diff 4f7d4491, annot diff main..HEAD -- src/) and from MCP (review_diff with target/pathspecs) — the same comparisons either way: working copy (uncommitted work vs its base), revision (one revision vs its parent), range (... diffs from the merge base), and staged (git only — jj has no index). Raw diffs come via stdin (git diff | annot) or MCP diff_content.
Revision strings are the repository's own dialect: git revspecs (HEAD~2, abc1234) in a git repo, jj revsets (@-, trunk(), your own aliases) in a jj one — resolved by jj's own engine against your own config, so a revset means here what it means in jj.
Which repository — the CLI reviews the repo enclosing your cwd; a human standing in a directory means "here". The MCP server can't rely on that: it's a sidecar of the calling agent and inherits the directory that agent was launched from, which stays fixed while the agent moves between repos. review_diff takes working_dir to name the repo explicitly (any directory inside it works), and it roots the session — the file picker and :save follow it. Without it, annot falls back to its own process directory and reviews whatever repo lives there.
Short change-id prefixes work exactly as jj log displays them: jj highlights the shortest prefix that is unique among the commits you can see (often one letter), and annot resolves prefixes against the same revset (revsets.short-prefixes, defaulting to revsets.log) — so a prefix jj shows you is a prefix you can type.
In jj a revset may name several commits. If they form one contiguous stack — one root, one head, no gaps — annot reviews them as a single changeset: the base the stack sits on versus its tip. So annot diff 'trunk()..@' reviews your whole branch as one diff, and a revset alias for it ('ready()' = 'trunk()..@') works the same way — annot diff 'ready()'. Anything else is an error naming the candidates, never a silent pick: mutable() & mine() names several unrelated stacks, and there is no single diff of that.
jj repositories are first-class, colocated or not. Three things differ from git, deliberately:
- The working copy is snapshotted. jj's working copy is a commit (
@) that only matches the filesystem because everyjjcommand snapshots on startup — so annot does too, and your uncommitted edits show up even if no jj command has run since you made them. This is the one and only write annot performs on a repository: asnapshot working copyentry injj op log, the same onejj statuswould leave. A clean working copy produces no operation at all. - Conflicts are content, not errors. A rebase can leave a committed conflict, and "what did this rebase break?" is exactly the review you want. Conflicted files render as jj's own marker text (
<<<<<<<,%%%%%%%,+++++++,>>>>>>>), with the marker lines tinted and a striped gutter down the conflict region. They annotate like any other line. (In git, an unmerged path still errors — there it means "you're mid-merge, go fix it".) - Change IDs lead. Window labels name a revision by its change ID (
@ · kmxyzqrs), which survives rewrites — quote it back after an amend and it still resolves, where a commit ID may already be dangling.
Pathspecs become jj filesets in a jj repo (src/ as a bare path, glob:"*.rs" as an expression).
File tree — Cmd+B toggles a sidebar listing every changed file with its +/− counts. Clicking a file scrolls to it (expanding it if collapsed); the row for the file currently in view stays highlighted. The : palette's Files namespace does the same jump by fuzzy search.
Per-file collapse — every file gets a header bar (chevron, path, +/− counts) that sticks to the top while its lines scroll. Clicking the bar collapses the file to just its header; the titlebar shows the changeset's +A −D totals with a fold-all/unfold-all toggle. Files with more than 500 changed lines start collapsed. Search hits and file jumps auto-expand collapsed files. Collapse is pure presentation — annotations keep resolving to the same lines.
Split view — a titlebar button (two-column icon) or the : palette's View namespace switches the diff between unified (default) and side-by-side rendering. Context lines span both columns; each change run pairs its deletions and additions row by row, padding the shorter side with hatched filler. Selections drag within one column and annotate that side; annotations created in either view render in both and survive toggling. The choice is per-session — every review opens unified.
Unfold context — in repository-backed diffs (git or jj), fold arrows stacked in the @@ hunk-header gutter (GitHub-style, Octicon fold-down/fold-up: ▼ on top continues the hunk above downward, ▲ below grows the hunk beneath upward; each arrow is a full-width band one row tall, making the header two rows when both show) expand hidden context: each reveals 20 lines toward its edge of the gap, and a gap of 20 lines or fewer collapses to a single expand-all button. The fold after a file's last hunk has no header below it, so it keeps a standalone arrow row. The folded line count lives in the arrow tooltips. A fully unfolded gap's arrows disappear and its neighboring hunks merge. Unfolded lines are ordinary context rows — annotatable, searchable, and included in output like any other line. Raw pasted diffs have no file contents to slice, so no unfold chevrons appear there. Expansion is per-session.
Review agent-generated content — plans, drafts, analysis. Markdown rendering with Mermaid diagrams and portal links that embed live code.
- Click line numbers to select individual lines
- Shift+Drag for ranges
- Multi-line annotations supported
- Plain text with inline formatting
- Tags: Semantic mini-prompts like
[# SECURITY]or[# TODO]— composable with prose and each other - Images: Paste screenshots directly into annotations
- Excalidraw diagrams: Sketch ideas visually (can also convert Mermaid diagrams to Excalidraw)
- Replace blocks: Propose code replacements with before/after display
- References: Link to annotations, sections, and project files
/triggers command menu (Excalidraw, Replace block, etc.)#triggers tag autocomplete@triggers reference autocomplete (annotations, sections, files)
- Create custom tags with names and LLM instructions
- Tags persist across sessions
- Usage tracking for smart suggestions
Declare your intent before closing the window:
- Tab/Shift+Tab cycles through modes
- Selected mode colors the window footer
- Each mode has instruction text for AI interpretation
- Persistent: Saved modes like "Apply", "Reject", "Revise"
- Ephemeral: Agent-provided modes for context-specific workflows (via MCP)
- Command-linked: Modes that trigger Claude Code slash commands
In content review, embed live code snippets:
See the validation logic in [validate](src/lib.rs#L45-L60)This fetches and displays the actual code inline, syntax-highlighted. Max 50 portals, 500 lines each. Sensitive paths blocked.
annot isn't a document editor — humans shape content through reaction, not direct authorship:
- Propose code replacements via
/replacecommand - Shows original (tinted) alongside your replacement (green accent)
- Output includes diff-style format for AI to apply changes
- Export content as markdown via command palette
- Portal links expand to embedded code blocks
- Optionally overwrite source or create new file
- Send content to Obsidian vault
- Auto-names note from first H1 heading
- Open diagram editor from annotation
- Saves as JSON + PNG
- Embedded inline in annotation content
- Rendered from markdown code blocks
- Convert to Excalidraw for sketching
Press Shift+C to add a high-level comment that applies to the entire review (not a specific line). Appears in the SESSION block of output.
Press : (colon) to open. Eight namespaces:
- Browse, create, edit, delete tags
- Examples: TODO, SECURITY, REFACTOR, BUG, PERF
- Press
eto edit selected tag
- Browse, set, create, edit, delete, reorder modes
- Examples: Apply, Revise, Reject, Discuss
- Press
sto set as active - Press
rto reorder (drag with arrow keys)
- Fuzzy-jump to a changed file (diff review only)
- Copy content only
- Copy annotations only
- Copy both together
- Opens save modal for file export
- Export to configured vaults
- Add/edit/delete vault references
- System / Light / Dark toggle
| Key | Action |
|---|---|
| Click line # | Select/deselect line |
| Shift+Drag | Select range |
/ |
Slash command menu (Excalidraw, Replace) |
# |
Tag autocomplete |
@ |
Reference autocomplete (annotations, sections, files) |
| Tab/Shift+Tab | Cycle exit modes |
Shift+C |
Session context editor |
: |
Command palette |
Alt+Tab |
Command palette → Exit modes |
| Ctrl+F | Search |
Cmd+B |
Toggle file tree (diffs) |
e |
Edit item (in command palette) |
r |
Reorder items (exit modes only) |
Cmd+D |
Delete item (in command palette) |
Structured for AI consumption:
TAGS:
[# SECURITY] Review for security vulnerabilities
[# TODO] Items needing follow-up
CONTEXT: plan.md [embeds: src/lib.rs, src/main.rs]
GENERAL:
Please focus on the error handling approach
NEXT: Apply — Proceed with this plan as written
---
src/lib.rs:45-52:
44 | fn previous_line() { // context
> 45 | fn example() {
> 46 | // code
└──> [# SECURITY] Check for injection vulnerabilities here
Consider validating the input before processing
Saved to /path/to/file.md
- TAGS: Tag definitions used in annotations (only if tags are present)
- CONTEXT: What's being reviewed, with any embedded portal files
- GENERAL: High-level comment about the entire review (not line-specific)
- NEXT: What action the human wants (exit mode name + instruction)
Three tools exposed via Model Context Protocol:
- review_file — Open file at path
- review_diff — Review diffs from a git or jj repo (structured target: working copy / revision / range / staged) or raw diff content
- review_content — Review agent-generated content
All block until window closes, returning structured output with annotations, exit mode, and any images.
Ephemeral exit modes: Agents can provide context-specific exit modes each time they invoke annot — these appear alongside persistent modes but don't persist after the session.
| Flag | Description |
|---|---|
--json |
Output annotations as JSON with base64 images (for tool integrations) |
--exit-mode "Name:Instruction" |
Add a transient exit mode button (repeatable) |
-l, --label <LABEL> |
Label for stdin content (syntax highlighting) |
--json returns {"text": "...", "images": [{"figure": 1, "data": "base64...", "mime_type": "image/png"}]} instead of plain text. Used by the Pi extension to pipe images back to the agent.
--exit-mode can be repeated to add multiple buttons. Colors auto-cycle.
Persisted in the OS config directory (~/.config/annot/ on Linux, ~/Library/Application Support/annot/ on macOS, %APPDATA%\annot\ on Windows):
- Tags and exit modes
- Usage statistics
- Theme preference
- Content zoom level
- Obsidian vault paths
Atomic writes with file locking for concurrent safety.
Native Tauri app for:
- macOS — overlay title bar, traffic light positioning, native Cmd+W close
- Linux — borderless window with custom chrome (in-app title bar, close button,
and resize handles); tested on NixOS with Wayland (via XWayland); requires
GDK_BACKEND=x11andWEBKIT_DISABLE_DMABUF_RENDERER=1for correct WebKit2GTK rendering (baked into the Nix-installed binary automatically) - Windows — standard decorated OS window with the native title bar (no custom chrome);
builds and runs from source via WebView2; config stored in
%APPDATA%\annot\