mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-01 10:14:59 +00:00
Merge branch 'copypaste'
This commit is contained in:
commit
0fdf3fc851
6 changed files with 144 additions and 7 deletions
|
@ -535,6 +535,12 @@ def get_parser(default_config_files, git_root):
|
||||||
default=False,
|
default=False,
|
||||||
help="Enable/disable watching files for ai coding comments (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 = parser.add_argument_group("Fixing and committing")
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--lint",
|
"--lint",
|
||||||
|
|
|
@ -286,6 +286,7 @@ class Coder:
|
||||||
detect_urls=True,
|
detect_urls=True,
|
||||||
ignore_mentions=None,
|
ignore_mentions=None,
|
||||||
file_watcher=None,
|
file_watcher=None,
|
||||||
|
auto_copy_context=False,
|
||||||
):
|
):
|
||||||
# Fill in a dummy Analytics if needed, but it is never .enable()'d
|
# Fill in a dummy Analytics if needed, but it is never .enable()'d
|
||||||
self.analytics = analytics if analytics is not None else Analytics()
|
self.analytics = analytics if analytics is not None else Analytics()
|
||||||
|
@ -297,6 +298,8 @@ class Coder:
|
||||||
self.rejected_urls = set()
|
self.rejected_urls = set()
|
||||||
self.abs_root_path_cache = {}
|
self.abs_root_path_cache = {}
|
||||||
|
|
||||||
|
self.auto_copy_context = auto_copy_context
|
||||||
|
|
||||||
self.ignore_mentions = ignore_mentions
|
self.ignore_mentions = ignore_mentions
|
||||||
if not self.ignore_mentions:
|
if not self.ignore_mentions:
|
||||||
self.ignore_mentions = set()
|
self.ignore_mentions = set()
|
||||||
|
@ -792,9 +795,9 @@ class Coder:
|
||||||
self.io.user_input(with_message)
|
self.io.user_input(with_message)
|
||||||
self.run_one(with_message, preproc)
|
self.run_one(with_message, preproc)
|
||||||
return self.partial_response_content
|
return self.partial_response_content
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
self.copy_context()
|
||||||
user_message = self.get_input()
|
user_message = self.get_input()
|
||||||
self.run_one(user_message, preproc)
|
self.run_one(user_message, preproc)
|
||||||
self.show_undo_hint()
|
self.show_undo_hint()
|
||||||
|
@ -803,6 +806,10 @@ class Coder:
|
||||||
except EOFError:
|
except EOFError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def copy_context(self):
|
||||||
|
if self.auto_copy_context:
|
||||||
|
self.commands.cmd_copy_context()
|
||||||
|
|
||||||
def get_input(self):
|
def get_input(self):
|
||||||
inchat_files = self.get_inchat_relative_files()
|
inchat_files = self.get_inchat_relative_files()
|
||||||
read_only_files = [self.get_rel_fname(fname) for fname in self.abs_read_only_fnames]
|
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
|
# add the reminder anyway
|
||||||
total_tokens = 0
|
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
|
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.
|
# Add the reminder prompt if we still have room to include it.
|
||||||
|
@ -1125,7 +1135,7 @@ class Coder:
|
||||||
):
|
):
|
||||||
if self.main_model.reminder == "sys":
|
if self.main_model.reminder == "sys":
|
||||||
chunks.reminder = reminder_message
|
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
|
# stuff it into the user message
|
||||||
new_content = (
|
new_content = (
|
||||||
final["content"]
|
final["content"]
|
||||||
|
|
|
@ -1399,6 +1399,41 @@ class Commands:
|
||||||
if user_input.strip():
|
if user_input.strip():
|
||||||
self.io.set_placeholder(user_input.rstrip())
|
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):
|
def expand_subdir(file_path):
|
||||||
if file_path.is_file():
|
if file_path.is_file():
|
||||||
|
|
69
aider/copypaste.py
Normal file
69
aider/copypaste.py
Normal 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()
|
16
aider/io.py
16
aider/io.py
|
@ -176,6 +176,7 @@ class AutoCompleter(Completer):
|
||||||
class InputOutput:
|
class InputOutput:
|
||||||
num_error_outputs = 0
|
num_error_outputs = 0
|
||||||
num_user_asks = 0
|
num_user_asks = 0
|
||||||
|
clipboard_watcher = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -470,8 +471,11 @@ class InputOutput:
|
||||||
self.placeholder = None
|
self.placeholder = None
|
||||||
|
|
||||||
self.interrupted = False
|
self.interrupted = False
|
||||||
if not multiline_input and self.file_watcher:
|
if not multiline_input:
|
||||||
self.file_watcher.start()
|
if self.file_watcher:
|
||||||
|
self.file_watcher.start()
|
||||||
|
if self.clipboard_watcher:
|
||||||
|
self.clipboard_watcher.start()
|
||||||
|
|
||||||
line = self.prompt_session.prompt(
|
line = self.prompt_session.prompt(
|
||||||
show,
|
show,
|
||||||
|
@ -487,8 +491,10 @@ class InputOutput:
|
||||||
|
|
||||||
# Check if we were interrupted by a file change
|
# Check if we were interrupted by a file change
|
||||||
if self.interrupted:
|
if self.interrupted:
|
||||||
cmd = self.file_watcher.process_changes()
|
line = line or ""
|
||||||
return cmd
|
if self.file_watcher:
|
||||||
|
cmd = self.file_watcher.process_changes()
|
||||||
|
return cmd
|
||||||
|
|
||||||
except EOFError:
|
except EOFError:
|
||||||
return ""
|
return ""
|
||||||
|
@ -504,6 +510,8 @@ class InputOutput:
|
||||||
finally:
|
finally:
|
||||||
if self.file_watcher:
|
if self.file_watcher:
|
||||||
self.file_watcher.stop()
|
self.file_watcher.stop()
|
||||||
|
if self.clipboard_watcher:
|
||||||
|
self.clipboard_watcher.stop()
|
||||||
|
|
||||||
if line.strip("\r\n") and not multiline_input:
|
if line.strip("\r\n") and not multiline_input:
|
||||||
stripped = line.strip("\r\n")
|
stripped = line.strip("\r\n")
|
||||||
|
|
|
@ -20,6 +20,7 @@ from aider.args import get_parser
|
||||||
from aider.coders import Coder
|
from aider.coders import Coder
|
||||||
from aider.coders.base_coder import UnknownEditFormat
|
from aider.coders.base_coder import UnknownEditFormat
|
||||||
from aider.commands import Commands, SwitchCoder
|
from aider.commands import Commands, SwitchCoder
|
||||||
|
from aider.copypaste import ClipboardWatcher
|
||||||
from aider.format_settings import format_settings, scrub_sensitive_info
|
from aider.format_settings import format_settings, scrub_sensitive_info
|
||||||
from aider.history import ChatSummary
|
from aider.history import ChatSummary
|
||||||
from aider.io import InputOutput
|
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,
|
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:
|
if args.verbose:
|
||||||
io.tool_output("Model metadata:")
|
io.tool_output("Model metadata:")
|
||||||
io.tool_output(json.dumps(main_model.info, indent=4))
|
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,
|
suggest_shell_commands=args.suggest_shell_commands,
|
||||||
chat_language=args.chat_language,
|
chat_language=args.chat_language,
|
||||||
detect_urls=args.detect_urls,
|
detect_urls=args.detect_urls,
|
||||||
|
auto_copy_context=args.copy_paste,
|
||||||
)
|
)
|
||||||
except UnknownEditFormat as err:
|
except UnknownEditFormat as err:
|
||||||
io.tool_error(str(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)
|
file_watcher = FileWatcher(coder, gitignores=ignores, verbose=args.verbose)
|
||||||
coder.file_watcher = file_watcher
|
coder.file_watcher = file_watcher
|
||||||
|
|
||||||
|
if args.copy_paste:
|
||||||
|
ClipboardWatcher(coder.io, verbose=args.verbose)
|
||||||
|
|
||||||
coder.show_announcements()
|
coder.show_announcements()
|
||||||
|
|
||||||
if args.show_prompts:
|
if args.show_prompts:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue