diff --git a/aider/__init__.py b/aider/__init__.py index 89edca9d96d..a9686a84cb3 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.13.dev" +__version__ = "0.88.14.dev" safe_version = __version__ try: diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index c97bd041410..f11de0976a1 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -23,6 +23,7 @@ from aider.mcp.server import LocalServer from aider.repo import ANY_GIT_ERROR +# Import tool modules for registry # Import tool modules for registry from aider.tools import ( command, @@ -32,8 +33,10 @@ delete_lines, extract_lines, finished, + git_branch, git_diff, git_log, + git_remote, git_show, git_status, grep, @@ -159,8 +162,10 @@ def _build_tool_registry(self): delete_lines, extract_lines, finished, + git_branch, git_diff, git_log, + git_remote, git_show, git_status, grep, diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index f48b3ec2cb2..3f78eaf50f4 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1097,8 +1097,7 @@ async def _run_linear(self, with_message=None, preproc=True): self.show_announcements() self.suppress_announcements_for_next_prompt = True - self.io.input_task = asyncio.create_task(self.get_input()) - await asyncio.sleep(0) + await self.io.recreate_input() await self.io.input_task user_message = self.io.input_task.result() @@ -1161,10 +1160,7 @@ async def _run_patched(self, with_message=None, preproc=True): # Stop spinner before showing announcements or getting input self.io.stop_spinner() self.copy_context() - self.io.input_task = asyncio.create_task(self.get_input()) - - # Yield Control so input can actually get properly set up - await asyncio.sleep(0) + await self.io.recreate_input() if self.user_message: self.io.processing_task = asyncio.create_task( @@ -1232,11 +1228,8 @@ async def _run_patched(self, with_message=None, preproc=True): 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()) + if self.io.get_confirmation_acknowledgement(): + await self.io.recreate_input() tasks.add(self.io.input_task) done, pending = await asyncio.wait( @@ -1331,6 +1324,9 @@ async def preproc_user_input(self, inp): if not inp: return + # Strip whitespace from beginning and end + inp = inp.strip() + if self.commands.is_command(inp): if inp[0] in "!": inp = f"/run {inp[1:]}" diff --git a/aider/io.py b/aider/io.py index ae27a9ff3ab..35d9fdb083f 100644 --- a/aider/io.py +++ b/aider/io.py @@ -677,9 +677,6 @@ def rule(self): print() def interrupt_input(self): - 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 self.placeholder = self.prompt_session.app.current_buffer.text @@ -695,6 +692,14 @@ def reject_outstanding_confirmations(self): # This method is now a no-op since we removed the confirmation_future logic pass + async def recreate_input(self, future=None): + if 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) + async def get_input( self, root, @@ -1023,11 +1028,15 @@ def ai_output(self, content): hist = "\n" + content.strip() + "\n\n" self.append_chat_history(hist) - async def offer_url(self, url, prompt="Open URL for more info?", allow_never=True): + async def offer_url( + self, url, prompt="Open URL for more info?", allow_never=True, acknowledge=False + ): """Offer to open a URL in the browser, returns True if opened.""" if url in self.never_prompts: return False - if await self.confirm_ask(prompt, subject=url, allow_never=allow_never): + if await self.confirm_ask( + prompt, subject=url, allow_never=allow_never, acknowledge=acknowledge + ): webbrowser.open(url) return True return False @@ -1068,6 +1077,7 @@ async def _confirm_ask( group=None, group_response=None, allow_never=False, + acknowledge=False, ): self.num_user_asks += 1 @@ -1126,16 +1136,7 @@ async def _confirm_ask( while True: try: if self.prompt_session: - if ( - 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) + await self.recreate_input() if ( self.input_task @@ -1168,7 +1169,8 @@ async def _confirm_ask( good = any(valid_response.startswith(res) for valid_response in valid_responses) if good: - self.set_confirmation_acknowledgement() + if not acknowledge: + self.set_confirmation_acknowledgement() self.start_spinner(self.last_spinner_text) break diff --git a/aider/main.py b/aider/main.py index 0c58da839eb..46f613bb2f7 100644 --- a/aider/main.py +++ b/aider/main.py @@ -971,19 +971,6 @@ def get_io(pretty): analytics.event("exit", reason="Invalid lint command format") return 1 - if args.show_model_warnings: - problem = await models.sanity_check_models(io, main_model) - if problem: - analytics.event("model warning", main_model=main_model) - io.tool_output("You can skip this check with --no-show-model-warnings") - - try: - await io.offer_url(urls.model_warnings, "Open documentation url for more info?") - io.tool_output() - except KeyboardInterrupt: - analytics.event("exit", reason="Keyboard interrupt during model warnings") - return 1 - repo = None if args.git: try: @@ -1113,6 +1100,24 @@ def get_io(pretty): preserve_todo_list=args.preserve_todo_list, linear_output=args.linear_output, ) + + if args.show_model_warnings: + problem = await models.sanity_check_models(io, main_model) + if problem: + analytics.event("model warning", main_model=main_model) + io.tool_output("You can skip this check with --no-show-model-warnings") + + try: + await io.offer_url( + urls.model_warnings, + "Open documentation url for more info?", + acknowledge=True, + ) + io.tool_output() + except KeyboardInterrupt: + analytics.event("exit", reason="Keyboard interrupt during model warnings") + return 1 + except UnknownEditFormat as err: io.tool_error(str(err)) await io.offer_url(urls.edit_formats, "Open documentation about edit formats?") diff --git a/aider/sessions.py b/aider/sessions.py index cfff99fe98a..f4a29771981 100644 --- a/aider/sessions.py +++ b/aider/sessions.py @@ -5,6 +5,8 @@ from pathlib import Path from typing import Dict, List, Optional +from aider import models + class SessionManager: """Manages chat session saving, listing, and loading.""" @@ -128,6 +130,9 @@ def _build_session_data(self, session_name) -> Dict: "version": 1, "session_name": session_name, "model": self.coder.main_model.name, + "weak_model": self.coder.main_model.weak_model.name, + "editor_model": self.coder.main_model.editor_model.name, + "editor_edit_format": self.coder.main_model.editor_edit_format, "edit_format": self.coder.edit_format, "chat_history": { "done_messages": self.coder.done_messages, @@ -207,6 +212,17 @@ def _apply_session_data(self, session_data: Dict, session_file: Path) -> bool: else: self.io.tool_warning(f"File not found, skipping: {rel_fname}") + if session_data.get("model"): + self.coder.main_model = models.Model( + session_data.get("model", self.coder.args.model), + weak_model=session_data.get("weak_model", self.coder.args.weak_model), + editor_model=session_data.get("editor_model", self.coder.args.editor_model), + editor_edit_format=session_data.get( + "editor_edit_format", self.coder.args.editor_edit_format + ), + verbose=self.coder.args.verbose, + ) + # Load settings settings = session_data.get("settings", {}) if "auto_commits" in settings: diff --git a/aider/tools/__init__.py b/aider/tools/__init__.py index c1b2f6c710d..60b3b06c5af 100644 --- a/aider/tools/__init__.py +++ b/aider/tools/__init__.py @@ -10,8 +10,10 @@ delete_lines, extract_lines, finished, + git_branch, git_diff, git_log, + git_remote, git_show, git_status, grep, @@ -43,8 +45,10 @@ delete_lines, extract_lines, finished, + git_branch, git_diff, git_log, + git_remote, git_show, git_status, grep, diff --git a/aider/tools/command.py b/aider/tools/command.py index 6c7427a117c..a052327aef9 100644 --- a/aider/tools/command.py +++ b/aider/tools/command.py @@ -1,6 +1,4 @@ # Import necessary functions -import asyncio - from aider.run_cmd import run_cmd_subprocess schema = { @@ -46,10 +44,7 @@ async def _execute_command(coder, command_string): ) ) - 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) + await coder.io.recreate_input() if not confirmed: # This happens if the user explicitly says 'no' this time. diff --git a/aider/tools/command_interactive.py b/aider/tools/command_interactive.py index 0af5b1488df..eae1b94e1ae 100644 --- a/aider/tools/command_interactive.py +++ b/aider/tools/command_interactive.py @@ -69,10 +69,7 @@ async def _execute_command_interactive(coder, command_string): 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) + await coder.io.recreate_input() # Format the output for the result message, include more content output_content = combined_output or "" diff --git a/aider/tools/git_branch.py b/aider/tools/git_branch.py new file mode 100644 index 00000000000..6a8d2c75d3d --- /dev/null +++ b/aider/tools/git_branch.py @@ -0,0 +1,129 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitBranch", + "description": ( + "List branches in the repository with various filtering and formatting options." + ), + "parameters": { + "type": "object", + "properties": { + "remotes": { + "type": "boolean", + "description": "List remote-tracking branches (-r/--remotes flag)", + }, + "all": { + "type": "boolean", + "description": "List both local and remote branches (-a/--all flag)", + }, + "verbose": { + "type": "boolean", + "description": ( + "Show verbose information including commit hash and subject (-v flag)" + ), + }, + "very_verbose": { + "type": "boolean", + "description": ( + "Show very verbose information including upstream branch (-vv flag)" + ), + }, + "merged": { + "type": "string", + "description": "Show branches merged into specified commit (--merged flag)", + }, + "no_merged": { + "type": "string", + "description": ( + "Show branches not merged into specified commit (--no-merged flag)" + ), + }, + "sort": { + "type": "string", + "description": ( + "Sort branches by key (committerdate, authordate, refname, etc.) (--sort" + " flag)" + ), + }, + "format": { + "type": "string", + "description": "Custom output format using placeholders (--format flag)", + }, + "show_current": { + "type": "boolean", + "description": "Show only current branch name (--show-current flag)", + }, + }, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitbranch" + + +def _execute_git_branch(coder, params=None): + """ + List branches in the repository with various filtering and formatting options. + """ + if not coder.repo: + return "Not in a git repository." + + try: + # Build git command arguments + args = [] + + # Handle boolean flags + if params: + if params.get("remotes"): + args.append("--remotes") + if params.get("all"): + args.append("--all") + if params.get("verbose"): + args.append("--verbose") + if params.get("very_verbose"): + args.append("--verbose") + args.append("--verbose") + if params.get("show_current"): + args.append("--show-current") + + # Handle string parameters + if params.get("merged"): + args.extend(["--merged", params["merged"]]) + if params.get("no_merged"): + args.extend(["--no-merged", params["no_merged"]]) + if params.get("sort"): + args.extend(["--sort", params["sort"]]) + if params.get("format"): + args.extend(["--format", params["format"]]) + + # Execute git command + result = coder.repo.repo.git.branch(*args) + + # If no result and show_current was used, get current branch directly + if not result and params and params.get("show_current"): + current_branch = coder.repo.repo.active_branch.name + return current_branch + + return result if result else "No branches found matching the criteria." + + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git branch: {e}") + return f"Error running git branch: {e}" + + +def process_response(coder, params): + """ + Process the GitBranch tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters for GitBranch + + Returns: + str: Result message + """ + return _execute_git_branch(coder, params) diff --git a/aider/tools/git_remote.py b/aider/tools/git_remote.py new file mode 100644 index 00000000000..5f839683c17 --- /dev/null +++ b/aider/tools/git_remote.py @@ -0,0 +1,53 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitRemote", + "description": "List remote repositories.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitremote" + + +def _execute_git_remote(coder): + """ + List remote repositories. + """ + if not coder.repo: + return "Not in a git repository." + + try: + remotes = coder.repo.repo.remotes + if not remotes: + return "No remotes configured." + + result = [] + for remote in remotes: + result.append(f"{remote.name}\t{remote.url}") + return "\n".join(result) + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git remote: {e}") + return f"Error running git remote: {e}" + + +def process_response(coder, params): + """ + Process the GitRemote tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters (should be empty for GitRemote) + + Returns: + str: Result message + """ + # GitRemote tool has no parameters to validate + return _execute_git_remote(coder)