From d8bc11d3ec2187ffb4909a93eeec4aa05119c4be Mon Sep 17 00:00:00 2001 From: Alexander Luck Date: Sat, 26 Apr 2025 15:41:00 +0200 Subject: [PATCH 1/3] format_path_for_display, clamp max size of file names in a way that prioritizes file names --- aider/gui.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/aider/gui.py b/aider/gui.py index 7fa90bc38..51d6fb2a5 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -48,6 +48,52 @@ def search(text=None): return results +def format_path_for_display(path, max_len=00, separators=("/","\\")): + """ + Formats a path for display, prioritizing filename and parent directory. + Tries to show: + 1. Full path if it fits. + 2. .../parent/filename if it fits. + 3. .../filename if it fits. + 4. Simple left truncation as a fallback. + """ + if len(path) <= max_len: + return path + + sep = os.sep # Use the OS-specific separator + + try: + filename = os.path.basename(path) + dirname = os.path.dirname(path) + parent_dir = os.path.basename(dirname) + + # Try .../parent/filename + if parent_dir: + short_path = os.path.join("...", parent_dir, filename) + else: + short_path = os.path.join("...", filename) + + + if len(short_path) <= max_len: + simple_truncated = "..." + path[-(max_len - 3):] + if len(short_path) <= len(simple_truncated): + return short_path + else: + return simple_truncated + + # Try .../filename if .../parent/filename was too long or parent didn't exist + short_path_fname_only = os.path.join("...", filename) + if len(short_path_fname_only) <= max_len: + return short_path_fname_only + + # Fallback: Simple left truncation if nothing else fits + return "..." + path[-(max_len - 3):] + + except Exception: + # Safety fallback in case of weird paths + return "..." + path[-(max_len - 3):] + + # Keep state as a resource, which survives browser reloads (since Coder does too) class State: keys = set() @@ -187,10 +233,11 @@ class GUI: def do_add_files(self): fnames = st.multiselect( "Add files to the chat", - self.coder.get_all_relative_files(), + options=self.coder.get_all_relative_files(), default=self.state.initial_inchat_files, placeholder="Files to edit", disabled=self.prompt_pending(), + format_func=lambda path: format_path_for_display(path, max_len=60), help=( "Only add the files that need to be *edited* for the task you are working" " on. Aider will pull in other relevant code to provide context to the LLM." @@ -533,6 +580,38 @@ def gui_main(): }, ) + # --- Inject CSS for wider multiselect tags --- + # NOTE: These selectors target internal Streamlit/BaseWeb structures and might + # break in future Streamlit versions. Inspect element if styles don't apply. + st.markdown(""" + + """, unsafe_allow_html=True) + # config_options = st.config._config_options # for key, value in config_options.items(): # print(f"{key}: {value.value}") From 2ec7d74d38d7b245ab90d8d2493a19126c8efe64 Mon Sep 17 00:00:00 2001 From: Alexander Luck Date: Sat, 26 Apr 2025 16:30:45 +0200 Subject: [PATCH 2/3] Add multiple files via "add folder" element --- aider/gui.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 51d6fb2a5..fa250a382 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -141,6 +141,7 @@ class GUI: last_undo_empty = None recent_msgs_empty = None web_content_empty = None + folder_content_empty = None def announce(self): lines = self.coder.get_announcements() @@ -228,13 +229,19 @@ class GUI: def do_add_to_chat(self): # with st.expander("Add to the chat", expanded=True): self.do_add_files() + self.do_add_folder() self.do_add_web_page() def do_add_files(self): - fnames = st.multiselect( + current_inchat_files = self.coder.get_inchat_relative_files() + current_inchat_files_set = set(current_inchat_files) + widget_key = "multiselect_add_files" + + fnames_selected_in_widget = st.multiselect( "Add files to the chat", options=self.coder.get_all_relative_files(), - default=self.state.initial_inchat_files, + default=current_inchat_files, # Reflect current coder state + key=widget_key, placeholder="Files to edit", disabled=self.prompt_pending(), format_func=lambda path: format_path_for_display(path, max_len=60), @@ -243,16 +250,79 @@ class GUI: " on. Aider will pull in other relevant code to provide context to the LLM." ), ) + fnames_selected_in_widget_set = set(fnames_selected_in_widget) - for fname in fnames: - if fname not in self.coder.get_inchat_relative_files(): - self.coder.add_rel_fname(fname) - self.info(f"Added {fname} to the chat") + # Compare widget state to coder state *before* this render + files_to_add = fnames_selected_in_widget_set - current_inchat_files_set + files_to_remove = current_inchat_files_set - fnames_selected_in_widget_set - for fname in self.coder.get_inchat_relative_files(): - if fname not in fnames: - self.coder.drop_rel_fname(fname) - self.info(f"Removed {fname} from the chat") + for fname in files_to_add: + self.coder.add_rel_fname(fname) + self.info(f"Added {fname} to the chat") + + for fname in files_to_remove: + self.coder.drop_rel_fname(fname) + self.info(f"Removed {fname} from the chat") + + def do_add_folder(self): + with st.popover("Add a folder to the chat"): + st.markdown("Add all files from a folder to the chat") + + folder_input_key = f"folder_content_{self.state.folder_content_num}" + # Correctly assign the text input result + self.folder_content = st.text_input( + "Folder path", + placeholder="path/to/folder", + key=folder_input_key, + disabled=self.prompt_pending(), + ) + + if self.folder_content: + clean_folder_path = os.path.normpath(self.folder_content) + if not clean_folder_path or clean_folder_path == '.': + st.warning("Please enter a valid folder path.") + return + + if not self.coder or not self.coder.repo: + st.error("Error: Coder or Git repository not initialized correctly.") + return + + try: + tracked_files_set = self.coder.repo.get_tracked_files() + except Exception as e: + st.error(f"Error getting tracked files from git: {e}") + return + + all_files = self.coder.get_all_relative_files() + inchat_files = set(self.coder.get_inchat_relative_files()) + + prefix = clean_folder_path + os.sep + files_to_consider = [ + f for f in all_files + if f not in inchat_files and (f.startswith(prefix) or f == clean_folder_path) + ] + + if files_to_consider: + button_key = f"add_folder_button_{self.state.folder_content_num}" + if st.button("Add all files from folder", key=button_key, disabled=self.prompt_pending()): + added_count = 0 + for file_path in files_to_consider: + # Check if the file is tracked by git + if file_path in tracked_files_set: + self.coder.add_rel_fname(file_path) + added_count += 1 + + if added_count > 0: + self.info(f"Added {added_count} tracked files from '{clean_folder_path}' to the chat") + self.state.folder_content_num += 1 + st.rerun() + else: + st.warning(f"No *new*, tracked files found in folder '{clean_folder_path}' to add.") + + elif self.folder_content: + is_single_tracked_file = clean_folder_path in tracked_files_set and clean_folder_path not in inchat_files + if not is_single_tracked_file: + st.warning(f"No files found in the repository matching the path '{clean_folder_path}'.") def do_add_web_page(self): with st.popover("Add a web page to the chat"): @@ -383,6 +453,7 @@ class GUI: self.state.init("last_undone_commit_hash") self.state.init("recent_msgs_num", 0) self.state.init("web_content_num", 0) + self.state.init("folder_content_num", 0) self.state.init("prompt") self.state.init("scraper") From 84aa177b3575c66f23ed13df0d858922353d46d4 Mon Sep 17 00:00:00 2001 From: Alexander Luck Date: Sat, 26 Apr 2025 16:52:00 +0200 Subject: [PATCH 3/3] filters for single file & folder elements --- aider/gui.py | 130 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index fa250a382..32def9012 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import fnmatch import os import random import sys @@ -143,6 +144,26 @@ class GUI: web_content_empty = None folder_content_empty = None + def filter_files(self, files, allow_patterns_str, deny_patterns_str): + allow_patterns = [p.strip() for p in allow_patterns_str.splitlines() if p.strip()] + deny_patterns = [p.strip() for p in deny_patterns_str.splitlines() if p.strip()] + + filtered_files = files + + if allow_patterns: + allowed_set = set() + for pattern in allow_patterns: + allowed_set.update(fnmatch.filter(files, pattern)) + filtered_files = list(allowed_set) + + if deny_patterns: + denied_set = set() + for pattern in deny_patterns: + denied_set.update(fnmatch.filter(filtered_files, pattern)) + filtered_files = [f for f in filtered_files if f not in denied_set] + + return sorted(filtered_files) + def announce(self): lines = self.coder.get_announcements() lines = " \n".join(lines) @@ -228,48 +249,104 @@ class GUI: def do_add_to_chat(self): # with st.expander("Add to the chat", expanded=True): - self.do_add_files() + self.do_add_files_multiselect() self.do_add_folder() + self.do_add_files_filter_ui() + st.divider() self.do_add_web_page() - def do_add_files(self): + def do_add_files_multiselect(self): + # Initialize state if needed (might be set by filter UI first) + if 'allow_patterns' not in st.session_state: + st.session_state.allow_patterns = "" + if 'deny_patterns' not in st.session_state: + st.session_state.deny_patterns = "" + + # --- Apply Filtering (reads state set by filter UI) --- + all_files = self.coder.get_all_relative_files() + filtered_options = self.filter_files( + all_files, + st.session_state.allow_patterns, + st.session_state.deny_patterns + ) + current_inchat_files = self.coder.get_inchat_relative_files() current_inchat_files_set = set(current_inchat_files) widget_key = "multiselect_add_files" + options_for_widget = sorted(list(set(filtered_options) | current_inchat_files_set)) + + # --- Render Multiselect --- fnames_selected_in_widget = st.multiselect( "Add files to the chat", - options=self.coder.get_all_relative_files(), - default=current_inchat_files, # Reflect current coder state + options=options_for_widget, + default=current_inchat_files, key=widget_key, - placeholder="Files to edit", + placeholder="Type to search or select files...", disabled=self.prompt_pending(), format_func=lambda path: format_path_for_display(path, max_len=60), help=( - "Only add the files that need to be *edited* for the task you are working" - " on. Aider will pull in other relevant code to provide context to the LLM." + "Select files to edit. Use filter controls below to narrow down this list. " + "Aider automatically includes relevant context from other files." ), ) fnames_selected_in_widget_set = set(fnames_selected_in_widget) - # Compare widget state to coder state *before* this render + # --- Handle adding/removing files --- files_to_add = fnames_selected_in_widget_set - current_inchat_files_set files_to_remove = current_inchat_files_set - fnames_selected_in_widget_set for fname in files_to_add: - self.coder.add_rel_fname(fname) - self.info(f"Added {fname} to the chat") + full_path = os.path.join(self.coder.root, fname) + if os.path.exists(full_path): + self.coder.add_rel_fname(fname) + self.info(f"Added `{fname}` to the chat") + else: + st.warning(f"Could not add `{fname}` as it seems to no longer exist.") for fname in files_to_remove: self.coder.drop_rel_fname(fname) - self.info(f"Removed {fname} from the chat") + self.info(f"Removed `{fname}` from the chat") + + # --- New method to render just the filter UI --- + def do_add_files_filter_ui(self): + # Initialize state if needed (might be set by multiselect first) + if 'allow_patterns' not in st.session_state: + st.session_state.allow_patterns = "" + if 'deny_patterns' not in st.session_state: + st.session_state.deny_patterns = "" + + # --- Render Filter Expander --- + with st.expander("Add file filters"): + allow_patterns_input = st.text_area( + "Allow patterns (globs)", + value=st.session_state.allow_patterns, + key="allow_patterns_input", + help="Show only files matching these glob patterns (e.g., `*.py`, `src/**`), one per line. Applied first.", + height=68, + disabled=self.prompt_pending(), + placeholder="*.py\nsrc/**" + ) + deny_patterns_input = st.text_area( + "Deny patterns (globs)", + value=st.session_state.deny_patterns, + key="deny_patterns_input", + help="Hide files matching these glob patterns (e.g., `.venv/*`, `*.log`), one per line. Applied after allow patterns.", + height=68, + disabled=self.prompt_pending(), + placeholder=".venv/*\n*.log" + ) + # Update session state when inputs change (triggers rerun) + st.session_state.allow_patterns = allow_patterns_input + st.session_state.deny_patterns = deny_patterns_input + def do_add_folder(self): + # --- This function remains unchanged --- with st.popover("Add a folder to the chat"): - st.markdown("Add all files from a folder to the chat") + st.markdown("Add all *tracked* files from a folder to the chat (ignores filters)") folder_input_key = f"folder_content_{self.state.folder_content_num}" - # Correctly assign the text input result self.folder_content = st.text_input( "Folder path", placeholder="path/to/folder", @@ -304,25 +381,38 @@ class GUI: if files_to_consider: button_key = f"add_folder_button_{self.state.folder_content_num}" - if st.button("Add all files from folder", key=button_key, disabled=self.prompt_pending()): + if st.button("Add all tracked files from folder", key=button_key, disabled=self.prompt_pending()): added_count = 0 + files_actually_added = [] for file_path in files_to_consider: - # Check if the file is tracked by git if file_path in tracked_files_set: self.coder.add_rel_fname(file_path) + files_actually_added.append(file_path) added_count += 1 if added_count > 0: - self.info(f"Added {added_count} tracked files from '{clean_folder_path}' to the chat") + added_files_str = ", ".join(f"`{f}`" for f in files_actually_added) + self.info(f"Added {added_count} tracked files from `{clean_folder_path}` to the chat: {added_files_str}") self.state.folder_content_num += 1 + self.folder_content = "" st.rerun() else: - st.warning(f"No *new*, tracked files found in folder '{clean_folder_path}' to add.") + st.warning(f"No *new*, *tracked* files found in folder `{clean_folder_path}` to add.") elif self.folder_content: - is_single_tracked_file = clean_folder_path in tracked_files_set and clean_folder_path not in inchat_files - if not is_single_tracked_file: - st.warning(f"No files found in the repository matching the path '{clean_folder_path}'.") + is_single_tracked_file_not_in_chat = clean_folder_path in tracked_files_set and clean_folder_path not in inchat_files + if is_single_tracked_file_not_in_chat: + button_key = f"add_folder_button_{self.state.folder_content_num}" + if st.button(f"Add file `{clean_folder_path}`", key=button_key, disabled=self.prompt_pending()): + self.coder.add_rel_fname(clean_folder_path) + self.info(f"Added `{clean_folder_path}` to the chat") + self.state.folder_content_num += 1 + self.folder_content = "" + st.rerun() + elif not os.path.isdir(os.path.join(self.coder.root, clean_folder_path)) and not is_single_tracked_file_not_in_chat: + st.warning(f"Path `{clean_folder_path}` is not a valid folder or tracked file in the repository.") + else: + st.warning(f"No *new*, *tracked* files found in folder `{clean_folder_path}`.") def do_add_web_page(self): with st.popover("Add a web page to the chat"):