From 25a477050dbf280bf63d9de8f687ed97387b34ee Mon Sep 17 00:00:00 2001 From: wangjunwei Date: Wed, 1 Jul 2026 10:55:50 +0800 Subject: [PATCH] fix(cli): make the xterm modifyOtherKeys Shift+Enter encoding insert a newline "\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. The previous `setdefault()` call was therefore a silent no-op against it: this Shift+Enter variant kept behaving like a submit instead of inserting a newline, even after the ControlJ/WSL fix, since setdefault only sets missing keys. Assign directly to override that default for both known Shift+Enter sequences, since inserting a newline is the whole point of the binding. Add a regression test driving a real PromptSession/parser with this exact sequence, since a mocked-buffer test can't observe prompt_toolkit's default ANSI_SEQUENCES entries taking priority. Co-Authored-By: Claude Opus 4.8 --- nanobot/cli/commands.py | 10 +++++++++- tests/cli/test_cli_input.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 0f69bf79..9e150195 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -326,11 +326,19 @@ def _build_cli_key_bindings() -> KeyBindings: # 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.setdefault(_seq, Keys.ControlF3) + _aes.ANSI_SEQUENCES[_seq] = Keys.ControlF3 kb = KeyBindings() diff --git a/tests/cli/test_cli_input.py b/tests/cli/test_cli_input.py index b38cd20f..824409f2 100644 --- a/tests/cli/test_cli_input.py +++ b/tests/cli/test_cli_input.py @@ -118,6 +118,28 @@ 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" + + def test_thinking_spinner_pause_stops_and_restarts(): """Pause should stop the active spinner and restart it afterward.""" spinner = MagicMock()