feat(service,features,desktop): comment on lines in Files changed, and count and discard the pending review
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 areleftorright, lines count from 1, the body can't be blank, andheadShais 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,nullwhen 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 onmain.
Service
addLineComment:singlesends oneaddPullRequestReview(commitOID: headSha, event: COMMENT, threads: [ … ]). That publishes immediately, as GitHub's own "Add single comment" does.reviewfinds the member's own pending review (viewerDidAuthor, the same lookup B1's submit now shares throughownPendingReview). If there is none, it starts one atheadSha. Then it sendsaddPullRequestReviewThreadto that review.
discardPendingReviewdeletes 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 asnull, and the hosted answer staysprivate, no-store. - Changes read: reads the pull request alongside its files to get
headSha. If that read doesn't land,headShais left out.
UI
- Picking lines. Files changed's
CodeViewlist 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 (commentLinesinchanges-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:




Tests
- Service (
coding-agents-app.test.ts):- a single comment is one
addPullRequestReviewwith the thread, atheadSha, on the member's token; reviewstarts 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.
- a single comment is one
- 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;
commentLinesgets 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:
tscfor desktop and service- vitest: the renderer task suites, the main act row, the host composer, the wire goldens,
coding-agents-appandgithub/(1,470 tests) ./scripts/oxlint.sh,biome,pnpm knipand./scripts/repository-checks.sh- the desktop build
The desktop build fits the bundle budget #3305 re-recorded.
Made with Luke
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-commentanddiscard-reviewrun 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-reviewrefuses in words when the member holds no pending review.- The conversation answer gains
pendingReviewand the changes answer gainsheadSha.