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
This commit is contained in:
Ed Zynda
2026-05-15 13:06:35 +03:00
parent f561f4cfd9
commit b1387d837e
2 changed files with 51 additions and 0 deletions
+6
View File
@@ -161,6 +161,12 @@ var SlashCommands = []SlashCommand{
Category: "Navigation", Category: "Navigation",
Aliases: []string{"/r"}, Aliases: []string{"/r"},
}, },
{
Name: "/copy",
Description: "Copy the last message to the system clipboard",
Category: "System",
Aliases: []string{"/cp"},
},
{ {
Name: "/export", Name: "/export",
Description: "Export session (JSONL by default, or /export path.jsonl)", Description: "Export session (JSONL by default, or /export path.jsonl)",
+45
View File
@@ -3110,6 +3110,8 @@ func (m *AppModel) handleSlashCommand(sc *commands.SlashCommand, args string) te
return m.handleResumeCommand() return m.handleResumeCommand()
case "/export": case "/export":
return m.handleExportCommand(args) return m.handleExportCommand(args)
case "/copy":
return m.handleCopyCommand()
case "/share": case "/share":
return m.handleShareCommand() return m.handleShareCommand()
case "/import": case "/import":
@@ -3524,6 +3526,7 @@ func (m *AppModel) printHelpMessage() {
"**System:**\n" + "**System:**\n" +
"- `/compact [instructions]`: Summarise older messages to free context space\n" + "- `/compact [instructions]`: Summarise older messages to free context space\n" +
"- `/clear`: Clear message history\n" + "- `/clear`: Clear message history\n" +
"- `/copy`: Copy the last message to the system clipboard\n" +
"- `/export [path]`: Export session as JSONL\n" + "- `/export [path]`: Export session as JSONL\n" +
"- `/import <path.jsonl>`: Import session from JSONL file\n" + "- `/import <path.jsonl>`: Import session from JSONL file\n" +
"- `/reset-usage`: Reset usage statistics\n" + "- `/reset-usage`: Reset usage statistics\n" +
@@ -4284,6 +4287,48 @@ func (m *AppModel) handleNameCommand(args string) tea.Cmd {
return nil 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. // handleExportCommand exports the current session to a file.
// Usage: /export — copies the JSONL file to cwd with a descriptive name. // Usage: /export — copies the JSONL file to cwd with a descriptive name.
// //