From b49813d0e7f26cce63155bbb0695d44320998e50 Mon Sep 17 00:00:00 2001 From: Rishav Naskar <59786899+rishavnaskar@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:02:44 +0530 Subject: [PATCH] feat(context): include diffHunk in PR review comment context (#1584) * feat(context): include diffHunk in PR review comment context Review comments arrived with only path and line, so the code they were written against was missing from the prompt. Fetch diffHunk in the PR GraphQL query and render it under the comment as a diff block. The hunk is PR-authored content, so it goes through sanitizeContent like the comment body. Comments without a hunk are unchanged. Fixes #855 * test(formatter): cover outdated review comments with an empty diff hunk GitHub returns diffHunk: "" (not null) for comments whose line no longer exists in the diff, so the render guard has to reject empty strings too. Found running the real query against anthropics/claude-code-action#1025. --- src/github/api/queries/github.ts | 1 + src/github/data/formatter.ts | 11 +++- src/github/types.ts | 1 + test/data-formatter.test.ts | 106 +++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/github/api/queries/github.ts b/src/github/api/queries/github.ts index 1702061f..a6e8a185 100644 --- a/src/github/api/queries/github.ts +++ b/src/github/api/queries/github.ts @@ -84,6 +84,7 @@ export const PR_QUERY = ` body path line + diffHunk author { login } diff --git a/src/github/data/formatter.ts b/src/github/data/formatter.ts index 82dbe568..ad428ebe 100644 --- a/src/github/data/formatter.ts +++ b/src/github/data/formatter.ts @@ -118,7 +118,16 @@ export function formatReviewComments( body = sanitizeContent(body); - return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; + let formatted = ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; + + // The diff hunk is the code the comment was left on. Without it the + // comment arrives without the context it was written against. + if (comment.diffHunk) { + const diffHunk = sanitizeContent(comment.diffHunk); + formatted += `\n Diff context:\n\`\`\`diff\n${diffHunk}\n\`\`\``; + } + + return formatted; }) .join("\n"); if (comments) { diff --git a/src/github/types.ts b/src/github/types.ts index d5053f42..c5140c12 100644 --- a/src/github/types.ts +++ b/src/github/types.ts @@ -22,6 +22,7 @@ export type GitHubComment = { export type GitHubReviewComment = GitHubComment & { path: string; line: number | null; + diffHunk?: string | null; }; export type GitHubCommit = { diff --git a/test/data-formatter.test.ts b/test/data-formatter.test.ts index 423bbcda..8735f23e 100644 --- a/test/data-formatter.test.ts +++ b/test/data-formatter.test.ts @@ -508,6 +508,112 @@ describe("formatReviewComments", () => { ); }); + test("includes the diff hunk as context when present", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "This can overflow", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: 42, + diffHunk: "@@ -40,3 +40,3 @@\n-const a = 1;\n+const a = 2;", + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:42]: This can overflow"); + expect(result).toContain("Diff context:"); + expect(result).toContain("@@ -40,3 +40,3 @@"); + expect(result).toContain("+const a = 2;"); + }); + + test("omits the diff context when the comment has no diff hunk", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "No hunk here", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: 42, + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:42]: No hunk here"); + expect(result).not.toContain("Diff context:"); + }); + + // GitHub returns line: null and diffHunk: "" for outdated comments whose + // line no longer exists in the diff (observed on anthropics/claude-code-action#1025). + test("omits the diff context for an outdated comment with an empty diff hunk", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "Outdated comment", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: null, + diffHunk: "", + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:?]: Outdated comment"); + expect(result).not.toContain("Diff context:"); + expect(result).not.toContain("```diff"); + }); + test("formats review with only body (no comments) correctly", () => { const reviewData = { nodes: [