diff --git a/aider/__init__.py b/aider/__init__.py index 55df828dee6..67ec05c2f26 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.3.dev" +__version__ = "0.88.4.dev" safe_version = __version__ try: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 16a1cb8410b..e46099d22c1 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -42,7 +42,7 @@ from aider import __version__, models, prompts, urls, utils from aider.analytics import Analytics -from aider.commands import Commands +from aider.commands import Commands, SwitchCoder from aider.exceptions import LiteLLMExceptions from aider.history import ChatSummary from aider.io import ConfirmGroup, InputOutput @@ -585,8 +585,6 @@ def __init__( self.summarizer_thread = None self.summarized_done_messages = [] self.summarizing_messages = None - self.input_task = None - self.confirmation_in_progress = False self.files_edited_by_tools = set() @@ -1057,7 +1055,7 @@ def init_before_message(self): self.commit_before_message.append(self.repo.get_head_commit_sha()) async def run(self, with_message=None, preproc=True): - while self.confirmation_in_progress: + while self.io.confirmation_in_progress: await asyncio.sleep(0.1) # Yield control and wait briefly if self.io.prompt_session: @@ -1067,8 +1065,6 @@ async def run(self, with_message=None, preproc=True): return await self._run_patched(with_message, preproc) async def _run_patched(self, with_message=None, preproc=True): - input_task = None - processing_task = None try: if with_message: self.io.user_input(with_message) @@ -1076,91 +1072,134 @@ async def _run_patched(self, with_message=None, preproc=True): return self.partial_response_content user_message = None + await self.io.cancel_input_task() + await self.io.cancel_processing_task() while True: try: if ( - not self.confirmation_in_progress - and not input_task + not self.io.confirmation_in_progress and not user_message - and (not processing_task or not self.io.placeholder) + and ( + not self.io.input_task + or self.io.input_task.done() + or self.io.input_task.cancelled() + ) + and (not self.io.processing_task or not self.io.placeholder) ): if not self.suppress_announcements_for_next_prompt: self.show_announcements() - self.suppress_announcements_for_next_prompt = False + self.suppress_announcements_for_next_prompt = True # Stop spinner before showing announcements or getting input self.io.stop_spinner() - self.copy_context() - self.input_task = asyncio.create_task(self.get_input()) - input_task = self.input_task + self.io.input_task = asyncio.create_task(self.get_input()) + + # Yield Control so input can actually get properly set up + await asyncio.sleep(0) tasks = set() - if processing_task: - tasks.add(processing_task) - if input_task: - tasks.add(input_task) + + if self.io.processing_task: + if self.io.processing_task.done(): + exception = self.io.processing_task.exception() + if exception: + if isinstance(exception, SwitchCoder): + await self.io.processing_task + elif ( + not self.io.processing_task.done() + and not self.io.processing_task.cancelled() + ): + tasks.add(self.io.processing_task) + + if ( + self.io.input_task + and not self.io.input_task.done() + and not self.io.input_task.cancelled() + ): + tasks.add(self.io.input_task) if tasks: done, pending = await asyncio.wait( tasks, return_when=asyncio.FIRST_COMPLETED ) - if input_task and input_task in done: - if processing_task: - if not self.confirmation_in_progress: - processing_task.cancel() - try: - await processing_task - except asyncio.CancelledError: - pass + if self.io.input_task and self.io.input_task in done: + if self.io.processing_task: + if not self.io.confirmation_in_progress: + await self.io.cancel_processing_task() self.io.stop_spinner() - processing_task = None try: - user_message = input_task.result() + user_message = self.io.input_task.result() + await self.io.cancel_input_task() except (asyncio.CancelledError, KeyboardInterrupt): user_message = None - input_task = None - self.input_task = None - if user_message is None: + + if not user_message: + await self.io.cancel_input_task() continue - if processing_task and processing_task in done: + if self.io.processing_task and self.io.processing_task in pending: try: - await processing_task + tasks = set() + tasks.add(self.io.processing_task) + + # We just did a confirmation so add a new input task + if ( + not self.io.input_task + and self.io.get_confirmation_acknowledgement() + ): + self.io.input_task = asyncio.create_task(self.get_input()) + tasks.add(self.io.input_task) + + done, pending = await asyncio.wait( + tasks, return_when=asyncio.FIRST_COMPLETED + ) + + if self.io.input_task and self.io.input_task in done: + await self.io.cancel_processing_task() + self.io.stop_spinner() + self.io.acknowledge_confirmation() + + try: + user_message = self.io.input_task.result() + await self.io.cancel_input_task() + except (asyncio.CancelledError, KeyboardInterrupt): + user_message = None + except (asyncio.CancelledError, KeyboardInterrupt): + print("error of some sort") pass - processing_task = None + # Stop spinner when processing task completes self.io.stop_spinner() - if user_message and self.run_one_completed and self.compact_context_completed: - processing_task = asyncio.create_task( + if user_message and not self.io.acknowledge_confirmation(): + self.io.processing_task = asyncio.create_task( self._processing_logic(user_message, preproc) ) # Start spinner for processing task self.io.start_spinner("Processing...") - user_message = None # Clear message after starting task + + self.io.ring_bell() + user_message = None except KeyboardInterrupt: - if processing_task: - processing_task.cancel() - processing_task = None - # Stop spinner when processing task is cancelled - self.io.stop_spinner() - if input_task: + if self.io.input_task: self.io.set_placeholder("") - input_task.cancel() - input_task = None + await self.io.cancel_input_task() + + if self.io.processing_task: + await self.io.cancel_processing_task() + self.io.stop_spinner() + self.keyboard_interrupt() except EOFError: return finally: - if input_task: - input_task.cancel() - if processing_task: - processing_task.cancel() + await self.io.cancel_input_task() + await self.io.cancel_processing_task() async def _processing_logic(self, user_message, preproc): try: @@ -1188,6 +1227,7 @@ async def get_input(self): all_read_only_files = [self.get_rel_fname(fname) for fname in all_read_only_fnames] all_files = sorted(set(inchat_files + all_read_only_files)) edit_format = "" if self.edit_format == self.main_model.edit_format else self.edit_format + return await self.io.get_input( self.root, all_files, @@ -1214,6 +1254,8 @@ async def run_one(self, user_message, preproc): self.init_before_message() if preproc: + if user_message[0] in "!": + user_message = f"/run {user_message[1:]}" message = await self.preproc_user_input(user_message) else: message = user_message @@ -2704,7 +2746,7 @@ async def show_send_output_stream(self, completion): async for chunk in completion: # Check if confirmation is in progress and wait if needed - while self.confirmation_in_progress: + while self.io.confirmation_in_progress: await asyncio.sleep(0.1) # Yield control and wait briefly if isinstance(chunk, str): diff --git a/aider/io.py b/aider/io.py index 138efaca9cb..bf1e2281beb 100644 --- a/aider/io.py +++ b/aider/io.py @@ -320,6 +320,7 @@ def __init__( root=".", notifications=False, notifications_command=None, + verbose=False, ): self.console = Console() self.pretty = pretty @@ -337,6 +338,8 @@ def __init__( self.multiline_mode = multiline_mode self.bell_on_next_input = False self.notifications = notifications + self.verbose = verbose + if notifications and notifications_command is None: self.notifications_command = self.get_default_notification_command() else: @@ -363,7 +366,7 @@ def __init__( ) self.fzf_available = shutil.which("fzf") - if not self.fzf_available: + if not self.fzf_available and self.verbose: self.tool_warning( "fzf not found, fuzzy finder features will be disabled. Install it for enhanced" " file/history search." @@ -460,7 +463,18 @@ def __init__( self.file_watcher = file_watcher self.root = root self.outstanding_confirmations = [] + + # Variables used to interface with base_coder self.coder = None + self.input_task = None + self.processing_task = None + self.confirmation_in_progress = False + self.confirmation_acknowledgement = False + + # State tracking for confirmation input + self.confirmation_input_active = False + self.saved_input_text = "" + self.confirmation_future = None # Validate color settings after console is initialized self._validate_color_settings() @@ -657,16 +671,8 @@ def rule(self): print() def interrupt_input(self): - coder = self.coder() if self.coder else None - # interrupted_for_confirmation = False - - if ( - coder - and hasattr(coder, "input_task") - and coder.input_task - and not coder.input_task.done() - ): - coder.input_task.cancel() + if self.input_task and not self.input_task.done(): + self.input_task.cancel() if self.prompt_session and self.prompt_session.app: # Store any partial input before interrupting @@ -698,9 +704,6 @@ async def get_input( self.reject_outstanding_confirmations() self.rule() - # Ring the bell if needed - self.ring_bell() - rel_fnames = list(rel_fnames) show = "" if rel_fnames: @@ -879,6 +882,11 @@ def get_continuation(width, line_number, is_soft_wrap): self.tool_error(str(err)) return "" except Exception as err: + try: + self.prompt_session.app.exit() + except Exception: + pass + import traceback self.tool_error(str(err)) @@ -931,6 +939,26 @@ def get_continuation(width, line_number, is_soft_wrap): self.user_input(inp) return inp + async def cancel_input_task(self): + if self.input_task: + input_task = self.input_task + self.input_task = None + try: + input_task.cancel() + await input_task + except asyncio.CancelledError: + pass + + async def cancel_processing_task(self): + if self.processing_task: + processing_task = self.processing_task + self.processing_task = None + try: + processing_task.cancel() + await processing_task + except asyncio.CancelledError: + pass + def add_to_input_history(self, inp): if not self.input_history_file: return @@ -1001,32 +1029,33 @@ async def offer_url(self, url, prompt="Open URL for more info?", allow_never=Tru return True return False + def set_confirmation_acknowledgement(self): + self.confirmation_acknowledgement = True + + def get_confirmation_acknowledgement(self): + return self.confirmation_acknowledgement + + def acknowledge_confirmation(self): + outstanding_confirmation = self.confirmation_acknowledgement + self.confirmation_acknowledgement = False + return outstanding_confirmation + @restore_multiline_async async def confirm_ask( self, *args, **kwargs, ): - coder = self.coder() if self.coder else None - interrupted_for_confirmation = False - if ( - coder - and hasattr(coder, "input_task") - and coder.input_task - and not coder.input_task.done() - ): - coder.confirmation_in_progress = True - interrupted_for_confirmation = True - # self.interrupt_input() + self.confirmation_in_progress = True try: + self.set_confirmation_acknowledgement() return await asyncio.create_task(self._confirm_ask(*args, **kwargs)) except KeyboardInterrupt: # Re-raise KeyboardInterrupt to allow it to propagate raise finally: - if interrupted_for_confirmation: - coder.confirmation_in_progress = False + self.confirmation_in_progress = False async def _confirm_ask( self, @@ -1039,9 +1068,6 @@ async def _confirm_ask( ): self.num_user_asks += 1 - # Ring the bell if needed - self.ring_bell() - question_id = (question, subject) confirmation_future = asyncio.get_running_loop().create_future() @@ -1086,47 +1112,45 @@ async def _confirm_ask( else: self.tool_output(subject, bold=True) - style = self._get_style() - if self.yes is True: res = "n" if explicit_yes_required else "y" + self.acknowledge_confirmation() elif self.yes is False: res = "n" + self.acknowledge_confirmation() elif group and group.preference: res = group.preference self.user_input(f"{question}{res}", log_only=False) + self.acknowledge_confirmation() else: + # Ring the bell if needed + self.ring_bell() + while True: try: if self.prompt_session: - coder = self.coder() if self.coder else None if ( - coder - and hasattr(coder, "input_task") - and coder.input_task - and not coder.input_task.done() + not self.input_task + or self.input_task.done() + or self.input_task.cancelled() + ): + coder = self.coder() if self.coder else None + + if coder: + self.input_task = asyncio.create_task(coder.get_input()) + await asyncio.sleep(0) + + if ( + self.input_task + and not self.input_task.done() + and not self.input_task.cancelled() ): self.prompt_session.message = question self.prompt_session.app.invalidate() - res = await coder.input_task else: - prompt_task = asyncio.create_task( - self.prompt_session.prompt_async( - question, - style=style, - complete_while_typing=False, - ) - ) - done, pending = await asyncio.wait( - {prompt_task, confirmation_future}, - return_when=asyncio.FIRST_COMPLETED, - ) - - if confirmation_future in done: - prompt_task.cancel() - return await confirmation_future - - res = await prompt_task + continue + + res = await self.input_task else: res = await asyncio.get_event_loop().run_in_executor( None, input, question @@ -1241,17 +1265,22 @@ def _tool_message(self, message="", strip=True, color=None): if not isinstance(message, Text): message = Text(message) - color = ensure_hash_prefix(color) if color else None - style = dict(style=color) if self.pretty and color else dict() + + style = dict() + + if self.pretty: + color = ensure_hash_prefix(color) if color else None + if color: + style["color"] = color try: - self.stream_print(message, **style) + self.stream_print(message, style=RichStyle(**style)) except UnicodeEncodeError: # Fallback to ASCII-safe output if isinstance(message, Text): message = message.plain message = str(message).encode("ascii", errors="replace").decode("ascii") - self.stream_print(message, **style) + self.stream_print(message, style=RichStyle(**style)) if self.prompt_session and self.prompt_session.app: self.prompt_session.app.invalidate() diff --git a/aider/main.py b/aider/main.py index a8b795cf510..4b58887f2be 100644 --- a/aider/main.py +++ b/aider/main.py @@ -601,6 +601,7 @@ def get_io(pretty): multiline_mode=args.multiline, notifications=args.notifications, notifications_command=args.notifications_command, + verbose=args.verbose, ) io = get_io(args.pretty) diff --git a/aider/urls.py b/aider/urls.py index cff92e36dc2..b3b6c53dccf 100644 --- a/aider/urls.py +++ b/aider/urls.py @@ -8,7 +8,7 @@ token_limits = "https://aider.chat/docs/troubleshooting/token-limits.html" llms = "https://aider.chat/docs/llms.html" large_repos = "https://aider.chat/docs/faq.html#can-i-use-aider-in-a-large-mono-repo" -github_issues = "https://github.com/Aider-AI/aider/issues/new" +github_issues = "https://github.com/dwash96/aider-ce/issues/new" git_index_version = "https://github.com/Aider-AI/aider/issues/211" install_properly = "https://aider.chat/docs/troubleshooting/imports.html" analytics = "https://aider.chat/docs/more/analytics.html" diff --git a/aider/versioncheck.py b/aider/versioncheck.py index ac511a0227a..1de5f3da1f1 100644 --- a/aider/versioncheck.py +++ b/aider/versioncheck.py @@ -21,7 +21,7 @@ def install_from_main_branch(io): io, None, "Install the development version of aider from the main branch?", - ["git+https://github.com/Aider-AI/aider.git"], + ["git+https://github.com/dwash96/aider-ce.git"], self_update=True, ) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 9495985192e..c37069aaef5 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -7,6 +7,7 @@ from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document +from aider.coders import Coder from aider.dump import dump # noqa: F401 from aider.io import AutoCompleter, ConfirmGroup, InputOutput from aider.utils import ChdirTemporaryDirectory @@ -389,10 +390,11 @@ def test_tool_message_unicode_fallback(self): # The invalid Unicode should be replaced with '?' self.assertEqual(converted_message, "Hello ?World") - def test_multiline_mode_restored_after_interrupt(self): + async def test_multiline_mode_restored_after_interrupt(self): """Test that multiline mode is restored after KeyboardInterrupt""" io = InputOutput(fancy_input=True) io.prompt_session = MagicMock() + await Coder.create(self.GPT35, None, io) # Use AsyncMock for prompt_async (for confirm_ask) io.prompt_session.prompt_async = AsyncMock(side_effect=KeyboardInterrupt) @@ -413,10 +415,11 @@ def test_multiline_mode_restored_after_interrupt(self): io.prompt_ask("Test prompt?") self.assertTrue(io.multiline_mode) # Should be restored - def test_multiline_mode_restored_after_normal_exit(self): + async def test_multiline_mode_restored_after_normal_exit(self): """Test that multiline mode is restored after normal exit""" io = InputOutput(fancy_input=True) io.prompt_session = MagicMock() + await Coder.create(self.GPT35, None, io) # Use AsyncMock for prompt_async that returns "y" io.prompt_session.prompt_async = AsyncMock(return_value="y")