diff --git a/aider/__init__.py b/aider/__init__.py index 178b19751ad..85def92b45c 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.32.dev" +__version__ = "0.88.33.dev" safe_version = __version__ try: diff --git a/aider/commands.py b/aider/commands.py index 924e949ab30..0f3b63205d7 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -89,7 +89,8 @@ def __init__( async def cmd_model(self, args): "Switch the Main Model to a new LLM" - model_name = args.strip() + arg_split = args.split(" ", 1) + model_name = arg_split[0].strip() if not model_name: announcements = "\n".join(self.coder.get_announcements()) self.io.tool_output(announcements) @@ -111,19 +112,57 @@ async def cmd_model(self, args): # If the user was using the old model's default, switch to the new model's default new_edit_format = model.edit_format - raise SwitchCoder(main_model=model, edit_format=new_edit_format) + if len(arg_split) > 1: + # implement architect coder-like generation call for model + message = arg_split[1].strip() - async def cmd_editor_model(self, args): - "Switch the Editor Model to a new LLM" + # Store the original model configuration + original_main_model = self.coder.main_model + original_edit_format = self.coder.edit_format - model_name = args.strip() - model = models.Model( - self.coder.main_model.name, - editor_model=model_name, - weak_model=self.coder.main_model.weak_model.name, - ) - await models.sanity_check_models(self.io, model) - raise SwitchCoder(main_model=model) + # Create a temporary coder with the new model + from aider.coders import Coder + + kwargs = dict() + kwargs["main_model"] = model + kwargs["edit_format"] = new_edit_format + kwargs["suggest_shell_commands"] = False + kwargs["total_cost"] = self.coder.total_cost + kwargs["num_cache_warming_pings"] = 0 + kwargs["summarize_from_coder"] = False + + new_kwargs = dict(io=self.io, from_coder=self.coder) + new_kwargs.update(kwargs) + + temp_coder = await Coder.create(**new_kwargs) + temp_coder.cur_messages = [] + temp_coder.done_messages = [] + + if self.verbose: + temp_coder.show_announcements() + + try: + await temp_coder.generate(user_message=message, preproc=False) + self.coder.move_back_cur_messages( + f"Model {model_name} made those changes to the files." + ) + self.coder.total_cost = temp_coder.total_cost + self.coder.aider_commit_hashes = temp_coder.aider_commit_hashes + + # Restore the original model configuration + raise SwitchCoder(main_model=original_main_model, edit_format=original_edit_format) + except Exception as e: + # If there's an error, still restore the original model + if not isinstance(e, SwitchCoder): + self.io.tool_error(e) + raise SwitchCoder( + main_model=original_main_model, edit_format=original_edit_format + ) + else: + # Re-raise SwitchCoder if that's what was thrown + raise + else: + raise SwitchCoder(main_model=model, edit_format=new_edit_format) async def cmd_weak_model(self, args): "Switch the Weak Model to a new LLM" diff --git a/aider/io.py b/aider/io.py index 4a754503e15..e0cdc5e7aae 100644 --- a/aider/io.py +++ b/aider/io.py @@ -2,6 +2,7 @@ import base64 import functools import os +import re import shutil import signal import subprocess @@ -32,6 +33,7 @@ from rich.columns import Columns from rich.console import Console from rich.markdown import Markdown +from rich.markup import escape from rich.spinner import SPINNERS from rich.style import Style as RichStyle from rich.text import Text @@ -294,6 +296,13 @@ class InputOutput: bell_on_next_input = False notifications_command = None encoding = "utf-8" + VALID_STYLES = {"bold", "red", "green", "blue", "orange"} + VALID_OPEN_TAG_PATTERN = re.compile( + r"\\\[(" + "|".join(re.escape(s) for s in VALID_STYLES) + r")\]" + ) + VALID_CLOSE_TAG_PATTERN = re.compile( + r"\\\[/(?:" + "|".join(re.escape(s) for s in VALID_STYLES) + r"|)]" + ) def __init__( self, @@ -1373,7 +1382,7 @@ def tool_output(self, *messages, log_only=False, bold=False): if log_only: return - messages = list(map(Text, messages)) + messages = list(map(lambda message: self.escape(message), messages)) style = dict() if self.pretty: if self.tool_output_color: @@ -1385,6 +1394,28 @@ def tool_output(self, *messages, log_only=False, bold=False): self.stream_print(*messages, style=style) + def escape(self, text): + """Formats valid Rich tags and prints invalid ones as literal text using a single regex pass.""" + + # 1. Escape everything initially using Rich's built-in function + escaped_text = escape(text) + + if text == escaped_text: + return Text(text) + + # 2. Un-escape ONLY the valid opening tags + # Replaces '\[style]' with '[style]' + unescaped_text = self.__class__.VALID_OPEN_TAG_PATTERN.sub( + lambda m: f"[{m.group(1)}]", escaped_text + ) + + # 3. Un-escape ONLY the valid closing tags (handles both [/style] and [/] formats) + # Replaces '\[/style]' or '\[/]' with '[/]' + final_text = self.__class__.VALID_CLOSE_TAG_PATTERN.sub(r"[/]", unescaped_text) + + # 4. Print the result + return Text(final_text) + def profile(self, *messages, start=False): if not self.verbose: return @@ -1656,7 +1687,7 @@ def format_files_for_input(self, rel_fnames, rel_read_only_fnames, rel_read_only return "\n".join(lines) + "\n" output = StringIO() - console = Console(file=output, force_terminal=False) + console = Console(file=output, force_terminal=False, markup=False) # Handle read-only files if rel_read_only_fnames or rel_read_only_stubs_fnames: diff --git a/aider/main.py b/aider/main.py index eb6ca8f80c4..0a9ede262d7 100644 --- a/aider/main.py +++ b/aider/main.py @@ -353,6 +353,13 @@ def register_models(git_root, model_settings_fname, io, verbose=False): io.tool_output(f" - {file_loaded}") # noqa: E221 elif verbose: io.tool_output("No model settings files loaded") + + if ( + model_settings_fname + and model_settings_fname not in files_loaded + and model_settings_fname != ".aider.model.settings.yml" + ): + io.tool_warning(f"Model Settings File Not Found: {model_settings_fname}") except Exception as e: io.tool_error(f"Error loading aider model settings: {e}") return 1 @@ -411,6 +418,13 @@ def register_litellm_models(git_root, model_metadata_fname, io, verbose=False): io.tool_output("Loaded model metadata from:") for model_metadata_file in model_metadata_files_loaded: io.tool_output(f" - {model_metadata_file}") # noqa: E221 + + if ( + model_metadata_fname + and model_metadata_fname not in model_metadata_files_loaded + and model_metadata_fname != ".aider.model.metadata.json" + ): + io.tool_warning(f"Model Metadata File Not Found: {model_metadata_fname}") except Exception as e: io.tool_error(f"Error loading model metadata models: {e}") return 1 @@ -1324,6 +1338,7 @@ def get_io(pretty): coder.suppress_announcements_for_next_prompt = True except SystemExit: analytics.event("exit", reason="/exit command") + sys.settrace(None) return await graceful_exit(coder) @@ -1419,6 +1434,8 @@ def load_slow_imports(swallow=True): async def graceful_exit(coder=None, exit_code=0): + sys.settrace(None) + if coder: if hasattr(coder, "_autosave_future"): await coder._autosave_future diff --git a/aider/versioncheck.py b/aider/versioncheck.py index 68aac2b28a6..81bc537df83 100644 --- a/aider/versioncheck.py +++ b/aider/versioncheck.py @@ -84,8 +84,9 @@ async def check_version(io, just_check=False, verbose=False): io.tool_output(f"Current version: {current_version}") io.tool_output(f"Latest version: {latest_version}") - is_update_available = packaging.version.parse(latest_version) > packaging.version.parse( - current_version + is_update_available = ( + packaging.version.parse(latest_version).release + > packaging.version.parse(current_version).release ) except Exception as err: io.tool_error(f"Error checking pypi for new version: {err}")