fix(cli): stop hijacking ControlJ for Shift+Enter, it breaks Enter on WSL

Keys.ControlJ is also the literal LF byte ("\x0a") that some terminals send
for a plain Enter keypress -- prompt_toolkit's own default bindings handle
this by re-feeding it as ControlM/submit, and calls out WSL by name as the
case that needs it. Binding our Shift+Enter handler to ControlJ shadowed
that default, so on any terminal sending LF for Enter, pressing Enter only
ever inserted a newline and the prompt could never be submitted.

Register the CSI-u Shift+Enter sequences against Keys.ControlF3 instead: an
enum member prompt_toolkit declares but never wires to a default ANSI
sequence or key binding, so it's only reachable through our own mapping.

Also add a regression test that drives a real PromptSession/Vt100Parser
with a raw LF byte -- the existing key-binding test invoked handlers
directly against a mocked buffer, which exercises the handler logic but not
prompt_toolkit's key-resolution precedence, so it couldn't have caught this.

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 18a230de75
commit ea0516e655
2 changed files with 35 additions and 8 deletions
+26 -3
View File
@@ -88,13 +88,36 @@ 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")
# ControlJ (mapped Shift+Enter on CSI-u terminals) -> newline
ctrl_j = bound[(Keys.ControlJ.value,)]
# 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()
ctrl_j.call(MagicMock(current_buffer=buf))
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():
"""A raw LF byte (\\x0a) is what some terminals -- e.g. WSL -- send for a
plain Enter keypress. It must submit the buffer, not insert a newline;
a mock buffer can't catch a key binding shadowing prompt_toolkit's own
default \\n-as-\\r handling, so this drives a real PromptSession/parser.
"""
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("hello\x0aworld\r")
result = await session.prompt_async("> ")
assert result == "hello"
def test_thinking_spinner_pause_stops_and_restarts():
"""Pause should stop the active spinner and restart it afterward."""
spinner = MagicMock()