import { DynamicBorder, type ExtensionAPI, type ExtensionContext, } from "@mariozechner/pi-coding-agent"; import { Container, Text } from "@mariozechner/pi-tui"; const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im; const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im; type PromptMatch = { kind: "pr" | "issue"; url: string; }; type GhMetadata = { title?: string; author?: { login?: string; name?: string | null; }; }; function extractPromptMatch(prompt: string): PromptMatch | undefined { const prMatch = prompt.match(PR_PROMPT_PATTERN); if (prMatch?.[1]) { return { kind: "pr", url: prMatch[1].trim() }; } const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN); if (issueMatch?.[1]) { return { kind: "issue", url: issueMatch[1].trim() }; } return undefined; } async function fetchGhMetadata( pi: ExtensionAPI, kind: PromptMatch["kind"], url: string, ): Promise { const args = kind === "pr" ? ["pr", "view", url, "--json", "title,author"] : ["issue", "view", url, "--json", "title,author"]; try { const result = await pi.exec("gh", args); if (result.code !== 0 || !result.stdout) return undefined; return JSON.parse(result.stdout) as GhMetadata; } catch { return undefined; } } function formatAuthor(author?: GhMetadata["author"]): string | undefined { if (!author) return undefined; const name = author.name?.trim(); const login = author.login?.trim(); if (name && login) return `${name} (@${login})`; if (login) return `@${login}`; if (name) return name; return undefined; } export default function promptUrlWidgetExtension(pi: ExtensionAPI) { const setWidget = ( ctx: ExtensionContext, match: PromptMatch, title?: string, authorText?: string, ) => { ctx.ui.setWidget("prompt-url", (_tui, thm) => { const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url); const authorLine = authorText ? thm.fg("muted", authorText) : undefined; const urlLine = thm.fg("dim", match.url); const lines = [titleText]; if (authorLine) lines.push(authorLine); lines.push(urlLine); const container = new Container(); container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s))); container.addChild(new Text(lines.join("\n"), 1, 0)); return container; }); }; const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => { const label = match.kind === "pr" ? "PR" : "Issue"; const trimmedTitle = title?.trim(); const fallbackName = `${label}: ${match.url}`; const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName; const currentName = pi.getSessionName()?.trim(); if (!currentName) { pi.setSessionName(desiredName); return; } if (currentName === match.url || currentName === fallbackName) { pi.setSessionName(desiredName); } }; pi.on("before_agent_start", async (event, ctx) => { if (!ctx.hasUI) return; const match = extractPromptMatch(event.prompt); if (!match) { return; } setWidget(ctx, match); applySessionName(ctx, match); void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { const title = meta?.title?.trim(); const authorText = formatAuthor(meta?.author); setWidget(ctx, match, title, authorText); applySessionName(ctx, match, title); }); }); pi.on("session_switch", async (_event, ctx) => { rebuildFromSession(ctx); }); const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => { if (!content) return ""; if (typeof content === "string") return content; return ( content .filter((block): block is { type: "text"; text: string } => block.type === "text") .map((block) => block.text) .join("\n") ?? "" ); }; const rebuildFromSession = (ctx: ExtensionContext) => { if (!ctx.hasUI) return; const entries = ctx.sessionManager.getEntries(); const lastMatch = [...entries].reverse().find((entry) => { if (entry.type !== "message" || entry.message.role !== "user") return false; const text = getUserText(entry.message.content); return !!extractPromptMatch(text); }); const content = lastMatch?.type === "message" && lastMatch.message.role === "user" ? lastMatch.message.content : undefined; const text = getUserText(content); const match = text ? extractPromptMatch(text) : undefined; if (!match) { ctx.ui.setWidget("prompt-url", undefined); return; } setWidget(ctx, match); applySessionName(ctx, match); void fetchGhMetadata(pi, match.kind, match.url).then((meta) => { const title = meta?.title?.trim(); const authorText = formatAuthor(meta?.author); setWidget(ctx, match, title, authorText); applySessionName(ctx, match, title); }); }; pi.on("session_start", async (_event, ctx) => { rebuildFromSession(ctx); }); }