diff --git a/aider/__init__.py b/aider/__init__.py index 500230c5098..88d5b450ff0 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.9.dev" +__version__ = "0.88.10.dev" safe_version = __version__ try: diff --git a/aider/args.py b/aider/args.py index 98b42145e5b..0a1bb532b87 100644 --- a/aider/args.py +++ b/aider/args.py @@ -758,6 +758,12 @@ def get_parser(default_config_files, git_root): ), default=False, ) + group.add_argument( + "--debug", + action="store_true", + help="Turn on verbose debugging (default: False)", + default=False, + ) ########## group = parser.add_argument_group("Voice settings") group.add_argument( diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 7fabf2929d2..6ea6ae4f9ec 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -59,7 +59,7 @@ from aider.repo import ANY_GIT_ERROR, GitRepo from aider.repomap import RepoMap from aider.run_cmd import run_cmd -from aider.utils import format_content, format_messages, format_tokens, is_image_file +from aider.utils import format_tokens, is_image_file from ..dump import dump # noqa: F401 from .chat_chunks import ChatChunks @@ -870,6 +870,8 @@ def get_repo_map(self, force_refresh=False): if not self.repo_map: return + 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) @@ -887,6 +889,10 @@ def _include_in_map(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)} @@ -921,6 +927,7 @@ def _include_in_map(abs_path): all_abs_files, ) + self.io.update_spinner(self.io.last_spinner_text) return repo_content def get_repo_messages(self): @@ -970,7 +977,7 @@ def get_chat_files_messages(self): files_content = self.gpt_prompts.files_content_prefix files_content += self.get_files_content() files_reply = self.gpt_prompts.files_content_assistant_reply - elif self.get_repo_map() and self.gpt_prompts.files_no_full_files_with_repo_map: + elif self.gpt_prompts.files_no_full_files_with_repo_map: files_content = self.gpt_prompts.files_no_full_files_with_repo_map files_reply = self.gpt_prompts.files_no_full_files_with_repo_map_reply else: @@ -1125,15 +1132,12 @@ async def _run_patched(self, with_message=None, preproc=True): return self.partial_response_content user_message = None + self.user_message = "" await self.io.cancel_input_task() await self.io.cancel_processing_task() while True: try: - if self.commands.cmd_running: - await asyncio.sleep(0.1) - continue - if ( not self.io.confirmation_in_progress and not user_message @@ -1156,6 +1160,19 @@ async def _run_patched(self, with_message=None, preproc=True): # Yield Control so input can actually get properly set up await asyncio.sleep(0) + if self.user_message: + self.io.processing_task = asyncio.create_task( + self._processing_logic(self.user_message, preproc) + ) + + self.user_message = "" + # Start spinner for processing task + self.io.start_spinner("Processing...") + + if self.commands.cmd_running: + await asyncio.sleep(0.1) + continue + tasks = set() if self.io.processing_task: @@ -1189,8 +1206,9 @@ async def _run_patched(self, with_message=None, preproc=True): self.io.stop_spinner() try: - user_message = self.io.input_task.result() - await self.io.cancel_input_task() + if self.io.input_task: + user_message = self.io.input_task.result() + await self.io.cancel_input_task() if self.commands.is_run_command(user_message): self.commands.cmd_running = True @@ -1237,12 +1255,7 @@ async def _run_patched(self, with_message=None, preproc=True): self.io.stop_spinner() if user_message and not self.io.acknowledge_confirmation(): - self.io.processing_task = asyncio.create_task( - self._processing_logic(user_message, preproc) - ) - - # Start spinner for processing task - self.io.start_spinner("Processing...") + self.user_message = user_message self.io.ring_bell() user_message = None @@ -1263,6 +1276,8 @@ async def _run_patched(self, with_message=None, preproc=True): await self.io.cancel_processing_task() async def _processing_logic(self, user_message, preproc): + await asyncio.sleep(0.1) + try: self.compact_context_completed = False await self.compact_context_if_needed() @@ -1894,7 +1909,8 @@ async def send_message(self, inp): dict(role="user", content=inp), ] - chunks = self.format_messages() + loop = asyncio.get_running_loop() + chunks = await loop.run_in_executor(None, self.format_messages) messages = chunks.all_messages() if not await self.check_tokens(messages): @@ -2704,8 +2720,6 @@ async def send(self, messages, model=None, functions=None, tools=None): self.partial_response_function_call = dict() self.partial_response_tool_calls = [] - self.io.log_llm_history("TO LLM", format_messages(messages)) - completion = None try: @@ -2738,11 +2752,6 @@ async def send(self, messages, model=None, functions=None, tools=None): self.keyboard_interrupt() raise kbi finally: - self.io.log_llm_history( - "LLM RESPONSE", - format_content("ASSISTANT", self.partial_response_content), - ) - self.preprocess_response() if self.partial_response_content: @@ -3127,6 +3136,7 @@ def show_usage_report(self): self.total_tokens_received += self.message_tokens_received self.io.tool_output(self.usage_report) + self.io.rule() prompt_tokens = self.message_tokens_sent completion_tokens = self.message_tokens_received diff --git a/aider/coders/navigator_coder.py b/aider/coders/navigator_coder.py index df7e4159ccd..cfa0505353a 100644 --- a/aider/coders/navigator_coder.py +++ b/aider/coders/navigator_coder.py @@ -2298,15 +2298,15 @@ def get_directory_structure(self): current = current[part] # Function to recursively print the tree - def print_tree(node, prefix="- ", indent=" ", path=""): + def print_tree(node, prefix="- ", indent=" ", current_path=""): lines = [] # First print all directories dirs = sorted([k for k in node.keys() if k != "."]) for i, dir_name in enumerate(dirs): - full_path = f"{path}/{dir_name}" if path else dir_name - lines.append(f"{prefix}{full_path}/") + # Only print the current directory name, not the full path + lines.append(f"{prefix}{dir_name}/") sub_lines = print_tree( - node[dir_name], prefix=prefix, indent=indent, path=full_path + node[dir_name], prefix=prefix, indent=indent, current_path=dir_name ) for sub_line in sub_lines: lines.append(f"{indent}{sub_line}") @@ -2314,9 +2314,8 @@ def print_tree(node, prefix="- ", indent=" ", path=""): # Then print all files if "." in node: for file_name in sorted(node["."]): - lines.append( - f"{prefix}{path}/{file_name}" if path else f"{prefix}{file_name}" - ) + # Only print the current file name, not the full path + lines.append(f"{prefix}{file_name}") return lines diff --git a/aider/io.py b/aider/io.py index 7c6c845fba4..aad266683df 100644 --- a/aider/io.py +++ b/aider/io.py @@ -509,6 +509,9 @@ def start_spinner(self, text, update_last_text=True): self.fallback_spinner = Spinner(text) self.fallback_spinner.step() + def update_spinner(self, text): + self.spinner_text = text + def stop_spinner(self): """Stop the spinner.""" self.spinner_running = False diff --git a/aider/main.py b/aider/main.py index dfbc36f6278..729eabe6ec1 100644 --- a/aider/main.py +++ b/aider/main.py @@ -470,6 +470,37 @@ def expand_glob_patterns(patterns, root="."): return expanded_files +PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) +log_file = None +file_blacklist = ["get_bottom_toolbar", ""] + + +def custom_tracer(frame, event, arg): + # Get the absolute path of the file where the code is executing + filename = os.path.abspath(frame.f_code.co_filename) + + # --- THE FILTERING LOGIC --- + # Only proceed if the file path is INSIDE the project root + if not filename.startswith(PROJECT_ROOT): + return None # Returning None means no local trace function for this scope + + if filename.endswith("repo.py"): + return None + + # If it's your code, trace the call + if event == "call": + func_name = frame.f_code.co_name + line_no = frame.f_lineno + + if func_name not in file_blacklist: + log_file.write( + f"-> CALL (My Code): {func_name}() in {os.path.basename(filename)}:{line_no}\n" + ) + + # Must return the trace function (or a local one) for subsequent events + return custom_tracer + + def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): return asyncio.run(main_async(argv, input, output, force_git_root, return_coder)) @@ -529,6 +560,11 @@ async def main_async(argv=None, input=None, output=None, force_git_root=None, re # Parse again to include any arguments that might have been defined in .env args = parser.parse_args(argv) + if args.debug: + global log_file + log_file = open(".aider-debug.log", "w", buffering=1) + sys.settrace(custom_tracer) + if args.shell_completions: # Ensure parser.prog is set for shtab, though it should be by default parser.prog = "aider" @@ -1182,7 +1218,7 @@ def get_io(pretty): io.tool_output() try: await coder.run(with_message=args.message) - except (SwitchCoder, KeyboardInterrupt): + except (SwitchCoder, KeyboardInterrupt, SystemExit): pass analytics.event("exit", reason="Completed --message") return @@ -1192,6 +1228,8 @@ def get_io(pretty): message_from_file = io.read_text(args.message_file) io.tool_output() await coder.run(with_message=message_from_file) + except (SwitchCoder, KeyboardInterrupt, SystemExit): + pass except FileNotFoundError: io.tool_error(f"Message file not found: {args.message_file}") analytics.event("exit", reason="Message file not found") @@ -1235,6 +1273,9 @@ def get_io(pretty): if switch.kwargs.get("show_announcements") is False: coder.suppress_announcements_for_next_prompt = True + except SystemExit: + analytics.event("exit", reason="/exit command") + return def is_first_run_of_new_version(io, verbose=False): diff --git a/aider/mcp/server.py b/aider/mcp/server.py index ba74727460a..7fe770978a0 100644 --- a/aider/mcp/server.py +++ b/aider/mcp/server.py @@ -51,12 +51,15 @@ async def connect(self): ) try: - stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) - read, write = stdio_transport - session = await self.exit_stack.enter_async_context(ClientSession(read, write)) - await session.initialize() - self.session = session - return session + with open(".aider-mcp-errors.log", "w") as err_file: + stdio_transport = await self.exit_stack.enter_async_context( + stdio_client(server_params, errlog=err_file) + ) + read, write = stdio_transport + session = await self.exit_stack.enter_async_context(ClientSession(read, write)) + await session.initialize() + self.session = session + return session except Exception as e: logging.error(f"Error initializing server {self.name}: {e}") await self.disconnect() diff --git a/aider/repomap.py b/aider/repomap.py index b56684363f5..275fba3d206 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -19,7 +19,6 @@ from aider.dump import dump from aider.special import filter_important_files from aider.tools.tool_utils import ToolError -from aider.waiting import Spinner # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) @@ -83,6 +82,10 @@ class RepoMap: warned_files = set() + # Class variable to store initial ranked tags results + _initial_ranked_tags = None + _initial_ident_to_files = None + # Define kinds that typically represent definitions across languages # Used by NavigatorCoder to filter tags for the symbol outline definition_kinds = { @@ -430,6 +433,27 @@ def get_symbol_definition_location(self, file_path, symbol_name): return definition_tag.start_line, definition_tag.end_line # Check if the file is in the cache and if the modification time has not changed + def shared_path_components(self, path1_str, path2_str): + """ + Calculates distance based on how many parent components are shared. + Distance = Total parts - (2 * Shared parts). Lower is closer. + """ + p1 = Path(path1_str).parts + p2 = Path(path2_str).parts + + # Count the number of common leading parts + common_count = 0 + for comp1, comp2 in zip(p1, p2): + if comp1 == comp2: + common_count += 1 + else: + break + + # A simple metric of difference: + # (Total parts in P1 + Total parts in P2) - (2 * Common parts) + distance = len(p1) + len(p2) - (2 * common_count) + return distance + def get_tags_raw(self, fname, rel_fname): lang = filename_to_lang(fname) if not lang: @@ -530,7 +554,7 @@ def get_tags_raw(self, fname, rel_fname): ) def get_ranked_tags( - self, chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, progress=None + self, chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, progress=True ): import networkx as nx @@ -568,7 +592,7 @@ def get_ranked_tags( if self.verbose: self.io.tool_output(f"Processing {fname}") if progress and not showing_bar: - progress(f"{UPDATING_REPO_MAP_MESSAGE}: {fname}") + self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {fname}") try: file_ok = Path(fname).is_file() @@ -644,11 +668,11 @@ def get_ranked_tags( if ident in references: continue for definer in defines[ident]: - G.add_edge(definer, definer, weight=0.01, ident=ident) + G.add_edge(definer, definer, weight=0.000001, ident=ident) for ident in idents: if progress: - progress(f"{UPDATING_REPO_MAP_MESSAGE}: {ident}") + self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {ident}") definers = defines[ident] @@ -658,7 +682,7 @@ def get_ranked_tags( is_kebab = ("-" in ident) and any(c.isalpha() for c in ident) is_camel = any(c.isupper() for c in ident) and any(c.islower() for c in ident) if ident in mentioned_idents: - mul *= 10 + mul *= 16 # Prioritize function-like identifiers if ( @@ -666,13 +690,14 @@ def get_ranked_tags( and len(ident) >= 8 and "test" not in ident.lower() ): - mul *= 10 + mul *= 16 # Downplay repetitive definitions in case of common boiler plate # Scale down logarithmically given the increasing number of references in a codebase # Ideally, this will help downweight boiler plate in frameworks, interfaces, and abstract classes - if len(defines[ident]) > 5: - mul *= math.log((5 / (len(defines[ident]) ** 2)) + 1) + if len(defines[ident]) > 4: + exp = min(len(defines[ident]), 32) + mul *= math.log((4 / (2**exp)) + 1) # Calculate multiplier: log(number of unique file references * total references ^ 2) # Used to balance the number of times an identifier appears with its number of refs per file @@ -684,13 +709,11 @@ def get_ranked_tags( # With absolute number of references throughout a codebase unique_file_refs = len(set(references[ident])) total_refs = len(references[ident]) - ext_mul = math.log(unique_file_refs * total_refs**2 + 1) + ext_mul = round(math.log(unique_file_refs * total_refs**2 + 1)) for referencer, num_refs in Counter(references[ident]).items(): for definer in definers: # dump(referencer, definer, num_refs, mul) - # if referencer == definer: - # continue # Only add edge if file extensions match referencer_ext = Path(referencer).suffix @@ -699,13 +722,17 @@ def get_ranked_tags( continue use_mul = mul * ext_mul + if referencer in chat_rel_fnames: - use_mul *= 50 + use_mul *= 64 + elif referencer == definer: + use_mul *= 1 / 128 # scale down so high freq (low value) mentions don't dominate - num_refs = math.sqrt(num_refs) - - G.add_edge(referencer, definer, weight=use_mul * num_refs, ident=ident) + # num_refs = math.sqrt(num_refs) + path_distance = self.shared_path_components(referencer, definer) + weight = num_refs * use_mul * 2 ** (-1 * path_distance) + G.add_edge(referencer, definer, weight=weight, ident=ident) if not references: pass @@ -728,7 +755,7 @@ def get_ranked_tags( ranked_definitions = defaultdict(float) for src in G.nodes: if progress: - progress(f"{UPDATING_REPO_MAP_MESSAGE}: {src}") + self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {src}") src_rank = ranked[src] total_weight = sum(data["weight"] for _src, _dst, data in G.out_edges(src, data=True)) @@ -744,6 +771,10 @@ def get_ranked_tags( ) # dump(ranked_definitions) + # with open('defs.txt', 'w') as out_file: + # import pprint + # printer = pprint.PrettyPrinter(indent=2, stream=out_file) + # printer.pprint(ranked_definitions) for (fname, ident), rank in ranked_definitions: # print(f"{rank:.03f} {fname} {ident}") @@ -837,14 +868,10 @@ def get_ranked_tags_map_uncached( if not mentioned_idents: mentioned_idents = set() - spin = Spinner(UPDATING_REPO_MAP_MESSAGE) + self.io.update_spinner(UPDATING_REPO_MAP_MESSAGE) ranked_tags = self.get_ranked_tags( - chat_fnames, - other_fnames, - mentioned_fnames, - mentioned_idents, - progress=spin.step, + chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, True ) other_rel_fnames = sorted(set(self.get_rel_fname(fname) for fname in other_fnames)) @@ -855,8 +882,6 @@ def get_ranked_tags_map_uncached( ranked_tags = special_fnames + ranked_tags - spin.step() - num_tags = len(ranked_tags) lower_bound = 0 upper_bound = num_tags @@ -875,7 +900,8 @@ def get_ranked_tags_map_uncached( show_tokens = f"{middle / 1000.0:.1f}K" else: show_tokens = str(middle) - spin.step(f"{UPDATING_REPO_MAP_MESSAGE}: {show_tokens} tokens") + + self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {show_tokens} tokens") tree = self.to_tree(ranked_tags[:middle], chat_rel_fnames) num_tokens = self.token_count(tree) @@ -896,7 +922,6 @@ def get_ranked_tags_map_uncached( middle = int((lower_bound + upper_bound) // 2) - spin.end() return best_tree tree_cache = dict()