From b1387d837e37090f4dc534c68591b4d1c08d4f7c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 15 May 2026 13:06:35 +0300 Subject: [PATCH] feat(ui): add /copy slash command to copy last message - Register /copy (alias /cp) in the System command category - Walk the scrollback to find the last user/assistant/reasoning message, skipping transient system messages - Reuse internal/ui/clipboard.CopyToClipboard for OSC 52 + native clipboard support (works over SSH) - Document the command in /help --- internal/ui/commands/commands.go | 6 +++++ internal/ui/model.go | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/internal/ui/commands/commands.go b/internal/ui/commands/commands.go index c68ef8c3..c48534a7 100644 --- a/internal/ui/commands/commands.go +++ b/internal/ui/commands/commands.go @@ -161,6 +161,12 @@ var SlashCommands = []SlashCommand{ Category: "Navigation", Aliases: []string{"/r"}, }, + { + Name: "/copy", + Description: "Copy the last message to the system clipboard", + Category: "System", + Aliases: []string{"/cp"}, + }, { Name: "/export", Description: "Export session (JSONL by default, or /export path.jsonl)", diff --git a/internal/ui/model.go b/internal/ui/model.go index b7e34b65..a0568dde 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -3110,6 +3110,8 @@ func (m *AppModel) handleSlashCommand(sc *commands.SlashCommand, args string) te return m.handleResumeCommand() case "/export": return m.handleExportCommand(args) + case "/copy": + return m.handleCopyCommand() case "/share": return m.handleShareCommand() case "/import": @@ -3524,6 +3526,7 @@ func (m *AppModel) printHelpMessage() { "**System:**\n" + "- `/compact [instructions]`: Summarise older messages to free context space\n" + "- `/clear`: Clear message history\n" + + "- `/copy`: Copy the last message to the system clipboard\n" + "- `/export [path]`: Export session as JSONL\n" + "- `/import `: Import session from JSONL file\n" + "- `/reset-usage`: Reset usage statistics\n" + @@ -4284,6 +4287,48 @@ func (m *AppModel) handleNameCommand(args string) tea.Cmd { return nil } +// handleCopyCommand copies the last user or assistant message to the system +// clipboard. Skips transient system messages (e.g. /help output) so the user +// gets the actual last conversational message. +func (m *AppModel) handleCopyCommand() tea.Cmd { + if len(m.messages) == 0 { + m.printSystemMessage("No messages to copy.") + return nil + } + + var ( + text string + role string + ) + for i := len(m.messages) - 1; i >= 0; i-- { + switch msg := m.messages[i].(type) { + case *TextMessageItem: + if msg.role == "user" || msg.role == "assistant" { + text = msg.content + role = msg.role + } + case *StreamingMessageItem: + if msg.role == "assistant" || msg.role == "reasoning" { + text = msg.content.String() + role = msg.role + } + } + if text != "" { + break + } + } + + if strings.TrimSpace(text) == "" { + m.printSystemMessage("No copyable message found.") + return nil + } + + m.printSystemMessage(fmt.Sprintf( + "Copied last %s message to clipboard (%d chars).", role, len(text), + )) + return clipboard.CopyToClipboard(text) +} + // handleExportCommand exports the current session to a file. // Usage: /export — copies the JSONL file to cwd with a descriptive name. //