Skip to content
Merged
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
2 changes: 1 addition & 1 deletion aider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.88.11.dev"
__version__ = "0.88.13.dev"
safe_version = __version__

try:
Expand Down
6 changes: 6 additions & 0 deletions aider/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
38 changes: 23 additions & 15 deletions aider/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions aider/run_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 21 additions & 8 deletions aider/tools/command.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Import necessary functions
import asyncio

from aider.run_cmd import run_cmd_subprocess

schema = {
Expand All @@ -23,21 +25,32 @@
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.
"""
try:
# 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.
Expand Down Expand Up @@ -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.

Expand All @@ -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"
39 changes: 36 additions & 3 deletions aider/tools/command_interactive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Import necessary functions
import asyncio

from aider.run_cmd import run_cmd

schema = {
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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"
3 changes: 2 additions & 1 deletion aider/website/docs/config/agent-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 8 additions & 5 deletions tests/basic/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down