feat(service,features,panel): the member's pending review comments on their lines, with Edit and Delete
Summary
The orchestrator asked for the member's own pending review comments to show in Files changed on their lines, as GitHub shows them: a card marked Pending, in the member's own name, with Edit and Delete. They should use PR 1's placement (#4008, merged) and appear in the minimized chip's counts. Adding a comment should refresh them, and Submit or Discard should clear them. They should also show in the Conversation where that's cheap: here, a list in the Review panel.
This changes the hosted service, so it needs a ./scripts/run.sh --preview run before merging. Auto-merge is held until that's done.
Read (member token, parallel, dropped on failure, never cached)
-
viewerPendingReviewOfstill runs beside the conversation read on the member's own token. It is still one GraphQL query, and a failure still leavespendingReviewout. -
Beside
reviews(states: PENDING), it now also readspullRequest.reviewThreads(last: 100). Each thread bringsid path line startLine diffSide startDiffSide, its first comment's state (opening), and its last 50 comments (id state body bodyHTML createdAt author pullRequestReview { id }). The read keeps the comments that arePENDINGand belong to the member's own pending review. -
Why not REST? I checked on GitHub with a throwaway pending review on #4008, deleted straight after:
GET pulls/{n}/reviews/{id}/commentsanswersline,side,start_lineandstart_sideas null for a pending comment.GET pulls/{n}/commentsleaves pending comments out entirely.- GraphQL
reviewThreadsincludes the author's own pending threads, with sides.
-
Rate limit: GitHub counts one request for
reviews, one forreviewThreads, and two per thread for its first comment and its last comments (up to 200). That's 202 requests, divided by 100, so the query costs 2 points (up to ~5,200 nodes, far under the 500,000 cap). The first comment is read so a thread whose last 50 comments are all pending replies still counts as published (Devin's finding). -
More than 100 threads: a pending comment on a thread older than the newest 100 is missed, so it isn't drawn. The Review count is still right, because it comes from the review's own
comments.totalCount. A pending comment the member just made starts a new thread, which is among the newest, so in practice only an old pending reply on a long-lived PR could be missed. -
Wire:
pendingReviewstays{ comments }, plus an optionaldrafts(at mostMAX_PENDING_COMMENTS= 100). Each draft carries:id, plusthreadIdfor a reply on a published threadby, named as the conversation's people are (the member)path,line,startLine,side,startSidehtml,body,at
It's an optional key, so a newer client reading an older service decodes. Goldens were updated with
LUKE_UPDATE_FIXTURES=1; the diff only adds lines: thedraftsitems in the two conversation answer schemas, and the two new actions in the two action request schemas.
Draw (shares PR 1's placement)
pendingThreadsOf(threads, drafts)(tasks/pending-comments.tsx):- A pending reply on a published thread becomes that thread's last comment, drawn inside its card, as GitHub does.
- Every other draft becomes a
ReviewThreadof its own, keyed by the comment's own id.
- The merged list is what Files changed is handed as its threads. So
threadsOn,gapLines(PR 1's gap placement), the minimized chip and its counts, the tree's thread counts, hover lighting and the lit range all apply unchanged. PendingCommentsProvidertells each comment whether it's pending. A pending one draws the member's head, a Pending badge, its words, and Edit and Delete. A thread a draft starts wears Pending in its head and has no ⋯ menu (it has no page or id yet) and no Reply or Resolve.
Act
- There are two new actions:
EDIT_REVIEW_COMMENT {commentId, body}→updatePullRequestReviewComment, andDELETE_REVIEW_COMMENT {commentId}→deletePullRequestReviewComment. Both go throughmemberPullRequestWrite, on the member's token, never the App's. - Check before each mutation (
ownPendingComment): it readsnode(id) { ... on PullRequestReviewComment { viewerDidAuthor state pullRequest { id } } }on the member's token. It refuses, with "That comment is not one of your pending review's on this pull request.", unless:viewerDidAuthoris true,stateisPENDING,- and the pull request is this one.
- Edit opens the shared
MarkdownFieldin place. Cancel and Escape keep nothing, ⌘↩ saves, and GitHub's refusal stands under the comment in its words. - Delete asks first with the destructive
ConfirmDialog. - Adding a line comment, Edit, Delete, Submit and Discard all re-read the conversation, so the drafts refresh, and they clear once the review is submitted or discarded.
Conversation
- The Review panel lists the pending comments under "N pending comments will be published". Each shows its file and lines, then the same Pending comment with Edit and Delete. A Delete's question over the window leaves the panel open.
Screenshots
Real AgentPullRequestTab with the real diff bundle and fixture reads.
| light | dark | |
|---|---|---|
| A new pending thread on line 5, and a pending reply inside the thread on line 22 (a gap line, so it stands above until opened) | ![]() | ![]() |
| The gap opened: the thread with its pending reply moves onto line 22 (PR 1's placement) | ![]() | ![]() |
| The Review panel's Pending list | ![]() | ![]() |
| Edit open in place | ![]() | ![]() |
Evidence
pnpm check:affectedruns out of memory in this sandbox, so I ran the per-package checks instead:tsc --noEmitfor panel, service and features, and the desktop typecheck: clean@luke/webtypecheck (5.5 GB heap): clean./scripts/oxlint.sh: 0 errorsbiome check .: cleanpnpm knip: clean./scripts/repository-checks.sh: passedpnpm discarded-effect: clean
vitest run --project panel: 233 files, 2508 tests passed.vitest run packages/service/src/agents packages/service/src/github packages/features/src: 130 files, 1516 tests passed (the hosted-wire goldens among them).cd apps/desktop && node scripts/build.mjs: built within the bundle budget.- New and updated tests:
coding-agents-app.test.ts:- The pending read, now with threads. A pending reply on a published thread names that thread, a new pending thread on a left-side range keeps its range, another member's pending comment and published comments are left out, and the member's own token reads it.
- New: edit and delete of one's own pending comment reach GitHub on the member's token. Each is refused in words for someone else's comment, a published one, one on another pull request, and an unknown id.
pending-comments.test.ts(new, 2):- A reply goes into its thread, and a reply on a thread the conversation didn't hold stands on its own line.
- A pending comment in a gap waits above the diff and moves onto its line once the gap opens.
agent-pull-request-tab.test.ts(new, 1): drafts stand on their lines, with the reply adding no card of its own; Edit sends new words, while Cancel sends nothing; Delete asks first; the Review panel lists both.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens changed with
LUKE_UPDATE_FIXTURES=1; the diff only adds lines (explained above). - Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count: +4 new tests (1 service, 2 pending-comments, 1 pull request tab), 1 service test extended.
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdunchanged: the pending comments are the member's own, read on their own token for them alone, as the count already was. - Package deps unchanged.
- Barrel/door rule unaffected.
Notes
-
Devin's three findings are fixed in d249be3b1, each with a test. ⌘↩ in a listed comment's edit saves only that comment and never submits the review. Pending replies stay inside long threads. A Delete question asked in Files changed goes when another view is chosen.
-
Preview run needed before merging (
./scripts/run.sh --preview,docs/WORKFLOW.mdstep 3). -
If the member deletes their review's last comment, the pending review itself remains, as GitHub keeps it. Review then counts 0 comments, and Discard still clears it.
Made with Luke







