From 52fb12a3836671c9bd07006f72a2f15160e1006f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 13 Nov 2025 20:58:50 -0500 Subject: [PATCH 01/10] Bump Version --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index dc47253c91e..9be6a040faa 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.16.dev" +__version__ = "0.88.17.dev" safe_version = __version__ try: From 8cae354abeb655b015c4f479b48db904024a5f7a Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 13 Nov 2025 21:00:01 -0500 Subject: [PATCH 02/10] Auto acknolwledge pre-Coder confirmations, pass args dict() down through SwitchCoder events --- aider/main.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/aider/main.py b/aider/main.py index c05c6da5a05..9aa6f9cf059 100644 --- a/aider/main.py +++ b/aider/main.py @@ -447,7 +447,9 @@ async def sanity_check_repo(repo, io): io.tool_error("Aider only works with git repos with version number 1 or 2.") io.tool_output("You may be able to convert your repo: git update-index --index-version=2") io.tool_output("Or run aider --no-git to proceed without using git.") - await io.offer_url(urls.git_index_version, "Open documentation url for more info?") + await io.offer_url( + urls.git_index_version, "Open documentation url for more info?", acknowledge=True + ) return False io.tool_error("Unable to read git repository, it may be corrupt?") @@ -891,7 +893,9 @@ def get_io(pretty): io.tool_error( f"Unable to proceed without an OpenRouter API key for model '{args.model}'." ) - await io.offer_url(urls.models_and_keys, "Open documentation URL for more info?") + await io.offer_url( + urls.models_and_keys, "Open documentation URL for more info?", acknowledge=True + ) analytics.event( "exit", reason="OpenRouter key missing for specified model and OAuth failed/declined", @@ -1120,7 +1124,9 @@ def get_io(pretty): except UnknownEditFormat as err: io.tool_error(str(err)) - await io.offer_url(urls.edit_formats, "Open documentation about edit formats?") + await io.offer_url( + urls.edit_formats, "Open documentation about edit formats?", acknowledge=True + ) analytics.event("exit", reason="Unknown edit format") return 1 except ValueError as err: @@ -1217,6 +1223,7 @@ def get_io(pretty): urls.release_notes, "Would you like to see what's new in this version?", allow_never=False, + acknowledge=True, ) if git_root and Path.cwd().resolve() != Path(git_root).resolve(): @@ -1300,6 +1307,7 @@ def get_io(pretty): # Disable cache warming for the new coder kwargs["num_cache_warming_pings"] = 0 + kwargs["args"] = coder.args coder = await Coder.create(**kwargs) @@ -1365,7 +1373,9 @@ async def check_and_load_imports(io, is_first_run, verbose=False): except Exception as err: io.tool_error(str(err)) io.tool_output("Error loading required imports. Did you install aider properly?") - await io.offer_url(urls.install_properly, "Open documentation url for more info?") + await io.offer_url( + urls.install_properly, "Open documentation url for more info?", acknowledge=True + ) sys.exit(1) if verbose: From de686d02274a86060ac5bdc63fb0e960f9cca94e Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 13 Nov 2025 23:33:14 -0500 Subject: [PATCH 03/10] Handle exiting in linear output mode --- aider/commands.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 1a2802350da..c54e5f7be68 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1248,7 +1248,14 @@ async def cmd_exit(self, args): pass await asyncio.sleep(0) - sys.exit() + + try: + if self.coder.args.linear_output: + os._exit(0) + else: + sys.exit() + except Exception: + sys.exit() def cmd_quit(self, args): "Exit the application" From be5f8f385d928c0259519fad7c2572ec5729dfc5 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 13 Nov 2025 23:34:24 -0500 Subject: [PATCH 04/10] #140: Expand user path in generate_search_path list --- aider/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/main.py b/aider/main.py index 9aa6f9cf059..f4bd9b8c827 100644 --- a/aider/main.py +++ b/aider/main.py @@ -321,7 +321,7 @@ def generate_search_path_list(default_file, git_root, command_line_file): resolved_files = [] for fn in files: try: - resolved_files.append(Path(fn).resolve()) + resolved_files.append(Path(fn).expanduser().resolve()) except OSError: pass From 21f30a16c7c2176ef6ce3bc0359b0a8a46b2c12f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 00:09:56 -0500 Subject: [PATCH 05/10] Rename processing_task to output_task for clarity --- aider/coders/base_coder.py | 56 ++++++++++++++++++-------------------- aider/io.py | 14 +++++----- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 04b32bcb5d0..1d3365c44d5 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1085,7 +1085,7 @@ async def _run_linear(self, with_message=None, preproc=True): user_message = None await self.io.cancel_input_task() - await self.io.cancel_processing_task() + await self.io.cancel_output_task() while True: try: @@ -1101,11 +1101,11 @@ async def _run_linear(self, with_message=None, preproc=True): await self.io.input_task user_message = self.io.input_task.result() - self.io.processing_task = asyncio.create_task( + self.io.output_task = asyncio.create_task( self._processing_logic(user_message, preproc) ) - await self.io.processing_task + await self.io.output_task self.io.ring_bell() user_message = None @@ -1114,8 +1114,8 @@ async def _run_linear(self, with_message=None, preproc=True): self.io.set_placeholder("") await self.io.cancel_input_task() - if self.io.processing_task: - await self.io.cancel_processing_task() + if self.io.output_task: + await self.io.cancel_output_task() self.io.stop_spinner() self.keyboard_interrupt() @@ -1127,7 +1127,7 @@ async def _run_linear(self, with_message=None, preproc=True): return finally: await self.io.cancel_input_task() - await self.io.cancel_processing_task() + await self.io.cancel_output_task() async def _run_patched(self, with_message=None, preproc=True): try: @@ -1139,7 +1139,7 @@ async def _run_patched(self, with_message=None, preproc=True): user_message = None self.user_message = "" await self.io.cancel_input_task() - await self.io.cancel_processing_task() + await self.io.cancel_output_task() while True: try: @@ -1151,7 +1151,7 @@ async def _run_patched(self, with_message=None, preproc=True): or self.io.input_task.done() or self.io.input_task.cancelled() ) - and (not self.io.processing_task or not self.io.placeholder) + and (not self.io.output_task or not self.io.placeholder) ): if not self.suppress_announcements_for_next_prompt: self.show_announcements() @@ -1163,7 +1163,7 @@ async def _run_patched(self, with_message=None, preproc=True): await self.io.recreate_input() if self.user_message: - self.io.processing_task = asyncio.create_task( + self.io.output_task = asyncio.create_task( self._processing_logic(self.user_message, preproc) ) @@ -1177,17 +1177,14 @@ async def _run_patched(self, with_message=None, preproc=True): tasks = set() - if self.io.processing_task: - if self.io.processing_task.done(): - exception = self.io.processing_task.exception() + if self.io.output_task: + if self.io.output_task.done(): + exception = self.io.output_task.exception() if exception: if isinstance(exception, SwitchCoder): - await self.io.processing_task - elif ( - not self.io.processing_task.done() - and not self.io.processing_task.cancelled() - ): - tasks.add(self.io.processing_task) + await self.io.output_task + elif not self.io.output_task.done() and not self.io.output_task.cancelled(): + tasks.add(self.io.output_task) if ( self.io.input_task @@ -1202,9 +1199,9 @@ async def _run_patched(self, with_message=None, preproc=True): ) if self.io.input_task and self.io.input_task in done: - if self.io.processing_task: + if self.io.output_task: if not self.io.confirmation_in_progress: - await self.io.cancel_processing_task() + await self.io.cancel_output_task() self.io.stop_spinner() try: @@ -1222,10 +1219,10 @@ async def _run_patched(self, with_message=None, preproc=True): await self.io.cancel_input_task() continue - if self.io.processing_task and self.io.processing_task in pending: + if self.io.output_task and self.io.output_task in pending: try: tasks = set() - tasks.add(self.io.processing_task) + tasks.add(self.io.output_task) # We just did a confirmation so add a new input task if self.io.get_confirmation_acknowledgement(): @@ -1241,7 +1238,7 @@ async def _run_patched(self, with_message=None, preproc=True): and self.io.input_task in done and not self.io.confirmation_in_progress ): - await self.io.cancel_processing_task() + await self.io.cancel_output_task() self.io.stop_spinner() self.io.acknowledge_confirmation() @@ -1263,14 +1260,12 @@ async def _run_patched(self, with_message=None, preproc=True): self.io.ring_bell() user_message = None except KeyboardInterrupt: - if self.io.input_task: - self.io.set_placeholder("") - await self.io.cancel_input_task() + self.io.set_placeholder("") - if self.io.processing_task: - await self.io.cancel_processing_task() - self.io.stop_spinner() + await self.io.cancel_input_task() + await self.io.cancel_output_task() + self.io.stop_spinner() self.keyboard_interrupt() self.auto_save_session() @@ -1278,7 +1273,7 @@ async def _run_patched(self, with_message=None, preproc=True): return finally: await self.io.cancel_input_task() - await self.io.cancel_processing_task() + await self.io.cancel_output_task() async def _processing_logic(self, user_message, preproc): await asyncio.sleep(0.1) @@ -2729,6 +2724,7 @@ async def check_for_file_mentions(self, content): if await self.io.confirm_ask( "Add file to the chat?", subject=rel_fname, group=group, allow_never=True ): + await self.io.recreate_input() self.add_rel_fname(rel_fname) added_fnames.append(rel_fname) else: diff --git a/aider/io.py b/aider/io.py index 6b18569490b..3d9c1bb4277 100644 --- a/aider/io.py +++ b/aider/io.py @@ -343,7 +343,7 @@ def __init__( # Variables used to interface with base_coder self.coder = None self.input_task = None - self.processing_task = None + self.output_task = None # State tracking for confirmation input self.confirmation_in_progress = False @@ -961,13 +961,13 @@ async def cancel_input_task(self): except (asyncio.CancelledError, EOFError, IndexError): pass - async def cancel_processing_task(self): - if self.processing_task: - processing_task = self.processing_task - self.processing_task = None + async def cancel_output_task(self): + if self.output_task: + output_task = self.output_task + self.output_task = None try: - processing_task.cancel() - await processing_task + output_task.cancel() + await output_task except (asyncio.CancelledError, EOFError, IndexError): pass From 340178c2612cf5bf1214fc6e7e8f040415aa0ff9 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 00:12:59 -0500 Subject: [PATCH 06/10] _processing_logic to _generate for clarity --- aider/coders/base_coder.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 1d3365c44d5..adc4f58893d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1101,9 +1101,7 @@ async def _run_linear(self, with_message=None, preproc=True): await self.io.input_task user_message = self.io.input_task.result() - self.io.output_task = asyncio.create_task( - self._processing_logic(user_message, preproc) - ) + self.io.output_task = asyncio.create_task(self._generate(user_message, preproc)) await self.io.output_task @@ -1164,7 +1162,7 @@ async def _run_patched(self, with_message=None, preproc=True): if self.user_message: self.io.output_task = asyncio.create_task( - self._processing_logic(self.user_message, preproc) + self._generate(self.user_message, preproc) ) self.user_message = "" @@ -1275,7 +1273,7 @@ async def _run_patched(self, with_message=None, preproc=True): await self.io.cancel_input_task() await self.io.cancel_output_task() - async def _processing_logic(self, user_message, preproc): + async def _generate(self, user_message, preproc): await asyncio.sleep(0.1) try: From 697ccca2219e083274d424de6f0b6dd309019773 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 00:22:06 -0500 Subject: [PATCH 07/10] Update repo sanity check test --- tests/basic/test_sanity_check_repo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/basic/test_sanity_check_repo.py b/tests/basic/test_sanity_check_repo.py index 5c3afeb4a3e..5a45cc48daf 100644 --- a/tests/basic/test_sanity_check_repo.py +++ b/tests/basic/test_sanity_check_repo.py @@ -135,6 +135,7 @@ async def test_git_index_version_greater_than_2(mock_browser, create_repo, mock_ mock_io.offer_url.assert_any_call( urls.git_index_version, "Open documentation url for more info?", + acknowledge=True, ) From 752721604e1c760fe2aea3865ef1c7bb65a9f987 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 01:25:30 -0500 Subject: [PATCH 08/10] Cancel output task on keyboard interrupt --- aider/io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aider/io.py b/aider/io.py index 3d9c1bb4277..9a035f63412 100644 --- a/aider/io.py +++ b/aider/io.py @@ -887,6 +887,7 @@ def get_continuation(width, line_number, is_soft_wrap): raise except KeyboardInterrupt: self.console.print() + await self.cancel_output_task() return "" except UnicodeEncodeError as err: self.tool_error(str(err)) From 1b8a42c50bfa7249a90f4f67be69b6774f3ab21a Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 01:26:12 -0500 Subject: [PATCH 09/10] Cancel task and then print new line --- aider/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/io.py b/aider/io.py index 9a035f63412..03f7c36dee2 100644 --- a/aider/io.py +++ b/aider/io.py @@ -886,8 +886,8 @@ def get_continuation(width, line_number, is_soft_wrap): except EOFError: raise except KeyboardInterrupt: - self.console.print() await self.cancel_output_task() + self.console.print() return "" except UnicodeEncodeError as err: self.tool_error(str(err)) From f364f30c536a1df7830b3fbb14bf1c204b9f4ead Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Fri, 14 Nov 2025 02:29:33 -0500 Subject: [PATCH 10/10] Add caching to prevent unnecessarily iterating over the entire file system in large repos for RepoMap calculation --- aider/coders/base_coder.py | 103 ++++++++++++++++++++++++------------- aider/repomap.py | 5 +- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index adc4f58893d..48159af9dd1 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -475,7 +475,6 @@ def __init__( self.dry_run = dry_run self.pretty = self.io.pretty self.linear_output = linear_output - self.main_model = main_model # Set the reasoning tag name based on model settings or default @@ -493,6 +492,8 @@ def __init__( self.commands = commands or Commands(self.io, self) self.commands.coder = self + self.data_cache = {"repo": {"last_key": ""}, "relative_files": None} + self.repo = repo if use_git and self.repo is None: try: @@ -877,41 +878,64 @@ def get_repo_map(self, force_refresh=False): self.io.update_spinner("Updating repo map") cur_msg_text = self.get_cur_message_text() - mentioned_fnames = self.get_file_mentions(cur_msg_text) - mentioned_idents = self.get_ident_mentions(cur_msg_text) + staged_files_hash = hash(str([item.a_path for item in self.repo.repo.index.diff("HEAD")])) + read_only_count = len(set(self.abs_read_only_fnames)) + len( + set(self.abs_read_only_stubs_fnames) + ) + self.data_cache["repo"]["mentioned_idents"] = self.get_ident_mentions(cur_msg_text) - mentioned_fnames.update(self.get_ident_filename_matches(mentioned_idents)) + if ( + staged_files_hash != self.data_cache["repo"]["last_key"] + or read_only_count != self.data_cache["repo"]["read_only_count"] + ): + self.data_cache["repo"]["last_key"] = staged_files_hash - all_abs_files = set(self.get_all_abs_files()) + mentioned_idents = self.data_cache["repo"]["mentioned_idents"] + mentioned_fnames = self.get_file_mentions(cur_msg_text) + mentioned_fnames.update(self.get_ident_filename_matches(mentioned_idents)) - # Exclude metadata/docs from repo map inputs to reduce parsing overhead - def _include_in_map(abs_path): - try: - rel = self.get_rel_fname(abs_path) - except Exception: - rel = str(abs_path) - parts = Path(rel).parts - if ".meta" in parts or ".docs" in parts: - return False - if ".min." in parts[-1]: - return False - if self.repo.git_ignored_file(abs_path): - return False - return True + all_abs_files = set(self.get_all_abs_files()) - all_abs_files = {p for p in all_abs_files if _include_in_map(p)} - repo_abs_read_only_fnames = set(self.abs_read_only_fnames) & all_abs_files - repo_abs_read_only_stubs_fnames = set(self.abs_read_only_stubs_fnames) & all_abs_files - chat_files = ( - set(self.abs_fnames) | repo_abs_read_only_fnames | repo_abs_read_only_stubs_fnames - ) - other_files = all_abs_files - chat_files + # Exclude metadata/docs from repo map inputs to reduce parsing overhead + def _include_in_map(abs_path): + try: + rel = self.get_rel_fname(abs_path) + except Exception: + rel = str(abs_path) + parts = Path(rel).parts + if ".meta" in parts or ".docs" in parts: + return False + if ".min." in parts[-1]: + return False + if self.repo.git_ignored_file(abs_path): + return False + return True + + all_abs_files = {p for p in all_abs_files if _include_in_map(p)} + repo_abs_read_only_fnames = set(self.abs_read_only_fnames) & all_abs_files + repo_abs_read_only_stubs_fnames = set(self.abs_read_only_stubs_fnames) & all_abs_files + chat_files = ( + set(self.abs_fnames) | repo_abs_read_only_fnames | repo_abs_read_only_stubs_fnames + ) + other_files = all_abs_files - chat_files + + self.data_cache["repo"].update( + { + "chat_files": chat_files, + "other_files": other_files, + "mentioned_fnames": mentioned_fnames, + "all_abs_files": all_abs_files, + "read_only_count": len(set(self.abs_read_only_fnames)) + len( + set(self.abs_read_only_stubs_fnames) + ), + } + ) repo_content = self.repo_map.get_repo_map( - chat_files, - other_files, - mentioned_fnames=mentioned_fnames, - mentioned_idents=mentioned_idents, + self.data_cache["repo"]["chat_files"], + self.data_cache["repo"]["other_files"], + mentioned_fnames=self.data_cache["repo"]["mentioned_fnames"], + mentioned_idents=self.data_cache["repo"]["mentioned_idents"], force_refresh=force_refresh, ) @@ -919,16 +943,16 @@ def _include_in_map(abs_path): if not repo_content: repo_content = self.repo_map.get_repo_map( set(), - all_abs_files, - mentioned_fnames=mentioned_fnames, - mentioned_idents=mentioned_idents, + self.data_cache["repo"]["all_abs_files"], + mentioned_fnames=self.data_cache["repo"]["mentioned_fnames"], + mentioned_idents=self.data_cache["repo"]["mentioned_idents"], ) # fall back to completely unhinted repo if not repo_content: repo_content = self.repo_map.get_repo_map( set(), - all_abs_files, + self.data_cache["repo"]["all_abs_files"], ) self.io.update_spinner(self.io.last_spinner_text) @@ -3209,6 +3233,13 @@ def is_file_safe(self, fname): return def get_all_relative_files(self): + staged_files_hash = hash(str([item.a_path for item in self.repo.repo.index.diff("HEAD")])) + if ( + staged_files_hash == self.data_cache["repo"]["last_key"] + and self.data_cache["relative_files"] + ): + return self.data_cache["relative_files"] + if self.repo: files = self.repo.get_tracked_files() else: @@ -3217,7 +3248,9 @@ def get_all_relative_files(self): # This is quite slow in large repos # files = [fname for fname in files if self.is_file_safe(fname)] - return sorted(set(files)) + self.data_cache["relative_files"] = sorted(set(files)) + + return self.data_cache["relative_files"] def get_all_abs_files(self): files = self.get_all_relative_files() diff --git a/aider/repomap.py b/aider/repomap.py index b1af7d176d1..2408ebcd58a 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -810,7 +810,7 @@ def get_ranked_tags_map( # Create a cache key cache_key = [ tuple(sorted(chat_fnames)) if chat_fnames else None, - tuple(sorted(other_fnames)) if other_fnames else None, + len(other_fnames) if other_fnames else None, max_map_tokens, ] @@ -819,7 +819,8 @@ def get_ranked_tags_map( tuple(sorted(mentioned_fnames)) if mentioned_fnames else None, tuple(sorted(mentioned_idents)) if mentioned_idents else None, ] - cache_key = tuple(cache_key) + + cache_key = hash(str(tuple(cache_key))) use_cache = False if not force_refresh: