diff --git a/cli.py b/cli.py index 31ba863f9f..1b2a81dfc4 100644 --- a/cli.py +++ b/cli.py @@ -1774,6 +1774,20 @@ _TERMINAL_INPUT_MODE_RESET_SEQ = ( ) +def _bind_prompt_submit_keys(kb, handler) -> None: + """Bind both CR and LF terminal Enter forms to the submit handler.""" + for key in ("enter", "c-j"): + kb.add(key)(handler) + + +def _disable_prompt_toolkit_cpr_warning(app) -> None: + """Let prompt_toolkit fall back from CPR without printing into the prompt.""" + try: + app.renderer.cpr_not_supported_callback = None + except Exception: + pass + + def _strip_leaked_terminal_responses_with_meta(text: str) -> tuple[str, bool]: """Strip leaked terminal control-response sequences from user input. @@ -10338,7 +10352,6 @@ class HermesCLI: # Key bindings for the input area kb = KeyBindings() - @kb.add('enter') def handle_enter(event): """Handle Enter key - submit input. @@ -10497,17 +10510,14 @@ class HermesCLI: else: self._pending_input.put(payload) event.app.current_buffer.reset(append_to_history=True) + + _bind_prompt_submit_keys(kb, handle_enter) @kb.add('escape', 'enter') def handle_alt_enter(event): """Alt+Enter inserts a newline for multi-line input.""" event.current_buffer.insert_text('\n') - @kb.add('c-j') - def handle_ctrl_enter(event): - """Ctrl+Enter (c-j) inserts a newline. Most terminals send c-j for Ctrl+Enter.""" - event.current_buffer.insert_text('\n') - # VSCode/Cursor bind Ctrl+G to "Find Next" at the editor level, so # the keystroke never reaches the embedded terminal. Alt+G is unbound # in those IDEs and arrives here as ('escape', 'g') — register it as @@ -11106,7 +11116,7 @@ class HermesCLI: def get_prompt(): return cli_ref._get_tui_prompt_fragments() - # Create the input area with multiline (shift+enter), autocomplete, and paste handling + # Create the input area with multiline (Alt+Enter), autocomplete, and paste handling from prompt_toolkit.auto_suggest import AutoSuggestFromHistory @@ -11848,6 +11858,7 @@ class HermesCLI: mouse_support=False, **({'cursor': _STEADY_CURSOR} if _STEADY_CURSOR is not None else {}), ) + _disable_prompt_toolkit_cpr_warning(app) self._app = app # Store reference for clarify_callback # ── Fix ghost status-bar lines on terminal resize ────────────── diff --git a/tests/cli/test_cli_init.py b/tests/cli/test_cli_init.py index bf1f347e50..c9ecf2c7df 100644 --- a/tests/cli/test_cli_init.py +++ b/tests/cli/test_cli_init.py @@ -3,6 +3,7 @@ that only manifest at runtime (not in mocked unit tests).""" import os import sys +from types import SimpleNamespace from unittest.mock import MagicMock, patch sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) @@ -161,6 +162,35 @@ class TestBusyInputMode: assert cli._pending_input.empty() +class TestPromptToolkitTerminalCompatibility: + def test_lf_enter_binds_to_submit_handler(self): + """Some thin PTYs deliver Enter as LF/c-j instead of CR/enter.""" + from prompt_toolkit.key_binding import KeyBindings + + from cli import _bind_prompt_submit_keys + + kb = KeyBindings() + + def submit_handler(event): + return None + + _bind_prompt_submit_keys(kb, submit_handler) + + bindings = {tuple(key.value for key in binding.keys): binding.handler for binding in kb.bindings} + assert bindings[("c-m",)] is submit_handler + assert bindings[("c-j",)] is submit_handler + + def test_cpr_warning_callback_is_disabled(self): + from cli import _disable_prompt_toolkit_cpr_warning + + renderer = SimpleNamespace(cpr_not_supported_callback=lambda: None) + app = SimpleNamespace(renderer=renderer) + + _disable_prompt_toolkit_cpr_warning(app) + + assert renderer.cpr_not_supported_callback is None + + class TestSingleQueryState: def test_voice_and_interrupt_state_initialized_before_run(self): """Single-query mode calls chat() without going through run()."""