diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b11926ffa..afea5d127 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,8 +1,11 @@ # Changelog ## [Unreleased] + ### Added +- Added subcommands to `/copy` command: `code` (copy last code block), `all` (copy all code blocks), `cmd` (copy last bash/python command), and `last` (copy full message) +- Added support for copying last executed bash or python command via `/copy cmd` subcommand - Added `assignment` field to task progress and result objects to track the raw per-task assignment text separately from the full templated task - Added `details` field to todo items for storing implementation specifics, file paths, and edge cases (shown only when task is active) - Added support for multi-line details in todo items with automatic indentation in interactive and reminder displays diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index 5f9fa2bfb..7a4c80fa7 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -5,6 +5,7 @@ import { getEnvApiKey, getProviderDetails, type ProviderDetails, + type ToolCall, type UsageLimit, type UsageReport, } from "@oh-my-pi/pi-ai"; @@ -213,16 +214,87 @@ export class CommandController { } } - handleCopyCommand() { + handleCopyCommand(sub?: string) { + switch (sub) { + case "code": + return this.#copyCode(); + case "all": + return this.#copyAllCode(); + case "cmd": + return this.#copyLastCommand(); + case "last": + case undefined: + return this.#copyLastMessage(); + default: + this.ctx.showError(`Unknown subcommand: ${sub}. Use code, all, cmd, or last.`); + } + } + + #copyLastMessage() { const text = this.ctx.session.getLastAssistantText(); if (!text) { this.ctx.showError("No agent messages to copy yet."); return; } + this.#doCopy(text, "Copied last agent message to clipboard"); + } + #copyCode() { + const text = this.ctx.session.getLastAssistantText(); + if (!text) { + this.ctx.showError("No agent messages to copy yet."); + return; + } + const matches = [...text.matchAll(/^```[^\n]*\n([\s\S]*?)^```/gm)]; + const lastMatch = matches.at(-1); + if (!lastMatch) { + this.ctx.showWarning("No code block found in the last agent message."); + return; + } + this.#doCopy(lastMatch[1].replace(/\n$/, ""), "Copied last code block to clipboard"); + } + + #copyAllCode() { + const text = this.ctx.session.getLastAssistantText(); + if (!text) { + this.ctx.showError("No agent messages to copy yet."); + return; + } + const matches = [...text.matchAll(/^```[^\n]*\n([\s\S]*?)^```/gm)]; + if (matches.length === 0) { + this.ctx.showWarning("No code blocks found in the last agent message."); + return; + } + const combined = matches.map(m => m[1].replace(/\n$/, "")).join("\n\n"); + this.#doCopy(combined, `Copied ${matches.length} code block${matches.length > 1 ? "s" : ""} to clipboard`); + } + + #copyLastCommand() { + const messages = this.ctx.session.messages; + // Walk backwards to find the last bash/python tool call + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if (msg.role !== "assistant") continue; + const toolCalls = msg.content.filter((c): c is ToolCall => c.type === "toolCall"); + for (let j = toolCalls.length - 1; j >= 0; j--) { + const tc = toolCalls[j]; + if (tc.name === "bash" && typeof tc.arguments.command === "string") { + this.#doCopy(tc.arguments.command, "Copied last bash command to clipboard"); + return; + } + if (tc.name === "python" && typeof tc.arguments.code === "string") { + this.#doCopy(tc.arguments.code, "Copied last python code to clipboard"); + return; + } + } + } + this.ctx.showWarning("No bash or python command found in the conversation."); + } + + #doCopy(content: string, label: string) { try { - copyToClipboard(text); - this.ctx.showStatus("Copied last agent message to clipboard"); + copyToClipboard(content); + this.ctx.showStatus(label); } catch (error) { this.ctx.showError(error instanceof Error ? error.message : String(error)); } diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 3b19a09db..faec12381 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -1048,8 +1048,8 @@ export class InteractiveMode implements InteractiveModeContext { return this.#commandController.handleShareCommand(); } - handleCopyCommand() { - return this.#commandController.handleCopyCommand(); + handleCopyCommand(sub?: string) { + return this.#commandController.handleCopyCommand(sub); } handleSessionCommand(): Promise { diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index 9520d2cd4..b6c4fc7d0 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -161,7 +161,7 @@ export interface InteractiveModeContext { // Command handling handleExportCommand(text: string): Promise; handleShareCommand(): Promise; - handleCopyCommand(): void; + handleCopyCommand(sub?: string): void; handleSessionCommand(): Promise; handleJobsCommand(): Promise; handleUsageCommand(reports?: UsageReport[] | null): Promise; diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index 95c045a08..89615b98e 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -214,8 +214,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ { name: "copy", description: "Copy last agent message to clipboard", - handle: async (_command, runtime) => { - await runtime.ctx.handleCopyCommand(); + subcommands: [ + { name: "last", description: "Copy full last agent message" }, + { name: "code", description: "Copy last code block" }, + { name: "all", description: "Copy all code blocks from last message" }, + { name: "cmd", description: "Copy last bash/python command" }, + ], + allowArgs: true, + handle: async (command, runtime) => { + const sub = command.args.trim().toLowerCase() || undefined; + await runtime.ctx.handleCopyCommand(sub); runtime.ctx.editor.setText(""); }, },