feat(service,features,desktop): comment on lines in Files changed, and count and discard the pending review

Merged
DA
1d ago
+1734-54

B3b of the "Review and comment" stack, after #3313 (B3a, merged). The task: comment on a line or range from Files changed, offering "Add single comment" and "Start a review" as GitHub does. Started reviews go into the pending review that B1's Review panel submits. As the orchestrator decided, it also adds a small read of the member's own pending review and a Discard action for it.

Wire

Two new members on the action union in pull-request-write-wire.ts, both running through memberPullRequestWrite, so they act as the member pressing the button and never fall back to the App:

  • add-review-comment { mode: "single" | "review", path, line, side, startLine?, startSide?, body, headSha }. Sides are left or right, lines count from 1, the body can't be blank, and headSha is the commit the diff was read at.
  • discard-review.

Two answers gain an optional field:

  • The conversation answer gains pendingReview: { comments } | null: the member's own pending review, null when they have none, have no sign-in, or GitHub didn't answer.
  • The changes answer gains headSha, taken from #3638's own read (imageCommits), now on main.

Service

  • addLineComment:
    • single sends one addPullRequestReview(commitOID: headSha, event: COMMENT, threads: [ … ]). That publishes immediately, as GitHub's own "Add single comment" does.
    • review finds the member's own pending review (viewerDidAuthor, the same lookup B1's submit now shares through ownPendingReview). If there is none, it starts one at headSha. Then it sends addPullRequestReviewThread to that review.
  • discardPendingReview deletes only the member's own pending review on this PR. If they have none, it is refused with the words "You have no pending review on this pull request."
  • Pending read (viewerPendingReviewOf): runs in parallel with the conversation read, on the member's own token (memberReadToken, the same as #3619's, now merged) and never the App's. Any failure comes back as null, and the hosted answer stays private, no-store.
  • Changes read: reads the pull request alongside its files to get headSha. If that read doesn't land, headSha is left out.

UI

  • Picking lines. Files changed's CodeView list now uses Pierre's own line picking (enableLineSelection, enableGutterUtility, onGutterUtilityClick). You can press the gutter "+" on one line, drag it across lines, or pick lines by their numbers and then press "+". Files with no lines of their own (binary, too large, or images #3638 draws as pictures) never offer a comment. Ranges GitHub won't take, such as one spanning two hunks or a line with no diff row, open nothing (commentLines in changes-model.ts).
  • The comment (line-comment.tsx) is drawn as a note under the last picked line, with those lines lit:
    • It has the shared Markdown field and offers Add single comment and Start a review. If you already have a pending review, it offers only Add review comment, as GitHub does.
    • ⌘↩ sends it; Escape and Cancel close it.
    • When GitHub accepts it, the comment closes and clears its text, and the conversation is read again. That draws the new thread on its line and updates the pending count.
    • GitHub's refusal shows in its words beside Refresh changes, and the text stays.
    • Text is kept per file, so it survives the comment scrolling out of the virtual list.
  • Review button: shows a badge with the pending comment count, and its panel says "N pending comments will be published." Discard appears only while you have a pending review. It asks first in the destructive tone, then sends discard-review.

Screenshots from the real diff bundle (diff-view/index.ts), driven with Playwright:
A range picked with the gutter's +, its comment open
GitHub's refusal under the comment, beside Refresh changes
Review counting the pending review, with Discard
Discard asking first

Tests

  • Service (coding-agents-app.test.ts):
    • a single comment is one addPullRequestReview with the thread, at headSha, on the member's token;
    • review starts a pending review when there is none, or joins the member's own and nobody else's;
    • Discard deletes the member's own pending review only, and is refused in words when there is none;
    • an invalid line, side, head or mode never reaches GitHub;
    • the pending read with two members: each sees their own, a member with no sign-in or a 502 gets null, and the App's token is never used;
    • the changes answer names headSha.
  • Renderer:
    • the "+" opens the comment under the last line, normalising a range dragged upward;
    • Add single comment sends the exact request, closes, and clears the text;
    • with a pending review, only Add review comment is offered, and Review shows the count and Discard (with confirm);
    • a refusal keeps the text and Refresh changes reads the files again;
    • commentLines gets the right sides, a one-line pick, a cross-hunk pick, a line with no row, and a binary file.
  • Goldens: six re-recorded with LUKE_UPDATE_FIXTURES=1:
    • the action request and the view's params, each gaining the two actions;
    • the changes answer, wire and view, gaining headSha;
    • the conversation answer, wire and view, gaining pendingReview.

Evidence

pnpm check:affected runs out of memory in this sandbox, so I ran the parts separately and all passed:

  • tsc for desktop and service
  • vitest: the renderer task suites, the main act row, the host composer, the wire goldens, coding-agents-app and github/ (1,470 tests)
  • ./scripts/oxlint.sh, biome, pnpm knip and ./scripts/repository-checks.sh
  • the desktop build

The desktop build fits the bundle budget #3305 re-recorded.

Made with Luke


Devin Review


Summary by cubic

Adds line commenting to Files changed, as GitHub's diff offers it, with comments published at once or held in the member's pending review, which Review now counts and can discard.

New Features

  • The gutter "+" picks one line or a range; binary and oversized files, and ranges GitHub won't take, open nothing.
  • The comment box offers Add single comment and Start a review (or Add review comment when a pending review exists), keeps text per file, and shows GitHub's refusal in its own words.
  • Review shows the pending comment count and Discard, which confirms first and deletes only the member's own pending review.

Service

  • add-review-comment and discard-review run as the member on their own GitHub token: single publishes at the head sha the diff was read at, review joins the member's pending review and starts one, with its first comment, if none.
  • A comment for a pending review held on another commit is refused in words, and a refused first comment leaves no empty review behind.
  • discard-review refuses in words when the member holds no pending review.
  • The conversation answer gains pendingReview and the changes answer gains headSha.