import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent"; import { Container, Text } from "@mariozechner/pi-tui"; type PromptMatch = { kind: "pr" | "issue" | "forum"; url: string; forumId?: string; }; type GhMetadata = { title?: string; author?: { login?: string; name?: string | null; }; }; const URL_PATTERNS: { kind: PromptMatch["kind"]; pattern: RegExp }[] = [ { kind: "pr", pattern: /https?:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+[^\s]*/gi }, { kind: "issue", pattern: /https?:\/\/github\.com\/[^\s/]+\/[^\s/]+\/issues\/\d+[^\s]*/gi }, { kind: "forum", pattern: /https?:\/\/(?:www\.)?esotericsoftware\.com\/forum\/d\/(\d+)[^\s]*/gi }, ]; function extractPromptMatch(prompt: string): PromptMatch | undefined { let last: PromptMatch | undefined; for (const { kind, pattern } of URL_PATTERNS) { pattern.lastIndex = 0; let m; while ((m = pattern.exec(prompt)) !== null) { const url = m[0].trim(); const forumId = kind === "forum" ? m[1] : undefined; last = { kind, url, forumId }; } } return last; } async function fetchForumMetadata(forumId: string): Promise<{ title?: string } | undefined> { try { const res = await fetch(`https://esotericsoftware.com/forum/api/discussions/${forumId}`); if (!res.ok) return undefined; const data = (await res.json()) as { data?: { attributes?: { title?: string } } }; return { title: data?.data?.attributes?.title }; } catch { 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 fetchMetadata = async ( api: ExtensionAPI, match: PromptMatch, ): Promise<{ title?: string; authorText?: string }> => { if (match.kind === "forum" && match.forumId) { const meta = await fetchForumMetadata(match.forumId); return { title: meta?.title?.trim() }; } const meta = await fetchGhMetadata(api, match.kind as "pr" | "issue", match.url); return { title: meta?.title?.trim(), authorText: formatAuthor(meta?.author) }; }; const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => { const label = match.kind === "pr" ? "PR" : match.kind === "issue" ? "Issue" : "Forum"; 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 fetchMetadata(pi, match).then(({ title, authorText }) => { 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 fetchMetadata(pi, match).then(({ title, authorText }) => { setWidget(ctx, match, title, authorText); applySessionName(ctx, match, title); }); }; pi.on("session_start", async (_event, ctx) => { rebuildFromSession(ctx); }); }