From ad1f51d704b16990ceb95b8d17bd0bc0def87cad Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 30 Dec 2025 20:14:49 -0800 Subject: [PATCH] Implement Plan button --- entry | 58 +++++++++++++++++++++++++++++---- external.ts | 8 +++++ main.ts | 1 + mcp/comment.ts | 83 ++++++++++++++++++++++++++++++++++++++++++----- mcp/selectMode.ts | 3 ++ 5 files changed, 138 insertions(+), 15 deletions(-) diff --git a/entry b/entry index 9fab05e..1a0bd0d 100755 --- a/entry +++ b/entry @@ -98415,7 +98415,11 @@ var addTools = (ctx, server, tools) => { // mcp/comment.ts var LEAPING_INTO_ACTION_PREFIX = "Leaping into action"; -async function buildCommentFooter(payload, octokit) { +async function buildCommentFooter({ + payload, + octokit, + customParts +}) { const repoContext = parseRepoContext(); const runId = process.env.GITHUB_RUN_ID; const agentName = payload.agent; @@ -98432,7 +98436,7 @@ async function buildCommentFooter(payload, octokit) { } catch { } } - return buildPullfrogFooter({ + const footerParams = { triggeredBy: true, agent: { displayName: agentInfo?.displayName || "Unknown agent", @@ -98444,11 +98448,19 @@ async function buildCommentFooter(payload, octokit) { runId, ...workflowRunHtmlUrl ? { htmlUrl: workflowRunHtmlUrl } : {} } : void 0 - }); + }; + if (customParts && customParts.length > 0) { + return buildPullfrogFooter({ ...footerParams, customParts }); + } + return buildPullfrogFooter(footerParams); +} +function buildImplementPlanLink(owner, repo, issueNumber, commentId) { + const apiUrl = process.env.API_URL || "https://pullfrog.com"; + return `[Implement plan \u2794](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`; } async function addFooter(body, payload, octokit) { const bodyWithoutFooter = stripExistingFooter(body); - const footer = await buildCommentFooter(payload, octokit); + const footer = await buildCommentFooter({ payload, octokit }); return `${bodyWithoutFooter}${footer}`; } var Comment = type({ @@ -98532,9 +98544,18 @@ var ReportProgress = type({ body: type.string.describe("the progress update content to share") }); async function reportProgress(ctx, { body }) { - const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit); const existingCommentId = getProgressCommentId(); + const issueNumber = ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number; + const isPlanMode = ctx.toolState.selectedMode === "Plan"; if (existingCommentId) { + const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, existingCommentId)] : void 0; + const bodyWithoutFooter = stripExistingFooter(body); + const footer = await buildCommentFooter({ + payload: ctx.payload, + octokit: ctx.octokit, + customParts + }); + const bodyWithFooter = `${bodyWithoutFooter}${footer}`; const result2 = await ctx.octokit.rest.issues.updateComment({ owner: ctx.owner, repo: ctx.name, @@ -98549,18 +98570,40 @@ async function reportProgress(ctx, { body }) { action: "updated" }; } - const issueNumber = ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number; if (issueNumber === void 0) { return void 0; } + const initialBody = await addFooter(body, ctx.payload, ctx.octokit); const result = await ctx.octokit.rest.issues.createComment({ owner: ctx.owner, repo: ctx.name, issue_number: issueNumber, - body: bodyWithFooter + body: initialBody }); setProgressCommentId(result.data.id); progressCommentWasUpdated = true; + if (isPlanMode) { + const customParts = [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, result.data.id)]; + const bodyWithoutFooter = stripExistingFooter(body); + const footer = await buildCommentFooter({ + payload: ctx.payload, + octokit: ctx.octokit, + customParts + }); + const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`; + const updateResult = await ctx.octokit.rest.issues.updateComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: result.data.id, + body: bodyWithPlanLink + }); + return { + commentId: updateResult.data.id, + url: updateResult.data.html_url, + body: updateResult.data.body || "", + action: "created" + }; + } return { commentId: result.data.id, url: result.data.html_url, @@ -124718,6 +124761,7 @@ function SelectModeTool(ctx) { availableModes: ctx.modes.map((m) => ({ name: m.name, description: m.description })) }; } + ctx.toolState.selectedMode = selectedMode.name; return { modeName: selectedMode.name, description: selectedMode.description, diff --git a/external.ts b/external.ts index da37ce2..1ef4f33 100644 --- a/external.ts +++ b/external.ts @@ -212,6 +212,13 @@ interface FixReviewEvent extends BasePayloadEvent { unapproved_comments: ReviewCommentData[]; } +interface ImplementPlanEvent extends BasePayloadEvent { + trigger: "implement_plan"; + issue_number: number; + plan_comment_id: number; + plan_content: string; +} + interface UnknownEvent extends BasePayloadEvent { trigger: "unknown"; } @@ -231,6 +238,7 @@ export type PayloadEvent = | CheckSuiteCompletedEvent | WorkflowDispatchEvent | FixReviewEvent + | ImplementPlanEvent | UnknownEvent; export interface DispatchOptions { diff --git a/main.ts b/main.ts index 64f350c..0178b32 100644 --- a/main.ts +++ b/main.ts @@ -337,6 +337,7 @@ export interface DependencyInstallationState { export interface ToolState { prNumber?: number; issueNumber?: number; + selectedMode?: string; review?: { id: number; // REST API database ID (for fix URLs) nodeId: string; // GraphQL node ID (for mutations) diff --git a/mcp/comment.ts b/mcp/comment.ts index 6999aff..8ec2679 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -15,7 +15,17 @@ import { execute, tool } from "./shared.ts"; */ export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action"; -async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise { +interface BuildCommentFooterParams { + payload: Payload; + octokit?: Octokit | undefined; + customParts?: string[] | undefined; +} + +async function buildCommentFooter({ + payload, + octokit, + customParts, +}: BuildCommentFooterParams): Promise { const repoContext = parseRepoContext(); const runId = process.env.GITHUB_RUN_ID; @@ -38,7 +48,7 @@ async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise< } } - return buildPullfrogFooter({ + const footerParams = { triggeredBy: true, agent: { displayName: agentInfo?.displayName || "Unknown agent", @@ -52,12 +62,27 @@ async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise< ...(workflowRunHtmlUrl ? { htmlUrl: workflowRunHtmlUrl } : {}), } : undefined, - }); + }; + + if (customParts && customParts.length > 0) { + return buildPullfrogFooter({ ...footerParams, customParts }); + } + return buildPullfrogFooter(footerParams); +} + +function buildImplementPlanLink( + owner: string, + repo: string, + issueNumber: number, + commentId: number +): string { + const apiUrl = process.env.API_URL || "https://pullfrog.com"; + return `[Implement plan ➔](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`; } async function addFooter(body: string, payload: Payload, octokit?: Octokit): Promise { const bodyWithoutFooter = stripExistingFooter(body); - const footer = await buildCommentFooter(payload, octokit); + const footer = await buildCommentFooter({ payload, octokit }); return `${bodyWithoutFooter}${footer}`; } @@ -181,11 +206,26 @@ export async function reportProgress( } | undefined > { - const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit); const existingCommentId = getProgressCommentId(); + const issueNumber = + ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number; + const isPlanMode = ctx.toolState.selectedMode === "Plan"; // if we already have a progress comment, update it if (existingCommentId) { + const customParts = + isPlanMode && issueNumber !== undefined + ? [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, existingCommentId)] + : undefined; + + const bodyWithoutFooter = stripExistingFooter(body); + const footer = await buildCommentFooter({ + payload: ctx.payload, + octokit: ctx.octokit, + customParts, + }); + const bodyWithFooter = `${bodyWithoutFooter}${footer}`; + const result = await ctx.octokit.rest.issues.updateComment({ owner: ctx.owner, repo: ctx.name, @@ -205,24 +245,51 @@ export async function reportProgress( // no existing comment - create one // use fallback chain: dynamically set context > event payload - const issueNumber = - ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number; if (issueNumber === undefined) { // cannot create comment without issue_number (e.g., workflow_dispatch events) return undefined; } + // for new comments, we need to create first, then update with Plan link if in Plan mode + const initialBody = await addFooter(body, ctx.payload, ctx.octokit); + const result = await ctx.octokit.rest.issues.createComment({ owner: ctx.owner, repo: ctx.name, issue_number: issueNumber, - body: bodyWithFooter, + body: initialBody, }); // store the comment ID for future updates setProgressCommentId(result.data.id); progressCommentWasUpdated = true; + // if Plan mode, update the comment to add the "Implement plan" link + if (isPlanMode) { + const customParts = [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, result.data.id)]; + const bodyWithoutFooter = stripExistingFooter(body); + const footer = await buildCommentFooter({ + payload: ctx.payload, + octokit: ctx.octokit, + customParts, + }); + const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`; + + const updateResult = await ctx.octokit.rest.issues.updateComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: result.data.id, + body: bodyWithPlanLink, + }); + + return { + commentId: updateResult.data.id, + url: updateResult.data.html_url, + body: updateResult.data.body || "", + action: "created", + }; + } + return { commentId: result.data.id, url: result.data.html_url, diff --git a/mcp/selectMode.ts b/mcp/selectMode.ts index 369afe5..61a51c1 100644 --- a/mcp/selectMode.ts +++ b/mcp/selectMode.ts @@ -25,6 +25,9 @@ export function SelectModeTool(ctx: ToolContext) { }; } + // store selected mode in toolState for use by other tools (e.g., report_progress) + ctx.toolState.selectedMode = selectedMode.name; + return { modeName: selectedMode.name, description: selectedMode.description,