diff --git a/aider/__init__.py b/aider/__init__.py index 856da189bd7..89edca9d96d 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.11.dev" +__version__ = "0.88.13.dev" safe_version = __version__ try: diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index 80f1d97484c..c97bd041410 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -135,6 +135,9 @@ def __init__(self, *args, **kwargs): self.context_blocks_cache = {} self.tokens_calculated = False + self.skip_cli_confirmations = False + + self._get_agent_config() super().__init__(*args, **kwargs) def _build_tool_registry(self): @@ -247,6 +250,9 @@ def _get_agent_config(self): # Apply configuration to instance self.large_file_token_threshold = config["large_file_token_threshold"] + self.skip_cli_confirmations = config.get( + "skip_cli_confirmations", config.get("yolo", False) + ) return config diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 13e67e8cb3e..f48b3ec2cb2 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1243,7 +1243,11 @@ async def _run_patched(self, with_message=None, preproc=True): tasks, return_when=asyncio.FIRST_COMPLETED ) - if self.io.input_task and self.io.input_task in done: + if ( + self.io.input_task + and self.io.input_task in done + and not self.io.confirmation_in_progress + ): await self.io.cancel_processing_task() self.io.stop_spinner() self.io.acknowledge_confirmation() @@ -2226,7 +2230,7 @@ async def process_tool_calls(self, tool_call_response): if server_tool_calls and self.num_tool_calls < self.max_tool_calls: self._print_tool_call_info(server_tool_calls) - if await self.io.confirm_ask("Run tools?"): + if await self.io.confirm_ask("Run tools?", group_response="Run MCP Tools"): tool_responses = await self._execute_tool_calls(server_tool_calls) # Add all tool responses diff --git a/aider/io.py b/aider/io.py index aad266683df..ae27a9ff3ab 100644 --- a/aider/io.py +++ b/aider/io.py @@ -385,6 +385,7 @@ def __init__( self.pretty = False self.yes = yes + self.group_responses = dict() self.input_history_file = input_history_file if self.input_history_file: @@ -1051,7 +1052,6 @@ async def confirm_ask( 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 @@ -1066,6 +1066,7 @@ async def _confirm_ask( subject=None, explicit_yes_required=False, group=None, + group_response=None, allow_never=False, ): self.num_user_asks += 1 @@ -1083,8 +1084,9 @@ async def _confirm_ask( valid_responses = ["yes", "no", "skip", "all"] options = " (Y)es/(N)o" - if group: - if not explicit_yes_required: + + if group or group_response: + if not explicit_yes_required or group_response: options += "/(A)ll" options += "/(S)kip all" if allow_never: @@ -1109,16 +1111,13 @@ async def _confirm_ask( else: self.tool_output(subject, bold=True) - 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() + if self.yes is True and not explicit_yes_required: + res = "y" elif group and group.preference: res = group.preference - self.user_input(f"{question}{res}", log_only=False) - self.acknowledge_confirmation() + self.user_input(f"{question} - {res}", log_only=False) + elif group_response and group_response in self.group_responses: + return self.group_responses[group_response] else: # Ring the bell if needed self.ring_bell() @@ -1146,13 +1145,15 @@ async def _confirm_ask( self.prompt_session.message = question self.prompt_session.app.invalidate() else: - continue + await asyncio.sleep(0) res = await self.input_task + await asyncio.sleep(0) else: res = await asyncio.get_event_loop().run_in_executor( None, input, question ) + except EOFError: # Treat EOF (Ctrl+D) as if the user pressed Enter res = default @@ -1167,6 +1168,7 @@ async def _confirm_ask( good = any(valid_response.startswith(res) for valid_response in valid_responses) if good: + self.set_confirmation_acknowledgement() self.start_spinner(self.last_spinner_text) break @@ -1181,13 +1183,15 @@ async def _confirm_ask( self.append_chat_history(hist, linebreak=True, blockquote=True) return False - if explicit_yes_required: + if explicit_yes_required and not group_response: is_yes = res == "y" else: is_yes = res in ("y", "a") - is_all = res == "a" and group is not None and not explicit_yes_required - is_skip = res == "s" and group is not None + is_all = res == "a" and ( + (group is not None and not explicit_yes_required) or group_response + ) + is_skip = res == "s" and (group is not None or group_response) if group: if is_all and not explicit_yes_required: @@ -1201,6 +1205,10 @@ async def _confirm_ask( return False finally: pass + + if group_response and (is_all or is_skip): + self.group_responses[group_response] = is_yes + return is_yes @restore_multiline diff --git a/aider/run_cmd.py b/aider/run_cmd.py index f201b41dcc6..df92503d7e4 100644 --- a/aider/run_cmd.py +++ b/aider/run_cmd.py @@ -65,6 +65,7 @@ def run_cmd_subprocess(command, verbose=False, cwd=None, encoding=sys.stdout.enc stderr=subprocess.STDOUT, text=True, shell=True, + executable=shell if platform.system() != "Windows" else None, encoding=encoding, errors="replace", bufsize=0, # Set bufsize to 0 for unbuffered output diff --git a/aider/tools/command.py b/aider/tools/command.py index 99b6c2ec96f..6c7427a117c 100644 --- a/aider/tools/command.py +++ b/aider/tools/command.py @@ -1,4 +1,6 @@ # Import necessary functions +import asyncio + from aider.run_cmd import run_cmd_subprocess schema = { @@ -23,7 +25,7 @@ NORM_NAME = "command" -def _execute_command(coder, command_string): +async def _execute_command(coder, command_string): """ Execute a non-interactive shell command after user confirmation. """ @@ -31,13 +33,24 @@ def _execute_command(coder, command_string): # Ask for confirmation before executing. # allow_never=True enables the 'Always' option. # confirm_ask handles remembering the 'Always' choice based on the subject. - confirmed = coder.io.confirm_ask( - "Allow execution of this command?", - subject=command_string, - explicit_yes_required=True, # Require explicit 'yes' or 'always' - allow_never=True, # Enable the 'Always' option + + confirmed = ( + True + if coder.skip_cli_confirmations + else await coder.io.confirm_ask( + "Allow execution of this command?", + subject=command_string, + explicit_yes_required=True, # Require explicit 'yes' or 'always' + allow_never=True, # Enable the 'Always' option + group_response="Command Tool", + ) ) + if not coder.io.input_task or coder.io.input_task.done() or coder.io.input_task.cancelled(): + coder.io.input_task = asyncio.create_task(coder.get_input()) + + await asyncio.sleep(0) + if not confirmed: # This happens if the user explicitly says 'no' this time. # If 'Always' was chosen previously, confirm_ask returns True directly. @@ -79,7 +92,7 @@ def _execute_command(coder, command_string): return f"Error executing command: {str(e)}" -def process_response(coder, params): +async def process_response(coder, params): """ Process the Command tool response. @@ -92,6 +105,6 @@ def process_response(coder, params): """ command_string = params.get("command_string") if command_string is not None: - return _execute_command(coder, command_string) + return await _execute_command(coder, command_string) else: return "Error: Missing 'command_string' parameter for Command" diff --git a/aider/tools/command_interactive.py b/aider/tools/command_interactive.py index d64c05e6756..0af5b1488df 100644 --- a/aider/tools/command_interactive.py +++ b/aider/tools/command_interactive.py @@ -1,4 +1,6 @@ # Import necessary functions +import asyncio + from aider.run_cmd import run_cmd schema = { @@ -23,13 +25,35 @@ NORM_NAME = "commandinteractive" -def _execute_command_interactive(coder, command_string): +async def _execute_command_interactive(coder, command_string): """ Execute an interactive shell command using run_cmd (which uses pexpect/PTY). """ try: + confirmed = ( + True + if coder.skip_cli_confirmations + else await coder.io.confirm_ask( + "Allow execution of this command?", + subject=command_string, + explicit_yes_required=True, # Require explicit 'yes' or 'always' + allow_never=True, # Enable the 'Always' option + group_response="Command Interactive Tool", + ) + ) + + if not confirmed: + # This happens if the user explicitly says 'no' this time. + # If 'Always' was chosen previously, confirm_ask returns True directly. + coder.io.tool_output(f"Skipped execution of shell command: {command_string}") + return "Shell command execution skipped by user." + coder.io.tool_output(f"⚙️ Starting interactive shell command: {command_string}") coder.io.tool_output(">>> You may need to interact with the command below <<<") + coder.io.tool_output(" \n") + + await coder.io.cancel_input_task() + await asyncio.sleep(1) # Use run_cmd which handles PTY logic exit_status, combined_output = run_cmd( @@ -39,8 +63,17 @@ def _execute_command_interactive(coder, command_string): cwd=coder.root, # Execute in the project root ) + await asyncio.sleep(1) + + coder.io.tool_output(" \n") + coder.io.tool_output(" \n") coder.io.tool_output(">>> Interactive command finished <<<") + if not coder.io.input_task or coder.io.input_task.done() or coder.io.input_task.cancelled(): + coder.io.input_task = asyncio.create_task(coder.get_input()) + + await asyncio.sleep(0) + # Format the output for the result message, include more content output_content = combined_output or "" # Use the existing token threshold constant as the character limit for truncation @@ -74,7 +107,7 @@ def _execute_command_interactive(coder, command_string): return f"Error executing interactive command: {str(e)}" -def process_response(coder, params): +async def process_response(coder, params): """ Process the CommandInteractive tool response. @@ -87,6 +120,6 @@ def process_response(coder, params): """ command_string = params.get("command_string") if command_string is not None: - return _execute_command_interactive(coder, command_string) + return await _execute_command_interactive(coder, command_string) else: return "Error: Missing 'command_string' parameter for CommandInteractive" diff --git a/aider/website/docs/config/agent-mode.md b/aider/website/docs/config/agent-mode.md index 038a04352bd..060b9624aa0 100644 --- a/aider/website/docs/config/agent-mode.md +++ b/aider/website/docs/config/agent-mode.md @@ -150,9 +150,10 @@ Agent Mode can be configured using the `--agent-config` command line argument, w #### Configuration Options +- **`large_file_token_threshold`**: Maximum token threshold for large file warnings (default: 25000) +- **`skip_cli_confirmations`**: YOLO mode, be brave and let the LLM cook, can also use the option `yolo` (default: False) - **`tools_includelist`**: Array of tool names to allow (only these tools will be available) - **`tools_excludelist`**: Array of tool names to exclude (these tools will be disabled) -- **`large_file_token_threshold`**: Maximum token threshold for large file warnings (default: 25000) #### Essential Tools diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index c37069aaef5..2d499c3c205 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -180,28 +180,31 @@ def test_confirm_ask_explicit_yes_required(self, mock_input): # Test case 1: explicit_yes_required=True, self.yes=True io.yes = True + mock_input.return_value = "n" result = asyncio.run(io.confirm_ask("Are you sure?", explicit_yes_required=True)) self.assertFalse(result) - mock_input.assert_not_called() + mock_input.assert_called() + mock_input.reset_mock() # Test case 2: explicit_yes_required=True, self.yes=False io.yes = False + mock_input.return_value = "n" result = asyncio.run(io.confirm_ask("Are you sure?", explicit_yes_required=True)) self.assertFalse(result) - mock_input.assert_not_called() + mock_input.assert_called() + mock_input.reset_mock() # Test case 3: explicit_yes_required=True, user input required io.yes = None mock_input.return_value = "y" result = asyncio.run(io.confirm_ask("Are you sure?", explicit_yes_required=True)) self.assertTrue(result) - mock_input.assert_called_once() - - # Reset mock_input + mock_input.assert_called() mock_input.reset_mock() # Test case 4: explicit_yes_required=False, self.yes=True io.yes = True + mock_input.return_value = "y" result = asyncio.run(io.confirm_ask("Are you sure?", explicit_yes_required=False)) self.assertTrue(result) mock_input.assert_not_called()