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: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 04b32bcb5d0..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) @@ -1085,7 +1109,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 +1125,9 @@ 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._processing_logic(user_message, preproc) - ) + self.io.output_task = asyncio.create_task(self._generate(user_message, preproc)) - await self.io.processing_task + await self.io.output_task self.io.ring_bell() user_message = None @@ -1114,8 +1136,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 +1149,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 +1161,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 +1173,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,8 +1185,8 @@ 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._processing_logic(self.user_message, preproc) + self.io.output_task = asyncio.create_task( + self._generate(self.user_message, preproc) ) self.user_message = "" @@ -1177,17 +1199,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 +1221,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 +1241,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 +1260,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 +1282,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,9 +1295,9 @@ 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): + async def _generate(self, user_message, preproc): await asyncio.sleep(0.1) try: @@ -2729,6 +2746,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: @@ -3215,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: @@ -3223,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/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" diff --git a/aider/io.py b/aider/io.py index 6b18569490b..03f7c36dee2 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 @@ -886,6 +886,7 @@ def get_continuation(width, line_number, is_soft_wrap): except EOFError: raise except KeyboardInterrupt: + await self.cancel_output_task() self.console.print() return "" except UnicodeEncodeError as err: @@ -961,13 +962,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 diff --git a/aider/main.py b/aider/main.py index c05c6da5a05..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 @@ -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: 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: 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, )