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
+
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",
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"))
diff --git a/aider/tui/app.py b/aider/tui/app.py
index 489f5687069..6e7fe1a5580 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()
@@ -611,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]]
@@ -653,6 +746,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..83a31002203 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())
@@ -257,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))
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