From cc9fa1b5e1c60f660d4f7a2d5857de1534cfe95a Mon Sep 17 00:00:00 2001 From: Ryan Nevius Date: Wed, 17 Dec 2025 13:34:12 +0100 Subject: [PATCH 1/5] chore(docs): use process substitution to avoid diff artifacts --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c872e3f9026..1490b40e68a 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,9 @@ LLMs are a part of our lives from here on out so join us in learning about and c * [Aider Original Documentation (still mostly applies)](https://aider.chat/) You can see a selection of the enhancements and updates by comparing the help output: + ```bash -aider --help > aider.help.txt -cecli --help > cecli.help.txt -diff aider.help.txt cecli.help.txt -uw --color +diff -uw --color <(aider --help) <(cecli --help) ``` ## Installation Instructions @@ -453,4 +452,4 @@ The current priorities are to improve core capabilities and user experience of t - \ No newline at end of file + From 0552c6a71a7924d0689ac6a7fe1eda08263c2b29 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Wed, 17 Dec 2025 19:49:58 -0500 Subject: [PATCH 2/5] #268: --ask mode parameter for initialization --- aider/args.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aider/args.py b/aider/args.py index b16d14c4bc5..e501d6d5506 100644 --- a/aider/args.py +++ b/aider/args.py @@ -169,6 +169,13 @@ def get_parser(default_config_files, git_root): default=None, help="Specify what edit format the LLM should use (default depends on model)", ) + group.add_argument( + "--ask", + action="store_const", + dest="edit_format", + const="ask", + help="Use ask edit format for the main chat", + ) group.add_argument( "--architect", action="store_const", From 48514e599ba4360368cdac332cde7176ba124ea7 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Wed, 17 Dec 2025 21:06:35 -0500 Subject: [PATCH 3/5] #267: Fix /tokens on removed files --- aider/commands.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 2b6e3f16152..843c0691574 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -581,6 +581,10 @@ def cmd_tokens(self, args): relative_fname = self.coder.get_rel_fname(fname) content = self.io.read_text(fname) + + if not content: + continue + if is_image_file(relative_fname): tokens = self.coder.main_model.token_count_for_image(fname) else: @@ -603,7 +607,11 @@ def cmd_tokens(self, args): relative_fname = self.coder.get_rel_fname(fname) content = self.io.read_text(fname) - if content is not None and not is_image_file(relative_fname): + + if not content: + continue + + if not is_image_file(relative_fname): # approximate content = f"{relative_fname}\n{fence}\n" + content + f"{fence}\n" tokens = self.coder.main_model.token_count(content) @@ -620,6 +628,10 @@ def cmd_tokens(self, args): relative_fname = self.coder.get_rel_fname(fname) if not is_image_file(relative_fname): stub = self.coder.get_file_stub(fname) + + if not stub: + continue + content = f"{relative_fname} (stub)\n{fence}\n" + stub + "{fence}\n" tokens = self.coder.main_model.token_count(content) res.append((tokens, f"{relative_fname} (read-only stub)", "/drop to remove")) From 8ce566f70bb48ff785103188ad55bdc60fc1b771 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 18 Dec 2025 01:07:28 -0500 Subject: [PATCH 4/5] Add key binding customizations and documentation for feature --- aider/tui/app.py | 102 ++++++++++++++++++++++++++-- aider/tui/widgets/completion_bar.py | 9 +++ aider/tui/widgets/input_area.py | 37 ++++++++-- aider/website/docs/config/tui.md | 46 +++++++++++-- 4 files changed, 179 insertions(+), 15 deletions(-) diff --git a/aider/tui/app.py b/aider/tui/app.py index 489f5687069..1e70049b32b 100644 --- a/aider/tui/app.py +++ b/aider/tui/app.py @@ -5,7 +5,8 @@ import queue from textual.app import App, ComposeResult -from textual.binding import Binding + +# from textual.binding import Binding from textual.containers import Vertical from textual.theme import Theme @@ -28,8 +29,8 @@ class TUI(App): BINDINGS = [ # Binding("ctrl+c", "quit", "Quit", show=True), - Binding("ctrl+l", "clear_output", "Clear", show=True), - Binding("escape", "interrupt", "Interrupt", show=True), + # Binding("ctrl+l", "clear_output", "Clear", show=True), + # Binding("escape", "interrupt", "Interrupt", show=True), ] def __init__(self, coder_worker, output_queue, input_queue, args): @@ -67,6 +68,42 @@ def __init__(self, coder_worker, output_queue, input_queue, args): }, ) + self.bind( + self.tui_config["key_bindings"]["newline"], "noop", description="New Line", show=True + ) + self.bind( + self.tui_config["key_bindings"]["submit"], "noop", description="Submit", show=True + ) + self.bind( + self.tui_config["key_bindings"]["cycle_forward"], + "noop", + description="Cycle Forward", + show=True, + ) + self.bind( + self.tui_config["key_bindings"]["cycle_backward"], + "noop", + description="Cycle Backward", + show=True, + ) + self.bind( + self.tui_config["key_bindings"]["cancel"], "noop", description="Cancel", show=True + ) + + self.bind( + self.tui_config["key_bindings"]["focus"], + "focus_input", + description="Focus Input", + show=True, + ) + self.bind( + self.tui_config["key_bindings"]["stop"], "interrupt", description="Interrupt", show=True + ) + self.bind( + self.tui_config["key_bindings"]["clear"], "clear_output", description="Clear", show=True + ) + self.bind(self.tui_config["key_bindings"]["focus"], "quit", description="Quit", show=True) + self.register_theme(BASE_THEME) self.theme = "aider" @@ -101,6 +138,12 @@ def _get_config(self): if "other" not in config: config["other"] = {} + if "key_bindings" not in config: + config["key_bindings"] = {} + + coder = self.worker.coder + is_multiline = coder.args.multiline + # Ensure colors dict has all expected keys with default values default_colors = { "primary": "#00ff5f", @@ -120,6 +163,18 @@ def _get_config(self): }, } + default_key_bindings = { + "newline": "enter" if is_multiline else "shift+enter", + "submit": "shift+enter" if is_multiline else "enter", + "stop": "escape", + "cycle_forward": "tab", + "cycle_backward": "shift+tab", + "focus": "ctrl+f", + "cancel": "ctrl+c", + "clear": "ctrl+l", + "quit": "ctrl+q", + } + # Merge default colors with user-provided colors for key, default_value in default_colors.items(): if key not in config["colors"]: @@ -132,6 +187,13 @@ def _get_config(self): if var_key not in config["colors"]["variables"]: config["colors"]["variables"][var_key] = var_default + for key, default_value in default_key_bindings.items(): + if key not in config["key_bindings"]: + config["key_bindings"][key] = self._encode_keys(default_value) + + for key, value in config["key_bindings"].items(): + config["key_bindings"][key] = self._encode_keys(value) + return config def compose(self) -> ComposeResult: @@ -205,9 +267,11 @@ def update_key_hints(self, generating=False): try: hints = self.query_one(KeyHints) if generating: - hints.update("escape to cancel") + stop = self.app._decode_keys(self.app.tui_config["key_bindings"]["stop"]) + hints.update(f"{stop} to cancel") else: - hints.update("ctrl+s to submit") + submit = self.app._decode_keys(self.app.tui_config["key_bindings"]["submit"]) + hints.update(f"{submit} to submit") except Exception: pass @@ -381,6 +445,11 @@ def on_input_area_submit(self, message: InputArea.Submit): self.input_queue.put({"text": user_input}) + def action_focus_input(self) -> None: + """Find the input widget and set focus to it.""" + input_area = self.query_one("#input", InputArea) + input_area.focus() + def action_clear_output(self): """Clear all output.""" output_container = self.query_one("#output", OutputContainer) @@ -413,6 +482,21 @@ def action_quit(self): # Delay exit to allow status bar to render self.set_timer(0.3, self._do_quit) + def action_noop(self): + pass + + def _encode_keys(self, key): + if key == "shift+enter": + return "ctrl+j" + + return key + + def _decode_keys(self, key): + if key == "ctrl+j": + return "shift+enter" + + return key + def _do_quit(self): """Perform the actual quit after UI updates.""" self.worker.stop() @@ -653,6 +737,14 @@ def on_input_area_completion_cycle(self, message: InputArea.CompletionCycle): except Exception: pass + def on_input_area_completion_cycle_previous(self, message: InputArea.CompletionCyclePrevious): + """Handle Tab to cycle through completions.""" + try: + completion_bar = self.query_one("#completion-bar", CompletionBar) + completion_bar.cycle_previous() + except Exception: + pass + def on_input_area_completion_accept(self, message: InputArea.CompletionAccept): """Handle Enter to accept current completion.""" try: diff --git a/aider/tui/widgets/completion_bar.py b/aider/tui/widgets/completion_bar.py index d233047b8ca..d4de302be37 100644 --- a/aider/tui/widgets/completion_bar.py +++ b/aider/tui/widgets/completion_bar.py @@ -280,6 +280,15 @@ def cycle_next(self) -> None: self.selected_index = (self.selected_index + 1) % len(self.suggestions) self._update_selection() + def cycle_previous(self) -> None: + """Cycle to next suggestion.""" + if 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: """Select current suggestion and dismiss.""" if self.suggestions: diff --git a/aider/tui/widgets/input_area.py b/aider/tui/widgets/input_area.py index 69451513d45..300a9767416 100644 --- a/aider/tui/widgets/input_area.py +++ b/aider/tui/widgets/input_area.py @@ -27,6 +27,11 @@ class CompletionCycle(Message): pass + class CompletionCyclePrevious(Message): + """User wants to cycle through completions backwards.""" + + pass + class CompletionAccept(Message): """User wants to accept current completion.""" @@ -54,7 +59,12 @@ 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: - self.placeholder = "> Type your message... (ctrl+s to submit, enter for new line)" + submit = self.app._decode_keys(self.app.tui_config["key_bindings"]["submit"]) + newline = self.app._decode_keys(self.app.tui_config["key_bindings"]["newline"]) + + self.placeholder = ( + f"> Type your message... ({submit} to submit, {newline} for new line)" + ) self.files = [] self.commands = [] @@ -198,7 +208,7 @@ def on_key(self, event) -> None: if self.disabled: return - if event.key == "ctrl+c": + if event.key == self.app.tui_config["key_bindings"]["cancel"]: event.stop() event.prevent_default() if self.text.strip(): @@ -206,14 +216,14 @@ def on_key(self, event) -> None: self.text = "" return - if event.key == "ctrl+s": + if event.key == self.app.tui_config["key_bindings"]["submit"]: # Submit message event.stop() event.prevent_default() self.post_message(self.Submit(self.text)) return - if event.key == "enter": + if event.key == self.app.tui_config["key_bindings"]["newline"]: if self.completion_active: # Accept completion self.post_message(self.CompletionAccept()) @@ -221,9 +231,15 @@ def on_key(self, event) -> None: event.prevent_default() return else: + if self.app.tui_config["key_bindings"]["newline"] != "enter": + self.insert("\n") + + current_row, current_col = self.cursor_location + self.cursor_location = (current_row + 1, 0) + return - if event.key == "tab": + if event.key == self.app.tui_config["key_bindings"]["cycle_forward"]: event.stop() event.prevent_default() if self.completion_active: @@ -232,7 +248,16 @@ def on_key(self, event) -> None: else: # Request completions self.post_message(self.CompletionRequested(self.text)) - elif event.key == "escape" and self.completion_active: + elif event.key == self.app.tui_config["key_bindings"]["cycle_backward"]: + event.stop() + event.prevent_default() + if self.completion_active: + # Cycle through completions + self.post_message(self.CompletionCyclePrevious()) + else: + # Request completions + self.post_message(self.CompletionRequested(self.text)) + elif event.key == self.app.tui_config["key_bindings"]["stop"] and self.completion_active: event.stop() event.prevent_default() self.post_message(self.CompletionDismiss()) diff --git a/aider/website/docs/config/tui.md b/aider/website/docs/config/tui.md index 49d7e43741b..f394afc41e1 100644 --- a/aider/website/docs/config/tui.md +++ b/aider/website/docs/config/tui.md @@ -25,7 +25,7 @@ tui: true ### Complete Configuration Example -Complete configuration example in YAML configuration file (`.aider.conf.yml` or `~/.aider.conf.yml`). The base theme is pretty nice but if you want different colors, do you thing: +Complete configuration example in YAML configuration file (`.aider.conf.yml` or `~/.aider.conf.yml`). The base theme is pretty nice but if you want different colors and key bindings, do you thing: ```yaml tui: true @@ -41,15 +41,53 @@ tui-config: error: "#ff3333" surface: "transparent" panel: "transparent" - dark: true - variables: - input-cursor-foreground: "#00ff87" + input-cursor-foreground: "#00ff87" other: dark: true input-cursor-text-style: "underline" + key_bindings: + newline: "enter" + submit: "shift+enter" + stop: "escape" + cycle_forward: "tab" + cycle_backward: "shift+tab" + focus: "ctrl+f" + cancel: "ctrl+c" + clear: "ctrl+l" + quit: "ctrl+q" ``` +### Key Command Configuration + +The TUI provides customizable key bindings for all major actions. The default key bindings are: + +| Action | Default Key | Description | +|--------|-------------|-------------| +| New Line | `enter` (multiline mode) / `shift+enter` (single-line mode) | Insert a new line in the input area | +| Submit | `shift+enter` (multiline mode) / `enter` (single-line mode) | Submit the current input | +| Cancel | `ctrl+c` | Stop and stash current input prompt | +| Stop | `escape` | Interrupt the current LLM response or task | +| Cycle Forward | `tab` | Cycle forward through completion suggestions | +| Cycle Backward | `shift+tab` | Cycle backward through completion suggestions | +| Focus | `ctrl+f` | Focus the input area | +| Clear | `ctrl+l` | Clear the output area | +| Quit | `ctrl+q` | Exit the TUI | + +#### Customizing Key Bindings + +You can customize any key binding by adding a `key_bindings` section to your `tui-config`. For example, to change the quit key to `ctrl+x`: + +```yaml +tui-config: + key_bindings: + quit: "ctrl+x" +``` + +All key bindings use Textual's key syntax: +- Single keys: `enter`, `escape`, `tab` +- Modifier combinations: `ctrl+c`, `shift+enter`, etc. + ## Benefits - **Improved Productivity**: Reduced context switching with all information visible at once From bd7451d2179d0c10faf51243b78b0cda1faf8f62 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Thu, 18 Dec 2025 01:23:18 -0500 Subject: [PATCH 5/5] Allow path completions based on presence of slash in text as well as @ sign --- aider/tui/app.py | 11 ++++++++++- aider/tui/widgets/input_area.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/aider/tui/app.py b/aider/tui/app.py index 1e70049b32b..6e7fe1a5580 100644 --- a/aider/tui/app.py +++ b/aider/tui/app.py @@ -695,7 +695,16 @@ def _get_suggestions(self, text: str) -> list[str]: at_index = text.rfind("@") prefix = text[at_index + 1 :] suggestions = self._get_symbol_completions(prefix) - # No file completion for regular text - use @ for files/symbols + else: + # Check if last contiguous, no-space separated string contains a forward slash + # This allows path completions even without a leading slash + words = text.rsplit(maxsplit=1) + + if words: + last_word = words[-1] + if "/" in last_word: + # Provide path completions for the partial path + suggestions = self._get_symbol_completions(last_word) return [str(s) for s in suggestions[:50]] diff --git a/aider/tui/widgets/input_area.py b/aider/tui/widgets/input_area.py index 300a9767416..83a31002203 100644 --- a/aider/tui/widgets/input_area.py +++ b/aider/tui/widgets/input_area.py @@ -282,6 +282,15 @@ def on_text_area_changed(self, event) -> None: # Note: Event name for TextArea change is 'Changed' but handler is on_text_area_changed if not self.disabled: val = self.text + possible_path = False + # Auto-trigger for slash commands, @ symbols, or update existing completions - if val.startswith("/") or "@" in val or self.completion_active: + words = val.rsplit(maxsplit=1) + + if words: + last_word = words[-1] + if "/" in last_word: + possible_path = True + + if val.startswith("/") or "@" in val or possible_path or self.completion_active: self.post_message(self.CompletionRequested(val))