Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -453,4 +452,4 @@ The current priorities are to improve core capabilities and user experience of t
<td></td>
</tr>
</tbody>
</table>
</table>
7 changes: 7 additions & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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"))
Expand Down
113 changes: 107 additions & 6 deletions aider/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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",
Expand All @@ -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"]:
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]]

Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions aider/tui/widgets/completion_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
48 changes: 41 additions & 7 deletions aider/tui/widgets/input_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -198,32 +208,38 @@ 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():
self.save_to_history(self.text)
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())
event.stop()
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:
Expand All @@ -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())
Expand All @@ -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))
Loading
Loading