diff --git a/aider/__init__.py b/aider/__init__.py index 478a2f82558..935725a74c0 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.90.4.dev" +__version__ = "0.90.5.dev" safe_version = __version__ try: diff --git a/aider/args.py b/aider/args.py index 1453cca90a1..005fe0a0d04 100644 --- a/aider/args.py +++ b/aider/args.py @@ -332,7 +332,7 @@ def get_parser(default_config_files, git_root): ) group.add_argument( "--context-compaction-max-tokens", - type=int, + type=float, default=None, help=( "The maximum number of tokens in the conversation before context compaction is" diff --git a/aider/coders/agent_prompts.py b/aider/coders/agent_prompts.py index 0aa2bb771f6..136a961a999 100644 --- a/aider/coders/agent_prompts.py +++ b/aider/coders/agent_prompts.py @@ -19,6 +19,7 @@ class AgentPrompts(CoderPrompts): - **Act Proactively**: Autonomously use file discovery and context management tools (`ViewFilesAtGlob`, `ViewFilesMatching`, `Ls`, `View`, `Remove`) to gather information and fulfill the user's request. Chain tool calls across multiple turns to continue exploration. - **Be Decisive**: Trust that your initial findings are valid. Refrain from asking the same question or searching for the same term in multiple similar ways. - **Be Concise**: Keep all responses brief and direct (1-3 sentences). Avoid preamble, postamble, and unnecessary explanations. Do not repeat yourself. +- **Be Careful**: Break updates down into smaller, more manageable chunks. Focus on one thing at a time. diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index fc74626e096..47c7d765130 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -306,7 +306,6 @@ def __init__( context_compaction_summary_tokens=8192, map_cache_dir=".", repomap_in_memory=False, - preserve_todo_list=False, linear_output=False, ): # initialize from args.map_cache_dir @@ -321,13 +320,6 @@ def __init__( self.auto_copy_context = auto_copy_context self.auto_accept_architect = auto_accept_architect - self.preserve_todo_list = preserve_todo_list - - if self.preserve_todo_list: - self.io.tool_warning( - "--preserve-todo-list is deprecated; todo lists are now saved and restored with" - " sessions. The flag will be removed in a future release." - ) self.ignore_mentions = ignore_mentions if not self.ignore_mentions: @@ -2344,6 +2336,8 @@ async def send_message(self, inp): return except Exception as e: self.io.tool_error(f"Error processing tool calls: {str(e)}") + self.reflected_message = True + return # Continue without tool processing self.num_tool_calls = 0 diff --git a/aider/io.py b/aider/io.py index 2b8a6ed55a7..b2d5d811197 100644 --- a/aider/io.py +++ b/aider/io.py @@ -1414,7 +1414,11 @@ def replace_json(match): # Match b'{...}', b"[...]", '{...}', "[...]" # Handle escaped quotes with (? list[str]: suggestions = [] commands = self.worker.coder.commands - if text.startswith("/"): + if len(text) and text[-1] == " ": + return + + if "@" in text: + # Symbol completion triggered by @ + # Find the @ and get the prefix after it + at_index = text.rfind("@") + prefix = text[at_index + 1 :] + suggestions = self._get_symbol_completions(prefix) + elif text.startswith("/"): # Command completion parts = text.split(maxsplit=1) cmd_part = parts[0] @@ -661,7 +690,7 @@ def _get_suggestions(self, text: str) -> list[str]: cmd_name = cmd_part end_lookup = text.rsplit(maxsplit=1) - arg_prefix = end_lookup[1] + arg_prefix = end_lookup[-1] arg_prefix_lower = arg_prefix.lower() # Check if this command needs path-based completion @@ -689,12 +718,6 @@ def _get_suggestions(self, text: str) -> list[str]: suggestions = list(cmd_completions) except Exception: pass - elif "@" in text: - # Symbol completion triggered by @ - # Find the @ and get the prefix after it - at_index = text.rfind("@") - prefix = text[at_index + 1 :] - suggestions = self._get_symbol_completions(prefix) else: # Check if last contiguous, no-space separated string contains a forward slash # This allows path completions even without a leading slash @@ -708,6 +731,34 @@ def _get_suggestions(self, text: str) -> list[str]: return [str(s) for s in suggestions[:50]] + def _get_completed_text(self, current_text: str, completion: str) -> str: + """Calculate the new text after applying completion.""" + if current_text.startswith("/"): + parts = current_text.rsplit(maxsplit=1) + if len(parts) == 1: + # Replace entire command + # Only add space if command takes arguments + commands = self.worker.coder.commands + has_completions = commands.get_completions(completion) is not None + if has_completions: + return completion + " " + else: + return completion + else: + # Replace argument + return parts[0] + " " + completion + elif "@" in current_text: + # Replace from @ onwards with the symbol + at_index = current_text.rfind("@") + return current_text[:at_index] + completion + " " + else: + # Replace last word with completion + words = current_text.rsplit(maxsplit=1) + if len(words) > 1: + return words[0] + " " + completion + else: + return completion + def on_input_area_completion_requested(self, message: InputArea.CompletionRequested): """Handle completion request - show or update completion bar.""" input_area = self.query_one("#input", InputArea) @@ -743,6 +794,13 @@ def on_input_area_completion_cycle(self, message: InputArea.CompletionCycle): try: completion_bar = self.query_one("#completion-bar", CompletionBar) completion_bar.cycle_next() + selected = completion_bar.current_selection + if selected: + input_area = self.query_one("#input", InputArea) + # Use completion_prefix as base + base_text = input_area.completion_prefix + new_text = self._get_completed_text(base_text, selected) + input_area.set_completion_preview(new_text) except Exception: pass @@ -751,6 +809,13 @@ def on_input_area_completion_cycle_previous(self, message: InputArea.CompletionC try: completion_bar = self.query_one("#completion-bar", CompletionBar) completion_bar.cycle_previous() + selected = completion_bar.current_selection + if selected: + input_area = self.query_one("#input", InputArea) + # Use completion_prefix as base + base_text = input_area.completion_prefix + new_text = self._get_completed_text(base_text, selected) + input_area.set_completion_preview(new_text) except Exception: pass @@ -775,37 +840,17 @@ def on_input_area_completion_dismiss(self, message: InputArea.CompletionDismiss) def on_completion_bar_selected(self, message: CompletionBar.Selected): """Handle completion selection.""" input_area = self.query_one("#input", InputArea) - input_area.completion_active = False - # Insert the completion - current = input_area.value + # Use stored prefix as base for completion + current = input_area.completion_prefix selected = message.value - if current.startswith("/"): - parts = current.split(maxsplit=1) - if len(parts) == 1: - # Replace entire command - # Only add space if command takes arguments - commands = self.worker.coder.commands - has_completions = commands.get_completions(selected) is not None - if has_completions: - input_area.value = selected + " " - else: - input_area.value = selected - else: - # Replace argument - input_area.value = parts[0] + " " + selected - elif "@" in current: - # Replace from @ onwards with the symbol - at_index = current.rfind("@") - input_area.value = current[:at_index] + selected + " " - else: - # Replace last word with completion - words = current.rsplit(maxsplit=1) - if len(words) > 1: - input_area.value = words[0] + " " + selected - else: - input_area.value = selected + new_text = self._get_completed_text(current, selected) + + # Reset cycling state so the new value is registered as the new prefix + input_area._cycling = False + input_area.value = new_text + input_area.completion_active = False input_area.focus() input_area.cursor_position = len(input_area.value) @@ -813,5 +858,11 @@ def on_completion_bar_selected(self, message: CompletionBar.Selected): def on_completion_bar_dismissed(self, message: CompletionBar.Dismissed): """Handle completion bar dismissal.""" input_area = self.query_one("#input", InputArea) + + # Restore original text if we were cycling + if input_area._cycling: + input_area.value = input_area.completion_prefix + input_area._cycling = False + input_area.completion_active = False input_area.focus() diff --git a/aider/tui/io.py b/aider/tui/io.py index 382453651f2..cf06f45012e 100644 --- a/aider/tui/io.py +++ b/aider/tui/io.py @@ -19,6 +19,9 @@ def __init__(self, output_queue, input_queue, **kwargs): input_queue: queue.Queue for receiving input from TUI **kwargs: Passed to InputOutput parent class """ + # Lazy-initialized console for TUI rendering + self._tui_console = None + # Initialize parent (fancy_input should already be False from caller) super().__init__(**kwargs) @@ -26,9 +29,6 @@ def __init__(self, output_queue, input_queue, **kwargs): self.output_queue = output_queue self.input_queue = input_queue - # Lazy-initialized console for TUI rendering - self._tui_console = None - # Current task tracking self.current_task_id = None diff --git a/aider/tui/widgets/completion_bar.py b/aider/tui/widgets/completion_bar.py index d4de302be37..a516f147f3e 100644 --- a/aider/tui/widgets/completion_bar.py +++ b/aider/tui/widgets/completion_bar.py @@ -93,6 +93,13 @@ def __init__(self, suggestions: list[str] = None, prefix: str = "", **kwargs): self._display_names: list[str] = [] self._compute_display_names() + @property + def current_selection(self) -> str | None: + """Get currently selected suggestion.""" + if self.suggestions and 0 <= self.selected_index < len(self.suggestions): + return self.suggestions[self.selected_index] + return None + def _compute_display_names(self) -> None: """Compute common directory prefix and short display names.""" if not self.suggestions: diff --git a/aider/tui/widgets/input_area.py b/aider/tui/widgets/input_area.py index 83a31002203..67b8da021bd 100644 --- a/aider/tui/widgets/input_area.py +++ b/aider/tui/widgets/input_area.py @@ -59,8 +59,8 @@ def __init__(self, history_file: str = None, **kwargs): # Let's assume kwargs might handle it or we set it. # Actually, let's just set the default if it's empty. if not self.placeholder: - submit = self.app._decode_keys(self.app.tui_config["key_bindings"]["submit"]) - newline = self.app._decode_keys(self.app.tui_config["key_bindings"]["newline"]) + submit = self.app.get_keys_for("submit") + newline = self.app.get_keys_for("newline") self.placeholder = ( f"> Type your message... ({submit} to submit, {newline} for new line)" @@ -70,6 +70,9 @@ def __init__(self, history_file: str = None, **kwargs): self.commands = [] self.completion_active = False + self._cycling = False + self._completion_prefix = "" + # History support - lazy loaded self.history_file = history_file self._history: list[str] | None = None # None = not loaded yet @@ -81,10 +84,9 @@ def value(self) -> str: """Alias for text property to maintain compatibility.""" return self.text - @value.setter - def value(self, new_value: str): - """Alias for text property to maintain compatibility.""" - self.text = new_value + @property + def completion_prefix(self) -> str: + return self._completion_prefix @property def cursor_position(self) -> int: @@ -100,6 +102,11 @@ def cursor_position(self) -> int: # So it uses setter. return 0 # Dummy getter + @value.setter + def value(self, new_value: str): + """Alias for text property to maintain compatibility.""" + self.text = new_value + @cursor_position.setter def cursor_position(self, pos: int): """ @@ -203,12 +210,28 @@ def _history_next(self) -> None: self.cursor_position = len(self.text) # Will move to end + def set_completion_preview(self, text: str): + self._cycling = True + self.value = text + self.cursor_position = len(text) + def on_key(self, event) -> None: """Handle keys for completion and history navigation.""" if self.disabled: return - if event.key == self.app.tui_config["key_bindings"]["cancel"]: + # Reset cycling if not a cycle command + is_cycle = self.app.is_key_for("cycle_forward", event.key) or self.app.is_key_for( + "cycle_backward", event.key + ) + if not is_cycle: + self._cycling = False + + if event.key == "space" and self.completion_active: + self.completion_active = False + self.post_message(self.CompletionDismiss()) + + if self.app.is_key_for("cancel", event.key): event.stop() event.prevent_default() if self.text.strip(): @@ -216,30 +239,23 @@ def on_key(self, event) -> None: self.text = "" return - if event.key == self.app.tui_config["key_bindings"]["submit"]: + if self.app.is_key_for("submit", event.key): # Submit message event.stop() event.prevent_default() self.post_message(self.Submit(self.text)) return - if event.key == self.app.tui_config["key_bindings"]["newline"]: - if self.completion_active: - # Accept completion - self.post_message(self.CompletionAccept()) - event.stop() - event.prevent_default() - return - else: - if self.app.tui_config["key_bindings"]["newline"] != "enter": - self.insert("\n") + if self.app.is_key_for("newline", event.key): + if self.app.get_keys_for("newline") != "enter": + self.insert("\n") - current_row, current_col = self.cursor_location - self.cursor_location = (current_row + 1, 0) + current_row, current_col = self.cursor_location + self.cursor_location = (current_row + 1, 0) - return + return - if event.key == self.app.tui_config["key_bindings"]["cycle_forward"]: + if self.app.is_key_for("cycle_forward", event.key): event.stop() event.prevent_default() if self.completion_active: @@ -248,7 +264,7 @@ def on_key(self, event) -> None: else: # Request completions self.post_message(self.CompletionRequested(self.text)) - elif event.key == self.app.tui_config["key_bindings"]["cycle_backward"]: + elif self.app.is_key_for("cycle_backward", event.key): event.stop() event.prevent_default() if self.completion_active: @@ -257,7 +273,7 @@ def on_key(self, event) -> None: else: # Request completions self.post_message(self.CompletionRequested(self.text)) - elif event.key == self.app.tui_config["key_bindings"]["stop"] and self.completion_active: + elif self.app.is_key_for("stop", event.key) and self.completion_active: event.stop() event.prevent_default() self.post_message(self.CompletionDismiss()) @@ -280,6 +296,14 @@ def on_key(self, event) -> None: def on_text_area_changed(self, event) -> None: """Update completions as user types.""" # Note: Event name for TextArea change is 'Changed' but handler is on_text_area_changed + if self.disabled: + return + + if self._cycling: + return + + self._completion_prefix = self.text + if not self.disabled: val = self.text possible_path = False diff --git a/aider/website/docs/config/tui.md b/aider/website/docs/config/tui.md index f394afc41e1..7843a3681cb 100644 --- a/aider/website/docs/config/tui.md +++ b/aider/website/docs/config/tui.md @@ -48,6 +48,7 @@ tui-config: key_bindings: newline: "enter" submit: "shift+enter" + completion: "tab" stop: "escape" cycle_forward: "tab" cycle_backward: "shift+tab" diff --git a/tests/basic/test_sessions.py b/tests/basic/test_sessions.py index 2b24607e831..c08e123ab5a 100644 --- a/tests/basic/test_sessions.py +++ b/tests/basic/test_sessions.py @@ -252,7 +252,7 @@ async def test_preserve_todo_list_deprecated(self): io = InputOutput(pretty=False, fancy_input=False, yes=True) with mock.patch.object(io, "tool_warning") as mock_tool_warning: - await Coder.create(self.GPT35, None, io, preserve_todo_list=True) + await Coder.create(self.GPT35, None, io) self.assertFalse(todo_path.exists()) self.assertTrue(