fix(cli): make Alt+Enter insert a newline on LF-as-Enter terminals

On terminals that send a bare LF for plain Enter (WSL is the case
prompt_toolkit itself calls out), Alt+Enter arrives as ESC + LF
("\x1b\x0a" = Escape + ControlJ), not the ESC + CR the existing
"escape","enter" binding matches. The Escape was swallowed and the bare
LF hit prompt_toolkit's default submit, so the documented "universally
supported" Alt+Enter newline fallback failed on exactly the terminal path
plain Enter is preserved for.

Bind ESC + ControlJ to insert a newline too, and add a real-PromptSession
regression test covering the WSL Alt+Enter path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjunwei
2026-07-07 15:41:08 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent 25a477050d
commit 77a6003255
2 changed files with 33 additions and 1 deletions
+10 -1
View File
@@ -346,7 +346,16 @@ def _build_cli_key_bindings() -> KeyBindings:
def _(event):
event.current_buffer.validate_and_handle()
@kb.add("escape", "enter") # Alt+Enter / Meta+Enter
@kb.add("escape", "enter") # Alt+Enter / Meta+Enter (ESC + CR, "\x1b\r")
def _(event):
event.current_buffer.insert_text("\n")
# On the same LF-as-Enter terminals (WSL) we preserve plain Enter for,
# Alt+Enter arrives as ESC + LF ("\x1b\x0a" = Escape + ControlJ) rather than
# ESC + CR, so the "escape","enter" binding above never matches: Escape is
# swallowed and the bare LF triggers prompt_toolkit's default submit. Bind
# ESC + ControlJ too so Alt+Enter reliably inserts a newline there as well.
@kb.add("escape", Keys.ControlJ) # Alt+Enter on LF-as-Enter terminals
def _(event):
event.current_buffer.insert_text("\n")
+23
View File
@@ -140,6 +140,29 @@ async def test_xterm_modifyotherkeys_shift_enter_inserts_newline():
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
("\\x1b\\x0a" = Escape + ControlJ) rather than the ESC + CR that the
"escape","enter" binding matches. Without a dedicated ESC + ControlJ
binding the Escape is swallowed and the bare LF submits, so the documented
Alt+Enter newline fallback breaks on exactly the terminal path plain Enter
is preserved for. Drive a real PromptSession to prove it inserts a newline.
"""
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\x0abar\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()