diff --git a/entry b/entry index 4cdadd8..51cc4bf 100755 --- a/entry +++ b/entry @@ -121270,11 +121270,20 @@ query ($owner: String!, $repo: String!, $pullNumber: Int!) { } `; var MAX_BODY_PREVIEW = 80; +var MAX_THREAD_DEPTH = 10; function truncateBody(body) { const oneLine = body.replace(/\n/g, " ").trim(); if (oneLine.length <= MAX_BODY_PREVIEW) return oneLine; return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "..."; } +function getReplyChain(comment, commentMap, depth = 0) { + if (depth >= MAX_THREAD_DEPTH) return []; + const parentId = comment.replyTo?.databaseId; + if (!parentId) return []; + const parent = commentMap.get(parentId); + if (!parent) return []; + return [...getReplyChain(parent, commentMap, depth + 1), parent]; +} function escapeXml(text) { return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } @@ -121295,13 +121304,6 @@ function GetReviewCommentsTool(ctx) { description: "Get review comments for a pull request review, including thread context. When approved_by is provided, only returns comments that user approved with \u{1F44D}. Returns commentsPath pointing to a file with full comment details in XML format.", parameters: GetReviewComments, execute: execute(async ({ pull_number, review_id, approved_by }) => { - const review = await ctx.octokit.rest.pulls.getReview({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - pull_number, - review_id - }); - const reviewer = review.data.user?.login ?? "unknown"; const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, { owner: ctx.repo.owner, repo: ctx.repo.name, @@ -121312,12 +121314,19 @@ function GetReviewCommentsTool(ctx) { return { review_id, pull_number, - reviewer, + reviewer: "unknown", count: 0, commentsPath: null, message: "No review threads found" }; } + const commentMap = /* @__PURE__ */ new Map(); + for (const thread of pullRequest.reviewThreads.nodes) { + if (!thread?.comments?.nodes) continue; + for (const comment of thread.comments.nodes) { + if (comment) commentMap.set(comment.databaseId, comment); + } + } const leafComments = []; for (const thread of pullRequest.reviewThreads.nodes) { if (!thread?.comments?.nodes) continue; @@ -121328,12 +121337,10 @@ function GetReviewCommentsTool(ctx) { for (const comment of threadComments) { if (comment.pullRequestReview?.databaseId !== review_id) continue; if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue; - const threadContext = threadComments.filter( - (c) => c.createdAt < comment.createdAt || c.createdAt === comment.createdAt && c.databaseId < comment.databaseId - ); + const replyChain = getReplyChain(comment, commentMap); leafComments.push({ comment, - thread: threadContext, + thread: replyChain, side: thread.diffSide }); } @@ -121342,12 +121349,13 @@ function GetReviewCommentsTool(ctx) { return { review_id, pull_number, - reviewer, + reviewer: "unknown", count: 0, commentsPath: null, message: approved_by ? `No comments with \u{1F44D} from ${approved_by}` : "No comments found for this review" }; } + const reviewer = leafComments[0].comment.author?.login ?? "unknown"; const lines = []; lines.push( `` diff --git a/mcp/reviewComments.ts b/mcp/reviewComments.ts index 81858dc..26c7ef7 100644 --- a/mcp/reviewComments.ts +++ b/mcp/reviewComments.ts @@ -101,6 +101,7 @@ type GraphQLResponse = { }; const MAX_BODY_PREVIEW = 80; +const MAX_THREAD_DEPTH = 10; function truncateBody(body: string): string { const oneLine = body.replace(/\n/g, " ").trim(); @@ -108,6 +109,20 @@ function truncateBody(body: string): string { return oneLine.slice(0, MAX_BODY_PREVIEW - 3) + "..."; } +// walk up the replyTo chain to get actual conversation (oldest first) +function getReplyChain( + comment: GraphQLReviewComment, + commentMap: Map, + depth = 0 +): GraphQLReviewComment[] { + if (depth >= MAX_THREAD_DEPTH) return []; + const parentId = comment.replyTo?.databaseId; + if (!parentId) return []; + const parent = commentMap.get(parentId); + if (!parent) return []; + return [...getReplyChain(parent, commentMap, depth + 1), parent]; +} + function escapeXml(text: string): string { return text .replace(/&/g, "&") @@ -140,15 +155,6 @@ export function GetReviewCommentsTool(ctx: ToolContext) { "Returns commentsPath pointing to a file with full comment details in XML format.", parameters: GetReviewComments, execute: execute(async ({ pull_number, review_id, approved_by }) => { - // fetch the review to get reviewer username - const review = await ctx.octokit.rest.pulls.getReview({ - owner: ctx.repo.owner, - repo: ctx.repo.name, - pull_number, - review_id, - }); - const reviewer = review.data.user?.login ?? "unknown"; - // fetch all review threads using graphql (single API call) const response = await ctx.octokit.graphql(REVIEW_THREADS_QUERY, { owner: ctx.repo.owner, @@ -161,13 +167,22 @@ export function GetReviewCommentsTool(ctx: ToolContext) { return { review_id, pull_number, - reviewer, + reviewer: "unknown", count: 0, commentsPath: null, message: "No review threads found", }; } + // build a map of all comments for O(1) lookup when walking replyTo chains + const commentMap = new Map(); + for (const thread of pullRequest.reviewThreads.nodes) { + if (!thread?.comments?.nodes) continue; + for (const comment of thread.comments.nodes) { + if (comment) commentMap.set(comment.databaseId, comment); + } + } + // collect leaf comments (from target review) with their thread context type LeafComment = { comment: GraphQLReviewComment; @@ -191,16 +206,12 @@ export function GetReviewCommentsTool(ctx: ToolContext) { // filter by approved_by if specified if (approved_by && !hasThumbsUpFrom(comment, approved_by)) continue; - // get thread context (all comments before this one in the thread) - const threadContext = threadComments.filter( - (c) => - c.createdAt < comment.createdAt || - (c.createdAt === comment.createdAt && c.databaseId < comment.databaseId) - ); + // get thread context by walking up the replyTo chain (not just chronological) + const replyChain = getReplyChain(comment, commentMap); leafComments.push({ comment, - thread: threadContext, + thread: replyChain, side: thread.diffSide, }); } @@ -210,7 +221,7 @@ export function GetReviewCommentsTool(ctx: ToolContext) { return { review_id, pull_number, - reviewer, + reviewer: "unknown", count: 0, commentsPath: null, message: approved_by @@ -219,6 +230,9 @@ export function GetReviewCommentsTool(ctx: ToolContext) { }; } + // derive reviewer from first comment (all comments in a review are from the same user) + const reviewer = leafComments[0].comment.author?.login ?? "unknown"; + // build XML output const lines: string[] = []; lines.push(