diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 30a7969d..7cd6c5b9 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -312,34 +312,7 @@ def _build_cli_key_bindings() -> KeyBindings: * Alt+Enter -> insert a newline. Universally supported across terminal emulators, so this is the reliable way to compose multi-line input. - * Shift+Enter -> insert a newline *if* the terminal sends a dedicated - sequence for it (kitty / iTerm2 with the CSI-u / - fixterms keyboard protocol). Plain terminals collapse - Shift+Enter into Enter and cannot be distinguished, in - which case Alt+Enter is the fallback. """ - # prompt_toolkit has no symbolic Keys.ShiftEnter and @kb.add() rejects raw - # escape strings, so register the CSI-u Shift+Enter sequences against - # Keys.ControlF3 -- an enum member prompt_toolkit declares but never wires - # to a default ANSI sequence or key binding, unlike e.g. Keys.ControlJ, - # which is also the literal LF byte ("\x0a") that some terminals (WSL is - # the case prompt_toolkit itself calls out) send for a plain Enter - # keypress. Aliasing to ControlJ made Enter stop submitting there, since - # our handler shadowed prompt_toolkit's own "treat \n as \r" fallback. - # - # "\x1b[27;2;13~" (the older xterm modifyOtherKeys / rxvt encoding of - # Shift+Enter) is already registered by prompt_toolkit by default -- as - # Keys.ControlM, i.e. plain Enter/submit. A plain setdefault() would be a - # no-op against that existing entry, silently leaving this Shift+Enter - # variant behaving like a submit instead of inserting a newline. Assign - # directly to override it, since inserting a newline is the whole point - # of a Shift+Enter binding here. - with suppress(Exception): - from prompt_toolkit.input import ansi_escape_sequences as _aes - - for _seq in ("\x1b[13;2u", "\x1b[27;2;13~"): - _aes.ANSI_SEQUENCES[_seq] = Keys.ControlF3 - kb = KeyBindings() @kb.add("enter") @@ -359,10 +332,6 @@ def _build_cli_key_bindings() -> KeyBindings: def _(event): event.current_buffer.insert_text("\n") - @kb.add(Keys.ControlF3) # Shift+Enter on CSI-u capable terminals - def _(event): - event.current_buffer.insert_text("\n") - return kb @@ -385,8 +354,7 @@ def _init_prompt_session() -> None: history=SafeFileHistory(str(history_file)), enable_open_in_editor=False, # Multiline-capable buffer; Enter still submits via the custom key - # bindings, while Shift+Enter (supported terminals) or Alt+Enter adds - # a newline. + # bindings, while Alt+Enter adds a newline. multiline=True, key_bindings=_build_cli_key_bindings(), ) diff --git a/tests/cli/test_cli_input.py b/tests/cli/test_cli_input.py index bb0d5e07..e719003f 100644 --- a/tests/cli/test_cli_input.py +++ b/tests/cli/test_cli_input.py @@ -46,8 +46,8 @@ def test_init_prompt_session_creates_session(): # Ensure global is None before test commands._PROMPT_SESSION = None - with patch("nanobot.cli.commands.PromptSession") as MockSession, \ - patch("nanobot.cli.commands.FileHistory") as MockHistory, \ + with patch("nanobot.cli.commands.PromptSession") as mock_session_cls, \ + patch("nanobot.cli.commands.FileHistory"), \ patch("pathlib.Path.home") as mock_home: mock_home.return_value = MagicMock() @@ -55,17 +55,17 @@ def test_init_prompt_session_creates_session(): commands._init_prompt_session() assert commands._PROMPT_SESSION is not None - MockSession.assert_called_once() - _, kwargs = MockSession.call_args - # Buffer is multiline-capable so Shift+Enter / Alt+Enter can insert - # newlines; Enter-to-submit is restored via custom key bindings. + mock_session_cls.assert_called_once() + _, kwargs = mock_session_cls.call_args + # Buffer is multiline-capable so Alt+Enter can insert newlines; + # Enter-to-submit is restored via custom key bindings. assert kwargs["multiline"] is True assert kwargs["enable_open_in_editor"] is False assert kwargs.get("key_bindings") is not None def test_cli_key_bindings_enter_submits_and_alt_enter_newlines(): - """Enter submits the buffer; Alt+Enter and Shift+Enter insert a newline.""" + """Enter submits the buffer; Alt+Enter inserts a newline.""" from prompt_toolkit.keys import Keys kb = commands._build_cli_key_bindings() @@ -88,14 +88,6 @@ def test_cli_key_bindings_enter_submits_and_alt_enter_newlines(): alt_enter.call(MagicMock(current_buffer=buf)) buf.insert_text.assert_called_once_with("\n") - # ControlF3 (synthetic carrier for the Shift+Enter CSI-u sequence) -> newline. - # Not ControlJ: that's also the literal LF byte some terminals (e.g. WSL) - # send for a plain Enter keypress, so binding it would break submit there. - shift_enter = bound[(Keys.ControlF3.value,)] - buf = MagicMock() - shift_enter.call(MagicMock(current_buffer=buf)) - buf.insert_text.assert_called_once_with("\n") - @pytest.mark.asyncio async def test_raw_lf_enter_still_submits_like_wsl_terminals(): @@ -118,28 +110,6 @@ async def test_raw_lf_enter_still_submits_like_wsl_terminals(): assert result == "hello" -@pytest.mark.asyncio -async def test_xterm_modifyotherkeys_shift_enter_inserts_newline(): - """"\\x1b[27;2;13~" (the older xterm modifyOtherKeys/rxvt encoding of - Shift+Enter) is registered by prompt_toolkit *by default* as plain - Enter/submit, so a `setdefault()`-based override would silently no-op - against it. This must actually insert a newline like the kitty CSI-u - encoding does, which only a real PromptSession/parser can confirm. - """ - from prompt_toolkit.application import create_app_session - from prompt_toolkit.input import create_pipe_input - from prompt_toolkit.output import DummyOutput - - with create_pipe_input() as pipe_input: - with create_app_session(input=pipe_input, output=DummyOutput()): - commands._init_prompt_session() - session = commands._PROMPT_SESSION - pipe_input.send_text("foo\x1b[27;2;13~bar\r") - result = await session.prompt_async("> ") - - assert result == "foo\nbar" - - @pytest.mark.asyncio async def test_alt_enter_inserts_newline_on_lf_terminals(): """On LF-as-Enter terminals (WSL), Alt+Enter arrives as ESC + LF