Skip to content

Commit 1b60647

Browse files
authored
feat(code): show PR comments in review panel (#1536)
## Problem let's help our users get their PRs across the finish line <!-- Who is this for and what problem does it solve? --> <!-- Closes #ISSUE_ID --> ## Changes **_[only for cloud tasks right now - will bring over to local later too, just a bit differently]_** - displays github PR comments in the review panel - (only grabs inline file comments for now) - adds agent buttons for "fix with agent" and "ask agent" - **^ this is behind a feature flag** because the cloud resume stuff is a bit busted, and enabling it just creates a poor experience. i'll enable once we fix that - allows replying to github comments from the UI <!-- What did you change and why? --> <!-- If there are frontend changes, include screenshots. --> ## ![Screenshot 2026-04-08 at 3.26.40 PM.png](https://app.graphite.com/user-attachments/assets/93e90737-0536-4b36-a69b-39b37f667d61.png) ## How did you test this? manually <!-- Describe what you tested -- manual steps, automated tests, or both. --> <!-- If you're an agent, only list tests you actually ran. -->
1 parent 178803f commit 1b60647

File tree

16 files changed

+1002
-82
lines changed

16 files changed

+1002
-82
lines changed

apps/code/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@
186186
"react-markdown": "^10.1.0",
187187
"react-resizable-panels": "^3.0.6",
188188
"reflect-metadata": "^0.2.2",
189+
"rehype-raw": "^7.0.0",
190+
"rehype-sanitize": "^6.0.0",
189191
"remark-breaks": "^4.0.0",
190192
"remark-gfm": "^4.0.1",
191193
"smol-toml": "^1.6.0",

apps/code/src/main/services/git/schemas.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,48 @@ export const getPrDetailsByUrlOutput = z.object({
336336
});
337337
export type PrDetailsByUrlOutput = z.infer<typeof getPrDetailsByUrlOutput>;
338338

339+
// getPrReviewComments schemas
340+
export const prReviewCommentUserSchema = z.object({
341+
login: z.string(),
342+
avatar_url: z.string(),
343+
});
344+
345+
export const prReviewCommentSchema = z.object({
346+
id: z.number(),
347+
body: z.string(),
348+
path: z.string(),
349+
line: z.number().nullable(),
350+
original_line: z.number().nullable(),
351+
side: z.enum(["LEFT", "RIGHT"]),
352+
start_line: z.number().nullable(),
353+
start_side: z.enum(["LEFT", "RIGHT"]).nullable(),
354+
diff_hunk: z.string(),
355+
in_reply_to_id: z.number().nullish(),
356+
user: prReviewCommentUserSchema,
357+
created_at: z.string(),
358+
updated_at: z.string(),
359+
subject_type: z.enum(["line", "file"]).nullable(),
360+
});
361+
362+
export type PrReviewComment = z.infer<typeof prReviewCommentSchema>;
363+
364+
export const getPrReviewCommentsInput = z.object({
365+
prUrl: z.string(),
366+
});
367+
export const getPrReviewCommentsOutput = z.array(prReviewCommentSchema);
368+
369+
// replyToPrComment schemas
370+
export const replyToPrCommentInput = z.object({
371+
prUrl: z.string(),
372+
commentId: z.number(),
373+
body: z.string(),
374+
});
375+
export const replyToPrCommentOutput = z.object({
376+
success: z.boolean(),
377+
comment: prReviewCommentSchema.nullable(),
378+
});
379+
export type ReplyToPrCommentOutput = z.infer<typeof replyToPrCommentOutput>;
380+
339381
// updatePrByUrl schemas
340382
export const prActionType = z.enum(["close", "reopen", "ready", "draft"]);
341383
export type PrActionType = z.infer<typeof prActionType>;

apps/code/src/main/services/git/service.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ import type {
5757
OpenPrOutput,
5858
PrActionType,
5959
PrDetailsByUrlOutput,
60+
PrReviewComment,
6061
PrStatusOutput,
6162
PublishOutput,
6263
PullOutput,
6364
PushOutput,
65+
ReplyToPrCommentOutput,
6466
SyncOutput,
6567
UpdatePrByUrlOutput,
6668
} from "./schemas";
@@ -1010,6 +1012,71 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
10101012
}
10111013
}
10121014

1015+
public async getPrReviewComments(prUrl: string): Promise<PrReviewComment[]> {
1016+
const pr = parsePrUrl(prUrl);
1017+
if (!pr) return [];
1018+
1019+
const { owner, repo, number } = pr;
1020+
1021+
try {
1022+
const result = await execGh([
1023+
"api",
1024+
`repos/${owner}/${repo}/pulls/${number}/comments`,
1025+
"--paginate",
1026+
"--slurp",
1027+
]);
1028+
1029+
if (result.exitCode !== 0) {
1030+
throw new Error(
1031+
`Failed to fetch PR review comments: ${result.stderr || result.error || "Unknown error"}`,
1032+
);
1033+
}
1034+
1035+
const pages = JSON.parse(result.stdout) as PrReviewComment[][];
1036+
return pages.flat();
1037+
} catch (error) {
1038+
log.warn("Failed to fetch PR review comments", { prUrl, error });
1039+
throw error;
1040+
}
1041+
}
1042+
1043+
public async replyToPrComment(
1044+
prUrl: string,
1045+
commentId: number,
1046+
body: string,
1047+
): Promise<ReplyToPrCommentOutput> {
1048+
const pr = parsePrUrl(prUrl);
1049+
if (!pr) {
1050+
return { success: false, comment: null };
1051+
}
1052+
1053+
try {
1054+
const result = await execGh([
1055+
"api",
1056+
`repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/comments/${commentId}/replies`,
1057+
"-X",
1058+
"POST",
1059+
"-f",
1060+
`body=${body}`,
1061+
]);
1062+
1063+
if (result.exitCode !== 0) {
1064+
log.warn("Failed to reply to PR comment", {
1065+
prUrl,
1066+
commentId,
1067+
error: result.stderr || result.error,
1068+
});
1069+
return { success: false, comment: null };
1070+
}
1071+
1072+
const data = JSON.parse(result.stdout) as PrReviewComment;
1073+
return { success: true, comment: data };
1074+
} catch (error) {
1075+
log.warn("Failed to reply to PR comment", { prUrl, commentId, error });
1076+
return { success: false, comment: null };
1077+
}
1078+
}
1079+
10131080
public async getBranchChangedFiles(
10141081
repo: string,
10151082
branch: string,

apps/code/src/main/trpc/routers/git.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
getPrChangedFilesOutput,
4545
getPrDetailsByUrlInput,
4646
getPrDetailsByUrlOutput,
47+
getPrReviewCommentsInput,
48+
getPrReviewCommentsOutput,
4749
getPrTemplateInput,
4850
getPrTemplateOutput,
4951
ghAuthTokenOutput,
@@ -59,6 +61,8 @@ import {
5961
pullOutput,
6062
pushInput,
6163
pushOutput,
64+
replyToPrCommentInput,
65+
replyToPrCommentOutput,
6266
searchGithubIssuesInput,
6367
searchGithubIssuesOutput,
6468
stageFilesInput,
@@ -320,6 +324,18 @@ export const gitRouter = router({
320324
getService().updatePrByUrl(input.prUrl, input.action),
321325
),
322326

327+
getPrReviewComments: publicProcedure
328+
.input(getPrReviewCommentsInput)
329+
.output(getPrReviewCommentsOutput)
330+
.query(({ input }) => getService().getPrReviewComments(input.prUrl)),
331+
332+
replyToPrComment: publicProcedure
333+
.input(replyToPrCommentInput)
334+
.output(replyToPrCommentOutput)
335+
.mutation(({ input }) =>
336+
getService().replyToPrComment(input.prUrl, input.commentId, input.body),
337+
),
338+
323339
getBranchChangedFiles: publicProcedure
324340
.input(getBranchChangedFilesInput)
325341
.output(getBranchChangedFilesOutput)

apps/code/src/renderer/features/code-editor/stores/diffViewerStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface DiffViewerStoreState {
1111
loadFullFiles: boolean;
1212
wordDiffs: boolean;
1313
hideWhitespaceChanges: boolean;
14+
showReviewComments: boolean;
1415
}
1516

1617
interface DiffViewerStoreActions {
@@ -20,6 +21,7 @@ interface DiffViewerStoreActions {
2021
toggleLoadFullFiles: () => void;
2122
toggleWordDiffs: () => void;
2223
toggleHideWhitespaceChanges: () => void;
24+
toggleShowReviewComments: () => void;
2325
}
2426

2527
type DiffViewerStore = DiffViewerStoreState & DiffViewerStoreActions;
@@ -32,6 +34,7 @@ export const useDiffViewerStore = create<DiffViewerStore>()(
3234
loadFullFiles: false,
3335
wordDiffs: true,
3436
hideWhitespaceChanges: false,
37+
showReviewComments: true,
3538
setViewMode: (mode) =>
3639
set((state) => {
3740
if (state.viewMode === mode) {
@@ -64,6 +67,8 @@ export const useDiffViewerStore = create<DiffViewerStore>()(
6467
toggleWordDiffs: () => set((s) => ({ wordDiffs: !s.wordDiffs })),
6568
toggleHideWhitespaceChanges: () =>
6669
set((s) => ({ hideWhitespaceChanges: !s.hideWhitespaceChanges })),
70+
toggleShowReviewComments: () =>
71+
set((s) => ({ showReviewComments: !s.showReviewComments })),
6772
}),
6873
{
6974
name: "diff-viewer-storage",

apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useDiffViewerStore } from "@features/code-editor/stores/diffViewerStore";
2+
import { usePrDetails } from "@features/git-interaction/hooks/usePrDetails";
13
import { useCloudChangedFiles } from "@features/task-detail/hooks/useCloudChangedFiles";
24
import type { FileDiffMetadata } from "@pierre/diffs";
35
import { processFile } from "@pierre/diffs";
@@ -6,6 +8,7 @@ import { useReviewNavigationStore } from "@renderer/features/code-review/stores/
68
import type { ChangedFile, Task } from "@shared/types";
79
import { useMemo } from "react";
810
import type { DiffOptions } from "../types";
11+
import type { PrCommentThread } from "../utils/prCommentAnnotations";
912
import { InteractiveFileDiff } from "./InteractiveFileDiff";
1013
import {
1114
DeferredDiffPlaceholder,
@@ -23,8 +26,12 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
2326
const isReviewOpen = useReviewNavigationStore(
2427
(s) => (s.reviewModes[taskId] ?? "closed") !== "closed",
2528
);
29+
const showReviewComments = useDiffViewerStore((s) => s.showReviewComments);
2630
const { effectiveBranch, prUrl, isRunActive, remoteFiles, isLoading } =
2731
useCloudChangedFiles(taskId, task, isReviewOpen);
32+
const { commentThreads } = usePrDetails(prUrl, {
33+
includeComments: isReviewOpen && showReviewComments,
34+
});
2835

2936
const allPaths = useMemo(() => remoteFiles.map((f) => f.path), [remoteFiles]);
3037

@@ -44,23 +51,20 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
4451
if (!prUrl && !effectiveBranch && remoteFiles.length === 0) {
4552
if (isRunActive) {
4653
return (
47-
<Flex align="center" justify="center" height="100%">
48-
<Flex align="center" gap="2">
49-
<Spinner size="1" />
50-
<Text size="2" color="gray">
51-
Waiting for changes...
52-
</Text>
54+
<Flex
55+
align="center"
56+
justify="center"
57+
height="100%"
58+
className="text-gray-10"
59+
>
60+
<Flex direction="column" align="center" gap="2">
61+
<Spinner size="2" />
62+
<Text size="2">Waiting for changes...</Text>
5363
</Flex>
5464
</Flex>
5565
);
5666
}
57-
return (
58-
<Flex align="center" justify="center" height="100%">
59-
<Text size="2" color="gray">
60-
No file changes yet
61-
</Text>
62-
</Flex>
63-
);
67+
return null;
6468
}
6569

6670
return (
@@ -105,6 +109,7 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
105109
options={diffOptions}
106110
collapsed={isCollapsed}
107111
onToggle={() => toggleFile(file.path)}
112+
commentThreads={showReviewComments ? commentThreads : undefined}
108113
/>
109114
</div>
110115
);
@@ -120,13 +125,15 @@ function CloudFileDiff({
120125
options,
121126
collapsed,
122127
onToggle,
128+
commentThreads,
123129
}: {
124130
file: ChangedFile;
125131
taskId: string;
126132
prUrl: string | null;
127133
options: DiffOptions;
128134
collapsed: boolean;
129135
onToggle: () => void;
136+
commentThreads?: Map<number, PrCommentThread>;
130137
}) {
131138
const fileDiff = useMemo((): FileDiffMetadata | undefined => {
132139
if (!file.patch) return undefined;
@@ -157,6 +164,8 @@ function CloudFileDiff({
157164
fileDiff={fileDiff}
158165
options={{ ...options, collapsed }}
159166
taskId={taskId}
167+
prUrl={prUrl}
168+
commentThreads={commentThreads}
160169
renderCustomHeader={(fd) => (
161170
<DiffFileHeader
162171
fileDiff={fd}

apps/code/src/renderer/features/code-review/components/DiffSettingsMenu.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export function DiffSettingsMenu() {
1313
const toggleHideWhitespaceChanges = useDiffViewerStore(
1414
(s) => s.toggleHideWhitespaceChanges,
1515
);
16+
const showReviewComments = useDiffViewerStore((s) => s.showReviewComments);
17+
const toggleShowReviewComments = useDiffViewerStore(
18+
(s) => s.toggleShowReviewComments,
19+
);
1620

1721
return (
1822
<DropdownMenu.Root>
@@ -42,6 +46,14 @@ export function DiffSettingsMenu() {
4246
{hideWhitespaceChanges ? "Show whitespace" : "Hide whitespace"}
4347
</Text>
4448
</DropdownMenu.Item>
49+
<DropdownMenu.Separator />
50+
<DropdownMenu.Item onSelect={toggleShowReviewComments}>
51+
<Text size="1">
52+
{showReviewComments
53+
? "Hide review comments"
54+
: "Show review comments"}
55+
</Text>
56+
</DropdownMenu.Item>
4557
</DropdownMenu.Content>
4658
</DropdownMenu.Root>
4759
);

0 commit comments

Comments
 (0)