import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@earendil-works/pi-coding-agent"; import { resolve } from "node:path"; import { Type } from "typebox"; import { MdrAvailabilityError, createMdrClient, type MdrClient, type MdrSource, } from "./src/mdr.js"; export const renderMarkdownParameters = Type.Object({ source: StringEnum(["latest_response", "text", "path", "url"] as const, { description: "Markdown source kind", }), value: Type.Optional( Type.String({ description: "Raw Markdown, file path, or HTTP(S) URL for the selected source" }), ), outputPath: Type.Optional( Type.String({ description: "Exact HTML output path; relative paths resolve from Pi's cwd" }), ), open: Type.Optional( Type.Boolean({ description: "Open the generated HTML in the default browser", default: true }), ), }); export function latestAssistantText(entries: readonly SessionEntry[]): string | undefined { for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry?.type !== "message" || entry.message.role !== "assistant") continue; if (entry.message.content.some((block) => block.type === "toolCall")) continue; const text = entry.message.content .filter((block) => block.type === "text" && block.text.trim().length > 0) .map((block) => (block.type === "text" ? block.text : "")) .join("\n\n"); if (text) return text; } return undefined; } export function createPiMdrExtension(client: MdrClient = createMdrClient()) { return function piMdrExtension(pi: ExtensionAPI): void { pi.registerCommand("mdr", { description: "Render the latest assistant response, a Markdown file, or an HTTP(S) URL with MDR", handler: async (args, ctx) => { const argument = normalizeCommandArgument(args); let source: MdrSource; if (!argument) { const text = latestAssistantText(ctx.sessionManager.getBranch()); if (!text) { notify(ctx, "No completed assistant text is available to render.", "warning"); return; } source = { kind: "text", value: text }; } else if (isHttpUrl(argument)) { source = { kind: "url", value: argument }; } else { source = { kind: "path", value: normalizeLeadingAt(argument) }; } try { const result = await client.runMdr({ cwd: ctx.cwd, source, open: true }); notify(ctx, `Rendered Markdown: ${result.outputPath} (browser opening requested).`, "info"); } catch (error) { notify( ctx, errorMessage(error), error instanceof MdrAvailabilityError ? "warning" : "error", ); } }, }); pi.registerTool({ name: "render_markdown", label: "Render Markdown", description: "Render Markdown from the latest assistant response, raw text, a file path, or an HTTP(S) URL with MDR. Returns the absolute HTML path and opens the default browser unless open is false.", promptSnippet: "Render Markdown to HTML and open it in the default browser unless open is false", promptGuidelines: [ "Use render_markdown when the user asks to preview Markdown as rendered HTML.", "render_markdown opens the default browser unless its open field is false.", ], parameters: renderMarkdownParameters, async execute(_toolCallId, params, signal, _onUpdate, ctx) { const source = toolSource(params.source, params.value, ctx.sessionManager.getBranch()); const openRequested = params.open ?? true; const outputPath = params.outputPath ? resolve(ctx.cwd, normalizeLeadingAt(params.outputPath)) : undefined; const result = await client.runMdr({ cwd: ctx.cwd, source, outputPath, open: openRequested, signal, }); const opening = openRequested ? "requested" : "disabled"; return { content: [ { type: "text", text: `Rendered Markdown: ${result.outputPath}. Browser opening: ${opening}.`, }, ], details: { path: result.outputPath, openRequested, }, }; }, }); }; } function toolSource( source: "latest_response" | "text" | "path" | "url", value: string | undefined, entries: readonly SessionEntry[], ): MdrSource { if (source === "latest_response") { if (value !== undefined) { throw new Error("render_markdown value must be omitted for source latest_response."); } const text = latestAssistantText(entries); if (!text) throw new Error("No completed assistant text is available to render."); return { kind: "text", value: text }; } if (value === undefined || value.length === 0) { throw new Error(`render_markdown value is required for source ${source}.`); } if (source === "url") { if (!isValidHttpUrl(value)) { throw new Error("render_markdown source url requires a valid HTTP(S) URL."); } return { kind: "url", value }; } if (source === "path") return { kind: "path", value: normalizeLeadingAt(value) }; return { kind: "text", value }; } function normalizeCommandArgument(args: string): string { const trimmed = args.trim(); if (trimmed.length >= 2) { const first = trimmed[0]; if ((first === '"' || first === "'") && trimmed.at(-1) === first) { return trimmed.slice(1, -1); } } return trimmed; } function normalizeLeadingAt(value: string): string { return value.startsWith("@") ? value.slice(1) : value; } function isHttpUrl(value: string): boolean { return /^https?:\/\//iu.test(value); } function isValidHttpUrl(value: string): boolean { try { const url = new URL(value); return (url.protocol === "http:" || url.protocol === "https:") && Boolean(url.hostname); } catch { return false; } } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "MDR rendering failed."; } function notify( ctx: Pick, message: string, type: "info" | "warning" | "error", ): void { if (ctx.hasUI) { ctx.ui.notify(message, type); } else if (type === "info") { console.log(message); } else { console.error(message); } } export default createPiMdrExtension();