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
+9 -5
View File
@@ -319,14 +319,18 @@ def _build_cli_key_bindings() -> KeyBindings:
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 a
# spare key symbol (ControlJ) and bind that. Terminals that never emit
# these sequences simply won't trigger the binding.
# 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.
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.ControlJ)
_aes.ANSI_SEQUENCES.setdefault(_seq, Keys.ControlF3)
kb = KeyBindings()
@@ -338,7 +342,7 @@ def _build_cli_key_bindings() -> KeyBindings:
def _(event):
event.current_buffer.insert_text("\n")
@kb.add(Keys.ControlJ) # Shift+Enter on CSI-u capable terminals
@kb.add(Keys.ControlF3) # Shift+Enter on CSI-u capable terminals
def _(event):
event.current_buffer.insert_text("\n")