fix: issues with pagination not resolving correct url templates
This commit is contained in:
+41
-121
@@ -15,57 +15,41 @@ export {
|
||||
LEAPING_INTO_ACTION_PREFIX,
|
||||
} from "../utils/leapingComment.ts";
|
||||
|
||||
interface GiteaComment { id: number; body?: string | null; html_url?: string; updated_at?: string }
|
||||
|
||||
function buildCommentFooter(ctx: ToolContext): string {
|
||||
return buildShockbotFooter({ model: ctx.toolState.model });
|
||||
}
|
||||
|
||||
export function addFooter(ctx: ToolContext, body: string): string {
|
||||
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
||||
throw new Error(
|
||||
"body contains <br/> followed by a non-blank line, which breaks Gitea markdown rendering. always add a blank line after <br/> tags."
|
||||
);
|
||||
throw new Error("body contains <br/> followed by a non-blank line — add a blank line after <br/> tags.");
|
||||
}
|
||||
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
||||
const footer = buildCommentFooter(ctx);
|
||||
return `${bodyWithoutFooter}${footer}`;
|
||||
return `${stripExistingFooter(fixDoubleEscapedString(body))}${buildCommentFooter(ctx)}`;
|
||||
}
|
||||
|
||||
export const Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type
|
||||
.enumerated("Plan", "Comment")
|
||||
.describe("Plan: record as the plan for this run. Comment: regular comment (default).")
|
||||
.optional(),
|
||||
type: type.enumerated("Plan", "Comment").optional(),
|
||||
});
|
||||
|
||||
export function CreateCommentTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "create_issue_comment",
|
||||
description:
|
||||
"Create a comment on a Gitea issue or PR. " +
|
||||
'Example: `create_issue_comment({ issueNumber: 1234, body: "Thanks for the report." })`. ' +
|
||||
"For progress/plan updates use report_progress instead.",
|
||||
"Create a comment on a Gitea issue or PR. For progress/plan updates use report_progress instead.",
|
||||
parameters: Comment,
|
||||
execute: execute(async ({ issueNumber, body }) => {
|
||||
const bodyWithFooter = addFooter(ctx, body);
|
||||
|
||||
const result = await ctx.gitea.rest.issue.issueCreateComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
index: issueNumber,
|
||||
body: { body: bodyWithFooter },
|
||||
});
|
||||
|
||||
const r = await ctx.gitea.request(
|
||||
"POST /repos/{owner}/{repo}/issues/{index}/comments",
|
||||
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: issueNumber, body: bodyWithFooter }
|
||||
);
|
||||
const data = r.data as GiteaComment;
|
||||
ctx.toolState.wasUpdated = true;
|
||||
log.info(`» created comment ${result.data.id}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
};
|
||||
log.info(`» created comment ${data.id}`);
|
||||
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -82,48 +66,30 @@ export function EditCommentTool(ctx: ToolContext) {
|
||||
parameters: EditComment,
|
||||
execute: execute(async ({ commentId, body }) => {
|
||||
const bodyWithFooter = addFooter(ctx, body);
|
||||
|
||||
const result = await ctx.gitea.rest.issue.issueEditComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
id: commentId,
|
||||
body: { body: bodyWithFooter },
|
||||
});
|
||||
log.info(`» updated comment ${result.data.id}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
updatedAt: result.data.updated_at,
|
||||
};
|
||||
const r = await ctx.gitea.request(
|
||||
"PATCH /repos/{owner}/{repo}/issues/comments/{id}",
|
||||
{ owner: ctx.repo.owner, repo: ctx.repo.name, id: commentId, body: bodyWithFooter }
|
||||
);
|
||||
const data = r.data as GiteaComment;
|
||||
log.info(`» updated comment ${data.id}`);
|
||||
return { success: true, commentId: data.id, url: data.html_url, body: data.body, updatedAt: data.updated_at };
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export const ReportProgress = type({
|
||||
body: type.string.describe("the progress update content to share"),
|
||||
"target_plan_comment?": type("boolean").describe(
|
||||
"for revising an existing plan comment ONLY."
|
||||
),
|
||||
"target_plan_comment?": type("boolean"),
|
||||
});
|
||||
|
||||
export async function reportProgress(
|
||||
ctx: ToolContext,
|
||||
params: { body: string; target_plan_comment?: boolean }
|
||||
): Promise<{
|
||||
commentId?: number;
|
||||
url?: string;
|
||||
body: string;
|
||||
action: "created" | "updated" | "skipped";
|
||||
}> {
|
||||
): Promise<{ commentId?: number; url?: string; body: string; action: "created" | "updated" | "skipped" }> {
|
||||
const { body, target_plan_comment } = params;
|
||||
ctx.toolState.lastProgressBody = body;
|
||||
|
||||
if (ctx.payload.event.silent) {
|
||||
return { body, action: "skipped" };
|
||||
}
|
||||
if (ctx.payload.event.silent) return { body, action: "skipped" };
|
||||
|
||||
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
||||
const apiCtx = { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name };
|
||||
@@ -145,30 +111,14 @@ export async function reportProgress(
|
||||
return { commentId: result.id, url: result.html_url, body: result.body || "", action: "updated" };
|
||||
}
|
||||
|
||||
if (existingComment === null) {
|
||||
return { body, action: "skipped" };
|
||||
}
|
||||
|
||||
if (issueNumber === undefined) {
|
||||
return { body, action: "skipped" };
|
||||
}
|
||||
if (existingComment === null) return { body, action: "skipped" };
|
||||
if (issueNumber === undefined) return { body, action: "skipped" };
|
||||
|
||||
const initialBody = addFooter(ctx, body);
|
||||
const created = await createLeapingProgressComment(
|
||||
apiCtx,
|
||||
{ kind: "issue", issueNumber },
|
||||
initialBody
|
||||
);
|
||||
|
||||
const created = await createLeapingProgressComment(apiCtx, { kind: "issue", issueNumber }, initialBody);
|
||||
ctx.toolState.progressComment = created.comment;
|
||||
ctx.toolState.wasUpdated = true;
|
||||
|
||||
return {
|
||||
commentId: created.comment.id,
|
||||
url: created.html_url,
|
||||
body: created.body || "",
|
||||
action: "created",
|
||||
};
|
||||
return { commentId: created.comment.id, url: created.html_url, body: created.body || "", action: "created" };
|
||||
}
|
||||
|
||||
export function ReportProgressTool(ctx: ToolContext) {
|
||||
@@ -180,26 +130,18 @@ export function ReportProgressTool(ctx: ToolContext) {
|
||||
parameters: ReportProgress,
|
||||
execute: execute(async (params) => {
|
||||
let body = params.body;
|
||||
|
||||
if (!params.target_plan_comment && ctx.toolState.todoTracker) {
|
||||
ctx.toolState.todoTracker.cancel();
|
||||
await ctx.toolState.todoTracker.settled();
|
||||
const collapsible = ctx.toolState.todoTracker.renderCollapsible({ completeInProgress: true });
|
||||
if (collapsible) body = `${body}\n\n${collapsible}`;
|
||||
}
|
||||
|
||||
const reportParams: { body: string; target_plan_comment?: boolean } = { body };
|
||||
if (params.target_plan_comment !== undefined) reportParams.target_plan_comment = params.target_plan_comment;
|
||||
|
||||
const result = await reportProgress(ctx, reportParams);
|
||||
|
||||
if (result.action === "skipped") {
|
||||
return { success: true, message: "progress recorded (no comment created)" };
|
||||
}
|
||||
|
||||
if (result.action === "skipped") return { success: true, message: "progress recorded (no comment created)" };
|
||||
if (result.commentId !== undefined) log.info(`» ${result.action} comment ${result.commentId}`);
|
||||
if (!params.target_plan_comment) ctx.toolState.finalSummaryWritten = true;
|
||||
|
||||
return { success: true, ...result };
|
||||
}),
|
||||
});
|
||||
@@ -208,16 +150,11 @@ export function ReportProgressTool(ctx: ToolContext) {
|
||||
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
||||
const existing = ctx.toolState.progressComment;
|
||||
if (!existing) return false;
|
||||
|
||||
try {
|
||||
await deleteProgressCommentApi(
|
||||
{ gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name },
|
||||
existing
|
||||
);
|
||||
await deleteProgressCommentApi({ gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name }, existing);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && error.message.includes("Not Found"))) throw error;
|
||||
}
|
||||
|
||||
ctx.toolState.progressComment = null;
|
||||
return true;
|
||||
}
|
||||
@@ -229,10 +166,7 @@ export const ReplyToReviewComment = type({
|
||||
});
|
||||
|
||||
export interface DuplicateReplyDecision {
|
||||
kind: "already-replied";
|
||||
commentId: number;
|
||||
url: string | undefined;
|
||||
reason: string;
|
||||
kind: "already-replied"; commentId: number; url: string | undefined; reason: string;
|
||||
}
|
||||
|
||||
export function duplicateReplyDecision(params: {
|
||||
@@ -246,47 +180,33 @@ export function duplicateReplyDecision(params: {
|
||||
kind: "already-replied",
|
||||
commentId: existing.commentId,
|
||||
url: existing.url,
|
||||
reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`,
|
||||
reason: `reply ${existing.commentId} with identical body was already posted in this session`,
|
||||
};
|
||||
}
|
||||
|
||||
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "reply_to_review_comment",
|
||||
description:
|
||||
"Reply to a PR review comment. Posts an issue comment on the PR referencing the original. " +
|
||||
"Keep replies extremely brief (1 sentence max).",
|
||||
description: "Reply to a PR review comment. Posts an issue comment on the PR. Keep replies to 1 sentence max.",
|
||||
parameters: ReplyToReviewComment,
|
||||
execute: execute(async ({ pull_number, comment_id, body }) => {
|
||||
const bodyWithFooter = addFooter(ctx, body);
|
||||
|
||||
const dup = duplicateReplyDecision({
|
||||
existing: ctx.toolState.reviewReplies?.get(comment_id),
|
||||
bodyWithFooter,
|
||||
});
|
||||
const dup = duplicateReplyDecision({ existing: ctx.toolState.reviewReplies?.get(comment_id), bodyWithFooter });
|
||||
if (dup) {
|
||||
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
||||
return { success: true, skipped: true, reason: dup.reason, commentId: dup.commentId, url: dup.url };
|
||||
}
|
||||
|
||||
const replyBody = `> Reply to review comment #${comment_id}\n\n${bodyWithFooter}`;
|
||||
const result = await ctx.gitea.rest.issue.issueCreateComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
index: pull_number,
|
||||
body: { body: replyBody },
|
||||
});
|
||||
log.info(`» created reply comment ${result.data.id}`);
|
||||
|
||||
const r = await ctx.gitea.request(
|
||||
"POST /repos/{owner}/{repo}/issues/{index}/comments",
|
||||
{ owner: ctx.repo.owner, repo: ctx.repo.name, index: pull_number, body: replyBody }
|
||||
);
|
||||
const data = r.data as GiteaComment;
|
||||
log.info(`» created reply comment ${data.id}`);
|
||||
ctx.toolState.wasUpdated = true;
|
||||
ctx.toolState.reviewReplies ??= new Map();
|
||||
ctx.toolState.reviewReplies.set(comment_id, {
|
||||
commentId: result.data.id!,
|
||||
url: result.data.html_url,
|
||||
bodyWithFooter,
|
||||
});
|
||||
|
||||
return { success: true, commentId: result.data.id, url: result.data.html_url, body: result.data.body };
|
||||
ctx.toolState.reviewReplies.set(comment_id, { commentId: data.id!, url: data.html_url, bodyWithFooter });
|
||||
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
|
||||
}, "reply_to_review_comment"),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user