Each inline review comment under `pr view <n> --reviews` now renders its `id` (the handle a reply targets), `resolved` (`yes`/`no`, from whether Gitea populated the comment's `resolver`), and `diff_hunk`. The hunk is structurally trimmed to its `@@` header line plus its last two lines by default (hunks of three lines or fewer are left whole) and emitted verbatim under `--full`, so the trim never touches the char-based body truncation path. The raw `position`/`original_position` diff offsets stay unsurfaced. The fields ride the existing reviews-plus-per-review-comments fetch — no extra API calls.
9.5 KiB
Problem Statement
An agent doing a PR review round-trip with gitea-axi cannot complete it inside the tool.
When reading a reviewer's inline comments via pr view --reviews, the output gives the author, file path, and body, but not which line each comment is anchored to, nor the comment's id.
Comments like "what does this do?" or "wasn't this set earlier?" are unanswerable from the output alone, forcing a fallback to a raw Gitea API call (and scraping the token out of tea's config) just to recover the anchor.
When writing, pr review accepts only a single --body for the whole review, so "reply to each of ten inline comments" collapses into one consolidated body that restates each thread by hand, rather than a reply landing under each comment where the reviewer left it.
Solution
Complete the read and write halves of the inline-review round-trip, both at the existing pr commands.
On the read side, pr view --reviews surfaces each inline comment's id, its diff_hunk (so the anchoring code travels with the comment), and whether the thread is already resolved.
On the write side, pr review gains a --comments-file flag that carries a batch of inline comments — each either a reply into an existing thread (by comment id) or a fresh comment on a new-file line — mapped onto the review-submission payload Gitea already accepts.
User Stories
- As a reviewing agent, I want each inline review comment's anchoring
diff_hunkin the--reviewsoutput, so that I can answer a bare "what does this do?" without a second API call. - As a reviewing agent, I want each inline review comment's
idin the--reviewsoutput, so that I can target that exact comment when replying. - As a reviewing agent, I want to see whether each inline comment's thread is already
resolved, so that I skip settled conversations instead of re-answering them. - As a reviewing agent, I want the
diff_hunktrimmed to its header line plus its last couple of lines by default, so that I get the file-line anchor and the code at the comment without paying for the whole hunk. - As a reviewing agent, I want
--fullto expand eachdiff_hunkto its complete text, so that I can read the entire hunk when the trimmed tail is not enough. - As a PR author, I want to reply to an existing inline comment by its id, so that my reply lands in that reviewer's thread without me computing any line number or side.
- As a PR author, I want to post a fresh inline comment on a new-file line, so that I can raise a point on code no one has commented on yet.
- As a PR author, I want to submit a batch of inline comments in one file alongside my review, so that "reply to each of ten comments" is one command, not ten.
- As a PR author, I want gitea-axi to figure out the new-vs-old side of a reply for me, so that I never have to reason about diff sides.
- As a PR author, I want the inline-comment batch to compose with the existing review action and optional top-level body, so that I can approve/request-changes/comment while attaching inline replies.
- As a PR author, I want a clear validation error when a reply targets a comment id that isn't on the PR, so that a typo fails fast instead of silently posting nowhere.
Implementation Decisions
Read side — anchor fields on pr view --reviews
- The
--reviewsreview rows add three fields per inline comment:id,diff_hunk, andresolved. resolvedisyes/no, derived client-side from whether the comment'sresolveris set (Gitea returnsresolveras a populated user once a thread is resolved).- The raw
position/original_positiondiff offsets are deliberately not surfaced — they are diff offsets an agent cannot map to a file line without the patch, and thediff_hunkheader already carries the human-meaningful line range. diff_hunkis rendered with a bespoke structural trim, not the char-based content-truncation used for bodies: by default, the hunk's@@header line plus its last two lines (collapsed when the hunk is three lines or fewer). This keeps both the file-line anchor (the@@header) and the code at the comment (the tail), which the keep-head char truncation would get backwards by dropping the tail.- Under
--full, the entirediff_hunkis emitted verbatim, consistent with--fullmeaning "no trimming anywhere"; the hunk is never run through the body char-truncation path. - These fields ride the existing
--reviewsfetch (reviews list plus one inline-comments fetch per review); no extra API calls are introduced.
Write side — --comments-file on pr review
pr reviewgains--comments-file <path>, a JSON array of inline-comment entries submitted as part of the review; the existing action flag (one of--approve/--request-changes/--comment) is still required, and top-level--bodystays optional.- Each array entry is one of two shapes, and there is no
sidefield anywhere:- Reply:
{ "reply_to": <comment-id>, "body": "..." }. - New comment:
{ "path": "...", "line": <new-file line>, "body": "..." }.
- Reply:
- A new comment maps
linetonew_position; it is always the new side, because a line addressable by new-file number is by definition on the new side. A prototype against the live host confirmed the create payload treatsnew_positionas a file line number (not a diff offset) and that a single inline comment posts successfully this way. - A reply carries no line or side.
gitea-axi locates the target comment (there is no get-comment-by-id endpoint, so it reuses the same reviews-plus-comments fan-out the read side already performs), reconstructs that comment's anchor from its own
diff_hunk, and posts a matching inline comment; because Gitea threads comments by line, a same-line post joins the existing conversation, so side is inferred from the target rather than supplied. - All entries map onto the
comments[]array of the review-submission payload (each element a{ path, new_position | old_position, body }), which the SDK already accepts but gitea-axi previously left unpopulated — no new HTTP layer or endpoint is added. - A
reply_toid that is not found among the PR's review comments is aVALIDATION_ERRORraised before submission, mirroring howpr reviewalready validates its action flags up front. - Mutation output follows the established action-block/entity-block convention for
pr review; the inline-comment count is reflected in the reported result.
Sequencing
- The read side ships first: it is pure read, and its surfaced
idis the handle the write side's replies target. - The write side ships second, on top of the id exposed by the read side.
Testing Decisions
- Both halves are tested at the single existing fixture-server CLI seam: a fixture server maps request path/method to recorded Gitea JSON, the built CLI is driven via the run-CLI test helper, and assertions are on rendered
stdoutand on the recorded outbound requests. No new seam is introduced. - Good tests here assert external behavior only — the exact rendered TOON lines and the captured request payload — never internal rendering helpers.
- Read side: drive
pr view N --reviews(and again with--full) against fixture reviews and inline comments whose JSON carriesid,diff_hunk, and a set/unsetresolver; assert the renderedid, the trimmed-vs-fulldiff_hunk, andresolved: yes/no. Prior art: the existingpr view --reviewstest that already stubs the reviews and per-review comments endpoints and asserts thereviews[...]block. - Write side: write a
--comments-filevia the existing temp-file test helper, drivepr review N --comment --comments-file <f>against a stubbed review-submission endpoint, and assert the captured request body'scomments[](path +new_position, and the reply case's reconstructed anchor) plus the action-block output. Prior art: the existingpr reviewtest that inspects the recorded POST body via the fixture server's request log, and the reply case additionally stubs the reviews-plus-comments GETs used for the lookup. - The reply-lookup failure path is covered by asserting a
VALIDATION_ERRORand that no submission request was made, matching the existing "rejects ... before any API call" tests.
Out of Scope
- Resolving / unresolving review conversations (issue #39).
Gitea exposes no REST endpoint for this — verified exhaustively against the live host — and the only mechanism is a CSRF-guarded internal web route returning HTML, which a prototype confirmed rejects token auth.
Parked as blocked-upstream; the read side's
resolvedfield covers only seeing resolution state, not changing it. - A raw
apipassthrough / generic escape hatch (issue #40); dropped from this work. - Surfacing raw
position/original_positiondiff offsets as rendered fields. - Inline repeatable comment flags (e.g. paired
--on path:line/--body); the JSON--comments-fileis the sole input shape. - Old-side new comments authored by hand; old-side anchoring is reachable only through the reply path, where it is inferred from the target comment.
Further Notes
- The prototype that validated the write-side
new_positionsemantics left one non-removable residue on the live repo: a closed throwaway PR (#41) carrying one review comment, since Gitea cannot hard-delete PRs. - The "no REST resolve endpoint; web-route-only and CSRF-guarded" finding for the parked #39 is recorded so it is not re-derived.
- Keeping gitea-js as the sole HTTP layer is preserved: dropping the passthrough and confining resolve to out-of-scope means no raw-request path is introduced by this work.