/** * Copy Last Response * * Shortcuts: * Alt+C — Copy the last assistant response to clipboard * * Commands: * /copy-response — Copy last response to clipboard * * Text is reflowed: hard line breaks within paragraphs are joined, * while paragraph breaks, code blocks, lists, and headers are preserved. */ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { spawnSync } from "node:child_process"; type ContentBlock = { type?: string; text?: string; }; type SessionEntry = { type: string; message?: { role?: string; content?: unknown; }; }; function getLastAssistantText(ctx: ExtensionContext): string | null { const entries = ctx.sessionManager.getBranch() as SessionEntry[]; for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; if (entry.type !== "message" || entry.message?.role !== "assistant") continue; const content = entry.message.content; if (typeof content === "string") return content; if (!Array.isArray(content)) continue; const texts: string[] = []; for (const block of content) { if (block && typeof block === "object" && (block as ContentBlock).type === "text" && typeof (block as ContentBlock).text === "string") { texts.push((block as ContentBlock).text!); } } if (texts.length > 0) return texts.join("\n"); } return null; } /** * Reflow text: join hard-wrapped lines within paragraphs, but preserve * paragraph breaks, code blocks, list items, headers, and other structure. */ function reflowText(raw: string): string { const lines = raw.split("\n"); const result: string[] = []; let inCodeBlock = false; let paragraph: string[] = []; function flushParagraph() { if (paragraph.length > 0) { result.push(paragraph.join(" ")); paragraph = []; } } for (const line of lines) { if (line.trimStart().startsWith("```")) { flushParagraph(); inCodeBlock = !inCodeBlock; result.push(line); continue; } if (inCodeBlock) { result.push(line); continue; } const trimmed = line.trimStart(); if (trimmed === "") { flushParagraph(); result.push(""); continue; } if ( /^#{1,6}\s/.test(trimmed) || /^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed) || trimmed.startsWith(">") || /^[-*_]{3,}\s*$/.test(trimmed) || trimmed.startsWith("|") ) { flushParagraph(); result.push(line); continue; } paragraph.push(trimmed); } flushParagraph(); return result.join("\n"); } function copyToClipboard(text: string): boolean { const cmds = [ ["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]], ] as const; for (const [cmd, args] of cmds) { try { spawnSync(cmd, [...args], { input: text, stdio: ["pipe", "ignore", "ignore"] }); return true; } catch { continue; } } return false; } export default function (pi: ExtensionAPI) { // --- Shortcut: Alt+C → copy --- pi.registerShortcut("alt+c", { description: "Copy last assistant response to clipboard", handler: async (ctx) => { const text = getLastAssistantText(ctx); if (!text) { ctx.ui.notify("No assistant response to copy", "warning"); return; } const reflowed = reflowText(text); if (copyToClipboard(reflowed)) { ctx.ui.notify("Response copied to clipboard", "info"); } else { ctx.ui.notify("Failed to copy — no clipboard tool found", "error"); } }, }); // --- Command: /copy-response --- pi.registerCommand("copy-response", { description: "Copy last assistant response to clipboard", handler: async (_args, ctx) => { const text = getLastAssistantText(ctx); if (!text) { ctx.ui.notify("No assistant response to copy", "warning"); return; } const reflowed = reflowText(text); if (copyToClipboard(reflowed)) { ctx.ui.notify("Response copied to clipboard", "info"); } else { ctx.ui.notify("Failed to copy — no clipboard tool found", "error"); } }, }); }