From f6ad53ea8c2aa27604ac4aab2f27aaf06c17b95e Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Sun, 31 Aug 2025 20:05:44 -0400 Subject: [PATCH 1/7] added julia tree sitter --- .../tree-sitter-languages/julia-tags.scm | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 aider/queries/tree-sitter-languages/julia-tags.scm diff --git a/aider/queries/tree-sitter-languages/julia-tags.scm b/aider/queries/tree-sitter-languages/julia-tags.scm new file mode 100644 index 00000000000..efdd6466620 --- /dev/null +++ b/aider/queries/tree-sitter-languages/julia-tags.scm @@ -0,0 +1,57 @@ +(module + name: (identifier) @name.definition.module) @definition.module + +(module + name: (scoped_identifier) @name.definition.module) @definition.module + +(struct_definition + name: (type_identifier) @name.definition.class) @definition.class + +(mutable_struct_definition + name: (type_identifier) @name.definition.class) @definition.class + +(abstract_type_declaration + name: (type_identifier) @name.definition.class) @definition.class + +(constant_assignment + left: (identifier) @name.definition.class) @definition.class + +(function_definition + name: (identifier) @name.definition.function) @definition.function + +(function_definition + name: (scoped_identifier) @name.definition.function) @definition.function + +(assignment + left: (call_expression + function: (identifier) @name.definition.function)) @definition.function + +(method_definition + name: (identifier) @name.definition.method) @definition.method + +(macro_definition + name: (identifier) @name.definition.macro) @definition.macro + +(macro_call + name: (identifier) @name.reference.call) @reference.call + +(call_expression + function: (identifier) @name.reference.call) @reference.call + +(call_expression + function: (scoped_identifier) @name.reference.call) @reference.call + +(type_expression + name: (type_identifier) @name.reference.type) @reference.type + +(constant_assignment + left: (identifier) @name.definition.constant) @definition.constant + +(export_statement + (identifier) @name.reference.export) @reference.export + +(using_statement + (identifier) @name.reference.module) @reference.module + +(import_statement + (identifier) @name.reference.module) @reference.module From 4a1e5f3d52027658a360ef4d88868a8d5968928b Mon Sep 17 00:00:00 2001 From: Matteo Landi Date: Tue, 6 May 2025 18:29:03 +0200 Subject: [PATCH 2/7] feat: Disable history for confirm_ask prompts to avoid clutter --- aider/io.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/aider/io.py b/aider/io.py index ed6f22d51ae..e4c06acf0d5 100644 --- a/aider/io.py +++ b/aider/io.py @@ -71,6 +71,31 @@ def wrapper(self, *args, **kwargs): return wrapper +def without_input_history(func): + """Decorator to temporarily disable history saving for the prompt session buffer.""" + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + orig_buf_append = None + try: + orig_buf_append = self.prompt_session.default_buffer.append_to_history + self.prompt_session.default_buffer.append_to_history = ( + lambda: None + ) # Replace with no-op + except AttributeError: + pass + + try: + return func(self, *args, **kwargs) + except Exception: + raise + finally: + if orig_buf_append: + self.prompt_session.default_buffer.append_to_history = orig_buf_append + + return wrapper + + class CommandCompletionException(Exception): """Raised when a command should use the normal autocompleter instead of command-specific completion.""" @@ -804,6 +829,7 @@ def offer_url(self, url, prompt="Open URL for more info?", allow_never=True): return False @restore_multiline + @without_input_history def confirm_ask( self, question, From 60c578e2a1631294f5e6e4d181fe18f5ff98a313 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Fri, 5 Sep 2025 02:20:09 -0400 Subject: [PATCH 3/7] added source + license --- aider/queries/tree-sitter-languages/julia-tags.scm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/queries/tree-sitter-languages/julia-tags.scm b/aider/queries/tree-sitter-languages/julia-tags.scm index efdd6466620..b7d33d93b6c 100644 --- a/aider/queries/tree-sitter-languages/julia-tags.scm +++ b/aider/queries/tree-sitter-languages/julia-tags.scm @@ -1,3 +1,6 @@ +;; derived from: https://github.com/tree-sitter/tree-sitter-julia +;; License: MIT + (module name: (identifier) @name.definition.module) @definition.module From 660ab4926e86ea89b782c7f4a5f9224b80894cc9 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 6 Sep 2025 23:05:08 -0400 Subject: [PATCH 4/7] Bump Version --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 9eb73a1f92d..bd9897115d8 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.87.6.dev" +__version__ = "0.87.7.dev" safe_version = __version__ try: From a2a2495ab90ed91dff91f9a2d6e1296c2f0f98c3 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 7 Sep 2025 11:07:15 -0400 Subject: [PATCH 5/7] Merge Aider PR 3056: Read-Only Stubs, Fix for fuzzy finding compatibility and add test --- aider/coders/base_coder.py | 66 +++++++-- aider/commands.py | 272 ++++++++++++++++++++++++++++--------- aider/io.py | 95 ++++++++----- aider/repomap.py | 32 +++++ tests/basic/test_io.py | 67 ++++++--- 5 files changed, 400 insertions(+), 132 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 798198abb0f..45038cd14e2 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -91,6 +91,7 @@ def wrap_fence(name): class Coder: abs_fnames = None abs_read_only_fnames = None + abs_read_only_stubs_fnames = None repo = None last_aider_commit_hash = None aider_edited_files = None @@ -184,8 +185,10 @@ def create( # Bring along context from the old Coder update = dict( fnames=list(from_coder.abs_fnames), - # Copy read-only files - read_only_fnames=list(from_coder.abs_read_only_fnames), + read_only_fnames=list(from_coder.abs_read_only_fnames), # Copy read-only files + read_only_stubs_fnames=list( + from_coder.abs_read_only_stubs_fnames + ), # Copy read-only stubs done_messages=done_messages, cur_messages=from_coder.cur_messages, aider_commit_hashes=from_coder.aider_commit_hashes, @@ -301,6 +304,10 @@ def get_announcements(self): rel_fname = self.get_rel_fname(fname) lines.append(f"Added {rel_fname} to the chat (read-only).") + for fname in self.abs_read_only_stubs_fnames: + rel_fname = self.get_rel_fname(fname) + lines.append(f"Added {rel_fname} to the chat (read-only stub).") + if self.done_messages: lines.append("Restored previous conversation history.") @@ -319,6 +326,7 @@ def __init__( fnames=None, add_gitignore_files=False, read_only_fnames=None, + read_only_stubs_fnames=None, show_diffs=False, auto_commits=True, dirty_commits=True, @@ -420,6 +428,7 @@ def __init__( self.abs_fnames = set() self.abs_read_only_fnames = set() self.add_gitignore_files = add_gitignore_files + self.abs_read_only_stubs_fnames = set() if cur_messages: self.cur_messages = cur_messages @@ -513,6 +522,17 @@ def __init__( else: self.io.tool_warning(f"Error: Read-only file {fname} does not exist. Skipping.") + if read_only_stubs_fnames: + self.abs_read_only_stubs_fnames = set() + for fname in read_only_stubs_fnames: + abs_fname = self.abs_root_path(fname) + if os.path.exists(abs_fname): + self.abs_read_only_stubs_fnames.add(abs_fname) + else: + self.io.tool_warning( + f"Error: Read-only (stub) file {fname} does not exist. Skipping." + ) + if map_tokens is None: use_repo_map = main_model.use_repo_map map_tokens = 1024 @@ -647,6 +667,10 @@ def choose_fence(self): content = self.io.read_text(_fname) if content is not None: all_content += content + "\n" + for _fname in self.abs_read_only_stubs_fnames: + content = self.io.read_text(_fname) + if content is not None: + all_content += content + "\n" lines = all_content.splitlines() good = False @@ -720,6 +744,7 @@ def get_files_content(self, fnames=None): def get_read_only_files_content(self): prompt = "" + # Handle regular read-only files for fname in self.abs_read_only_fnames: content = self.io.read_text(fname) if content is not None and not is_image_file(fname): @@ -764,6 +789,17 @@ def get_read_only_files_content(self): prompt += content prompt += f"{self.fence[1]}\n" + + # Handle stub files + for fname in self.abs_read_only_stubs_fnames: + if not is_image_file(fname): + relative_fname = self.get_rel_fname(fname) + prompt += "\n" + prompt += f"{relative_fname} (stub)" + prompt += f"\n{self.fence[0]}\n" + stub = self.get_file_stub(fname) + prompt += stub + prompt += f"{self.fence[1]}\n" return prompt def get_cur_message_text(self): @@ -818,7 +854,10 @@ def get_repo_map(self, force_refresh=False): all_abs_files = set(self.get_all_abs_files()) repo_abs_read_only_fnames = set(self.abs_read_only_fnames) & all_abs_files - chat_files = set(self.abs_fnames) | repo_abs_read_only_fnames + 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 repo_content = self.repo_map.get_repo_map( @@ -877,7 +916,9 @@ def get_readonly_files_messages(self): ] # Handle image files - images_message = self.get_images_message(self.abs_read_only_fnames) + images_message = self.get_images_message( + list(self.abs_read_only_fnames) + list(self.abs_read_only_stubs_fnames) + ) if images_message is not None: readonly_messages += [ images_message, @@ -998,15 +1039,17 @@ def copy_context(self): def get_input(self): inchat_files = self.get_inchat_relative_files() - read_only_files = [self.get_rel_fname(fname) for fname in self.abs_read_only_fnames] - all_files = sorted(set(inchat_files + read_only_files)) + all_read_only_fnames = self.abs_read_only_fnames | self.abs_read_only_stubs_fnames + all_read_only_files = [self.get_rel_fname(fname) for fname in all_read_only_fnames] + all_files = sorted(set(inchat_files + all_read_only_files)) edit_format = "" if self.edit_format == self.main_model.edit_format else self.edit_format return self.io.get_input( self.root, all_files, self.get_addable_relative_files(), self.commands, - self.abs_read_only_fnames, + abs_read_only_fnames=self.abs_read_only_fnames, + abs_read_only_stubs_fnames=self.abs_read_only_stubs_fnames, edit_format=edit_format, ) @@ -2220,7 +2263,8 @@ def get_file_mentions(self, content, ignore_current=False): # Get basenames of files already in chat or read-only existing_basenames = {os.path.basename(f) for f in self.get_inchat_relative_files()} | { - os.path.basename(self.get_rel_fname(f)) for f in self.abs_read_only_fnames + os.path.basename(self.get_rel_fname(f)) + for f in self.abs_read_only_fnames | self.abs_read_only_stubs_fnames } mentioned_rel_fnames = set() @@ -2639,6 +2683,9 @@ def get_multi_response_content_in_progress(self, final=False): return cur + new + def get_file_stub(self, fname): + return RepoMap.get_file_stub(fname, self.io) + def get_rel_fname(self, fname): try: return os.path.relpath(fname, self.root) @@ -2675,7 +2722,8 @@ def get_addable_relative_files(self): all_files = set(self.get_all_relative_files()) inchat_files = set(self.get_inchat_relative_files()) read_only_files = set(self.get_rel_fname(fname) for fname in self.abs_read_only_fnames) - return all_files - inchat_files - read_only_files + stub_files = set(self.get_rel_fname(fname) for fname in self.abs_read_only_stubs_fnames) + return all_files - inchat_files - read_only_files - stub_files def check_for_dirty_commit(self, path): if not self.repo: diff --git a/aider/commands.py b/aider/commands.py index c9707697133..856c3a891fc 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -416,6 +416,7 @@ def cmd_clear(self, args): def _drop_all_files(self): self.coder.abs_fnames = set() + self.coder.abs_read_only_stubs_fnames = set() # When dropping all files, keep those that were originally provided via args.read if self.original_read_only_fnames: @@ -553,6 +554,15 @@ def cmd_tokens(self, args): file_res.sort() res.extend(file_res) + # stub files + for fname in self.coder.abs_read_only_stubs_fnames: + relative_fname = self.coder.get_rel_fname(fname) + if not is_image_file(relative_fname): + stub = self.coder.get_file_stub(fname) + content = f"{relative_fname} (stub)\n{fence}\n" + stub + "{fence}\n" + tokens = self.coder.main_model.token_count(content) + res.append((tokens, f"{relative_fname} (read-only stub)", "/drop to remove")) + self.io.tool_output( f"Approximate context window usage for {self.coder.main_model.name}, in tokens:" ) @@ -747,6 +757,9 @@ def quote_fname(self, fname): fname = f'"{fname}"' return fname + def completions_raw_read_only_stub(self, document, complete_event): + return self.completions_raw_read_only(document, complete_event) + def completions_raw_read_only(self, document, complete_event): # Get the text before the cursor text = document.text_before_cursor @@ -923,6 +936,17 @@ def cmd_add(self, args): if abs_file_path in self.coder.abs_fnames: self.io.tool_error(f"{matched_file} is already in the chat as an editable file") continue + elif abs_file_path in self.coder.abs_read_only_stubs_fnames: + if self.coder.repo and self.coder.repo.path_in_repo(matched_file): + self.coder.abs_read_only_stubs_fnames.remove(abs_file_path) + self.coder.abs_fnames.add(abs_file_path) + self.io.tool_output( + f"Moved {matched_file} from read-only (stub) to editable files in the chat" + ) + else: + self.io.tool_error( + f"Cannot add {matched_file} as it's not part of the repository" + ) elif abs_file_path in self.coder.abs_read_only_fnames: if self.coder.repo and self.coder.repo.path_in_repo(matched_file): self.coder.abs_read_only_fnames.remove(abs_file_path) @@ -962,7 +986,10 @@ def cmd_add(self, args): def completions_drop(self): files = self.coder.get_inchat_relative_files() - read_only_files = [self.coder.get_rel_fname(fn) for fn in self.coder.abs_read_only_fnames] + read_only_files = [ + self.coder.get_rel_fname(fn) + for fn in self.coder.abs_read_only_fnames | self.coder.abs_read_only_stubs_fnames + ] all_files = files + read_only_files all_files = [self.quote_fname(fn) for fn in all_files] return all_files @@ -989,6 +1016,26 @@ def completions_context_blocks(self): "Symbol Outline", ] + def _handle_read_only_files(self, expanded_word, file_set, description=""): + """Handle read-only files with substring matching and samefile check""" + matched = [] + for f in file_set: + if expanded_word in f: + matched.append(f) + continue + + # Try samefile comparison for relative paths + try: + abs_word = os.path.abspath(expanded_word) + if os.path.samefile(abs_word, f): + matched.append(f) + except (FileNotFoundError, OSError): + continue + + for matched_file in matched: + file_set.remove(matched_file) + self.io.tool_output(f"Removed {description} file {matched_file} from the chat") + def cmd_drop(self, args=""): "Remove files from the chat session to free up context space" @@ -1014,25 +1061,13 @@ def cmd_drop(self, args=""): # Expand tilde in the path expanded_word = os.path.expanduser(word) - # Handle read-only files with substring matching and samefile check - read_only_matched = [] - for f in self.coder.abs_read_only_fnames: - if expanded_word in f: - read_only_matched.append(f) - continue - - # Try samefile comparison for relative paths - try: - abs_word = os.path.abspath(expanded_word) - if os.path.samefile(abs_word, f): - read_only_matched.append(f) - except (FileNotFoundError, OSError): - continue - - for matched_file in read_only_matched: - self.coder.abs_read_only_fnames.remove(matched_file) - self.io.tool_output(f"Removed read-only file {matched_file} from the chat") - files_changed = True + # 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 editable files, use glob if word contains glob chars, otherwise use substring if any(c in expanded_word for c in "*?[]"): @@ -1269,6 +1304,7 @@ def cmd_ls(self, args): other_files = [] chat_files = [] read_only_files = [] + read_only_stub_files = [] for file in files: abs_file_path = self.coder.abs_root_path(file) if abs_file_path in self.coder.abs_fnames: @@ -1281,7 +1317,12 @@ def cmd_ls(self, args): rel_file_path = self.coder.get_rel_fname(abs_file_path) read_only_files.append(rel_file_path) - if not chat_files and not other_files and not read_only_files: + # Add read-only stub files + for abs_file_path in self.coder.abs_read_only_stubs_fnames: + rel_file_path = self.coder.get_rel_fname(abs_file_path) + read_only_stub_files.append(rel_file_path) + + if not chat_files and not other_files and not read_only_files and not read_only_stub_files: self.io.tool_output("\nNo files in chat, git repo, or read-only list.") return @@ -1290,10 +1331,13 @@ def cmd_ls(self, args): for file in other_files: self.io.tool_output(f" {file}") - if read_only_files: + # Read-only files: + if read_only_files or read_only_stub_files: self.io.tool_output("\nRead-only files:\n") for file in read_only_files: self.io.tool_output(f" {file}") + for file in read_only_stub_files: + self.io.tool_output(f" {file} (stub)") if chat_files: self.io.tool_output("\nFiles in chat:\n") @@ -1530,38 +1574,26 @@ def cmd_paste(self, args): except Exception as e: self.io.tool_error(f"Error processing clipboard content: {e}") - def cmd_read_only(self, args): - """Add files to the chat that are for reference only, or turn added files to read-only. - With no args, opens a fuzzy finder to select files from the repo. - If no files are selected, converts all editable files in chat to read-only. - """ + def _cmd_read_only_base(self, args, source_set, target_set, source_mode, target_mode): + """Base implementation for read-only and read-only-stub commands""" if not args.strip(): - if self.coder.repo: - all_files = self.coder.repo.get_tracked_files() - else: - all_files = self.coder.get_all_relative_files() - - if not all_files and not self.coder.abs_fnames: - self.io.tool_output("No files in the repo or chat to make read-only.") - return - - selected_files = [] - if all_files: - selected_files = run_fzf(all_files, multi=True) - - if not selected_files: - # Fallback behavior: convert all editable files to read-only - if self.coder.abs_fnames: - for fname in list(self.coder.abs_fnames): - self.coder.abs_fnames.remove(fname) - self.coder.abs_read_only_fnames.add(fname) - rel_fname = self.coder.get_rel_fname(fname) - self.io.tool_output(f"Converted {rel_fname} to read-only") - else: - self.io.tool_output("No files selected.") - return - - args = " ".join([self.quote_fname(f) for f in selected_files]) + # Handle editable files + for fname in list(self.coder.abs_fnames): + self.coder.abs_fnames.remove(fname) + target_set.add(fname) + rel_fname = self.coder.get_rel_fname(fname) + self.io.tool_output(f"Converted {rel_fname} from editable to {target_mode}") + + # Handle source set files if provided + if source_set: + for fname in list(source_set): + source_set.remove(fname) + target_set.add(fname) + rel_fname = self.coder.get_rel_fname(fname) + self.io.tool_output( + f"Converted {rel_fname} from {source_mode} to {target_mode}" + ) + return filenames = parse_quoted_filenames(args) all_paths = [] @@ -1596,13 +1628,28 @@ def cmd_read_only(self, args): for path in sorted(all_paths): abs_path = self.coder.abs_root_path(path) if os.path.isfile(abs_path): - self._add_read_only_file(abs_path, path) + self._add_read_only_file( + abs_path, + path, + target_set, + source_set, + source_mode=source_mode, + target_mode=target_mode, + ) elif os.path.isdir(abs_path): - self._add_read_only_directory(abs_path, path) + self._add_read_only_directory(abs_path, path, source_set, target_set, target_mode) else: self.io.tool_error(f"Not a file or directory: {abs_path}") - def _add_read_only_file(self, abs_path, original_name): + def _add_read_only_file( + self, + abs_path, + original_name, + target_set, + source_set, + source_mode="read-only", + target_mode="read-only", + ): if is_image_file(original_name) and not self.coder.main_model.info.get("supports_vision"): self.io.tool_error( f"Cannot add image file {original_name} as the" @@ -1610,38 +1657,123 @@ def _add_read_only_file(self, abs_path, original_name): ) return - if abs_path in self.coder.abs_read_only_fnames: - self.io.tool_error(f"{original_name} is already in the chat as a read-only file") + if abs_path in target_set: + self.io.tool_error(f"{original_name} is already in the chat as a {target_mode} file") return elif abs_path in self.coder.abs_fnames: self.coder.abs_fnames.remove(abs_path) - self.coder.abs_read_only_fnames.add(abs_path) + target_set.add(abs_path) + self.io.tool_output( + f"Moved {original_name} from editable to {target_mode} files in the chat" + ) + elif source_set and abs_path in source_set: + source_set.remove(abs_path) + target_set.add(abs_path) self.io.tool_output( - f"Moved {original_name} from editable to read-only files in the chat" + f"Moved {original_name} from {source_mode} to {target_mode} files in the chat" ) else: - self.coder.abs_read_only_fnames.add(abs_path) - self.io.tool_output(f"Added {original_name} to read-only files.") + target_set.add(abs_path) + self.io.tool_output(f"Added {original_name} to {target_mode} files.") - def _add_read_only_directory(self, abs_path, original_name): + def _add_read_only_directory( + self, abs_path, original_name, source_set, target_set, target_mode + ): added_files = 0 for root, _, files in os.walk(abs_path): for file in files: file_path = os.path.join(root, file) if ( file_path not in self.coder.abs_fnames - and file_path not in self.coder.abs_read_only_fnames + and file_path not in target_set + and (source_set is None or file_path not in source_set) ): - self.coder.abs_read_only_fnames.add(file_path) + target_set.add(file_path) added_files += 1 if added_files > 0: self.io.tool_output( - f"Added {added_files} files from directory {original_name} to read-only files." + f"Added {added_files} files from directory {original_name} to {target_mode} files." ) else: self.io.tool_output(f"No new files added from directory {original_name}.") + def cmd_read_only(self, args): + "Add files to the chat that are for reference only, or turn added files to read-only" + if not args.strip(): + # If no args provided, use fuzzy finder to select files to add as read-only + all_files = self.coder.get_all_relative_files() + files_in_chat = self.coder.get_inchat_relative_files() + addable_files = sorted(set(all_files) - set(files_in_chat)) + if not addable_files: + # If no files available to add, convert all editable files to read-only + self._cmd_read_only_base( + "", + source_set=self.coder.abs_read_only_stubs_fnames, + target_set=self.coder.abs_read_only_fnames, + source_mode="read-only (stub)", + target_mode="read-only", + ) + return + selected_files = run_fzf(addable_files, multi=True) + if not selected_files: + # If user didn't select any files, convert all editable files to read-only + self._cmd_read_only_base( + "", + source_set=self.coder.abs_read_only_stubs_fnames, + target_set=self.coder.abs_read_only_fnames, + source_mode="read-only (stub)", + target_mode="read-only", + ) + return + args = " ".join([self.quote_fname(f) for f in selected_files]) + + self._cmd_read_only_base( + args, + source_set=self.coder.abs_read_only_stubs_fnames, + target_set=self.coder.abs_read_only_fnames, + source_mode="read-only (stub)", + target_mode="read-only", + ) + + def cmd_read_only_stub(self, args): + "Add files to the chat as read-only stubs, or turn added files to read-only (stubs)" + if not args.strip(): + # If no args provided, use fuzzy finder to select files to add as read-only stubs + all_files = self.coder.get_all_relative_files() + files_in_chat = self.coder.get_inchat_relative_files() + addable_files = sorted(set(all_files) - set(files_in_chat)) + if not addable_files: + # If no files available to add, convert all editable files to read-only stubs + self._cmd_read_only_base( + "", + source_set=self.coder.abs_read_only_fnames, + target_set=self.coder.abs_read_only_stubs_fnames, + source_mode="read-only", + target_mode="read-only (stub)", + ) + return + selected_files = run_fzf(addable_files, multi=True) + if not selected_files: + # If user didn't select any files, convert all editable files to read-only stubs + self._cmd_read_only_base( + "", + source_set=self.coder.abs_read_only_fnames, + target_set=self.coder.abs_read_only_stubs_fnames, + source_mode="read-only", + target_mode="read-only (stub)", + ) + return + args = " ".join([self.quote_fname(f) for f in selected_files]) + + self._cmd_read_only_base( + args, + source_set=self.coder.abs_read_only_fnames, + target_set=self.coder.abs_read_only_stubs_fnames, + source_mode="read-only", + target_mode="read-only (stub)", + ) + def cmd_map(self, args): "Print out the current repository map" repo_map = self.coder.get_repo_map() @@ -1743,6 +1875,14 @@ def cmd_save(self, args): f.write(f"/read-only {rel_fname}\n") else: f.write(f"/read-only {fname}\n") + # Write commands to add read-only stubs files + for fname in sorted(self.coder.abs_read_only_stubs_fnames): + # Use absolute path for files outside repo root, relative path for files inside + if Path(fname).is_relative_to(self.coder.root): + rel_fname = self.coder.get_rel_fname(fname) + f.write(f"/read-only-stub {rel_fname}\n") + else: + f.write(f"/read-only-stub {fname}\n") self.io.tool_output(f"Saved commands to {args.strip()}") except Exception as e: diff --git a/aider/io.py b/aider/io.py index a3efc6cea52..c7fab2f97c4 100644 --- a/aider/io.py +++ b/aider/io.py @@ -579,6 +579,7 @@ def get_input( addable_rel_fnames, commands, abs_read_only_fnames=None, + abs_read_only_stubs_fnames=None, edit_format=None, ): self.rule() @@ -592,9 +593,15 @@ def get_input( rel_read_only_fnames = [ get_rel_fname(fname, root) for fname in (abs_read_only_fnames or []) ] - show = self.format_files_for_input(rel_fnames, rel_read_only_fnames) + rel_read_only_stubs_fnames = [ + get_rel_fname(fname, root) for fname in (abs_read_only_stubs_fnames or []) + ] + show = self.format_files_for_input( + rel_fnames, rel_read_only_fnames, rel_read_only_stubs_fnames + ) prompt_prefix = "" + if edit_format: prompt_prefix += edit_format if self.multiline_mode: @@ -616,7 +623,8 @@ def get_input( addable_rel_fnames, commands, self.encoding, - abs_read_only_fnames=abs_read_only_fnames, + abs_read_only_fnames=(abs_read_only_fnames or set()) + | (abs_read_only_stubs_fnames or set()), ) ) @@ -1212,67 +1220,84 @@ def append_chat_history(self, text, linebreak=False, blockquote=False, strip=Tru print(err) self.chat_history_file = None # Disable further attempts to write - def format_files_for_input(self, rel_fnames, rel_read_only_fnames): + def format_files_for_input(self, rel_fnames, rel_read_only_fnames, rel_read_only_stubs_fnames): # Optimization for large number of files - total_files = len(rel_fnames) + len(rel_read_only_fnames or []) + total_files = ( + len(rel_fnames) + + len(rel_read_only_fnames or []) + + len(rel_read_only_stubs_fnames or []) + ) # For very large numbers of files, use a summary display if total_files > 50: read_only_count = len(rel_read_only_fnames or []) + stub_file_count = len(rel_read_only_stubs_fnames or []) editable_count = len([f for f in rel_fnames if f not in (rel_read_only_fnames or [])]) summary = f"{editable_count} editable file(s)" if read_only_count > 0: summary += f", {read_only_count} read-only file(s)" + if stub_file_count > 0: + summary += f", {stub_file_count} stub file(s)" summary += " (use /ls to list all files)\n" return summary # Original implementation for reasonable number of files if not self.pretty: - read_only_files = [] - for full_path in sorted(rel_read_only_fnames or []): - read_only_files.append(f"{full_path} (read only)") - - editable_files = [] - for full_path in sorted(rel_fnames): - if full_path in rel_read_only_fnames: - continue - editable_files.append(f"{full_path}") - - return "\n".join(read_only_files + editable_files) + "\n" + lines = [] + # Handle regular read-only files + for fname in sorted(rel_read_only_fnames or []): + lines.append(f"{fname} (read only)") + # Handle stub files separately + for fname in sorted(rel_read_only_stubs_fnames or []): + lines.append(f"{fname} (read only stub)") + # Handle editable files + for fname in sorted(rel_fnames): + if fname not in rel_read_only_fnames and fname not in rel_read_only_stubs_fnames: + lines.append(fname) + return "\n".join(lines) + "\n" output = StringIO() console = Console(file=output, force_terminal=False) - read_only_files = sorted(rel_read_only_fnames or []) - editable_files = [f for f in sorted(rel_fnames) if f not in rel_read_only_fnames] - - if read_only_files: - # Use shorter of abs/rel paths for readonly files + # Handle read-only files + if rel_read_only_fnames or rel_read_only_stubs_fnames: ro_paths = [] - for rel_path in read_only_files: + # Regular read-only files + for rel_path in sorted(rel_read_only_fnames or []): abs_path = os.path.abspath(os.path.join(self.root, rel_path)) - ro_paths.append(Text(abs_path if len(abs_path) < len(rel_path) else rel_path)) - - files_with_label = [Text("Readonly:")] + ro_paths - read_only_output = StringIO() - Console(file=read_only_output, force_terminal=False).print(Columns(files_with_label)) - read_only_lines = read_only_output.getvalue().splitlines() - console.print(Columns(files_with_label)) - + ro_paths.append(abs_path if len(abs_path) < len(rel_path) else rel_path) + # Stub files with (stub) marker + for rel_path in sorted(rel_read_only_stubs_fnames or []): + abs_path = os.path.abspath(os.path.join(self.root, rel_path)) + path = abs_path if len(abs_path) < len(rel_path) else rel_path + ro_paths.append(f"{path} (stub)") + + if ro_paths: + files_with_label = ["Readonly:"] + ro_paths + read_only_output = StringIO() + Console(file=read_only_output, force_terminal=False).print( + Columns(files_with_label) + ) + read_only_lines = read_only_output.getvalue().splitlines() + console.print(Columns(files_with_label)) + + # Handle editable files + editable_files = [ + f + for f in sorted(rel_fnames) + if f not in rel_read_only_fnames and f not in rel_read_only_stubs_fnames + ] if editable_files: - text_editable_files = [Text(f) for f in editable_files] - files_with_label = text_editable_files - if read_only_files: - files_with_label = [Text("Editable:")] + text_editable_files + files_with_label = editable_files + if rel_read_only_fnames or rel_read_only_stubs_fnames: + files_with_label = ["Editable:"] + editable_files editable_output = StringIO() Console(file=editable_output, force_terminal=False).print(Columns(files_with_label)) editable_lines = editable_output.getvalue().splitlines() - if len(read_only_lines) > 1 or len(editable_lines) > 1: console.print() console.print(Columns(files_with_label)) - return output.getvalue() diff --git a/aider/repomap.py b/aider/repomap.py index 985b2ce159e..e96ed0446fe 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -102,6 +102,38 @@ class RepoMap: # Add more based on tree-sitter queries if needed } + @staticmethod + def get_file_stub(fname, io): + """Generate a complete structural outline of a source code file. + + Args: + fname (str): Absolute path to the source file + io: InputOutput instance for file operations + + Returns: + str: Formatted outline showing the file's structure + """ + # Use cached instance if available + if not hasattr(RepoMap, "_stub_instance"): + RepoMap._stub_instance = RepoMap(map_tokens=0, io=io) + + rm = RepoMap._stub_instance + + rel_fname = rm.get_rel_fname(fname) + + # Reuse existing tag parsing + tags = rm.get_tags(fname, rel_fname) + if not tags: + return "# No outline available" + + # Get all definition lines + lois = [tag.line for tag in tags if tag.kind == "def"] + + # Reuse existing tree rendering + outline = rm.render_tree(fname, rel_fname, lois) + + return f"{outline}" + def __init__( self, map_tokens=1024, diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 270a3c24795..ff8a618c76c 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -5,7 +5,6 @@ from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document -from rich.text import Text from aider.dump import dump # noqa: F401 from aider.io import AutoCompleter, ConfirmGroup, InputOutput @@ -482,6 +481,7 @@ def test_format_files_for_input_pretty_false(self, mock_is_dumb_terminal): io = InputOutput(pretty=False, fancy_input=False) rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"] rel_read_only_fnames = ["ro_file.txt"] + rel_read_only_stub_fnames = [] expected_output = "file1.txt\nfile[markup].txt\nro_file.txt (read only)\n" # Sort the expected lines because the order of editable vs read-only might vary @@ -504,7 +504,9 @@ def test_format_files_for_input_pretty_false(self, mock_is_dumb_terminal): ) expected_output = "\n".join(expected_output_lines) + "\n" - actual_output = io.format_files_for_input(rel_fnames, rel_read_only_fnames) + actual_output = io.format_files_for_input( + rel_fnames, rel_read_only_fnames, rel_read_only_stub_fnames + ) # Normalizing actual output by splitting, sorting, and rejoining actual_output_lines = sorted(filter(None, actual_output.splitlines())) @@ -519,7 +521,7 @@ def test_format_files_for_input_pretty_true_no_files( self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal ): io = InputOutput(pretty=True, root="test_root") - io.format_files_for_input([], []) + io.format_files_for_input([], [], []) mock_columns.assert_not_called() @patch("aider.io.Columns") @@ -531,17 +533,15 @@ def test_format_files_for_input_pretty_true_editable_only( io = InputOutput(pretty=True, root="test_root") rel_fnames = ["edit1.txt", "edit[markup].txt"] - io.format_files_for_input(rel_fnames, []) + io.format_files_for_input(rel_fnames, [], []) mock_columns.assert_called_once() args, _ = mock_columns.call_args renderables = args[0] self.assertEqual(len(renderables), 2) - self.assertIsInstance(renderables[0], Text) - self.assertEqual(renderables[0].plain, "edit1.txt") - self.assertIsInstance(renderables[1], Text) - self.assertEqual(renderables[1].plain, "edit[markup].txt") + self.assertEqual(renderables[0], "edit1.txt") + self.assertEqual(renderables[1], "edit[markup].txt") @patch("aider.io.Columns") @patch("os.path.abspath") @@ -558,20 +558,46 @@ def test_format_files_for_input_pretty_true_readonly_only( rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] # When all files in chat are read-only rel_fnames = list(rel_read_only_fnames) + rel_read_only_stub_fnames = [] - io.format_files_for_input(rel_fnames, rel_read_only_fnames) + io.format_files_for_input(rel_fnames, rel_read_only_fnames, rel_read_only_stub_fnames) self.assertEqual(mock_columns.call_count, 2) args, _ = mock_columns.call_args renderables = args[0] self.assertEqual(len(renderables), 3) # Readonly: + 2 files - self.assertIsInstance(renderables[0], Text) - self.assertEqual(renderables[0].plain, "Readonly:") - self.assertIsInstance(renderables[1], Text) - self.assertEqual(renderables[1].plain, "ro1.txt") - self.assertIsInstance(renderables[2], Text) - self.assertEqual(renderables[2].plain, "ro[markup].txt") + self.assertEqual(renderables[0], "Readonly:") + self.assertEqual(renderables[1], "ro1.txt") + self.assertEqual(renderables[2], "ro[markup].txt") + + @patch("aider.io.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_readonly_stub_only( + self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal + ): + io = InputOutput(pretty=True, root="test_root") + + # Mock path functions to ensure rel_path is chosen by the shortener logic + mock_join.side_effect = lambda *args: "/".join(args) + mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p) + + rel_read_only_fnames = [] + rel_read_only_stub_fnames = ["ro1.txt", "ro[markup].txt"] + # When all files in chat are read-only + rel_fnames = list(rel_read_only_stub_fnames) + + io.format_files_for_input(rel_fnames, rel_read_only_fnames, rel_read_only_stub_fnames) + + self.assertEqual(mock_columns.call_count, 2) + args, _ = mock_columns.call_args + renderables = args[0] + + self.assertEqual(len(renderables), 3) # Readonly: + 2 files + self.assertEqual(renderables[0], "Readonly:") + self.assertEqual(renderables[1], "ro1.txt (stub)") + self.assertEqual(renderables[2], "ro[markup].txt (stub)") @patch("aider.io.Columns") @patch("os.path.abspath") @@ -586,24 +612,21 @@ def test_format_files_for_input_pretty_true_mixed_files( rel_fnames = ["edit1.txt", "edit[markup].txt", "ro1.txt", "ro[markup].txt"] rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] + rel_read_only_stub_fnames = [] - io.format_files_for_input(rel_fnames, rel_read_only_fnames) + io.format_files_for_input(rel_fnames, rel_read_only_fnames, rel_read_only_stub_fnames) self.assertEqual(mock_columns.call_count, 4) # Check arguments for the first rendering of read-only files (call 0) args_ro, _ = mock_columns.call_args_list[0] renderables_ro = args_ro[0] - self.assertEqual( - renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")] - ) + self.assertEqual(renderables_ro, ["Readonly:", "ro1.txt", "ro[markup].txt"]) # Check arguments for the first rendering of editable files (call 2) args_ed, _ = mock_columns.call_args_list[2] renderables_ed = args_ed[0] - self.assertEqual( - renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")] - ) + self.assertEqual(renderables_ed, ["Editable:", "edit1.txt", "edit[markup].txt"]) if __name__ == "__main__": From 0e7cf3bf45aa52f01fe1be9afd6e33842e1e0f08 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 7 Sep 2025 11:22:51 -0400 Subject: [PATCH 6/7] Install aider-ce in inline pip install commands to account for version un-pinning --- aider/help.py | 2 +- aider/main.py | 3 ++- aider/scrape.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aider/help.py b/aider/help.py index c76188d1283..f6587e638e1 100755 --- a/aider/help.py +++ b/aider/help.py @@ -17,7 +17,7 @@ def install_help_extra(io): pip_install_cmd = [ - "aider-chat[help]", + "aider-ce[help]", "--extra-index-url", "https://download.pytorch.org/whl/cpu", ] diff --git a/aider/main.py b/aider/main.py index 361375a083b..f080f2ac150 100644 --- a/aider/main.py +++ b/aider/main.py @@ -211,7 +211,7 @@ def check_streamlit_install(io): io, "streamlit", "You need to install the aider browser feature", - ["aider-chat[browser]"], + ["aider-ce[browser]"], ) @@ -991,6 +991,7 @@ def get_io(pretty): repo=repo, fnames=fnames, read_only_fnames=read_only_fnames, + read_only_stubs_fnames=[], show_diffs=args.show_diffs, auto_commits=args.auto_commits, dirty_commits=args.dirty_commits, diff --git a/aider/scrape.py b/aider/scrape.py index 7ef78285ac4..1e44ad23772 100755 --- a/aider/scrape.py +++ b/aider/scrape.py @@ -42,7 +42,7 @@ def install_playwright(io): if has_pip and has_chromium: return True - pip_cmd = utils.get_pip_install(["aider-chat[playwright]"]) + pip_cmd = utils.get_pip_install(["aider-ce[playwright]"]) chromium_cmd = "-m playwright install --with-deps chromium" chromium_cmd = [sys.executable] + chromium_cmd.split() From 7dffa540e212e7fc65111d9943280a4945a7745f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 7 Sep 2025 11:30:12 -0400 Subject: [PATCH 7/7] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f15840b8162..4836ab1fc77 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This project aims to be compatible with upstream Aider, but with priority commit * [Map Cache Location Config: #2911](https://github.com/Aider-AI/aider/pull/2911) * [Enhanced System Prompts: #3804](https://github.com/Aider-AI/aider/pull/3804) * [Repo Map File Name Truncation Fix: #4320](https://github.com/Aider-AI/aider/pull/4320) +* [Read Only Stub Files For Context Window Management : #3056](https://github.com/Aider-AI/aider/pull/3056) ### Other Updates @@ -28,6 +29,7 @@ This project aims to be compatible with upstream Aider, but with priority commit * [Edit Before Adding Files and Reflecting](https://github.com/dwash96/aider-ce/pull/22) * [Fix Deepseek model configurations](https://github.com/Aider-AI/aider/commit/c839a6dd8964d702172cae007375e299732d3823) * [Relax Version Pinning For Easier Distribution](https://github.com/dwash96/aider-ce/issues/18) +* [Remove Confirm Responses from History](https://github.com/Aider-AI/aider/pull/3958) ### Other Notes * [MCP Configuration](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/mcp.md)