diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfff1d7d146..9dce2b91bdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,3 @@ - # Contributing to the Project We welcome contributions in the form of bug reports, feature requests, @@ -24,30 +23,29 @@ ensure that your contributions can be integrated smoothly. ```bash # Clone the repository -git clone https://github.com/dwash96/aider-ce.git -cd aider-ce +git clone https://github.com/dwash96/cecli.git +cd cecli # Make a venv python3 -m venv venv source venv/bin/activate -# Install UV because it's superior +# Install UV because it's superior (skip if you already have it installed globally) pip install uv # Build Project uv pip install --native-tls -e . # Add tool chain -uv install --native-tls pre-commit +uv pip install --native-tls pre-commit pre-commit install # Run Program -aider-ce - -# OR! - cecli +# OR! (legacy) +aider-ce + ``` ### Building the Docker Image diff --git a/README.md b/README.md index 94d99f0d637..a16a4732e1b 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,15 @@ LLMs are a part of our lives from here on out so join us in learning about and c ## Documentation/Other Notes: -* [Agent Mode](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/agent-mode.md) -* [MCP Configuration](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/mcp.md) -* [TUI Configuration](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/tui.md) -* [Skills](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/skills.md) -* [Session Management](https://github.com/dwash96/cecli/blob/main/aider/website/docs/sessions.md) +* [Agent Mode](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/agent-mode.md) +* [MCP Configuration](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/mcp.md) +* [TUI Configuration](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/tui.md) +* [Skills](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/skills.md) +* [Session Management](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/sessions.md) * [Custom Commands](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-commands.md) * [Custom System Prompts](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/custom-system-prompts.md) * [Custom Tools](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/agent-mode.md#creating-custom-tools) -* [Advanced Model Configuration](https://github.com/dwash96/cecli/blob/main/aider/website/docs/config/model-aliases.md#advanced-model-settings) +* [Advanced Model Configuration](https://github.com/dwash96/cecli/blob/main/cecli/website/docs/config/model-aliases.md#advanced-model-settings) * [Aider Original Documentation (still mostly applies)](https://aider.chat/) You can see a selection of the enhancements and updates by comparing the help output: @@ -101,7 +101,15 @@ DEEPSEEK_API_KEY="..." ### Run Program -If you are in the directory with your .aider.conf.yml file, then simply running `cecli` or `aider-ce` will start the agent with your configuration. If you want additional sandboxing, we publish a docker container that can be ran as follows: +If you are in the directory with your .aider.conf.yml file, then simply running `cecli` or `aider-ce` will start the agent with your configuration. For best results, since terminal emulators can be finicky, we highly suggest running: + +```bash +cecli --terminal-setup +``` + +On first run to configure keybindings for the program (notably `shift+enter`). Support for terminals is ongoing so feel free to make a github issue or chat in the discord for us to figure out what's needed to support automatically setting up a given terminal. + +If you want additional sandboxing, we publish a docker container that can be ran as follows: ```bash docker pull dustinwashington/aider-ce @@ -464,4 +472,4 @@ The current priorities are to improve core capabilities and user experience of t - \ No newline at end of file + diff --git a/cecli/__init__.py b/cecli/__init__.py index 8b88d2cdae1..b4224a3f34d 100644 --- a/cecli/__init__.py +++ b/cecli/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.95.7.dev" +__version__ = "0.95.8.dev" safe_version = __version__ try: diff --git a/cecli/args.py b/cecli/args.py index ceac5c75256..1ba9de0cd7f 100644 --- a/cecli/args.py +++ b/cecli/args.py @@ -456,6 +456,17 @@ def get_parser(default_config_files, git_root): default=False, help="Restore the previous chat history messages (default: False)", ) + ######### + group = parser.add_argument_group("Input settings") + group.add_argument( + "--terminal-setup", + action=argparse.BooleanOptionalAction, + default=False, + help=( + "Auto-configure terminal emulator for shift+enter support for new lines (default:" + " False)" + ), + ) ########## group = parser.add_argument_group("Output settings") group.add_argument( diff --git a/cecli/commands/__init__.py b/cecli/commands/__init__.py index 6b118475204..b399b855792 100644 --- a/cecli/commands/__init__.py +++ b/cecli/commands/__init__.py @@ -51,6 +51,7 @@ from .save import SaveCommand from .save_session import SaveSessionCommand from .settings import SettingsCommand +from .terminal_setup import TerminalSetupCommand from .test import TestCommand from .think_tokens import ThinkTokensCommand from .tokens import TokensCommand @@ -123,6 +124,7 @@ CommandRegistry.register(CommandPrefixCommand) CommandRegistry.register(LoadSkillCommand) CommandRegistry.register(RemoveSkillCommand) +CommandRegistry.register(TerminalSetupCommand) __all__ = [ @@ -187,6 +189,7 @@ "CommandPrefixCommand", "LoadSkillCommand", "RemoveSkillCommand", + "TerminalSetupCommand", "SwitchCoderSignal", "Commands", ] diff --git a/cecli/commands/terminal_setup.py b/cecli/commands/terminal_setup.py new file mode 100644 index 00000000000..3857ecd8d71 --- /dev/null +++ b/cecli/commands/terminal_setup.py @@ -0,0 +1,365 @@ +import json +import os +import platform +import shutil +from pathlib import Path +from typing import List + +from cecli.commands.utils.base_command import BaseCommand +from cecli.commands.utils.helpers import format_command_result + + +class TerminalSetupCommand(BaseCommand): + NORM_NAME = "terminal-setup" + DESCRIPTION = "Configure terminal config files to support shift+enter for newline" + + # Configuration constants + ALACRITTY_BINDING = """ +# Added by cecli terminal-setup command +[[keyboard.bindings]] +key = "Return" +mods = "Shift" +chars = "\\n" +""" + + KITTY_BINDING = "\n# Added by cecli terminal-setup command\nmap shift+enter send_text all \\n\n" + + WT_ACTION = { + "command": {"action": "sendInput", "input": "\n"}, + "id": "User.sendInput.shift_enter", + } + + WT_KEYBINDING = {"id": "User.sendInput.shift_enter", "keys": "shift+enter"} + + @classmethod + def _get_config_paths(cls): + """Determine paths based on the current OS.""" + system = platform.system() + home = Path.home() + paths = {} + + # Check for WSL specifically + is_wsl_env = "microsoft" in platform.uname().release.lower() + + if system == "Linux": + # Standard Linux paths (applies to WSL instances of Kitty/Alacritty too) + paths["alacritty"] = home / ".config" / "alacritty" / "alacritty.toml" + paths["kitty"] = home / ".config" / "kitty" / "kitty.conf" + + if is_wsl_env: + # Try to find Windows Terminal settings from inside WSL + # We have to guess the Windows username, usually defaults to the WSL user + # or requires searching /mnt/c/Users/ + try: + # Get Windows username by invoking cmd.exe (slow but accurate) + win_user = os.popen("cmd.exe /c 'echo %USERNAME%'").read().strip() + if win_user: + win_home = Path(f"/mnt/c/Users/{win_user}") + local_appdata = win_home / "AppData/Local" + wt_glob = list( + local_appdata.glob( + "Packages/Microsoft.WindowsTerminal_*/LocalState/settings.json" + ) + ) + if wt_glob: + paths["windows_terminal"] = wt_glob[0] + except Exception: + pass # cmd.exe might not be in path or accessible + + elif system == "Darwin": # macOS + paths["alacritty"] = home / ".config" / "alacritty" / "alacritty.toml" + paths["kitty"] = home / ".config" / "kitty" / "kitty.conf" + + elif system == "Windows": + appdata = Path(os.getenv("APPDATA")) + paths["alacritty"] = appdata / "alacritty" / "alacritty.toml" + paths["kitty"] = appdata / "kitty" / "kitty.conf" + + # Windows Terminal path is tricky (has a unique hash in folder name) + # We look for the folder starting with Microsoft.WindowsTerminal + local_appdata = Path(os.getenv("LOCALAPPDATA")) + wt_glob = list( + local_appdata.glob("Packages/Microsoft.WindowsTerminal_*/LocalState/settings.json") + ) + if wt_glob: + paths["windows_terminal"] = wt_glob[0] + + return paths + + @classmethod + def _backup_file(cls, file_path, io): + """Creates a copy of the file with .bak extension.""" + if file_path.exists(): + backup_path = file_path.with_suffix(file_path.suffix + ".bak") + shutil.copy(file_path, backup_path) + io.tool_output(f"Backed up {file_path.name} to {backup_path.name}") + return True + return False + + @classmethod + def _update_alacritty(cls, path, io, dry_run=False): + """Appends the TOML configuration if not already present.""" + if not path.exists(): + io.tool_output(f"Skipping Alacritty: File not found at {path}") + return False + + if dry_run: + io.tool_output(f"DRY-RUN: Would check Alacritty config at {path}") + io.tool_output(f"DRY-RUN: Would append binding:\n{cls.ALACRITTY_BINDING.strip()}") + # Simulate checking for duplicates + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + if ( + 'key = "Return"' in content + and 'mods = "Shift"' in content + and 'chars = "\\n"' in content + ): + io.tool_output("DRY-RUN: Alacritty already configured.") + return False + else: + io.tool_output("DRY-RUN: Would update Alacritty config.") + return True + except Exception as e: + io.tool_output(f"DRY-RUN: Error reading file: {e}") + return False + + cls._backup_file(path, io) + + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + # Simple check to avoid duplicates + if ( + 'key = "Return"' in content + and 'mods = "Shift"' in content + and 'chars = "\\n"' in content + ): + io.tool_output("Alacritty already configured.") + return False + + with open(path, "a", encoding="utf-8") as f: + f.write(cls.ALACRITTY_BINDING) + io.tool_output("Updated Alacritty config.") + return True + + @classmethod + def _update_kitty(cls, path, io, dry_run=False): + """Appends the Kitty mapping if not present.""" + if not path.exists(): + io.tool_output(f"Skipping Kitty: File not found at {path}") + return False + + if dry_run: + io.tool_output(f"DRY-RUN: Would check Kitty config at {path}") + io.tool_output(f"DRY-RUN: Would append binding:\n{cls.KITTY_BINDING.strip()}") + # Simulate checking for duplicates + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + if "map shift+enter send_text all \\n" in content: + io.tool_output("DRY-RUN: Kitty already configured.") + return False + else: + io.tool_output("DRY-RUN: Would update Kitty config.") + return True + except Exception as e: + io.tool_output(f"DRY-RUN: Error reading file: {e}") + return False + + cls._backup_file(path, io) + + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if "map shift+enter send_text all \\n" in content: + io.tool_output("Kitty already configured.") + return False + + with open(path, "a", encoding="utf-8") as f: + f.write(cls.KITTY_BINDING) + io.tool_output("Updated Kitty config.") + return True + + @classmethod + def _update_windows_terminal(cls, path, io, dry_run=False): + """Parses JSON, adds action to 'actions' list and keybinding to 'keybindings' list.""" + if not path or not path.exists(): + io.tool_output("Skipping Windows Terminal: File not found.") + return False + + if dry_run: + io.tool_output(f"DRY-RUN: Would check Windows Terminal config at {path}") + io.tool_output(f"DRY-RUN: Would add action: {json.dumps(cls.WT_ACTION, indent=2)}") + io.tool_output( + f"DRY-RUN: Would add keybinding: {json.dumps(cls.WT_KEYBINDING, indent=2)}" + ) + # Simulate checking for duplicates + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + # Check if already configured + already_configured = False + + # Check actions array for our action ID + actions_list = data.get("actions", []) + if isinstance(actions_list, list): + for action in actions_list: + if isinstance(action, dict) and action.get("id") == cls.WT_ACTION["id"]: + already_configured = True + break + + # Check keybindings array for shift+enter + keybindings_list = data.get("keybindings", []) + if isinstance(keybindings_list, list): + for binding in keybindings_list: + if isinstance(binding, dict) and binding.get("keys") == "shift+enter": + already_configured = True + break + + if already_configured: + io.tool_output("DRY-RUN: Windows Terminal already configured.") + return False + else: + io.tool_output("DRY-RUN: Would update Windows Terminal config.") + return True + except json.JSONDecodeError: + io.tool_output( + "DRY-RUN: Error: Could not parse Windows Terminal settings.json. Is it valid" + " JSON?" + ) + return False + except Exception as e: + io.tool_output(f"DRY-RUN: Error reading file: {e}") + return False + + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + # Check if already configured + already_configured = False + + # Check actions array for our action ID + actions_list = data.get("actions", []) + if not isinstance(actions_list, list): + actions_list = [] + data["actions"] = actions_list + + for action in actions_list: + if isinstance(action, dict) and action.get("id") == cls.WT_ACTION["id"]: + already_configured = True + break + + # Check keybindings array for shift+enter + keybindings_list = data.get("keybindings", []) + if not isinstance(keybindings_list, list): + keybindings_list = [] + data["keybindings"] = keybindings_list + + for binding in keybindings_list: + if isinstance(binding, dict) and binding.get("keys") == "shift+enter": + already_configured = True + break + + if already_configured: + io.tool_output("Windows Terminal already configured.") + return False + + # Add our action to actions array + actions_list.append(cls.WT_ACTION) + data["actions"] = actions_list + + # Add our keybinding to keybindings array + keybindings_list.append(cls.WT_KEYBINDING) + data["keybindings"] = keybindings_list + + cls._backup_file(path, io) + + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + io.tool_output("Updated Windows Terminal config.") + return True + + except json.JSONDecodeError: + io.tool_output( + "Error: Could not parse Windows Terminal settings.json. Is it valid JSON?" + ) + return False + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + """Configure terminal config files to support shift+enter for newline.""" + io.tool_output(f"Detecting OS: {platform.system()}") + paths = cls._get_config_paths() + + # Check for dry-run mode + dry_run = args == "dry_run" + if dry_run: + io.tool_output("DRY-RUN MODE: Showing what would be changed without modifying files\n") + + updated = False + + if "alacritty" in paths: + if cls._update_alacritty(paths["alacritty"], io, dry_run=dry_run): + updated = True + + if "kitty" in paths: + if cls._update_kitty(paths["kitty"], io, dry_run=dry_run): + updated = True + + if "windows_terminal" in paths: + if cls._update_windows_terminal(paths["windows_terminal"], io, dry_run=dry_run): + updated = True + + if dry_run: + if updated: + io.tool_output( + "\nDRY-RUN: Would make changes (restart terminals for changes to take effect)." + ) + else: + io.tool_output( + "\nDRY-RUN: No changes would be made (configurations already present or files" + " not found)." + ) + return format_command_result( + io, "terminal-setup", "Dry-run completed - showing what would be changed" + ) + elif updated: + io.tool_output("\nDone! Please restart your terminals for changes to take effect.") + return format_command_result( + io, "terminal-setup", "Successfully configured terminal settings" + ) + else: + io.tool_output( + "\nNo changes were made (configurations already present or files not found)." + ) + return format_command_result( + io, "terminal-setup", "No changes needed - configurations already present" + ) + + @classmethod + def get_completions(cls, io, coder, args) -> List[str]: + """Get completion options for terminal-setup command.""" + return [] + + @classmethod + def get_help(cls) -> str: + """Get help text for the terminal-setup command.""" + help_text = super().get_help() + help_text += "\nUsage:\n" + help_text += " /terminal-setup # Configure terminal to support shift+enter for newline\n" + help_text += ( + " /terminal-setup --dry-run # Show what would be changed without modifying files\n" + ) + help_text += ( + "\nNote: This command modifies terminal configuration files (Alacritty, Kitty, Windows" + " Terminal)\n" + ) + help_text += ( + "to add a key binding that sends a newline character when shift+enter is pressed.\n" + ) + help_text += "Backup copies are created with .bak extension before any modifications.\n" + help_text += "Use --dry-run to preview changes before applying them.\n" + return help_text diff --git a/cecli/io.py b/cecli/io.py index 482eebccf95..b4e18245059 100644 --- a/cecli/io.py +++ b/cecli/io.py @@ -396,9 +396,9 @@ def __init__( self.fzf_available = shutil.which("fzf") if not self.fzf_available and self.verbose: - self.tool_warning( - "fzf not found, fuzzy finder features will be disabled. Install it for enhanced" - " file/history search." + print( + "Warning: fzf not found, fuzzy finder features will be disabled. Install it for" + " enhanced file/history search." ) self.code_theme = code_theme @@ -421,7 +421,7 @@ def __init__( try: Path(self.input_history_file).parent.mkdir(parents=True, exist_ok=True) except (PermissionError, OSError) as e: - self.tool_warning(f"Could not create directory for input history: {e}") + print(f"Warning: Could not create directory for input history: {e}") self.input_history_file = None if chat_history_file is not None: @@ -490,11 +490,11 @@ def __init__( self.console = Console() # pretty console except Exception as err: self.console = Console(force_terminal=False, no_color=True) - self.tool_error(f"Can't initialize prompt toolkit: {err}") # non-pretty + print(f"Error: Can't initialize prompt toolkit: {err}") # non-pretty else: self.console = Console(force_terminal=False, no_color=True) # non-pretty if self.is_dumb_terminal: - self.tool_output("Detected dumb terminal, disabling fancy input and pretty output.") + print("Detected dumb terminal, disabling fancy input and pretty output.") self.file_watcher = file_watcher self.root = root diff --git a/cecli/main.py b/cecli/main.py index 371af61fc81..624af74cb4a 100644 --- a/cecli/main.py +++ b/cecli/main.py @@ -628,7 +628,7 @@ def get_io(pretty): output_queue = None input_queue = None pre_init_io = get_io(args.pretty) - if args.tui or args.linear_output is None: + if args.tui or (args.tui is None and not args.linear_output): try: from cecli.tui import create_tui_io @@ -765,7 +765,7 @@ def get_io(pretty): return await graceful_exit(None, 1) alias, model = parts models.MODEL_ALIASES[alias.strip()] = model.strip() - selected_model_name = await select_default_model(args, io) + selected_model_name = await select_default_model(args, pre_init_io) if not selected_model_name: return await graceful_exit(None, 1) args.model = selected_model_name @@ -1087,6 +1087,11 @@ def apply_model_overrides(model_name): io.tool_output("Dry run enabled, skipping commit.") else: await coder.commands.do_run("commit", "") + if args.terminal_setup: + if args.dry_run: + await coder.commands.do_run("terminal-setup", "dry_run") + else: + await coder.commands.do_run("terminal-setup", "") if args.lint or args.test or args.commit: return await graceful_exit(coder) if args.show_repo_map: diff --git a/cecli/tui/app.py b/cecli/tui/app.py index f656decd06f..567243ccc19 100644 --- a/cecli/tui/app.py +++ b/cecli/tui/app.py @@ -165,9 +165,6 @@ def _get_config(self): 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", @@ -188,8 +185,8 @@ def _get_config(self): } default_key_bindings = { - "newline": "enter" if is_multiline else "shift+enter", - "submit": "shift+enter" if is_multiline else "enter", + "newline": "shift+enter", + "submit": "enter", "stop": "escape", "cycle_forward": "tab", "cycle_backward": "shift+tab", diff --git a/cecli/tui/io.py b/cecli/tui/io.py index 6ae017eaeb6..71ca7ded7bc 100644 --- a/cecli/tui/io.py +++ b/cecli/tui/io.py @@ -22,13 +22,13 @@ def __init__(self, output_queue, input_queue, **kwargs): # Lazy-initialized console for TUI rendering self._tui_console = None - # Initialize parent (fancy_input should already be False from caller) - super().__init__(**kwargs) - # Store queues self.output_queue = output_queue self.input_queue = input_queue + # Initialize parent (fancy_input should already be False from caller) + super().__init__(**kwargs) + # Current task tracking self.current_task_id = None diff --git a/cecli/urls.py b/cecli/urls.py index 9a5c7b59d8a..7dfb384fe8e 100644 --- a/cecli/urls.py +++ b/cecli/urls.py @@ -8,9 +8,9 @@ token_limits = "https://cecli.dev/docs/troubleshooting/token-limits.html" llms = "https://cecli.dev/docs/llms.html" large_repos = "https://cecli.dev/docs/faq.html#can-i-use-cecli-in-a-large-mono-repo" -github_issues = "https://github.com/dwash96/aider-ce/issues/new" +github_issues = "https://github.com/dwash96/cecli/issues/new" git_index_version = "https://github.com/Aider-AI/aider/issues/211" install_properly = "https://cecli.dev/docs/troubleshooting/imports.html" -release_notes = "https://github.com/dwash96/aider-ce/releases/latest" +release_notes = "https://github.com/dwash96/cecli/releases/latest" edit_formats = "https://cecli.dev/docs/more/edit-formats.html" models_and_keys = "https://cecli.dev/docs/troubleshooting/models-and-keys.html" diff --git a/cecli/versioncheck.py b/cecli/versioncheck.py index 0563cd618d6..407f8725748 100644 --- a/cecli/versioncheck.py +++ b/cecli/versioncheck.py @@ -60,7 +60,7 @@ async def check_version(io, just_check=False, verbose=False): import requests try: - response = requests.get("https://pypi.org/pypi/aider-ce/json") + response = requests.get("https://pypi.org/pypi/cecli-dev/json") data = response.json() latest_version = data["info"]["version"] current_version = cecli.__version__ diff --git a/cecli/website/docs/config/tui.md b/cecli/website/docs/config/tui.md index c3f4f9e05b0..5710bfd70e9 100644 --- a/cecli/website/docs/config/tui.md +++ b/cecli/website/docs/config/tui.md @@ -46,8 +46,8 @@ tui-config: dark: true input-cursor-text-style: "underline" key_bindings: - newline: "enter" - submit: "shift+enter" + newline: "shift+enter" + submit: "enter" completion: "tab" stop: "escape" cycle_forward: "tab" @@ -65,8 +65,8 @@ The TUI provides customizable key bindings for all major actions. The default ke | 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 | +| New Line | `shift+enter` | Insert a new line in the input area (internally maps to `ctrl+j` if your editor hates `shift+enter`) | +| Submit | `enter` | 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 | @@ -87,7 +87,7 @@ tui-config: All key bindings use Textual's key syntax: - Single keys: `enter`, `escape`, `tab` -- Modifier combinations: `ctrl+c`, `shift+enter`, etc. +- Modifier combinations: `ctrl+c`, `shift+tab`, etc. ## Benefits