# AGENTS.md - tuicr

This document serves two purposes:
1. Help coding agents understand the tuicr codebase structure
2. Explain how to interpret review feedback generated by tuicr

---

## Project Structure

tuicr is a Rust terminal UI application and library for code reviews. Here's the module structure:

```
src/
├── lib.rs               # Public crate surface: ReviewStore, session/comment types, core modules
├── main.rs              # Entry point, event loop, action dispatch
├── config/
│   └── mod.rs           # User config loading (XDG on Unix, %APPDATA% on Windows)
├── app.rs               # Application state (App struct, InputMode, etc.)
├── error.rs             # Error types (TuicrError enum)
├── editor.rs            # External $EDITOR command construction and launch helpers
├── review_store.rs      # Library API for session listing/loading and shared comment insertion
├── review_cli.rs        # Non-interactive `tuicr review` subcommands over ReviewStore
├── tuicrignore.rs       # .tuicrignore loader + diff file filtering (gitignore-style patterns)
├── theme/
│   └── mod.rs           # Theme palette definitions + bundled/local theme resolution
│
├── vcs/                 # VCS abstraction layer
│   ├── mod.rs           # detect_vcs(): auto-detect VCS (jj first, then git, then hg)
│   ├── traits.rs        # VcsBackend trait, VcsInfo, VcsType, CommitInfo
│   ├── diff_parser.rs   # Unified diff text parser (shared by hg/jj and Git CLI)
│   │                    # DiffFormat enum: Hg (with timestamps), GitStyle (jj/git patches)
│   ├── git/             # Git backend selector (libgit2 by default, CLI for sparse/opt-in)
│   │   ├── mod.rs       # GitBackend wrapper, backend preference, repo mode detection
│   │   ├── cli.rs       # Git CLI backend used for sparse checkouts and backend = "cli"
│   │   ├── libgit2.rs   # libgit2-backed implementation used for normal Git repos by default
│   │   ├── repository.rs # CommitInfo, get_recent_commits()
│   │   ├── diff.rs      # get_working_tree_diff(), get_commit_range_diff()
│   │   └── context.rs   # fetch_context_lines() for gap expansion
│   ├── hg/              # Mercurial backend (always compiled)
│   │   └── mod.rs       # HgBackend: uses hg CLI, parses with diff_parser::Hg
│   └── jj/              # Jujutsu backend (always compiled)
│       └── mod.rs       # JjBackend: uses jj CLI, parses with diff_parser::GitStyle
│
├── forge/               # Remote forge integration (GitHub PR review, experimental)
│   ├── mod.rs           # detect_github_repository() — parse origin URL to ForgeRepository
│   ├── traits.rs        # ForgeBackend trait, ForgeRepository, PullRequestTarget,
│   │                    # PullRequestDetails, PrSessionKey, CreateReviewRequest,
│   │                    # GhCreateReviewResponse, ForgeFileLinesRequest
│   ├── pr_open.rs       # Async pr-open flow: build session from details + diff
│   ├── selector.rs      # Review target selector state (Local | Pull Requests tabs)
│   ├── context.rs       # Remote context expansion via ForgeBackend::fetch_file_lines
│   ├── remote_comments.rs # RemoteReviewThread shape + visibility filter
│   ├── submit.rs        # Submit pipeline: preflight mapping, resolver actions,
│   │                    # InlineComment payload, build_review_body, SubmitEvent
│   └── github/          # GitHub backend (only forge in v1, via `gh` CLI)
│       ├── mod.rs       # GitHubGhBackend: ForgeBackend impl
│       ├── gh.rs        # GhCommandRunner: spawn `gh`, parse output, error mapping
│       ├── models.rs    # JSON parsing for `gh` REST + GraphQL responses
│       ├── review_threads.rs # GraphQL query for existing review threads
│       └── submit.rs    # build_review_payload, create_review wiring
│
├── model/
│   ├── mod.rs
│   ├── comment.rs       # Comment, CommentType (Note/Suggestion/Issue/Praise)
│   ├── diff_types.rs    # DiffFile, DiffHunk, DiffLine, FileStatus, LineOrigin
│   └── review.rs        # ReviewSession, FileReview (the persisted review state)
│
├── input/
│   ├── mod.rs
│   ├── keybindings.rs   # Action enum, map_key_to_action() for each InputMode
│   └── mode.rs          # InputMode enum definition (unused, defined in app.rs)
│
├── persistence/
│   ├── mod.rs
│   └── storage.rs       # save_session, load_session, manifest-backed session lookups
│
├── output/
│   ├── mod.rs
│   └── markdown.rs      # export_to_clipboard(): generate markdown, copy to clipboard
│
└── ui/
    ├── mod.rs
    ├── app_layout.rs    # Main render function, file list, diff view with inline comments
    ├── comment_navigator.rs # Sidebar comment index for jumping local/remote comments
    ├── status_bar.rs    # Header, status bar, command line rendering
    ├── help_popup.rs    # Help overlay (? key)
    ├── comment_panel.rs # Comment input dialog, confirm dialog
    └── styles.rs        # Color constants and style helper functions
```

Repository-managed agent integrations:
- `skills/tuicr/` - Shared agent skill bundle for coding agents, for example Claude Code, Codex, and similar tools; launches tuicr in a tmux split pane

### Key Types

**App** (`src/app.rs`):
- Central application state
- Contains: `vcs` (Box<dyn VcsBackend>), `vcs_info`, `session`, `diff_files`, `input_mode`, scroll/cursor state
- Methods: `scroll_down/up`, `next/prev_file`, `next/prev_hunk`, `go_to_source_line`, `toggle_reviewed`, `save_comment`
- Comment navigation uses `CommentNavigatorState` plus `CommentNavigatorItem` rows derived from `line_annotations`, so local comments and visible remote PR threads jump to the same annotations the diff renders.

**VcsBackend** (`src/vcs/traits.rs`):
- Trait abstracting VCS operations
- Methods: `info()`, `get_working_tree_diff()`, `fetch_context_lines()`, `get_recent_commits()`, `get_commit_range_diff()`
- Capability hooks: `supports_sparse_checkout()` advertises whether the selected backend can operate on Git sparse-checkout repos
- Implementations: `GitBackend`, `HgBackend`, `JjBackend` (all always compiled)

**InputMode** (`src/app.rs`):
- `Normal` - default navigation mode
- `Command` - after pressing `:`, vim-style commands
- `Comment` - typing a comment (Ctrl-S saves, Ctrl-C cancels)
- `Search` - after pressing `/`, search pattern entry
- `Help` - showing help popup
- `Confirm` - Y/N confirmation dialog
- `CommitSelect` - selecting commits to review
- `VisualSelect` - visual mode for range comments

**ReviewSession** (`src/model/review.rs`):
- Persisted review state with `files: HashMap<PathBuf, FileReview>`
- Each `FileReview` has: `reviewed: bool`, `reviewed_hunks: BTreeSet<String>`, `file_comments: Vec<Comment>`, `line_comments: HashMap<u32, Vec<Comment>>`

**ReviewStore** (`src/review_store.rs`):
- Public library facade for persisted review sessions
- Methods: `list_sessions_for_repo()`, `get_review()`, `add_comment()`, `save_review()`
- Shared primitive: `add_comment_to_session()` is used by both the library facade and `App::save_comment()`

**Action** (`src/input/keybindings.rs`):
- All possible user actions (ScrollDown, NextFile, ToggleReviewed, AddLineComment, etc.)
- `map_key_to_action(key, mode)` returns the appropriate Action

### Data Flow

1. **Startup**: Parse CLI args (invalid `--theme` exits non-zero). With no subcommand, or with explicit `tuicr tui`, load config from `$XDG_CONFIG_HOME/tuicr/config.toml` (default `~/.config/tuicr/config.toml`, or `%APPDATA%\tuicr\config.toml` on Windows), ignore unknown config keys with startup warnings, resolve theme precedence (`--theme` > config > dark), then call `App::new()`. Theme selection first checks bundled names, then local theme files from `$XDG_CONFIG_HOME/tuicr/themes/` (default `~/.config/tuicr/themes/`, or `%APPDATA%\tuicr\themes\` on Windows). Local theme files may reference a local `.tmTheme` syntax theme. Some bat-compatible Base16 `.tmTheme` files encode ANSI palette slots as placeholders, and `src/syntax/mod.rs` translates those at render time. `App::new()` calls `detect_vcs()` (Jujutsu first, then Git, then Mercurial), using config `backend = "libgit2"` or `backend = "cli"` for Git. Normal Git repos default to libgit2; sparse checkout repos automatically use the Git CLI backend and show a startup warning when that overrides the default. It filters diff files via repo-root `.tuicrignore`, then enters commit selection mode by default. If staged/unstaged changes exist, the first selection rows are "Staged changes" and/or "Unstaged changes". The Pull Requests tab can toggle between all open PRs and forge PRs/MRs requesting the current user's review with `r`, which refetches page 1 using `gh pr list --search "review-requested:@me"` on GitHub or `glab mr list --reviewer=@me` on GitLab. With `-r/--revisions`, it opens the requested commit range directly. Config `show_file_list = false` hides the file list panel on startup (toggleable with `<leader>e`, where `leader` defaults to `;`). Config `diff_view = "side-by-side"` sets the default diff layout (toggleable with `:diff`). Config `wrap = true` enables line wrapping (toggleable with `:set wrap!`). Config `review_watch_interval_ms = 1000` controls persisted-session polling; set it to `0` to disable.
2. **Render**: `ui::render()` draws the TUI based on `App` state. When rendered comments exist, the left sidebar splits vertically into file tree and comment navigator; the navigator is hidden when there are no rendered comment rows.
3. **Input**: `crossterm` events → `map_key_to_action` → match on Action in main loop
4. **Comments**: `App::save_comment()` builds an `AddCommentRequest` and calls `add_comment_to_session()` so TUI and library callers share insertion behavior. The TUI creates a persisted session file as soon as a review session becomes active, so `tuicr review add` can target it immediately. Successful comment submits autosave the session using a locked, atomic write that merges externally added comments first.
5. **Review CLI**: `tuicr review list|add|comments` exits before TUI startup, uses `ReviewStore`, and always emits JSON; `review list` includes `active: true` for currently open TUI sessions, and `review add --input` accepts JSON literal, `@file`, or stdin payloads
6. **Persistence**: active TUI sessions, comment submit, and `:w` save the session JSON to `~/.local/share/tuicr/reviews/`; library callers use `ReviewStore`. Open TUI sessions are also recorded in `active_sessions.json` beside `index.json` with pid, slug, path, and last-seen timestamp. TUI-created empty session files are deleted on normal exit if they still contain no comments and no reviewed files.
7. **Reload diff/session**: `:e` reloads persisted comments/review state, then re-runs VCS diff loading and reapplies `.tuicrignore` filtering to refresh displayed files
8. **Export**: `:clip` (alias `:export`) calls `export_to_clipboard()`, generating markdown and copying it to the clipboard (or stdout with `--stdout` flag)

### Important Implementation Details

- **Infinite scroll**: All files rendered into one `Vec<Line>`, then sliced by `scroll_offset`
- **Inline comments**: Comments are rendered in `app_layout.rs` after file headers and after relevant diff lines
- **Comment navigator**: Built from `line_annotations` in rendered order. Local review/file/line comments and visible remote threads appear as compact rows; selecting one calls `move_cursor_to_annotation()` so the diff viewport scrolls to the comment.
- **Session loading**: `App::new()` calls manifest-backed persistence helpers to restore previous review
- **Collaborative session writes**: session JSON saves use a storage lock plus temp-file rename, with stale sidecar lock recovery if a process crashes while holding the lock. The TUI keeps a persisted-session snapshot so polling, `:e`, and autosave can merge external `tuicr review add` comments without overwriting local edits.
- **Clipboard**: Uses `arboard` crate for cross-platform clipboard support
- **Hunk navigation**: `next_hunk()`/`prev_hunk()` calculate positions by iterating through files
- **Ignore filtering**: `.tuicrignore` is applied whenever diffs are loaded/reloaded

### Dependencies

- `ratatui` + `crossterm`: TUI framework
- `git2`: Native git operations
- `serde` + `serde_json`: Session serialization
- `toml`: User config parsing
- `arboard`: Clipboard access
- `ignore`: Gitignore-style matcher for `.tuicrignore`
- `chrono`: Timestamps
- `thiserror` + `anyhow`: Error handling

## Forge integration

PR review (`tuicr pr <target>` or `tuicr tui pr <target>`) is the only feature in `src/forge/`. The trait shape is forge-agnostic so other forges can plug in later; v1 only ships a GitHub backend that shells out to `gh`.

### ForgeBackend trait

`src/forge/traits.rs`. Required methods:

- `list_pull_requests` — paged list for the selector's Pull Requests tab.
- `get_pull_request` — details for a `PullRequestTarget` (resolves to `PullRequestDetails`).
- `get_pull_request_diff` — the cumulative PR diff as a unified-diff string.
- `list_pull_request_commits` — commits on the PR for the inline subset selector.
- `get_pull_request_commit_range_diff` — cumulative diff for a contiguous subrange (`start_sha` is the parent of the first selected commit; `end_sha` is the last).
- `list_review_threads` — existing GitHub comments + resolved/outdated state.
- `fetch_file_lines` — remote context expansion in the diff view.
- `create_review` — POST a review with inline comments via `CreateReviewRequest`.

### Async pattern

Network calls run on a background thread. Parsing + state mutation run on the main thread. The pattern across `spawn_pr_open` / `spawn_pr_reload` / `spawn_pr_submit`:

1. Snapshot the request inputs on the main thread (no `&App` lives across the spawn).
2. Spawn a thread that returns only `Send` data — typically `Result<(PullRequestDetails, String, Vec<PullRequestCommit>)>` or similar tuples.
3. The thread sends the result on an `mpsc` channel; `poll_*_events()` drains the channel each tick.
4. The main-thread `finish_*` function parses the diff and builds the `ReviewSession`. `SyntaxHighlighter` is not trivially `Send`, so parsing has to happen on the main thread.

In-flight requests carry an identity tuple (repo, PR#, head SHA). A late result is discarded if the user has since opened a different PR.

### Session key + lifecycle

`PrSessionKey { repository, number, head_sha }` identifies a PR review session. Same PR + same head = same session = drafts reattach. New commit on the PR = new key = new session.

Each `Comment` carries a `lifecycle` field:

- `local_draft` — local-only, editable
- `pushed_draft` — submitted with event=Draft (still a GitHub pending review)
- `submitted` — submitted with Comment/Approve/RequestChanges, locked from further edits

Submit failure leaves comments at `local_draft`. Success transitions to `submitted` or `pushed_draft` atomically per the response.

### Submit pipeline (`src/forge/submit.rs`)

1. **Preflight** maps each local comment to a GitHub-style inline anchor (path + line + side) by walking the displayed diff hunks.
2. Unmappable comments go to the **resolver modal**: each one is either moved to the review summary or omitted.
3. The **confirmation modal** shows counts and warns if the PR head advanced since load.
4. The payload posts via `gh api --input -` (stdin, not CLI args, because CLI arg length limits would bite on multi-comment payloads).

### Hard-won gotchas

These are non-obvious things the implementation chain hit. Worth preserving for future maintainers.

1. **`gh pr diff` must NOT use `--patch`.** That flag returns per-commit mbox patches and produces duplicate file entries. Plain `gh pr diff` returns the cumulative diff. Regression test: `should_not_pass_patch_flag_to_gh_pr_diff`.

2. **GraphQL thread anchors live on `PullRequestReviewThread`, not the comment.** `path`, `line`, `originalLine`, `diffSide` are thread fields. Putting them on `PullRequestReviewComment` returns a schema error.

3. **Network on the background thread, parsing on the main thread.** `SyntaxHighlighter` is not `Send`. Background threads return Send-safe data; the main thread parses and builds the session. Used by `spawn_pr_open` / `spawn_pr_reload` / `spawn_pr_submit`.

4. **`apply_initial_load(Err(...))` is a no-op when the tab is already `Loaded`.** It only transitions `Loading → Error`. For transient errors during reload or submit, use `App::set_error()` (the message bar) instead.

5. **Anchor restore must scroll.** Setting `diff_state.cursor_line` without calling `move_cursor_to_annotation` leaves the viewport at the top.

6. **`Comment` line anchor lives in the `HashMap` key, not on `Comment.line_context`.** Production code creates comments via `Comment::new` without populating `line_context`. The submit mapper takes an explicit `CommentAnchor` parameter — never infer "file-level" from `line_context.is_none()`.

7. **`gh api --input -` over CLI args.** The only practical way to send a multi-comment payload. See `GhCommandRunner::run_with_stdin`.

8. **`gh api` writes the response body to STDOUT on non-2xx**, while STDERR only carries the short status line. The error formatter combines both so the user sees the actual GitHub error.

9. **Stale-result discard for submit needs (repo, PR#, head SHA).** A different PR opened mid-submit could otherwise consume the result and apply lifecycle writes to the wrong comments.

10. **TestBackend modal sizing.** A 60%×40% modal on 120×24 is 72×9 (7 content rows after borders). The submit confirmation modal needs 70% height to fit. The submit-action picker needs 50% height to fit the footer line.

11. **Subset-mode `commit_id`.** When a strict subset of PR commits is selected, the payload's `commit_id` must be `pr_commits[start_idx].oid` (the newest selected commit). Using the cumulative PR head returns a misleading 422: `commitOID is not part of the pull request` — even though the SHA *is* in the PR.

12. **`cd` into the worktree before running `cargo`.** `cargo` resolves `Cargo.toml` from `pwd`. Running gates from the wrong worktree silently exercises the wrong tree.

### Keeping Docs Updated

When adding user-facing features, update the relevant documentation:

| Document | Update when adding/changing... |
|----------|-------------------------------|
| `README.md` | Keybindings, commands (`:*`), CLI flags, features list, installation methods, agent integration setup, forge limitations |
| `src/ui/help_popup.rs` | Keybindings or commands (update the `help_text` vector) |
| `AGENTS.md` | Module structure, repo-managed agent integrations, key types, data flow, dependencies, forge invariants and gotchas |

---

## Review Format (for coding agents receiving tuicr output)

When a user reviews your changes with tuicr, they will provide you with a structured Markdown document optimized for agent consumption.

### Document Structure

```markdown
I reviewed your code and have the following comments. Please address them.

Comment types: ISSUE (problems to fix), SUGGESTION (improvements), NOTE (observations), PRAISE (positive feedback)

Summary: {optional overall notes}

1. **[ISSUE]** `src/auth.rs:42` - Magic number should be a named constant
2. **[SUGGESTION]** `src/api.rs` - Consider adding unit tests
3. **[NOTE]** `src/utils.rs:15` - Why was this approach chosen?
```

### Comment Types

| Type | Meaning | Action Required |
|------|---------|-----------------|
| **[ISSUE]** | Problem that must be fixed | Yes - address before proceeding |
| **[SUGGESTION]** | Improvement recommendation | Consider implementing |
| **[NOTE]** | Observation or question | Respond or acknowledge |
| **[PRAISE]** | Positive feedback | No action needed |

### Comment Format

Each comment is numbered and self-contained:
- `{n}. **[TYPE]** \`{file}:{line}\` - {content}` (line comment)
- `{n}. **[TYPE]** \`{file}\` - {content}` (file comment)

You can reference comments by number (e.g., "Regarding comment #2...").

### How to Respond

#### 1. Address Issues First

**[ISSUE]** comments are blocking and must be fixed:

```markdown
## Issues Addressed
- #1 `src/auth.rs:42` - Extracted magic number to `TOKEN_EXPIRY_SECONDS` constant
```

#### 2. Consider Suggestions

**[SUGGESTION]** comments are non-blocking but should be considered. Either implement or explain why not.

#### 3. Answer Questions

**[NOTE]** comments often contain questions. Provide brief explanations.

### Response Format

```markdown
## Changes Made

### Issues Addressed
- #1 - Extracted magic number to `TOKEN_EXPIRY_SECONDS` constant

### Suggestions Implemented
- #2 - Added unit tests for API module

### Suggestions Declined
- #5 - Kept current approach because [reason]

### Notes Addressed
- #3 - This approach was chosen for performance reasons
```

## Tips for Coding Agents

1. **Read the full review** before making changes
2. **Prioritize [ISSUE] items** - these are blocking
3. **Reference comments by number** when responding
4. **Preserve reviewer intent** - don't over-engineer solutions
5. **Be concise** in your response
6. **If unclear**, ask for clarification rather than guessing
