Merge branch 'copypaste'

This commit is contained in:
Paul Gauthier 2024-12-06 07:01:40 -08:00
commit 0fdf3fc851
6 changed files with 144 additions and 7 deletions

View file

@ -535,6 +535,12 @@ def get_parser(default_config_files, git_root):
default=False,
help="Enable/disable watching files for ai coding comments (default: False)",
)
group.add_argument(
"--copy-paste",
action=argparse.BooleanOptionalAction,
default=False,
help="Enable automatic copy/paste of chat between aider and web UI (default: False)",
)
group = parser.add_argument_group("Fixing and committing")
group.add_argument(
"--lint",

View file

@ -286,6 +286,7 @@ class Coder:
detect_urls=True,
ignore_mentions=None,
file_watcher=None,
auto_copy_context=False,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
self.analytics = analytics if analytics is not None else Analytics()
@ -297,6 +298,8 @@ class Coder:
self.rejected_urls = set()
self.abs_root_path_cache = {}
self.auto_copy_context = auto_copy_context
self.ignore_mentions = ignore_mentions
if not self.ignore_mentions:
self.ignore_mentions = set()
@ -792,9 +795,9 @@ class Coder:
self.io.user_input(with_message)
self.run_one(with_message, preproc)
return self.partial_response_content
while True:
try:
self.copy_context()
user_message = self.get_input()
self.run_one(user_message, preproc)
self.show_undo_hint()
@ -803,6 +806,10 @@ class Coder:
except EOFError:
return
def copy_context(self):
if self.auto_copy_context:
self.commands.cmd_copy_context()
def get_input(self):
inchat_files = self.get_inchat_relative_files()
read_only_files = [self.get_rel_fname(fname) for fname in self.abs_read_only_fnames]
@ -1114,7 +1121,10 @@ class Coder:
# add the reminder anyway
total_tokens = 0
final = chunks.cur[-1]
if chunks.cur:
final = chunks.cur[-1]
else:
final = None
max_input_tokens = self.main_model.info.get("max_input_tokens") or 0
# Add the reminder prompt if we still have room to include it.
@ -1125,7 +1135,7 @@ class Coder:
):
if self.main_model.reminder == "sys":
chunks.reminder = reminder_message
elif self.main_model.reminder == "user" and final["role"] == "user":
elif self.main_model.reminder == "user" and final and final["role"] == "user":
# stuff it into the user message
new_content = (
final["content"]

View file

@ -1399,6 +1399,41 @@ class Commands:
if user_input.strip():
self.io.set_placeholder(user_input.rstrip())
def cmd_copy_context(self, args=None):
"""Copy the current chat context as markdown, suitable to paste into a web UI"""
chunks = self.coder.format_chat_chunks()
markdown = ""
# Only include specified chunks in order
for messages in [chunks.repo, chunks.readonly_files, chunks.chat_files]:
for msg in messages:
# Only include user messages
if msg["role"] != "user":
continue
content = msg["content"]
# Handle image/multipart content
if isinstance(content, list):
for part in content:
if part.get("type") == "text":
markdown += part["text"] + "\n\n"
else:
markdown += content + "\n\n"
markdown += """
Just tell me how to edit the files to make the changes.
Don't give me back entire files.
Just show me the edits I need to make.
"""
pyperclip.copy(markdown)
self.io.tool_output("Copied context to clipboard.")
def expand_subdir(file_path):
if file_path.is_file():

69
aider/copypaste.py Normal file
View file

@ -0,0 +1,69 @@
import threading
import time
import pyperclip
class ClipboardWatcher:
"""Watches clipboard for changes and updates IO placeholder"""
def __init__(self, io, verbose=False):
self.io = io
self.verbose = verbose
self.stop_event = None
self.watcher_thread = None
self.last_clipboard = None
self.io.clipboard_watcher = self
def start(self):
"""Start watching clipboard for changes"""
self.stop_event = threading.Event()
self.last_clipboard = pyperclip.paste()
def watch_clipboard():
while not self.stop_event.is_set():
try:
current = pyperclip.paste()
if current != self.last_clipboard:
self.last_clipboard = current
self.io.interrupt_input()
self.io.placeholder = current
time.sleep(0.5)
except Exception as e:
if self.verbose:
from aider.dump import dump
dump(f"Clipboard watcher error: {e}")
continue
self.watcher_thread = threading.Thread(target=watch_clipboard, daemon=True)
self.watcher_thread.start()
def stop(self):
"""Stop watching clipboard for changes"""
if self.stop_event:
self.stop_event.set()
if self.watcher_thread:
self.watcher_thread.join()
self.watcher_thread = None
self.stop_event = None
def main():
"""Example usage of the clipboard watcher"""
from aider.io import InputOutput
io = InputOutput()
watcher = ClipboardWatcher(io, verbose=True)
try:
watcher.start()
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nStopped watching clipboard")
watcher.stop()
if __name__ == "__main__":
main()

View file

@ -176,6 +176,7 @@ class AutoCompleter(Completer):
class InputOutput:
num_error_outputs = 0
num_user_asks = 0
clipboard_watcher = None
def __init__(
self,
@ -470,8 +471,11 @@ class InputOutput:
self.placeholder = None
self.interrupted = False
if not multiline_input and self.file_watcher:
self.file_watcher.start()
if not multiline_input:
if self.file_watcher:
self.file_watcher.start()
if self.clipboard_watcher:
self.clipboard_watcher.start()
line = self.prompt_session.prompt(
show,
@ -487,8 +491,10 @@ class InputOutput:
# Check if we were interrupted by a file change
if self.interrupted:
cmd = self.file_watcher.process_changes()
return cmd
line = line or ""
if self.file_watcher:
cmd = self.file_watcher.process_changes()
return cmd
except EOFError:
return ""
@ -504,6 +510,8 @@ class InputOutput:
finally:
if self.file_watcher:
self.file_watcher.stop()
if self.clipboard_watcher:
self.clipboard_watcher.stop()
if line.strip("\r\n") and not multiline_input:
stripped = line.strip("\r\n")

View file

@ -20,6 +20,7 @@ from aider.args import get_parser
from aider.coders import Coder
from aider.coders.base_coder import UnknownEditFormat
from aider.commands import Commands, SwitchCoder
from aider.copypaste import ClipboardWatcher
from aider.format_settings import format_settings, scrub_sensitive_info
from aider.history import ChatSummary
from aider.io import InputOutput
@ -687,6 +688,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
editor_edit_format=args.editor_edit_format,
)
if args.copy_paste and args.edit_format is None:
if main_model.edit_format in ("diff", "whole"):
main_model.edit_format = "editor-" + main_model.edit_format
if args.verbose:
io.tool_output("Model metadata:")
io.tool_output(json.dumps(main_model.info, indent=4))
@ -800,6 +805,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
suggest_shell_commands=args.suggest_shell_commands,
chat_language=args.chat_language,
detect_urls=args.detect_urls,
auto_copy_context=args.copy_paste,
)
except UnknownEditFormat as err:
io.tool_error(str(err))
@ -825,6 +831,9 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
file_watcher = FileWatcher(coder, gitignores=ignores, verbose=args.verbose)
coder.file_watcher = file_watcher
if args.copy_paste:
ClipboardWatcher(coder.io, verbose=args.verbose)
coder.show_announcements()
if args.show_prompts: