diff --git a/README.md b/README.md index 2026c3ef340..fb596e5d72a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### Documentation and Other Notes +## Documentation and Other Notes * [Agent Mode](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/agent-mode.md) * [MCP Configuration](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/mcp.md) * [Session Management](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/sessions.md) @@ -6,7 +6,7 @@ * [Changelog](https://github.com/dwash96/aider-ce/blob/main/CHANGELOG.md) * [Discord Community](https://discord.gg/McwdCRuqkJ) -### Installation Instructions +## Installation Instructions This project can be installed using several methods: ### Package Installation @@ -29,6 +29,53 @@ uv tool install --python python3.12 aider-ce Use the tool installation so aider doesn't interfere with your development environment +## Configuration + +The documentation above contains the full set of allowed configuration options +but I highly recommend using an `.aider.conf.yml` file. A good place to get started is: + +```yaml +model: +agent: true +analytics: false +auto-commits: true +auto-save: true +auto-load: true +check-update: true +debug: false +enable-context-compaction: true +env-file: .aider.env +multiline: true +preserve-todo-list: true +show-model-warnings: true +watch-files: true +agent-config: | + { + "large_file_token_threshold": 12500, + "skip_cli_confirmations": false + } +mcp-servers: | + { + "mcpServers": + { + "context7":{ + "transport":"http", + "url":"https://mcp.context7.com/mcp" + } + } + } +``` + +Use the adjacent .aider.env file to store model api keys as environment variables, e.g: + +``` +ANTHROPIC_API_KEY="..." +GEMINI_API_KEY="..." +OPENAI_API_KEY="..." +OPENROUTER_API_KEY="..." +DEEPSEEK_API_KEY="..." +``` + ## Project Roadmap/Goals The current priorities are to improve core capabilities and user experience of the Aider project diff --git a/aider/__init__.py b/aider/__init__.py index aa833be4964..ebf15deb00b 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.25.dev" +__version__ = "0.88.26.dev" safe_version = __version__ try: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 43a316f469b..5617b86831c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -479,6 +479,7 @@ def __init__( self.dry_run = dry_run self.pretty = self.io.pretty self.linear_output = linear_output + self.io.linear = linear_output self.main_model = main_model # Set the reasoning tag name based on model settings or default @@ -1137,11 +1138,11 @@ async def _run_linear(self, with_message=None, preproc=True): await self.io.recreate_input() await self.io.input_task user_message = self.io.input_task.result() - + self.io.tool_output("Processing...\n") self.io.output_task = asyncio.create_task(self.generate(user_message, preproc)) await self.io.output_task - + self.io.tool_output("Finished.") self.io.ring_bell() user_message = None self.auto_save_session() @@ -2334,7 +2335,6 @@ async def process_tool_calls(self, tool_call_response): self._print_tool_call_info(server_tool_calls) if await self.io.confirm_ask("Run tools?", group_response="Run MCP Tools"): - await self.io.recreate_input() tool_responses = await self._execute_tool_calls(server_tool_calls) # Add all tool responses @@ -2836,7 +2836,6 @@ 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: diff --git a/aider/commands.py b/aider/commands.py index 7a55af82f53..4085b4510e9 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -414,7 +414,6 @@ async def cmd_lint(self, args="", fnames=None): ) lint_coder.add_rel_fname(fname) - await self.coder.io.recreate_input() await lint_coder.run_one(errors, preproc=False) lint_coder.abs_fnames = set() diff --git a/aider/io.py b/aider/io.py index 06bb8cb22bf..1183a946f68 100644 --- a/aider/io.py +++ b/aider/io.py @@ -348,6 +348,7 @@ def __init__( self.coder = None self.input_task = None self.output_task = None + self.linear = False # State tracking for confirmation input self.confirmation_in_progress = False @@ -529,7 +530,7 @@ def stop_spinner(self): def get_bottom_toolbar(self): """Get the current spinner frame and text for the bottom toolbar.""" - if not self.spinner_running or not self.spinner_frames: + if not self.spinner_running or not self.spinner_frames or self.linear: return None frame = self.spinner_frames[self.spinner_frame_index] @@ -1402,14 +1403,32 @@ def stream_output(self, text, final=False): self._stream_buffer = incomplete_line + should_print = False + should_reset = False + if not final: if len(lines) > 1: + should_print = True + else: + # Ensure any remaining buffered content is printed using the full response + should_print = True + should_reset = True + + if should_print: + try: self.console.print( Text.from_ansi(output) if self.has_ansi_codes(output) else output ) - else: - # Ensure any remaining buffered content is printed using the full response - self.console.print(Text.from_ansi(output) if self.has_ansi_codes(output) else output) + except Exception as e: + if self.verbose: + print(e) + + self.console.print( + (Text.from_ansi(output)) if self.has_ansi_codes(output) else output, + markup=False, + ) + + if should_reset: self.reset_streaming_response() def remove_consecutive_empty_strings(self, string_list): diff --git a/aider/main.py b/aider/main.py index ac7f2167b0b..18cdc6162a4 100644 --- a/aider/main.py +++ b/aider/main.py @@ -563,7 +563,7 @@ async def main_async(argv=None, input=None, output=None, force_git_root=None, re except AttributeError as e: if all(word in str(e) for word in ["bool", "object", "has", "no", "attribute", "strip"]): if check_config_files_for_yes(default_config_files): - return 1 + return await graceful_exit(None, 1) raise e if args.verbose: @@ -595,7 +595,7 @@ async def main_async(argv=None, input=None, output=None, force_git_root=None, re # Ensure parser.prog is set for shtab, though it should be by default parser.prog = "aider" print(shtab.complete(parser, shell=args.shell_completions)) - sys.exit(0) + return await graceful_exit(None, 0) if git is None: args.git = False @@ -684,7 +684,7 @@ def get_io(pretty): except ValueError: io.tool_error(f"Invalid --set-env format: {env_setting}") io.tool_output("Format should be: ENV_VAR_NAME=value") - return 1 + return await graceful_exit(None, 1) # Process any API keys set via --api-key if args.api_key: @@ -696,7 +696,7 @@ def get_io(pretty): except ValueError: io.tool_error(f"Invalid --api-key format: {api_setting}") io.tool_output("Format should be: provider=key") - return 1 + return await graceful_exit(None, 1) if args.anthropic_api_key: os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key @@ -754,11 +754,11 @@ def get_io(pretty): if args.gui and not return_coder: if not await check_streamlit_install(io): analytics.event("exit", reason="Streamlit not installed") - return + return await graceful_exit(None) analytics.event("gui session") launch_gui(argv) analytics.event("exit", reason="GUI session ended") - return + return await graceful_exit(None) if args.verbose: for fname in loaded_dotenvs: @@ -791,7 +791,7 @@ def get_io(pretty): "Provide either a single directory of a git repo, or a list of one or more files." ) analytics.event("exit", reason="Invalid directory input") - return 1 + return await graceful_exit(None, 1) git_dname = None if len(all_files) == 1: @@ -802,7 +802,7 @@ def get_io(pretty): else: io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.") analytics.event("exit", reason="Directory with --no-git") - return 1 + return await graceful_exit(None, 1) # We can't know the git repo for sure until after parsing the args. # If we guessed wrong, reparse because that changes things like @@ -816,17 +816,17 @@ def get_io(pretty): if args.just_check_update: update_available = await check_version(io, just_check=True, verbose=args.verbose) analytics.event("exit", reason="Just checking update") - return 0 if not update_available else 1 + return await graceful_exit(None, 0 if not update_available else 1) if args.install_main_branch: success = await install_from_main_branch(io) analytics.event("exit", reason="Installed main branch") - return 0 if success else 1 + return await graceful_exit(None, 0 if success else 1) if args.upgrade: success = await install_upgrade(io) analytics.event("exit", reason="Upgrade completed") - return 0 if success else 1 + return await graceful_exit(None, 0 if success else 1) if args.check_update: await check_version(io, verbose=args.verbose) @@ -848,7 +848,7 @@ def get_io(pretty): if args.list_models: models.print_matching_models(io, args.list_models) analytics.event("exit", reason="Listed models") - return 0 + return await graceful_exit(None) # Process any command line aliases if args.alias: @@ -859,7 +859,7 @@ def get_io(pretty): io.tool_error(f"Invalid alias format: {alias_def}") io.tool_output("Format should be: alias:model-name") analytics.event("exit", reason="Invalid alias format error") - return 1 + return await graceful_exit(None, 1) alias, model = parts models.MODEL_ALIASES[alias.strip()] = model.strip() @@ -868,7 +868,7 @@ def get_io(pretty): # Error message and analytics event are handled within select_default_model # It might have already offered OAuth if no model/keys were found. # If it failed here, we exit. - return 1 + return await graceful_exit(None, 1) args.model = selected_model_name # Update args with the selected model # Check if an OpenRouter model was selected/specified but the key is missing @@ -895,7 +895,7 @@ def get_io(pretty): "exit", reason="OpenRouter key missing after successful OAuth for specified model", ) - return 1 + return await graceful_exit(None, 1) else: # OAuth failed or was declined by the user io.tool_error( @@ -908,7 +908,7 @@ def get_io(pretty): "exit", reason="OpenRouter key missing for specified model and OAuth failed/declined", ) - return 1 + return await graceful_exit(None, 1) main_model = models.Model( args.model, @@ -976,7 +976,7 @@ def get_io(pretty): lint_cmds = parse_lint_cmds(args.lint_cmd, io) if lint_cmds is None: analytics.event("exit", reason="Invalid lint command format") - return 1 + return await graceful_exit(None, 1) repo = None if args.git: @@ -1002,7 +1002,7 @@ def get_io(pretty): if not args.skip_sanity_check_repo: if not await sanity_check_repo(repo, io): analytics.event("exit", reason="Repository sanity check failed") - return 1 + return await graceful_exit(None, 1) if repo and not args.skip_sanity_check_repo: num_files = len(repo.get_tracked_files()) @@ -1123,7 +1123,7 @@ def get_io(pretty): io.tool_output() except KeyboardInterrupt: analytics.event("exit", reason="Keyboard interrupt during model warnings") - return 1 + return await graceful_exit(coder, 1) if args.git: git_root = await setup_git(git_root, io) @@ -1136,11 +1136,12 @@ def get_io(pretty): urls.edit_formats, "Open documentation about edit formats?", acknowledge=True ) analytics.event("exit", reason="Unknown edit format") - return 1 + return await graceful_exit(None, 1) + except ValueError as err: io.tool_error(str(err)) analytics.event("exit", reason="ValueError during coder creation") - return 1 + return await graceful_exit(None, 1) if return_coder: analytics.event("exit", reason="Returning coder object") @@ -1173,7 +1174,7 @@ def get_io(pretty): messages = coder.format_messages().all_messages() utils.show_messages(messages) analytics.event("exit", reason="Showed prompts") - return + return await graceful_exit(coder) if args.lint: await coder.commands.cmd_lint(fnames=fnames) @@ -1182,7 +1183,7 @@ def get_io(pretty): if not args.test_cmd: io.tool_error("No --test-cmd provided.") analytics.event("exit", reason="No test command provided") - return 1 + return await graceful_exit(coder, 1) await coder.commands.cmd_test(args.test_cmd) if io.placeholder: await coder.run(io.placeholder) @@ -1195,27 +1196,27 @@ def get_io(pretty): if args.lint or args.test or args.commit: analytics.event("exit", reason="Completed lint/test/commit") - return + return await graceful_exit(coder) if args.show_repo_map: repo_map = coder.get_repo_map() if repo_map: io.tool_output(repo_map) analytics.event("exit", reason="Showed repo map") - return + return await graceful_exit(coder) if args.apply: content = io.read_text(args.apply) if content is None: analytics.event("exit", reason="Failed to read apply content") - return + return await graceful_exit(coder) coder.partial_response_content = content # For testing #2879 # from aider.coders.base_coder import all_fences # coder.fence = all_fences[1] await coder.apply_updates() analytics.event("exit", reason="Applied updates") - return + return await graceful_exit(coder) if args.apply_clipboard_edits: args.edit_format = main_model.editor_edit_format @@ -1257,7 +1258,7 @@ def get_io(pretty): except (SwitchCoder, KeyboardInterrupt, SystemExit): pass analytics.event("exit", reason="Completed --message") - return + return await graceful_exit(coder) if args.message_file: try: @@ -1269,18 +1270,18 @@ def get_io(pretty): except FileNotFoundError: io.tool_error(f"Message file not found: {args.message_file}") analytics.event("exit", reason="Message file not found") - return 1 + return await graceful_exit(coder, 1) except IOError as e: io.tool_error(f"Error reading message file: {e}") analytics.event("exit", reason="Message file IO error") - return 1 + return await graceful_exit(coder, 1) analytics.event("exit", reason="Completed --message-file") - return + return await graceful_exit(coder) if args.exit: analytics.event("exit", reason="Exit flag set") - return + return await graceful_exit(coder) analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format) @@ -1300,7 +1301,7 @@ def get_io(pretty): coder.ok_to_warm_cache = bool(args.cache_keepalive_pings) await coder.run() analytics.event("exit", reason="Completed main CLI coder.run") - return + return await graceful_exit(coder) except SwitchCoder as switch: coder.ok_to_warm_cache = False @@ -1323,7 +1324,7 @@ def get_io(pretty): coder.suppress_announcements_for_next_prompt = True except SystemExit: analytics.event("exit", reason="/exit command") - return + return await graceful_exit(coder) def is_first_run_of_new_version(io, verbose=False): @@ -1417,6 +1418,17 @@ def load_slow_imports(swallow=True): raise e +async def graceful_exit(coder=None, exit_code=0): + if coder: + for server in coder.mcp_servers: + try: + await server.exit_stack.aclose() + except Exception: + pass + + return exit_code + + if __name__ == "__main__": status = main() sys.exit(status) diff --git a/aider/tools/command.py b/aider/tools/command.py index a052327aef9..3ed883d8929 100644 --- a/aider/tools/command.py +++ b/aider/tools/command.py @@ -44,8 +44,6 @@ async def _execute_command(coder, command_string): ) ) - await coder.io.recreate_input() - if not confirmed: # This happens if the user explicitly says 'no' this time. # If 'Always' was chosen previously, confirm_ask returns True directly. diff --git a/aider/tools/command_interactive.py b/aider/tools/command_interactive.py index eae1b94e1ae..f47245dcf14 100644 --- a/aider/tools/command_interactive.py +++ b/aider/tools/command_interactive.py @@ -69,8 +69,6 @@ async def _execute_command_interactive(coder, command_string): coder.io.tool_output(" \n") coder.io.tool_output(">>> Interactive command finished <<<") - await coder.io.recreate_input() - # Format the output for the result message, include more content output_content = combined_output or "" # Use the existing token threshold constant as the character limit for truncation diff --git a/aider/tools/view_files_matching.py b/aider/tools/view_files_matching.py index 3ef6f9c75be..a700088f3b9 100644 --- a/aider/tools/view_files_matching.py +++ b/aider/tools/view_files_matching.py @@ -67,9 +67,19 @@ def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False): # Search for pattern in files matches = {} + num_matches = 0 + inspecific_search_flag = False + for file in files_to_search: abs_path = coder.abs_root_path(file) + + if num_matches >= 25: + inspecific_search_flag = True + try: + if coder.repo.ignored_file(abs_path): + continue + with open(abs_path, "r", encoding="utf-8") as f: content = f.read() match_count = 0 @@ -88,6 +98,7 @@ def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False): if match_count > 0: matches[file] = match_count + num_matches += 1 except Exception: # Skip files that can't be read (binary, etc.) pass @@ -102,6 +113,9 @@ def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False): result = ( f"Found '{pattern}' in {len(matches)} files: {', '.join(match_list[:10])} and" f" {len(matches) - 10} more" + "\nTry more specific search terms going forward" + if inspecific_search_flag + else "" ) coder.io.tool_output(f"🔍 Found '{pattern}' in {len(matches)} files") else: