Add reaction to one-comment PRs

This commit is contained in:
Colin McDonnell
2026-01-21 00:16:14 +00:00
committed by pullfrog[bot]
parent c3ac7d9ff0
commit 1edeaa0f4c
2 changed files with 53 additions and 31 deletions
+21 -13
View File
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
@@ -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(
`<review_comments count="${leafComments.length}" reviewer="${escapeXml(reviewer)}">`
+32 -18
View File
@@ -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<number, GraphQLReviewComment>,
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, "&amp;")
@@ -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<GraphQLResponse>(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<number, GraphQLReviewComment>();
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(