How Tuicr's PR Review Submission Pipeline Works: Inline Comment Mapping Explained

Tuicr submits pull-request reviews through a three-phase pipeline—pre‑flight mapping, resolver UI for unmappable comments, and payload construction—that transforms local draft comments into GitHub or GitLab API calls.

The tuicr terminal UI code review tool handles the tricky problem of turning your local, line‑anchored comments into properly positioned remote PR reviews. According to the agavra/tuicr source code, this process involves diff‑aware coordinate translation, edge‑case handling for binary files and deletions, and a fallback UI for comments that cannot be placed inline.

The Three Phases of PR Review Submission

Tuicr's pipeline is implemented in src/forge/submit.rs and orchestrated from src/app.rs. The flow splits into three distinct stages that progressively refine user input into a forge‑ready payload.

Phase 1: Pre‑flight—Mapping Draft Comments to Inline Positions

When you trigger :submit*, App::start_submit gathers every draft comment from the current ReviewSession. For each comment, it determines the anchor type (CommentAnchor)—file‑level, single‑line, or multi‑line range—then delegates to map_comment:

pub fn map_comment(
    comment: &Comment,
    anchor: CommentAnchor,
    file: &DiffFile,
    config: &ForgeConfig,
) -> MappedComment { … }

Source: src/forge/submit.rs, lines 1990‑2005

map_comment handles four distinct cases:

  • Binary or oversized files — Immediately marked unmappable with UnmappableReason::BinaryFile or TooLargeFile. The GitHub API cannot receive inline comments on these files.

  • File‑level comments — CommentAnchor::FileLevel attempts to locate the first valid line on the new side via file.first_valid_line(LineSide::New). If the file is a pure deletion, it becomes UnmappableReason::FileLevelNoAnchor. Otherwise, an InlineComment is built with side = GhSide::Right.

    Source: src/forge/submit.rs, lines 1616‑1639

  • Single‑line comments — CommentAnchor::Line searches the diff for the requested line on the requested side using find_line_with_counterpart. Missing lines yield UnmappableReason::LineNotInDiff. Found lines produce an InlineComment with GhSide::Left for deletions or GhSide::Right for additions.

    Source: src/forge/submit.rs, lines 1648‑1665

  • Range comments — CommentAnchor::Range validates that the entire range lies on a single side via range_endpoints_present. If comment.side is None or endpoints are missing, the result is MixedSideRange. Valid ranges emit an InlineComment with start_line/start_side populated for multi‑line spans.

    Source: src/forge/submit.rs, lines 1700‑1710 and 1734‑1745

During mapping, build_inline_body optionally prepends type prefixes ([ISSUE], [NOTE], etc.) when ForgeConfig::comment_type_prefix is enabled, and adds "File‑level:" markers for file‑level comments.

Source: src/forge/submit.rs, lines 52‑70

The pre‑flight phase produces a PreflightResult containing:

Field Purpose
event Review type: Comment, Approve, RequestChanges, or Draft
mappable Vec<InlineComment> ready for direct submission
unmappable Vec<UnmappableItem> requiring user resolution
commit_id PR head SHA captured at this moment to prevent anchor drift

Source: src/forge/submit.rs, lines 408‑416

Phase 2: Resolver UI—Handling Unmappable Comments

Comments that cannot be mapped inline surface in a resolver interface. Each UnmappableItem displays its UnmappableReason via human_label(), and you choose between two actions:

  • MoveToSummary — Places the comment into the review body under an "Unplaced comments" section
  • Omit — Discards the comment for this submission

The default selection is MoveToSummary. Your choices populate ResolverAction structures, which convert to MovedToSummaryItems for the final payload.

Source: src/forge/submit.rs, lines 28‑35 (ResolverAction enum)

Phase 3: Payload Construction and Remote Submission

build_review_body assembles the review body from three components:

  1. Review‑level comments — From session.review_comments
  2. Unplaced comments — The MovedToSummaryItems from phase 2
  3. Optional footer — Reserved for future extensions

Components join with \n\n separators, respecting the same comment_type_prefix toggle used during mapping.

Source: src/forge/submit.rs, lines 450‑492

The final payload structure sent to the forge contains:

{
    event: SubmitEvent::github_event(),  // "COMMENT", "APPROVE", etc.
    commit_id: "abc123...",              // Captured at pre‑flight
    comments: Vec<InlineComment>,        // From mappable list
    body: String,                        // From build_review_body
}

GitHub submission occurs in src/forge/github/submit.rs, where InlineComment serializes to GitHub's GraphQL API shape. The actual network call uses gh api --input -, passing JSON on STDIN to avoid command‑line length limits.

GitLab submission follows an identical pattern in src/forge/gitlab/submit.rs, using glab instead of gh.

Source: src/forge/github/submit.rs (see run_with_stdin usage)

Key Mapping Algorithm: Coordinate Translation

The core challenge—translating your local line numbers to GitHub's diff coordinates—happens inside map_comment via helper functions:

  • find_line_with_counterpart — Locates a line in the diff hunk structure, validating that the requested side (LineSide::Old or LineSide::New) actually exists in the displayed diff
  • range_endpoints_present — Ensures multi‑line ranges don't cross the old/new boundary, which GitHub's API cannot represent

The GhSide enum (aliased to GitHub's LEFT/RIGHT terminology) bridges Tuicr's internal LineSide to the remote API:

Tuicr LineSide GhSide GitHub meaning
Old Left Deletion (left side of split diff)
New Right Addition (right side of split diff)

Minimal Working Example

This Rust snippet demonstrates the complete mapping flow found in the real UI:

use tuicr::forge::submit::{map_comment, CommentAnchor, PreflightResult, build_review_body};
use tuicr::model::comment::Comment;
use tuicr::model::diff_types::{DiffFile, LineSide};
use tuicr::config::ForgeConfig;

// 1. Obtain a diff file from the current session
let diff_file: DiffFile = /* from App::diff_files */;

// 2. Create a draft comment targeting new line 42
let mut comment = Comment::new(
    "please rename this var".to_string(),
    tuicr::model::comment::CommentType::from_id("issue"),
    Some(LineSide::New),
);
comment.line_context = Some(tuicr::model::comment::LineContext {
    new_line: Some(42),
    old_line: None,
    content: String::new(),
});

// 3. Map to inline coordinate or unmappable reason
let anchor = CommentAnchor::Line { line: 42, side: LineSide::New };
let mapped = map_comment(&comment, anchor, &diff_file, &ForgeConfig::default());

// 4. Build preflight result with mapped comments
let preflight = PreflightResult {
    event: tuicr::forge::submit::SubmitEvent::Comment,
    mappable: match mapped {
        tuicr::forge::submit::MappedComment::Inline(ic) => vec![ic],
        _ => vec![],
    },
    unmappable: vec![],
    commit_id: "deadbeef1234".to_string(),
};

// 5. Assemble review body (empty unplaced list in this case)
let body = build_review_body(&[], &[], &ForgeConfig::default());

// 6. GitHub backend serializes preflight and calls: gh api --input -

This mirrors the actual production path: App::start_submit → map_comment → PreflightResult → forge backend → gh api call.

Critical Source Files

File Responsibility
src/forge/submit.rs Core pipeline: map_comment, PreflightResult, build_review_body, resolver types
src/forge/github/submit.rs GitHub‑specific payload serialization and gh CLI invocation
src/forge/gitlab/submit.rs Parallel GitLab implementation using glab
src/forge/traits.rs ForgeBackend trait defining the create_review interface
src/model/comment.rs Comment, CommentType, LineContext definitions
src/model/diff_types.rs DiffFile, DiffHunk, LineSide, LineRange structures
src/app.rs Orchestrates submission entry point and resolver UI

Summary

  • Three-phase pipeline — Pre‑flight mapping, resolver UI for edge cases, then payload construction and remote submission
  • Anchor-aware mapping — map_comment in src/forge/submit.rs translates local line numbers to GitHub/GitLab diff coordinates using CommentAnchor
  • Graceful degradation — Unmappable comments (binary files, pure deletions, mixed‑side ranges) surface in a resolver UI with Move/Omit options
  • STDIN-based submission — Large payloads bypass command‑line limits via gh api --input -
  • Commit pinning — commit_id captured at pre‑flight prevents anchor drift if the PR updates during your review

Frequently Asked Questions

How does Tuicr handle comments on binary files?

Binary files cannot receive inline comments on GitHub, so map_comment immediately returns UnmappableReason::BinaryFile. These appear in the resolver UI where you can move them to the review summary or omit them. The same applies to files exceeding size limits (TooLargeFile).

What happens to comments when the PR's head commit changes?

Tuicr captures the commit_id during the pre‑flight phase and includes it in the submission payload. This ensures your comments anchor to the SHA you reviewed, preventing GitHub from misplacing them against a newer commit. If you reload the PR and the commit changed, you must re‑map your drafts.

Why are some line ranges marked as "mixed side"?

GitHub's review API requires inline comments to sit entirely on the left side (deletions), entirely on the right side (additions), or span multiple lines on one side only. Tuicr's range_endpoints_present check rejects ranges that cross the old/new boundary, flagging them as MixedSideRange for the resolver UI.

Can I customize how comments appear in the review body?

Yes. The ForgeConfig::comment_type_prefix option enables prefixes like [ISSUE] or [NOTE] based on your comment's type. File‑level comments automatically receive a "File‑level:" marker. These formatting rules apply consistently in both map_comment via build_inline_body and build_review_body for unplaced comments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →