Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cmd_copy command to copy last assistant reply to clipboard #1790

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,8 +1042,29 @@ def cmd_voice(self, args):

return text

def cmd_copy(self, args):
"Copy the last reply from the LLM to the clipboard"
if not self.coder.cur_messages:
self.io.tool_error("No messages to copy.")
return

last_assistant_message = next(
(msg for msg in reversed(self.coder.cur_messages) if msg["role"] == "assistant"), None
)
if not last_assistant_message:
self.io.tool_error("No assistant reply to copy.")
return

content = last_assistant_message["content"]
try:
pyperclip.copy(content)
self.io.tool_output("Last assistant reply copied to clipboard.")
except pyperclip.PyperclipException as e:
self.io.tool_error(f"Failed to copy to clipboard: {e}")

def cmd_paste(self, args):
"Paste image/text from the clipboard into the chat (optionally provide a name for the image)"
"Paste image/text from the clipboard into the chat"
"(optionally provide a name for the image)"
try:
# Check for image first
image = ImageGrab.grabclipboard()
Expand Down
3 changes: 2 additions & 1 deletion aider/website/docs/usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cog.out(get_help_md())
| **/clear** | Clear the chat history |
| **/code** | Ask for changes to your code |
| **/commit** | Commit edits to the repo made outside the chat (commit message optional) |
| **/copy** | Copy the last reply from the LLM to the clipboard |
| **/diff** | Display the diff of changes since the last message |
| **/drop** | Remove files from the chat session to free up context space |
| **/exit** | Exit the application |
Expand All @@ -32,7 +33,7 @@ cog.out(get_help_md())
| **/map-refresh** | Force a refresh of the repository map |
| **/model** | Switch to a new LLM |
| **/models** | Search the list of available models |
| **/paste** | Paste image/text from the clipboard into the chat (optionally provide a name for the image) |
| **/paste** | Paste image/text from the clipboard into the chat |
| **/quit** | Exit the application |
| **/read-only** | Add files to the chat that are for reference, not to be edited |
| **/report** | Report a problem by opening a GitHub Issue |
Expand Down
64 changes: 64 additions & 0 deletions tests/basic/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from unittest import TestCase, mock

import git
import pyperclip

from aider.coders import Coder
from aider.commands import Commands, SwitchCoder
Expand Down Expand Up @@ -1098,3 +1099,66 @@ def test_cmd_reset(self):

del coder
del commands

def test_cmd_copy_successful(self):
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)

coder.cur_messages = [
{"role": "user", "content": "Test message"},
{"role": "assistant", "content": "Test reply"},
]

with mock.patch("pyperclip.copy") as mock_copy:
with mock.patch.object(io, "tool_output") as mock_output:
commands.cmd_copy("")

mock_copy.assert_called_once_with("Test reply")
mock_output.assert_called_once_with("Last assistant reply copied to clipboard.")

def test_cmd_copy_no_messages(self):
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)

coder.cur_messages = []

with mock.patch("pyperclip.copy") as mock_copy:
with mock.patch.object(io, "tool_error") as mock_error:
commands.cmd_copy("")

mock_copy.assert_not_called()
mock_error.assert_called_once_with("No messages to copy.")

def test_cmd_copy_no_assistant_messages(self):
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)

coder.cur_messages = [{"role": "user", "content": "Test message"}]

with mock.patch("pyperclip.copy") as mock_copy:
with mock.patch.object(io, "tool_error") as mock_error:
commands.cmd_copy("")

mock_copy.assert_not_called()
mock_error.assert_called_once_with("No assistant reply to copy.")

def test_cmd_copy_pyperclip_exception(self):
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)

coder.cur_messages = [
{"role": "user", "content": "Test message"},
{"role": "assistant", "content": "Test reply"},
]

with mock.patch(
"pyperclip.copy", side_effect=pyperclip.PyperclipException("Test exception")
):
with mock.patch.object(io, "tool_error") as mock_error:
commands.cmd_copy("")

mock_error.assert_called_once_with("Failed to copy to clipboard: Test exception")