diff --git a/dot_pi/agent/extensions/copy-command/index.ts b/dot_pi/agent/extensions/copy-command/index.ts index de5897d6..753a0ba0 100644 --- a/dot_pi/agent/extensions/copy-command/index.ts +++ b/dot_pi/agent/extensions/copy-command/index.ts @@ -1,18 +1,9 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { exec } from "node:child_process"; +import { + type ExtensionAPI, + copyToClipboard, +} from "@earendil-works/pi-coding-agent"; -async function copyToClipboard(text: string): Promise { - const proc = exec("pbcopy"); - proc.stdin?.write(text); - proc.stdin?.end(); - await new Promise((resolve, reject) => { - proc.on("close", (code) => - code === 0 ? resolve() : reject(new Error(`pbcopy exited ${code}`)) - ); - }); -} - -function extractCodeBlockCommands(text: string): string[] { +function extractCommands(text: string): string[] { const codeBlockRegex = /```(?:\w*)?\n([\s\S]*?)```/g; const commands: string[] = []; let match; @@ -23,41 +14,36 @@ function extractCodeBlockCommands(text: string): string[] { return commands; } +function truncate(text: string, max: number): string { + const firstLine = text.split("\n")[0]; + return firstLine.length > max + ? firstLine.slice(0, max - 3) + "..." + : firstLine; +} + export default function (pi: ExtensionAPI) { pi.registerCommand("copy-command", { - description: "Copy the command from the last assistant message to clipboard", + description: + "Copy the command from the last assistant message to clipboard", handler: async (_args, ctx) => { - const entries = ctx.sessionManager.getBranch(); - const commands: string[] = []; - - // Walk backwards to find the last assistant message - let foundAssistant = false; - for (let i = entries.length - 1; i >= 0; i--) { - const entry = entries[i]; - if (entry.type !== "message") continue; - - const msg = entry.message; - - if (msg.role === "assistant") { - if (foundAssistant) break; - foundAssistant = true; - - if (Array.isArray(msg.content)) { - for (const block of msg.content) { - // Extract from Bash tool calls - if ( - block.type === "tool_use" && - block.name === "Bash" && - block.input?.command - ) { - commands.push(block.input.command); - } + const lastAssistant = ctx.sessionManager + .getBranch() + .findLast( + (e) => e.type === "message" && e.message.role === "assistant", + ); - // Extract from markdown code blocks in text - if (block.type === "text" && block.text) { - commands.push(...extractCodeBlockCommands(block.text)); - } - } + const content = lastAssistant?.message.content; + const commands: string[] = []; + if (Array.isArray(content)) { + for (const block of content) { + if ( + block.type === "tool_use" && + block.name === "Bash" && + block.input?.command + ) { + commands.push(block.input.command); + } else if (block.type === "text" && block.text) { + commands.push(...extractCommands(block.text)); } } } @@ -65,41 +51,27 @@ export default function (pi: ExtensionAPI) { if (commands.length === 0) { ctx.ui.notify( "No commands found in the last assistant message", - "warning" + "warning", ); return; } - let selected: string; - - if (commands.length === 1) { - selected = commands[0]; - } else { - const displayCommands = commands.map((cmd, i) => { - const firstLine = cmd.split("\n")[0]; - const truncated = - firstLine.length > 80 ? firstLine.slice(0, 77) + "..." : firstLine; - return `${i + 1}. ${truncated}`; - }); - - const choice = await ctx.ui.select( - "Which command to copy?", - displayCommands + let selected = commands[0]; + if (commands.length > 1) { + const choices = commands.map( + (cmd, i) => `${i + 1}. ${truncate(cmd, 80)}`, ); + const choice = await ctx.ui.select("Which command to copy?", choices); if (choice === undefined) { ctx.ui.notify("Cancelled", "info"); return; } - - const idx = displayCommands.indexOf(choice); - selected = commands[idx]; + selected = commands[choices.indexOf(choice)]; } try { await copyToClipboard(selected); - const preview = - selected.length > 60 ? selected.slice(0, 57) + "..." : selected; - ctx.ui.notify(`Copied: ${preview}`, "info"); + ctx.ui.notify(`Copied: ${truncate(selected, 60)}`, "info"); } catch (e) { ctx.ui.notify(`Failed to copy: ${e}`, "error"); }