fix(cli): handle CSI-u Shift+Enter instead of dumping raw escapes

Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol -- kitty,
Ghostty, WezTerm, and terminal panes that default to it -- encode Shift+Enter
as the escape sequence "\x1b[13;2u". prompt_toolkit 3.0 has no support for
that protocol and no default mapping for the sequence, so its Vt100Parser
fails to recognise it and dumps the raw bytes ("^[[13;2u") straight into the
prompt buffer. The multiline-input work kept only Alt+Enter, so on these
terminals Shift+Enter now leaks visible escape garbage into the input.

Register "\x1b[13;2u" (absent from prompt_toolkit's default ANSI_SEQUENCES,
so setdefault() overrides nothing) and bind it to insert a newline, matching
the Alt+Enter behaviour. This is best-effort: prompt_toolkit cannot negotiate
the protocol, so we only react to a CSI-u sequence a terminal already emits;
terminals that collapse Shift+Enter into plain Enter fall back to Alt+Enter,
which stays the primary shortcut.

Add a real-PromptSession regression test asserting the sequence inserts a
newline rather than leaking raw escape bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjunwei
2026-07-10 19:32:47 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent 87602a74ea
commit 99e04c2da2
2 changed files with 53 additions and 1 deletions
+23
View File
@@ -127,6 +127,29 @@ async def test_alt_enter_inserts_newline_on_lf_terminals():
assert result == "foo\nbar"
@pytest.mark.asyncio
async def test_csi_u_shift_enter_inserts_newline_not_raw_escape():
"""Terminals speaking the CSI-u (kitty / fixterms) keyboard protocol send
Shift+Enter as "\\x1b[13;2u". prompt_toolkit has no support for that protocol
and would otherwise dump the raw escape bytes ("^[[13;2u") into the buffer;
the binding must instead parse it as one keypress and insert a newline. Only
a real PromptSession/parser exercises the ANSI_SEQUENCES registration.
"""
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[13;2ubar\r")
result = await session.prompt_async("> ")
# A newline is inserted and no raw escape bytes leak into the result.
assert result == "foo\nbar"
def test_thinking_spinner_pause_stops_and_restarts():
"""Pause should stop the active spinner and restart it afterward."""
spinner = MagicMock()