mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-30 17:24:59 +00:00
Merge 84aa177b35
into 3caab85931
This commit is contained in:
commit
ff76b2fad8
1 changed files with 260 additions and 20 deletions
280
aider/gui.py
280
aider/gui.py
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
@ -48,6 +49,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()
|
||||
|
@ -95,6 +142,27 @@ class GUI:
|
|||
last_undo_empty = None
|
||||
recent_msgs_empty = None
|
||||
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()
|
||||
|
@ -181,31 +249,170 @@ 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):
|
||||
fnames = st.multiselect(
|
||||
"Add files to the chat",
|
||||
self.coder.get_all_relative_files(),
|
||||
default=self.state.initial_inchat_files,
|
||||
placeholder="Files to edit",
|
||||
disabled=self.prompt_pending(),
|
||||
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."
|
||||
),
|
||||
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
|
||||
)
|
||||
|
||||
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")
|
||||
current_inchat_files = self.coder.get_inchat_relative_files()
|
||||
current_inchat_files_set = set(current_inchat_files)
|
||||
widget_key = "multiselect_add_files"
|
||||
|
||||
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")
|
||||
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=options_for_widget,
|
||||
default=current_inchat_files,
|
||||
key=widget_key,
|
||||
placeholder="Type to search or select files...",
|
||||
disabled=self.prompt_pending(),
|
||||
format_func=lambda path: format_path_for_display(path, max_len=60),
|
||||
help=(
|
||||
"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)
|
||||
|
||||
# --- 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:
|
||||
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")
|
||||
|
||||
# --- 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 *tracked* files from a folder to the chat (ignores filters)")
|
||||
|
||||
folder_input_key = f"folder_content_{self.state.folder_content_num}"
|
||||
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 tracked files from folder", key=button_key, disabled=self.prompt_pending()):
|
||||
added_count = 0
|
||||
files_actually_added = []
|
||||
for file_path in files_to_consider:
|
||||
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:
|
||||
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.")
|
||||
|
||||
elif self.folder_content:
|
||||
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"):
|
||||
|
@ -336,6 +543,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")
|
||||
|
||||
|
@ -533,6 +741,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("""
|
||||
<style>
|
||||
.stMultiSelect [data-baseweb="tag"] {
|
||||
max-width: 500px;
|
||||
}
|
||||
/* Optional: Ensure text inside tag respects the width, though format_func handles ellipsis now */
|
||||
.stMultiSelect [data-baseweb="tag"] span {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- CSS for Dropdown Items (Optional Tweaks) --- */
|
||||
/* Target list items within the multiselect popover */
|
||||
/* NOTE: Selector might change in future Streamlit versions */
|
||||
div[data-baseweb="popover"] ul li {
|
||||
/* Example: Slightly smaller font for dropdown to fit more */
|
||||
/* font-size: 0.95rem; */
|
||||
|
||||
/* Example: Adjust padding if needed */
|
||||
/* padding-top: 0.2rem; */
|
||||
/* padding-bottom: 0.2rem; */
|
||||
}
|
||||
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# config_options = st.config._config_options
|
||||
# for key, value in config_options.items():
|
||||
# print(f"{key}: {value.value}")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue