diff --git a/aider/__init__.py b/aider/__init__.py index 1195d736485..15229337b59 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.91.0.dev" +__version__ = "0.91.1.dev" safe_version = __version__ try: diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index 44175c63446..7cb60a16eaf 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -374,7 +374,7 @@ async def initialize_mcp_tools(self): if not local_tools: return - local_server_config = {"name": "local_tools"} + local_server_config = {"name": "Local"} local_server = LocalServer(local_server_config) if not self.mcp_servers: diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index fb1a4cb2df2..62c64bd35ba 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -799,30 +799,21 @@ def get_files_content(self, fnames=None): file_tokens = self.main_model.token_count(content) if file_tokens > self.large_file_token_threshold: - # Truncate the file content - lines = content.splitlines() - - # Keep the first and last parts of the file with a marker in between - keep_lines = ( - self.large_file_token_threshold // 40 - ) # Rough estimate of tokens per line - first_chunk = lines[: keep_lines // 2] - last_chunk = lines[-(keep_lines // 2) :] - - truncated_content = "\n".join(first_chunk) - truncated_content += ( - f"\n\n... [File truncated due to size ({file_tokens} tokens). Use" - " /context-management to toggle truncation off] ...\n\n" - ) - truncated_content += "\n".join(last_chunk) + # Instead of truncating, show the file's definitions/structure + file_stub = RepoMap.get_file_stub(fname, self.io) - # Add message about truncation + # Add message about showing definitions instead of full content self.io.tool_output( f"⚠️ '{relative_fname}' is very large ({file_tokens} tokens). " "Use /context-management to toggle truncation off if needed." ) - file_prompt += truncated_content + # Add a message in the content itself so the model knows it's truncated + truncation_note = ( + f"\n... [File content truncated due to size ({file_tokens} tokens)." + " Showing structure/definitions only.] ...\n\n" + ) + file_prompt += truncation_note + file_stub else: file_prompt += content else: @@ -876,30 +867,21 @@ def get_read_only_files_content(self): file_tokens = self.main_model.token_count(content) if file_tokens > self.large_file_token_threshold: - # Truncate the file content - lines = content.splitlines() - - # Keep the first and last parts of the file with a marker in between - keep_lines = ( - self.large_file_token_threshold // 40 - ) # Rough estimate of tokens per line - first_chunk = lines[: keep_lines // 2] - last_chunk = lines[-(keep_lines // 2) :] - - truncated_content = "\n".join(first_chunk) - truncated_content += ( - f"\n\n... [File truncated due to size ({file_tokens} tokens). Use" - " /context-management to toggle truncation off] ...\n\n" - ) - truncated_content += "\n".join(last_chunk) + # Instead of truncating, show the file's definitions/structure + file_stub = RepoMap.get_file_stub(fname, self.io) - # Add message about truncation + # Add message about showing definitions instead of full content self.io.tool_output( f"⚠️ '{relative_fname}' is very large ({file_tokens} tokens). " "Use /context-management to toggle truncation off if needed." ) - prompt += truncated_content + # Add a message in the content itself so the model knows it's truncated + truncation_note = ( + f"\n... [File content truncated due to size ({file_tokens} tokens)." + " Showing structure/definitions only.] ...\n\n" + ) + prompt += truncation_note + file_stub else: prompt += content else: @@ -1229,8 +1211,9 @@ def init_before_message(self): self.commit_before_message.append(self.repo.get_head_commit_sha()) async def run(self, with_message=None, preproc=True): - while self.io.confirmation_in_progress: - await asyncio.sleep(0.1) # Yield control and wait briefly + # Wait for confirmation to finish if in progress + if not self.io.confirmation_in_progress_event.is_set(): + await self.io.confirmation_in_progress_event.wait() if self.linear_output: return await self._run_linear(with_message, preproc) @@ -1253,8 +1236,9 @@ async def _run_linear(self, with_message=None, preproc=True): while True: try: - if self.commands.cmd_running: - await asyncio.sleep(0.1) + # Wait for commands to finish + if not self.commands.cmd_running_event.is_set(): + await self.commands.cmd_running_event.wait() continue if not self.suppress_announcements_for_next_prompt: @@ -1362,8 +1346,8 @@ async def input_task(self, preproc): while self.input_running: try: # Wait for commands to finish - if self.commands.cmd_running: - await asyncio.sleep(0.1) + if not self.commands.cmd_running_event.is_set(): + await self.commands.cmd_running_event.wait() continue # Wait for input task completion @@ -1372,7 +1356,7 @@ async def input_task(self, preproc): user_message = self.io.input_task.result() # Defer to confirmation handler to fix Windows event loop race. - if self.io.confirmation_in_progress: + if not self.io.confirmation_in_progress_event.is_set(): pass # Set user message for output task elif not self.io.acknowledge_confirmation(): @@ -1389,7 +1373,7 @@ async def input_task(self, preproc): # Check if we should show announcements if ( - not self.io.confirmation_in_progress + self.io.confirmation_in_progress_event.is_set() and not self.user_message and not coroutines.is_active(self.io.input_task) and (not coroutines.is_active(self.io.output_task) or not self.io.placeholder) @@ -1427,8 +1411,8 @@ async def output_task(self, preproc): while self.output_running: try: # Wait for commands to finish - if self.commands.cmd_running: - await asyncio.sleep(0.1) + if not self.commands.cmd_running_event.is_set(): + await self.commands.cmd_running_event.wait() continue # Check if we have a user message to process @@ -1530,7 +1514,7 @@ async def preproc_user_input(self, inp): inp = f"/run {inp[1:]}" if self.commands.is_run_command(inp): - self.commands.cmd_running = True + self.commands.cmd_running_event.clear() # Command is running return await self.commands.run(inp) @@ -3038,8 +3022,8 @@ async def show_send_output_stream(self, completion): print(chunk, file=f) # Check if confirmation is in progress and wait if needed - while self.io.confirmation_in_progress: - await asyncio.sleep(0.1) # Yield control and wait briefly + if not self.io.confirmation_in_progress_event.is_set(): + await self.io.confirmation_in_progress_event.wait() if isinstance(chunk, str): self.io.tool_error(chunk) @@ -3830,7 +3814,7 @@ async def run_shell_commands(self): accumulated_output = "" try: - self.commands.cmd_running = True + self.commands.cmd_running_event.clear() # Command is running for command in self.shell_commands: if command in done: @@ -3842,7 +3826,7 @@ async def run_shell_commands(self): return accumulated_output finally: - self.commands.cmd_running = False + self.commands.cmd_running_event.set() # Command finished async def handle_shell_commands(self, commands_str, group): commands = commands_str.strip().split(";") diff --git a/aider/commands.py b/aider/commands.py index 9e71ed704c6..7cb8811e4a3 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1,3 +1,4 @@ +import asyncio import re import sys from pathlib import Path @@ -21,6 +22,8 @@ def clone(self): self.io, None, voice_language=self.voice_language, + voice_input_device=self.voice_input_device, + voice_format=self.voice_format, verify_ssl=self.verify_ssl, args=self.args, parser=self.parser, @@ -62,7 +65,8 @@ def __init__( # Store the original read-only filenames provided via args.read self.original_read_only_fnames = set(original_read_only_fnames or []) - self.cmd_running = False + self.cmd_running_event = asyncio.Event() + self.cmd_running_event.set() # Initially set, meaning no command is running def is_command(self, inp): return inp[0] in "/!" @@ -108,13 +112,27 @@ async def do_run(self, cmd_name, args): self.io.tool_output(f"Error: Command {cmd_name} not found.") return + self.cmd_running_event.clear() # Command is running try: + # Generate a spreadable kwargs dict with all relevant Commands attributes + kwargs = { + "original_read_only_fnames": self.original_read_only_fnames, + "voice_language": self.voice_language, + "voice_format": self.voice_format, + "voice_input_device": self.voice_input_device, + "verify_ssl": self.verify_ssl, + "parser": self.parser, + "verbose": self.verbose, + "editor": self.editor, + "system_args": self.args, + } + return await CommandRegistry.execute( cmd_name, self.io, self.coder, args, - original_read_only_fnames=self.original_read_only_fnames, + **kwargs, ) except ANY_GIT_ERROR as err: self.io.tool_error(f"Unable to complete {cmd_name}: {err}") @@ -124,6 +142,10 @@ async def do_run(self, cmd_name, args): except Exception as e: self.io.tool_error(f"Error executing command {cmd_name}: {str(e)}") return + finally: + self.cmd_running_event.set() # Command finished + if self.coder.tui and self.coder.tui(): + self.coder.tui().refresh() def matching_commands(self, inp): words = inp.strip().split() diff --git a/aider/commands/reset.py b/aider/commands/reset.py index fdab6a7d98e..87bf923e8fe 100644 --- a/aider/commands/reset.py +++ b/aider/commands/reset.py @@ -21,8 +21,8 @@ async def execute(cls, io, coder, args, **kwargs): # Clear TUI output if available if coder.tui and coder.tui(): coder.tui().action_clear_output() - - io.tool_output("All files dropped and chat history cleared.") + else: + io.tool_output("All files dropped and chat history cleared.") # Recalculate context block tokens after dropping all files if hasattr(coder, "use_enhanced_context") and coder.use_enhanced_context: diff --git a/aider/commands/settings.py b/aider/commands/settings.py index eb19f589a8b..ace5230b528 100644 --- a/aider/commands/settings.py +++ b/aider/commands/settings.py @@ -13,7 +13,7 @@ class SettingsCommand(BaseCommand): async def execute(cls, io, coder, args, **kwargs): # Get parser and args from kwargs or use defaults parser = kwargs.get("parser") - cmd_args = kwargs.get("args") + cmd_args = kwargs.get("system_args") if not parser or not cmd_args: io.tool_error("Settings command requires parser and args context") diff --git a/aider/commands/utils/helpers.py b/aider/commands/utils/helpers.py index 8e93b6b520a..bb55e782ba9 100644 --- a/aider/commands/utils/helpers.py +++ b/aider/commands/utils/helpers.py @@ -104,10 +104,10 @@ def format_command_result(io, command_name: str, success_message: str, error: Ex Formatted result string """ if error: - io.tool_error(f"Error in {command_name}: {str(error)}") + io.tool_error(f"\nError in {command_name}: {str(error)}") return f"Error: {str(error)}" else: - io.tool_output(f"✅ {success_message}") + io.tool_output(f"\n✅ {success_message}") return f"Successfully executed {command_name}." diff --git a/aider/io.py b/aider/io.py index 052ccb02379..2b50c9d88f5 100644 --- a/aider/io.py +++ b/aider/io.py @@ -362,7 +362,8 @@ def __init__( self.linear = False # State tracking for confirmation input - self.confirmation_in_progress = False + self.confirmation_in_progress_event = asyncio.Event() + self.confirmation_in_progress_event.set() # Initially set, meaning no confirmation in progress self.confirmation_acknowledgement = False self.confirmation_input_active = False self.saved_input_text = "" @@ -939,7 +940,7 @@ def get_continuation(width, line_number, is_soft_wrap): coder = self.get_coder() if coder: - await coder.commands.cmd_exit(None) + await coder.commands.do_run("exit", "") else: raise SystemExit @@ -1081,7 +1082,7 @@ def user_input(self, inp, log_only=True): if ( len(inp) <= 1 - or self.confirmation_in_progress + or not self.confirmation_in_progress_event.is_set() or self.get_confirmation_acknowledgement() ): return @@ -1153,7 +1154,7 @@ async def confirm_ask( *args, **kwargs, ): - self.confirmation_in_progress = True + self.confirmation_in_progress_event.clear() # Confirmation is in progress try: return await asyncio.create_task(self._confirm_ask(*args, **kwargs)) @@ -1161,7 +1162,7 @@ async def confirm_ask( # Re-raise KeyboardInterrupt to allow it to propagate raise finally: - self.confirmation_in_progress = False + self.confirmation_in_progress_event.set() # Confirmation finished async def _confirm_ask( self, @@ -1671,7 +1672,10 @@ def toggle_multiline_mode(self): ) def append_chat_history(self, text, linebreak=False, blockquote=False, strip=True): - if self.confirmation_in_progress or self.get_confirmation_acknowledgement(): + if ( + not self.confirmation_in_progress_event.is_set() + or self.get_confirmation_acknowledgement() + ): return if blockquote: diff --git a/aider/repomap.py b/aider/repomap.py index 63a596eade5..7885979ea1b 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -15,7 +15,6 @@ from grep_ast import TreeContext, filename_to_lang from pygments.lexers import guess_lexer_for_filename from pygments.token import Token -from tqdm import tqdm from aider.dump import dump from aider.helpers.similarity import ( @@ -76,9 +75,9 @@ def __new__( SQLITE_ERRORS = (sqlite3.OperationalError, sqlite3.DatabaseError, OSError) -CACHE_VERSION = 6 +CACHE_VERSION = 7 if USING_TSL_PACK: - CACHE_VERSION = 8 + CACHE_VERSION = 9 UPDATING_REPO_MAP_MESSAGE = "Updating repo map" @@ -349,6 +348,46 @@ def get_mtime(self, fname): except FileNotFoundError: self.io.tool_warning(f"File not found error: {fname}") + def _compute_file_summary(self, tags, rel_fname): + """Compute file-level summary from tags.""" + defines = set() + references = defaultdict(int) + imports = set() + + for tag in tags: + if tag.kind == "def": + defines.add(tag.name) + elif tag.kind == "ref": + references[tag.name] += 1 + if tag.specific_kind == "import": + imports.add(tag.name) + + return {"defines": defines, "references": dict(references), "imports": imports} + + def _get_cached_summary(self, fname, file_mtime): + """Get cached summary for a file if available and up-to-date.""" + cache_key = fname + try: + val = self.TAGS_CACHE.get(cache_key) # Issue #1308 + except SQLITE_ERRORS as e: + self.tags_cache_error(e) + val = self.TAGS_CACHE.get(cache_key) + + if val is not None and val.get("mtime") == file_mtime: + # Handle backward compatibility: old cache entries won't have "summary" + summary = val.get("summary") + if summary is None: + # Compute summary from cached data + data = val.get("data") + if data is not None: + rel_fname = self.get_rel_fname(fname) + summary = self._compute_file_summary(data, rel_fname) + # Update cache with summary for future use + val["summary"] = summary + self.TAGS_CACHE[cache_key] = val + return summary + return None + def get_tags(self, fname, rel_fname): # Check if the file is in the cache and if the modification time has not changed file_mtime = self.get_mtime(fname) @@ -385,13 +424,16 @@ def get_tags(self, fname, rel_fname): # miss! data = list(self.get_tags_raw(fname, rel_fname)) + # Compute file summary + summary = self._compute_file_summary(data, rel_fname) + # Update the cache try: - self.TAGS_CACHE[cache_key] = {"mtime": file_mtime, "data": data} + self.TAGS_CACHE[cache_key] = {"mtime": file_mtime, "data": data, "summary": summary} self.save_tags_cache() except SQLITE_ERRORS as e: self.tags_cache_error(e) - self.TAGS_CACHE[cache_key] = {"mtime": file_mtime, "data": data} + self.TAGS_CACHE[cache_key] = {"mtime": file_mtime, "data": data, "summary": summary} return data @@ -631,19 +673,25 @@ def get_ranked_tags( self.io.tool_output( "Initial repo scan can be slow in larger repos, but only happens once." ) - fnames = tqdm(fnames, desc="Scanning repo") + self.io.update_spinner("Scanning repo") showing_bar = True else: showing_bar = False + num_fnames = len(fnames) + fname_index = 0 for fname in fnames: if self.verbose: self.io.tool_output(f"Processing {fname}") - if progress and not showing_bar: - self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {fname}") + if progress: + if showing_bar: + fname_index += 1 + self.io.update_spinner(f"Scanning repo: {fname_index}/{num_fnames}") + else: + self.io.update_spinner(f"{UPDATING_REPO_MAP_MESSAGE}: {fname}") try: - file_ok = Path(fname).is_file() + file_ok = os.path.isfile(fname) except OSError: file_ok = False @@ -685,23 +733,49 @@ def get_ranked_tags( if current_pers > 0: personalization[rel_fname] = current_pers # Assign the final calculated value - tags = list(self.get_tags(fname, rel_fname)) - - if tags is None: - continue + # Get file mtime and check for cached summary + file_mtime = self.get_mtime(fname) + summary = None + if file_mtime is not None: + summary = self._get_cached_summary(fname, file_mtime) + + if summary is not None: + # Use cached summary for defines and references + for ident in summary["defines"]: + defines[ident].add(rel_fname) + for ident, count in summary["references"].items(): + references[ident][rel_fname] += count + total_ref_count[ident] += count + for imp in summary["imports"]: + file_imports[rel_fname].add(imp) + + # Still need to parse tags for definitions (Tag objects) + # But only if this file has definitions + if summary["defines"]: + tags = list(self.get_tags(fname, rel_fname)) + if tags is not None: + for tag in tags: + if tag.kind == "def": + key = (rel_fname, tag.name) + definitions[key].add(tag) + else: + # No cached summary, parse all tags + tags = list(self.get_tags(fname, rel_fname)) + if tags is None: + continue - for tag in tags: - if tag.kind == "def": - defines[tag.name].add(rel_fname) - key = (rel_fname, tag.name) - definitions[key].add(tag) + for tag in tags: + if tag.kind == "def": + defines[tag.name].add(rel_fname) + key = (rel_fname, tag.name) + definitions[key].add(tag) - elif tag.kind == "ref": - references[tag.name][rel_fname] += 1 - total_ref_count[tag.name] += 1 + elif tag.kind == "ref": + references[tag.name][rel_fname] += 1 + total_ref_count[tag.name] += 1 - if tag.specific_kind == "import": - file_imports[rel_fname].add(tag.name) + if tag.specific_kind == "import": + file_imports[rel_fname].add(tag.name) self.io.profile("Process Files") @@ -1267,7 +1341,7 @@ def get_supported_languages_md(): for lang, ext in data: fn = get_scm_fname(lang) - repo_map = "✓" if Path(fn).exists() else "" + repo_map = "✓" if fn and os.path.exists(fn) else "" linter_support = "✓" res += f"| {lang:20} | {ext:20} | {repo_map:^8} | {linter_support:^6} |\n" @@ -1282,7 +1356,7 @@ def get_supported_languages_md(): chat_fnames = [] other_fnames = [] for fname in sys.argv[1:]: - if Path(fname).is_dir(): + if os.path.isdir(fname): chat_fnames += find_src_files(fname) else: chat_fnames.append(fname) diff --git a/aider/tui/widgets/completion_bar.py b/aider/tui/widgets/completion_bar.py index a516f147f3e..e3a9ba3fd84 100644 --- a/aider/tui/widgets/completion_bar.py +++ b/aider/tui/widgets/completion_bar.py @@ -44,6 +44,10 @@ class CompletionBar(Widget, can_focus=False): text-style: bold; } + CompletionBar .completion-item.preselected { + color: $secondary; + } + CompletionBar .completion-more { width: auto; height: 1; @@ -82,6 +86,7 @@ def __init__(self, suggestions: list[str] = None, prefix: str = "", **kwargs): self.suggestions = (suggestions or [])[: self.MAX_SUGGESTIONS] self.prefix = prefix self.selected_index = 0 + self._has_cycled = False # Track if user has actively cycled through suggestions self._item_widgets: list[Static] = [] self._prefix_widget: Static | None = None self._left_more: Static | None = None @@ -150,7 +155,8 @@ def compose(self) -> ComposeResult: self._item_widgets = [] for i in range(self.WINDOW_SIZE): if i < len(self._display_names): - classes = "completion-item selected" if i == 0 else "completion-item" + selected_class = "selected" if self._has_cycled else "preselected" + classes = f"completion-item {selected_class}" if i == 0 else "completion-item" item = Static(self._display_names[i], classes=classes) else: item = Static("", classes="completion-item") @@ -173,6 +179,7 @@ def update_suggestions(self, suggestions: list[str], prefix: str = "") -> None: self.suggestions = suggestions[: self.MAX_SUGGESTIONS] self.prefix = prefix self.selected_index = 0 + self._has_cycled = False # Reset cycling flag when suggestions change # Recompute display names self._compute_display_names() @@ -267,12 +274,20 @@ def _set_selection_classes(self) -> None: for i, item in enumerate(self._item_widgets): if not item.display: item.remove_class("selected") + item.remove_class("preselected") continue # First item is always the selected one if i == 0: - item.add_class("selected") + # Use "preselected" style if we haven't cycled yet and are at index 0 + if not self._has_cycled and self.selected_index == 0: + item.add_class("preselected") + item.remove_class("selected") + else: + item.add_class("selected") + item.remove_class("preselected") else: item.remove_class("selected") + item.remove_class("preselected") def _update_selection(self) -> None: """Update visual selection state.""" @@ -284,16 +299,24 @@ def _update_selection(self) -> None: def cycle_next(self) -> None: """Cycle to next suggestion.""" if self.suggestions: - self.selected_index = (self.selected_index + 1) % len(self.suggestions) + if not self._has_cycled: + self._has_cycled = True # User has actively cycled + else: + self.selected_index = (self.selected_index + 1) % len(self.suggestions) + self._update_selection() def cycle_previous(self) -> None: - """Cycle to next suggestion.""" + """Cycle to previous suggestion.""" if self.suggestions: - if not self.selected_index: - self.selected_index = len(self.suggestions) - 1 + if not self._has_cycled: + self._has_cycled = True # User has actively cycled else: - self.selected_index = (self.selected_index - 1) % len(self.suggestions) + if not self.selected_index: + self.selected_index = len(self.suggestions) - 1 + else: + self.selected_index = (self.selected_index - 1) % len(self.suggestions) + self._update_selection() def select_current(self) -> None: diff --git a/aider/tui/widgets/output.py b/aider/tui/widgets/output.py index 0b422baa67d..376cfb6c561 100644 --- a/aider/tui/widgets/output.py +++ b/aider/tui/widgets/output.py @@ -1,6 +1,7 @@ """Output widget for Aider TUI using Textual's RichLog widget.""" import re +import textwrap from rich.markdown import Markdown from rich.padding import Padding @@ -41,6 +42,8 @@ def __init__(self, **kwargs): super().__init__(**kwargs) # Line buffer for streaming text to avoid word-per-line issue self._line_buffer = "" + # Track if we're on the first line of the current response + self._first_line_of_response = True # Enable markup for rich formatting self.highlight = True @@ -51,6 +54,33 @@ async def start_response(self): """Start a new LLM response section with streaming support.""" # Clear the line buffer for new response self._line_buffer = "" + # Reset first line flag + self._first_line_of_response = True + + def _wrap_text_with_prefix(self, text: str, prefix: str = "• ") -> str: + """Wrap text with prefix and proper indentation. + + Args: + text: The text to wrap + prefix: The prefix to use for the first line + + Returns: + Wrapped text with prefix and indentation + """ + if not text.strip(): + return "" + + # Get available width for wrapping + # Subtract 2 to account for potential borders or scrollbars + width = self.content_size.width - 2 if self.content_size.width else 80 + indent = " " * len(prefix) + + # Wrap the text using textwrap + wrapped_text = textwrap.fill( + text, width=width, initial_indent=prefix, subsequent_indent=indent + ) + + return wrapped_text async def stream_chunk(self, text: str): """Stream a chunk of markdown text.""" @@ -66,10 +96,21 @@ async def stream_chunk(self, text: str): # Process complete lines from buffer while "\n" in self._line_buffer: line, self._line_buffer = self._line_buffer.split("\n", 1) - # self.write(Padding(line.strip(), (0, 0, 0, 1))) if line.rstrip(): self.set_last_write_type("assistant") - self.output(line.rstrip(), render_markdown=True) + # Format with prefix on first line, proper indentation on subsequent lines + if self._first_line_of_response: + wrapped_line = self._wrap_text_with_prefix(line.rstrip(), prefix="• ") + self._first_line_of_response = False + else: + # For subsequent lines, we need to wrap with proper indentation + # but without the bullet prefix + wrapped_line = self._wrap_text_with_prefix(line.rstrip(), prefix=" ") + + # Output each wrapped line + for wrapped in wrapped_line.split("\n"): + if wrapped.strip(): + self.output(wrapped, render_markdown=True) async def end_response(self): """End the current LLM response.""" @@ -79,7 +120,16 @@ async def _stop_stream(self): """Stop the current markdown stream.""" # Flush any remaining buffer content if self._line_buffer.rstrip(): - self.output(self._line_buffer.rstrip(), render_markdown=True) + # Format remaining content based on whether it's first line or not + if self._first_line_of_response: + wrapped_line = self._wrap_text_with_prefix(self._line_buffer.rstrip(), prefix="• ") + else: + wrapped_line = self._wrap_text_with_prefix(self._line_buffer.rstrip(), prefix=" ") + + # Output each wrapped line + for wrapped in wrapped_line.split("\n"): + if wrapped.strip(): + self.output(wrapped, render_markdown=True) self._line_buffer = "" def add_user_message(self, text: str): @@ -87,7 +137,15 @@ def add_user_message(self, text: str): # User messages shown with > prefix in green color self.auto_scroll = True self.set_last_write_type("user") - self.output(f"[bold medium_spring_green]> {text}[/bold medium_spring_green]") + + # Wrap the entire user message with "> " prefix + wrapped_text = self._wrap_text_with_prefix(text, prefix="> ") + + # Output each wrapped line with green styling + for line in wrapped_text.split("\n"): + if line.strip(): + self.output(f"[bold medium_spring_green]{line}[/bold medium_spring_green]") + self.scroll_end(animate=False) def add_system_message(self, text: str, dim=True): @@ -144,28 +202,30 @@ def add_tool_call(self, lines: list): if not lines: return + self.set_last_write_type("tool_call") for i, line in enumerate(lines): # Strip Rich markup clean_line = line.replace("[bright_cyan]", "").replace("[/bright_cyan]", "") - content = Text() if i == 0: # First line: reformat "Tool Call: server • function" to "Tool Call · server · function" clean_line = clean_line.replace("Tool Call:", "Tool Call •") - content.append(clean_line, style="dim bright_cyan") # $accent + self.output(Padding(Text(clean_line, style="dim bright_cyan"), (0, 0, 0, 2))) else: # Subsequent lines (arguments) - prefix with corner to show they belong to the call arg_string_list = re.split(r"(^\S+:)", clean_line, maxsplit=1)[1:] if len(arg_string_list) > 1: - content.append(f"ᴸ{arg_string_list[0]}", style="dim bright_cyan") + tool_property = arg_string_list[0].replace("_", " ").title() + content = Text() + content.append(f"ᴸ{tool_property}", style="dim bright_cyan") content.append(arg_string_list[1], style="dim") + self.output(Padding(content, (0, 0, 0, 2))) else: - content.append("ᴸ", style="dim bright_cyan") - content.append(clean_line, style="dim") + self.output(Padding(Text(clean_line, style="dim"), (0, 0, 0, 3))) - self.set_last_write_type("tool_call") - self.output(Padding(content, (0, 0, 0, 2))) + # self.set_last_write_type("tool_call") + # self.output(Padding(content, (0, 0, 0, 2))) def add_tool_result(self, text: str): """Add a tool result. diff --git a/aider/tui/worker.py b/aider/tui/worker.py index 8effbab3467..dcaf6f0dce0 100644 --- a/aider/tui/worker.py +++ b/aider/tui/worker.py @@ -106,6 +106,10 @@ async def _async_run(self): new_coder = await Coder.create(**kwargs) new_coder.args = self.coder.args + + if switch.kwargs.get("show_announcements") is False: + new_coder.suppress_announcements_for_next_prompt = True + # Transfer MCP state to avoid re-initialization new_coder.mcp_servers = self.coder.mcp_servers new_coder.mcp_tools = self.coder.mcp_tools diff --git a/tests/scrape/test_playwright_disable.py b/tests/scrape/test_playwright_disable.py index 68d72093145..d369c608115 100644 --- a/tests/scrape/test_playwright_disable.py +++ b/tests/scrape/test_playwright_disable.py @@ -89,6 +89,7 @@ def __init__(self): self.cur_messages = [] self.main_model = type("M", (), {"edit_format": "code", "name": "dummy", "info": {}}) self.args = type("Args", (), {"disable_playwright": True})() + self.tui = None def get_rel_fname(self, fname): return fname