Skip to content

fix(code-review): support resolving GitHub PR review threads#2337

Open
ShekharDhangar wants to merge 1 commit into
PostHog:mainfrom
ShekharDhangar:fix/pr-review-resolve
Open

fix(code-review): support resolving GitHub PR review threads#2337
ShekharDhangar wants to merge 1 commit into
PostHog:mainfrom
ShekharDhangar:fix/pr-review-resolve

Conversation

@ShekharDhangar
Copy link
Copy Markdown

@ShekharDhangar ShekharDhangar commented May 24, 2026

Problem

GitHub PR comments in Code only supported replying. There was no way to resolve or unresolve review threads from the app.

Changes

  • fetch PR review threads with the metadata needed for thread actions
  • add resolve / unresolve tRPC mutations
  • wire thread resolved state into code review annotations and UI
  • add Resolve / Unresolve actions in PrCommentThread
  • add bounded pagination safeguards for review thread fetching
  • add service tests for pagination and resolve / unresolve behavior

Notes

  • Review thread pagination is capped at 50 pages.
  • GitHub returns up to 100 threads per page, so this still allows up to 5,000 review threads on a single PR.
  • We chose this to avoid an unbounded pagination loop while keeping the limit far above any normal PR size.
  • If the cap is ever hit, we return partial results and log a warning.

How did you test this?

image
  • Verified in the app against a live PR thread that Resolve / Unresolve shows up and works.
  • pnpm --filter code typecheck
  • pnpm exec biome check apps/code/src/main/services/git/schemas.ts apps/code/src/main/services/git/service.test.ts apps/code/src/main/services/git/service.ts apps/code/src/main/trpc/routers/git.ts apps/code/src/renderer/features/code-review/components/PrCommentThread.tsx apps/code/src/renderer/features/code-review/hooks/usePrCommentActions.ts apps/code/src/renderer/features/code-review/types.ts apps/code/src/renderer/features/code-review/utils/prCommentAnnotations.ts apps/code/src/renderer/features/git-interaction/hooks/usePrDetails.ts
  • cd apps/code && pnpm exec vitest run src/main/services/git/service.test.ts

Publish to changelog?

no

Closes #2248

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 24, 2026

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/code/src/main/services/git/service.test.ts:442-495
**Non-parameterised resolve/unresolve tests**

The two test cases for resolve and unresolve in `GitService.resolveReviewThread` are almost identical — same fixture shape, same assertions, same non-zero exit-code test duplicated. The team guideline says parameterised tests are always preferred, and this is a textbook `it.each` opportunity: `[["resolves", true, "resolveReviewThread"], ["unresolves", false, "unresolveReviewThread"]]` covers both branches without repeating the mock setup and assertion blocks.

### Issue 2 of 3
apps/code/src/main/services/git/service.ts:1366
**`rootId` silently falls back to `0` for empty threads**

`comments[0]?.id ?? 0` means any thread that somehow has an empty `comments.nodes` array — possible if GitHub returns a malformed page — gets `rootId: 0`. Multiple such threads would overwrite each other in the `threadsToMap` `Map`, silently dropping all but the last one. A `null` fallback (matching the rule of using `null` to signal an intended absence) or an early guard that skips the thread would be more defensive.

```suggestion
          rootId: comments[0]?.id ?? null,
```

### Issue 3 of 3
apps/code/src/main/services/git/service.ts:1326-1340
**GraphQL-level errors are not checked**

GitHub's GraphQL endpoint returns HTTP 200 even for error responses (`{"errors":[...], "data": null}`). The code only checks `exitCode !== 0`, so a permission error, rate-limit, or invalid variable would leave `data.data` as `null` and throw a `TypeError` on the next line (`Cannot read properties of null (reading 'repository')`). The outer `catch` re-throws this as an opaque error rather than the descriptive message from `data.errors`. Adding an explicit check for `data.errors` before accessing `data.data` would produce a clearer failure here and in `resolveReviewThread`.

Reviews (1): Last reviewed commit: "fix(code-review): support resolving GitH..." | Re-trigger Greptile

Comment on lines +442 to +495
});

const result = await service.getPrReviewComments(
"https://github.com/owner/repo/pull/1",
);

expect(mockExecGh).toHaveBeenCalledTimes(50);
expect(result).toHaveLength(50);
expect(result[0]?.nodeId).toBe("T_1");
expect(result[49]?.nodeId).toBe("T_50");
});

it("throws when gh exits with non-zero", async () => {
mockExecGh.mockResolvedValueOnce({
exitCode: 1,
stderr: "auth error",
stdout: "",
});

await expect(
service.getPrReviewComments("https://github.com/owner/repo/pull/1"),
).rejects.toThrow("Failed to fetch PR review threads");
});
});

describe("GitService.resolveReviewThread", () => {
let service: GitService;

beforeEach(() => {
vi.clearAllMocks();
service = new GitService(
{} as LlmGatewayService,
{} as WorkspaceService,
{ getSessionEnvForTask: async () => ({}) } as unknown as AgentService,
);
});

it("resolves a thread and returns isResolved: true", async () => {
mockExecGh.mockResolvedValueOnce({
exitCode: 0,
stdout: JSON.stringify({
data: {
resolveReviewThread: { thread: { id: "T_1", isResolved: true } },
},
}),
});

const result = await service.resolveReviewThread("T_1", true);

expect(result).toEqual({ success: true, isResolved: true });
const body = JSON.parse(mockExecGh.mock.calls[0][1].input);
expect(body.query).toContain("resolveReviewThread");
expect(body.variables.threadId).toBe("T_1");
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-parameterised resolve/unresolve tests

The two test cases for resolve and unresolve in GitService.resolveReviewThread are almost identical — same fixture shape, same assertions, same non-zero exit-code test duplicated. The team guideline says parameterised tests are always preferred, and this is a textbook it.each opportunity: [["resolves", true, "resolveReviewThread"], ["unresolves", false, "unresolveReviewThread"]] covers both branches without repeating the mock setup and assertion blocks.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/git/service.test.ts
Line: 442-495

Comment:
**Non-parameterised resolve/unresolve tests**

The two test cases for resolve and unresolve in `GitService.resolveReviewThread` are almost identical — same fixture shape, same assertions, same non-zero exit-code test duplicated. The team guideline says parameterised tests are always preferred, and this is a textbook `it.each` opportunity: `[["resolves", true, "resolveReviewThread"], ["unresolves", false, "unresolveReviewThread"]]` covers both branches without repeating the mock setup and assertion blocks.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

return {
nodeId: thread.id,
isResolved: thread.isResolved,
rootId: comments[0]?.id ?? 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 rootId silently falls back to 0 for empty threads

comments[0]?.id ?? 0 means any thread that somehow has an empty comments.nodes array — possible if GitHub returns a malformed page — gets rootId: 0. Multiple such threads would overwrite each other in the threadsToMap Map, silently dropping all but the last one. A null fallback (matching the rule of using null to signal an intended absence) or an early guard that skips the thread would be more defensive.

Suggested change
rootId: comments[0]?.id ?? 0,
rootId: comments[0]?.id ?? null,
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/git/service.ts
Line: 1366

Comment:
**`rootId` silently falls back to `0` for empty threads**

`comments[0]?.id ?? 0` means any thread that somehow has an empty `comments.nodes` array — possible if GitHub returns a malformed page — gets `rootId: 0`. Multiple such threads would overwrite each other in the `threadsToMap` `Map`, silently dropping all but the last one. A `null` fallback (matching the rule of using `null` to signal an intended absence) or an early guard that skips the thread would be more defensive.

```suggestion
          rootId: comments[0]?.id ?? null,
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1326 to 1340
if (!reviewThreads.pageInfo.hasNextPage) {
completed = true;
break;
}
cursor = reviewThreads.pageInfo.endCursor;
}

if (!completed) {
log.warn(
"getPrReviewComments hit MAX_THREAD_PAGES; returning partial results",
{
prUrl,
returned: allNodes.length,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 GraphQL-level errors are not checked

GitHub's GraphQL endpoint returns HTTP 200 even for error responses ({"errors":[...], "data": null}). The code only checks exitCode !== 0, so a permission error, rate-limit, or invalid variable would leave data.data as null and throw a TypeError on the next line (Cannot read properties of null (reading 'repository')). The outer catch re-throws this as an opaque error rather than the descriptive message from data.errors. Adding an explicit check for data.errors before accessing data.data would produce a clearer failure here and in resolveReviewThread.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/git/service.ts
Line: 1326-1340

Comment:
**GraphQL-level errors are not checked**

GitHub's GraphQL endpoint returns HTTP 200 even for error responses (`{"errors":[...], "data": null}`). The code only checks `exitCode !== 0`, so a permission error, rate-limit, or invalid variable would leave `data.data` as `null` and throw a `TypeError` on the next line (`Cannot read properties of null (reading 'repository')`). The outer `catch` re-throws this as an opaque error rather than the descriptive message from `data.errors`. Adding an explicit check for `data.errors` before accessing `data.data` would produce a clearer failure here and in `resolveReviewThread`.

How can I resolve this? If you propose a fix, please make it concise.

@ShekharDhangar ShekharDhangar changed the title Support resolving GitHub PR review threads fix(code-review): support resolving GitHub PR review threads May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

missing resolve button on github comments

1 participant