From b95cb7503ed9ca9e32e055d33909094bd2737560 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 9 Jan 2026 21:40:29 +0100 Subject: [PATCH] Add PR/issue widget metadata and spacing --- .pi/extensions/prompt-url-widget.ts | 95 +++++++++++++++++++ .../src/modes/interactive/interactive-mode.ts | 2 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 .pi/extensions/prompt-url-widget.ts diff --git a/.pi/extensions/prompt-url-widget.ts b/.pi/extensions/prompt-url-widget.ts new file mode 100644 index 00000000..446359a9 --- /dev/null +++ b/.pi/extensions/prompt-url-widget.ts @@ -0,0 +1,95 @@ +import { DynamicBorder, type ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Container, Text } from "@mariozechner/pi-tui"; + +const PR_PROMPT_PATTERN = /^You are given one or more GitHub PR URLs:\s*(\S+)/im; +const ISSUE_PROMPT_PATTERN = /^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, + signal?: AbortSignal, +): 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, { signal }); + 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) { + pi.on("before_agent_start", async (event, ctx) => { + if (!ctx.hasUI) return; + const match = extractPromptMatch(event.prompt); + if (!match) { + ctx.ui.setWidget("prompt-url", undefined); + return; + } + + const meta = await fetchGhMetadata(pi, match.kind, match.url, event.signal); + const title = meta?.title?.trim(); + const authorText = formatAuthor(meta?.author); + + 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; + }); + }); + + pi.on("session_start", async (_event, ctx) => { + if (!ctx.hasUI) return; + ctx.ui.setWidget("prompt-url", undefined); + }); +} diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index f8cc3fae..1ae569c6 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -398,7 +398,6 @@ export class InteractiveMode { this.ui.addChild(this.pendingMessagesContainer); this.ui.addChild(this.statusContainer); this.ui.addChild(this.widgetContainer); - this.ui.addChild(new Spacer(1)); this.ui.addChild(this.editorContainer); this.ui.addChild(this.footer); this.ui.setFocus(this.editor); @@ -847,6 +846,7 @@ export class InteractiveMode { return; } + this.widgetContainer.addChild(new Spacer(1)); for (const [_key, component] of this.extensionWidgets) { this.widgetContainer.addChild(component); }