diff --git a/aider/__init__.py b/aider/__init__.py index 71d22aafe3d..dc47253c91e 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.15.dev" +__version__ = "0.88.16.dev" safe_version = __version__ try: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3f78eaf50f4..04b32bcb5d0 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -2227,6 +2227,7 @@ async def process_tool_calls(self, tool_call_response): self._print_tool_call_info(server_tool_calls) if await self.io.confirm_ask("Run tools?", group_response="Run MCP Tools"): + await self.io.recreate_input() tool_responses = await self._execute_tool_calls(server_tool_calls) # Add all tool responses @@ -2512,7 +2513,7 @@ async def get_server_tools(server): ) return (server.name, server_tools) except Exception as e: - if server.name != "unnamed-server": + if server.name != "unnamed-server" and server.name != "local_tools": self.io.tool_warning(f"Error initializing MCP server {server.name}: {e}") return None diff --git a/aider/commands.py b/aider/commands.py index b9ea302db89..1a2802350da 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1051,66 +1051,87 @@ def _handle_read_only_files(self, expanded_word, file_set, description=""): file_set.remove(matched_file) self.io.tool_output(f"Removed {description} file {matched_file} from the chat") - def cmd_drop(self, args=""): + async def cmd_drop(self, args=""): "Remove files from the chat session to free up context space" - if not args.strip(): - if self.original_read_only_fnames: - self.io.tool_output( - "Dropping all files from the chat session except originally read-only files." - ) - else: - self.io.tool_output("Dropping all files from the chat session.") - self._drop_all_files() + try: + if not args.strip(): + if self.original_read_only_fnames: + self.io.tool_output( + "Dropping all files from the chat session except originally read-only" + " files." + ) + else: + self.io.tool_output("Dropping all files from the chat session.") + self._drop_all_files() - # Recalculate context block tokens after dropping all files - if hasattr(self.coder, "use_enhanced_context") and self.coder.use_enhanced_context: - if hasattr(self.coder, "_calculate_context_block_tokens"): - self.coder._calculate_context_block_tokens() - return + # Recalculate context block tokens after dropping all files + if hasattr(self.coder, "use_enhanced_context") and self.coder.use_enhanced_context: + if hasattr(self.coder, "_calculate_context_block_tokens"): + self.coder._calculate_context_block_tokens() - filenames = parse_quoted_filenames(args) - files_changed = False + return - for word in filenames: - # Expand tilde in the path - expanded_word = os.path.expanduser(word) + filenames = parse_quoted_filenames(args) + files_changed = False - # Handle read-only files - self._handle_read_only_files( - expanded_word, self.coder.abs_read_only_fnames, "read-only" - ) - self._handle_read_only_files( - expanded_word, self.coder.abs_read_only_stubs_fnames, "read-only (stub)" - ) + for word in filenames: + # Expand tilde in the path + expanded_word = os.path.expanduser(word) - # For editable files, use glob if word contains glob chars, otherwise use substring - if any(c in expanded_word for c in "*?[]"): - matched_files = self.glob_filtered_to_repo(expanded_word) - else: - # Use substring matching like we do for read-only files - matched_files = [ - self.coder.get_rel_fname(f) for f in self.coder.abs_fnames if expanded_word in f - ] + # Handle read-only files + self._handle_read_only_files( + expanded_word, self.coder.abs_read_only_fnames, "read-only" + ) + self._handle_read_only_files( + expanded_word, self.coder.abs_read_only_stubs_fnames, "read-only (stub)" + ) - if not matched_files: - matched_files.append(expanded_word) - - for matched_file in matched_files: - abs_fname = self.coder.abs_root_path(matched_file) - if abs_fname in self.coder.abs_fnames: - self.coder.abs_fnames.remove(abs_fname) - self.io.tool_output(f"Removed {matched_file} from the chat") - files_changed = True - - # Recalculate context block tokens if any files were changed and using agent mode - if ( - files_changed - and hasattr(self.coder, "use_enhanced_context") - and self.coder.use_enhanced_context - ): - if hasattr(self.coder, "_calculate_context_block_tokens"): - self.coder._calculate_context_block_tokens() + # For editable files, use glob if word contains glob chars, otherwise use substring + if any(c in expanded_word for c in "*?[]"): + matched_files = self.glob_filtered_to_repo(expanded_word) + else: + # Use substring matching like we do for read-only files + matched_files = [ + self.coder.get_rel_fname(f) + for f in self.coder.abs_fnames + if expanded_word in f + ] + + if not matched_files: + matched_files.append(expanded_word) + + for matched_file in matched_files: + abs_fname = self.coder.abs_root_path(matched_file) + if abs_fname in self.coder.abs_fnames: + self.coder.abs_fnames.remove(abs_fname) + self.io.tool_output(f"Removed {matched_file} from the chat") + files_changed = True + + # Recalculate context block tokens if any files were changed and using agent mode + if ( + files_changed + and hasattr(self.coder, "use_enhanced_context") + and self.coder.use_enhanced_context + ): + if hasattr(self.coder, "_calculate_context_block_tokens"): + self.coder._calculate_context_block_tokens() + finally: + if self.coder.repo_map: + map_tokens = self.coder.repo_map.max_map_tokens + map_mul_no_files = self.coder.repo_map.map_mul_no_files + else: + map_tokens = 0 + map_mul_no_files = 1 + + raise SwitchCoder( + edit_format=self.coder.edit_format, + summarize_from_coder=False, + from_coder=self.coder, + map_tokens=map_tokens, + map_mul_no_files=map_mul_no_files, + show_announcements=False, + ) def cmd_git(self, args): "Run a git command (output excluded from chat)" diff --git a/aider/io.py b/aider/io.py index b307790d576..6b18569490b 100644 --- a/aider/io.py +++ b/aider/io.py @@ -94,7 +94,7 @@ def without_input_history(func): """Decorator to temporarily disable history saving for the prompt session buffer.""" @functools.wraps(func) - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): orig_buf_append = None try: orig_buf_append = self.prompt_session.default_buffer.append_to_history @@ -105,7 +105,7 @@ def wrapper(self, *args, **kwargs): pass try: - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except Exception: raise finally: @@ -340,6 +340,17 @@ def __init__( self.notifications = notifications self.verbose = verbose + # Variables used to interface with base_coder + self.coder = None + self.input_task = None + self.processing_task = None + + # State tracking for confirmation input + self.confirmation_in_progress = False + self.confirmation_acknowledgement = False + self.confirmation_input_active = False + self.saved_input_text = "" + if notifications and notifications_command is None: self.notifications_command = self.get_default_notification_command() else: @@ -413,7 +424,6 @@ def __init__( self.dry_run = dry_run current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.append_chat_history(f"\n# aider chat started at {current_time}\n\n") self.is_dumb_terminal = is_dumb_terminal() self.is_tty = sys.stdout.isatty() @@ -465,19 +475,9 @@ def __init__( self.file_watcher = file_watcher self.root = root - # 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 = "" - # Validate color settings after console is initialized self._validate_color_settings() + self.append_chat_history(f"\n# aider chat started at {current_time}\n\n") def _spinner_supports_unicode(self) -> bool: if not self.is_tty: @@ -908,6 +908,8 @@ def get_continuation(width, line_number, is_soft_wrap): if self.clipboard_watcher: self.clipboard_watcher.stop() + line = line or "" + if line.strip("\r\n") and not multiline_input: stripped = line.strip("\r\n") if stripped == "{": @@ -1012,6 +1014,13 @@ def user_input(self, inp, log_only=True): if not log_only: self.display_user_input(inp) + if ( + len(inp) <= 1 + or self.confirmation_in_progress + or self.get_confirmation_acknowledgement() + ): + return + prefix = "####" if inp: hist = inp.splitlines() @@ -1055,6 +1064,7 @@ def acknowledge_confirmation(self): return outstanding_confirmation @restore_multiline_async + @without_input_history async def confirm_ask( self, *args, @@ -1324,12 +1334,7 @@ def assistant_output(self, message, pretty=None): if pretty is None: pretty = self.pretty - if pretty: - show_resp = Markdown( - message, style=self.assistant_output_color, code_theme=self.code_theme - ) - else: - show_resp = Text(message or "(empty response)") + show_resp = Text(message or "(empty response)") self.stream_print(show_resp) @@ -1487,6 +1492,9 @@ def toggle_multiline_mode(self): ) def append_chat_history(self, text, linebreak=False, blockquote=False, strip=True): + if self.confirmation_in_progress or self.get_confirmation_acknowledgement(): + return + if blockquote: if strip: text = text.strip() diff --git a/aider/tools/grep.py b/aider/tools/grep.py index 4be93b162c8..158e75826e6 100644 --- a/aider/tools/grep.py +++ b/aider/tools/grep.py @@ -116,6 +116,9 @@ def _execute_grep( cmd_args.append("-n") # Line numbers for rg and grep # ag includes line numbers by default + if tool_name in ["rg"]: + cmd_args.append("--heading") # Filename above output for ripgrep + # Context lines (Before and After) if context_before > 0: # All tools use -B for lines before