import { type } from "arktype";
import { buildShockbotFooter, stripExistingFooter } from "../utils/buildShockbotFooter.ts";
import { log } from "../utils/cli.ts";
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
import {
createLeapingProgressComment,
deleteProgressCommentApi,
updateProgressComment,
} from "../utils/progressComment.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export {
isLeapingIntoActionCommentBody,
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 (/
[ \t]*\n(?!\s*\n)/i.test(body)) {
throw new Error("body contains
followed by a non-blank line — add a blank line after
tags.");
}
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").optional(),
});
export function CreateCommentTool(ctx: ToolContext) {
return tool({
name: "create_issue_comment",
description:
"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 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 ${data.id}`);
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
}),
});
}
export const EditComment = type({
commentId: type.number.describe("the ID of the comment to edit"),
body: type.string.describe("the new comment body content"),
});
export function EditCommentTool(ctx: ToolContext) {
return tool({
name: "edit_issue_comment",
description: "Edit a Gitea issue comment by its ID",
parameters: EditComment,
execute: execute(async ({ commentId, body }) => {
const bodyWithFooter = addFooter(ctx, body);
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"),
});
export async function reportProgress(
ctx: ToolContext,
params: { body: string; target_plan_comment?: boolean }
): 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" };
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
const apiCtx = { gitea: ctx.gitea, owner: ctx.repo.owner, repo: ctx.repo.name };
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== undefined) {
const commentId = ctx.toolState.existingPlanCommentId;
const bodyWithFooter = `${stripExistingFooter(body)}${buildCommentFooter(ctx)}`;
const result = await updateProgressComment(apiCtx, { id: commentId, type: "issue" }, bodyWithFooter);
ctx.toolState.wasUpdated = true;
return { commentId: result.id, url: result.html_url, body: result.body || "", action: "updated" };
}
const existingComment = ctx.toolState.progressComment;
if (existingComment) {
const bodyWithFooter = `${stripExistingFooter(body)}${buildCommentFooter(ctx)}`;
const result = await updateProgressComment(apiCtx, existingComment, bodyWithFooter);
ctx.toolState.wasUpdated = true;
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" };
const initialBody = addFooter(ctx, body);
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" };
}
export function ReportProgressTool(ctx: ToolContext) {
return tool({
name: "report_progress",
description:
"Share progress on the associated Gitea issue/PR. First call creates a comment; subsequent calls update it. " +
"Call once at the end of every run with a brief final summary (1-3 sentences).",
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.commentId !== undefined) log.info(`» ${result.action} comment ${result.commentId}`);
if (!params.target_plan_comment) ctx.toolState.finalSummaryWritten = true;
return { success: true, ...result };
}),
});
}
export async function deleteProgressComment(ctx: ToolContext): Promise {
const existing = ctx.toolState.progressComment;
if (!existing) return false;
try {
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;
}
export const ReplyToReviewComment = type({
pull_number: type.number.describe("the pull request number"),
comment_id: type.number.describe("the ID of the review comment to reply to"),
body: type.string.describe("extremely brief reply (1 sentence max)"),
});
export interface DuplicateReplyDecision {
kind: "already-replied"; commentId: number; url: string | undefined; reason: string;
}
export function duplicateReplyDecision(params: {
existing: { commentId: number; url: string | undefined; bodyWithFooter: string } | undefined;
bodyWithFooter: string;
}): DuplicateReplyDecision | null {
const existing = params.existing;
if (!existing) return null;
if (existing.bodyWithFooter !== params.bodyWithFooter) return null;
return {
kind: "already-replied",
commentId: existing.commentId,
url: existing.url,
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. 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 });
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 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: data.id!, url: data.html_url, bodyWithFooter });
return { success: true, commentId: data.id, url: data.html_url, body: data.body };
}, "reply_to_review_comment"),
});
}