Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/en/guides/interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ By default, up to 4 background tasks can run simultaneously. This can be adjuste

## Multi-line input

Sometimes you need to enter multiple lines, such as pasting a code snippet or error log. Press `Ctrl-J` or `Alt-Enter` to insert a newline instead of sending the message immediately.
Sometimes you need to enter multiple lines, such as pasting a code snippet or error log. Press `Shift+Enter`, `Ctrl-J`, or `Alt-Enter` to insert a newline instead of sending the message immediately.

After finishing your input, press `Enter` to send the complete message.

Expand Down
8 changes: 5 additions & 3 deletions docs/en/reference/keyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Kimi Code CLI shell mode supports the following keyboard shortcuts.
| `Ctrl-X` | Toggle agent/shell mode |
| `Shift-Tab` | Toggle plan mode (read-only research and planning) |
| `Ctrl-O` | Edit in external editor (`$VISUAL`/`$EDITOR`) |
| `Ctrl-J` | Insert newline |
| `Alt-Enter` | Insert newline (same as `Ctrl-J`) |
| `Shift-Enter` | Insert newline |
| `Ctrl-J` | Insert newline (same as `Shift+Enter`) |
| `Alt-Enter` | Insert newline (same as `Shift+Enter`) |
| `Ctrl-S` | Steer: inject input immediately into the running turn (during streaming) |
| `Ctrl-V` | Paste (supports images and video files) |
| `Ctrl-E` | Expand full approval request content |
Expand Down Expand Up @@ -60,10 +61,11 @@ Useful for writing multi-line prompts, complex code snippets, etc.

## Multi-line input

### `Ctrl-J` / `Alt-Enter`: Insert newline
### `Shift+Enter` / `Ctrl-J` / `Alt-Enter`: Insert newline

By default, pressing `Enter` submits the input. To enter multi-line content, use:

- `Shift+Enter`: Insert newline at any position
- `Ctrl-J`: Insert newline at any position
- `Alt-Enter`: Insert newline at any position

Expand Down
50 changes: 48 additions & 2 deletions src/kimi_cli/ui/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from prompt_toolkit.filters import Condition, has_completions, has_focus, is_done
from prompt_toolkit.formatted_text import AnyFormattedText, FormattedText, to_formatted_text
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import (
Expand Down Expand Up @@ -72,6 +73,20 @@
from kimi_cli.utils.slashcmd import SlashCommand
from kimi_cli.wire.types import ContentPart

# Register the xterm modifyOtherKeys ANSI sequences for modified Enter keys.
# This is safer than the kitty keyboard protocol because it only affects
# a narrow set of modified keys and does not change how other shortcuts
# (Ctrl-X, Ctrl-O, Ctrl-V, etc.) are encoded.
_SHIFT_ENTER_SEQ = "\x1b[27;2;13~" # Shift+Enter
_ALT_ENTER_SEQ = "\x1b[27;3;13~" # Alt+Enter
# Map modified Enter sequences to private-use characters so we can bind
# them independently of the plain Enter key. Using Keys.ControlM would
# force them through the same handler that is gated by ~has_completions,
# breaking newline insertion when the completion menu is open.
for _seq, _key in ((_SHIFT_ENTER_SEQ, "\ue001"), (_ALT_ENTER_SEQ, "\ue000")):
if ANSI_SEQUENCES.get(_seq) != _key:
ANSI_SEQUENCES[_seq] = _key # type: ignore[assignment]

AttachmentCache = prompt_placeholders.AttachmentCache
CachedAttachment = prompt_placeholders.CachedAttachment
_parse_attachment_kind = prompt_placeholders.parse_attachment_kind
Expand Down Expand Up @@ -1157,7 +1172,7 @@ def _build_toolbar_tips(clipboard_available: bool) -> list[str]:
"ctrl-x: toggle mode",
"shift-tab: plan mode",
"ctrl-o: editor",
"ctrl-j: newline",
"shift-enter / ctrl-j: newline",
"/feedback: send feedback",
"/theme: switch dark/light",
]
Expand Down Expand Up @@ -1312,10 +1327,24 @@ async def _toggle() -> None:
event.app.create_background_task(_toggle())
event.app.invalidate()

@_kb.add("enter", eager=True, filter=~has_completions)
Comment thread
donbeave marked this conversation as resolved.
def _(event: KeyPressEvent) -> None:
"""Submit when plain Enter is pressed and no completion menu is open."""
event.current_buffer.validate_and_handle()

@_kb.add("escape", "enter", eager=True)
@_kb.add("c-j", eager=True)
@_kb.add("\ue000", eager=True)
@_kb.add("\ue001", eager=True)
def _(event: KeyPressEvent) -> None:
"""Insert a newline when Alt-Enter or Ctrl-J is pressed."""
"""Insert a newline when Alt-Enter, Shift+Enter, or Ctrl-J is pressed.

The \ue000 binding catches Alt+Enter from terminals that emit the
xterm modifyOtherKeys sequence \x1b[27;3;13~ instead of escape+enter.
The \ue001 binding catches Shift+Enter from terminals that emit
\x1b[27;2;13~. Both are unfiltered so they work even when the
completion menu is open.
"""
from kimi_cli.telemetry import track

track("shortcut_newline")
Expand Down Expand Up @@ -1514,6 +1543,14 @@ def _(event: KeyPressEvent) -> None:
bottom_toolbar=self._render_bottom_toolbar,
style=get_prompt_style(),
)

# Enable xterm modifyOtherKeys so terminals that support it can
# distinguish Shift+Enter from plain Enter.
try:
self._session.app.output.write_raw("\x1b[>4;1m")
self._session.app.output.flush()
Comment on lines +1550 to +1551
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Alt-Enter newline after enabling modifyOtherKeys

Enabling xterm modifyOtherKeys here changes how modified Enter keys are encoded in supporting terminals, so Alt+Enter is no longer sent as escape + enter and the existing @_kb.add("escape", "enter") newline binding won’t fire. Because this patch only registers \x1b[27;2;13~ (Shift+Enter), users on terminals that honor \x1b[>4;1m will lose the previously supported Alt-Enter newline shortcut, which is a behavioral regression from prior releases and contradicts the updated docs/PR notes.

Useful? React with 👍 / 👎.

except Exception:
pass
self._session.default_buffer.read_only = Condition(
lambda: (
(delegate := self._active_prompt_delegate()) is not None
Expand Down Expand Up @@ -1880,6 +1917,15 @@ def __exit__(self, *_) -> None:
self._status_refresh_task.cancel()
self._status_refresh_task = None

# Disable xterm modifyOtherKeys on exit.
try:
session = getattr(self, "_session", None)
if session is not None:
session.app.output.write_raw("\x1b[>4;0m")
session.app.output.flush()
except Exception:
pass

def _get_placeholder_manager(self) -> PromptPlaceholderManager:
manager = getattr(self, "_placeholder_manager", None)
if manager is None:
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e/shell_pty_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ def send_key(self, key: str) -> None:
"right": b"\x1b[C",
"ctrl_c": b"\x03",
"ctrl_d": b"\x04",
"alt_enter": b"\x1b[27;3;13~",
"ctrl_j": b"\x0a",
"ctrl_x": b"\x18",
"s_enter": b"\x1b[27;2;13~",
}
payload = key_map.get(key)
if payload is None:
Expand Down
60 changes: 60 additions & 0 deletions tests/e2e/test_shell_pty_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,66 @@ def test_shell_ctrl_c_from_idle_prompt_after_completed_turn_shows_tip(tmp_path:
shell.close()


def test_shell_shift_enter_inserts_newline(tmp_path: Path) -> None:
"""Shift+Enter should insert a newline instead of submitting the message."""
config_path = write_scripted_config(tmp_path, ["text: First turn finished."])
work_dir = make_work_dir(tmp_path)
home_dir = make_home_dir(tmp_path)
shell = start_shell_pty(
config_path=config_path,
work_dir=work_dir,
home_dir=home_dir,
yolo=True,
)

try:
shell.read_until_contains("Welcome to Kimi Code CLI!")
_read_until_prompt(shell, after=shell.mark())

turn_mark = shell.mark()
shell.send_text("hello")
shell.send_key("s_enter")
shell.send_text("world")
shell.send_key("enter")
shell.read_until_contains("First turn finished.", after=turn_mark, timeout=15.0)
prompt_mark = shell.mark()
_read_until_prompt(shell, after=prompt_mark)

assert list_turn_begin_inputs(home_dir, work_dir) == ["hello\nworld"]
finally:
shell.close()


def test_shell_alt_enter_inserts_newline(tmp_path: Path) -> None:
"""Alt+Enter (xterm modifyOtherKeys sequence) should insert a newline."""
config_path = write_scripted_config(tmp_path, ["text: First turn finished."])
work_dir = make_work_dir(tmp_path)
home_dir = make_home_dir(tmp_path)
shell = start_shell_pty(
config_path=config_path,
work_dir=work_dir,
home_dir=home_dir,
yolo=True,
)

try:
shell.read_until_contains("Welcome to Kimi Code CLI!")
_read_until_prompt(shell, after=shell.mark())

turn_mark = shell.mark()
shell.send_text("hello")
shell.send_key("alt_enter")
shell.send_text("world")
shell.send_key("enter")
shell.read_until_contains("First turn finished.", after=turn_mark, timeout=15.0)
prompt_mark = shell.mark()
_read_until_prompt(shell, after=prompt_mark)

assert list_turn_begin_inputs(home_dir, work_dir) == ["hello\nworld"]
finally:
shell.close()


def test_shell_question_roundtrip_with_other_answer(tmp_path: Path) -> None:
question_payload = [
{
Expand Down
48 changes: 48 additions & 0 deletions tests/ui_and_conv/test_prompt_shift_enter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests for Shift+Enter newline support in the interactive prompt."""

from __future__ import annotations

from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES

# Import the module under test so its module-level side effects run.
import kimi_cli.ui.shell.prompt as _prompt_module


class TestShiftEnterAnsiSequences:
"""Verify that modified Enter ANSI sequences are registered at import time."""

def test_xterm_modifyotherkeys_shift_enter_registered(self) -> None:
"""Shift+Enter is mapped to a private-use character so it can be bound
independently of the plain Enter key. This preserves newline insertion
even when the completion menu is open.
"""
seq = "\x1b[27;2;13~"
assert seq in ANSI_SEQUENCES
assert ANSI_SEQUENCES[seq] == "\ue001"

def test_xterm_modifyotherkeys_alt_enter_registered(self) -> None:
"""Alt+Enter is mapped to a private-use character so it can be bound
independently of the plain Enter key. This preserves the existing
behaviour where Alt+Enter inserts a newline even when the completion
menu is open.
"""
seq = "\x1b[27;3;13~"
assert seq in ANSI_SEQUENCES
assert ANSI_SEQUENCES[seq] == "\ue000"

def test_kitty_sequence_is_not_registered(self) -> None:
"""The kitty keyboard protocol is intentionally not enabled because
it changes encoding for many modified keys and prompt_toolkit only
recognises a subset of CSI-u sequences. Using it would risk breaking
shortcuts like Ctrl-X, Ctrl-O and Ctrl-V on kitty-protocol terminals.
"""
assert "\x1b[13;2u" not in ANSI_SEQUENCES

def test_sequences_are_idempotent_on_reimport(self) -> None:
"""Re-importing the module must not raise or change the mapping."""
import importlib

importlib.reload(_prompt_module)

assert ANSI_SEQUENCES["\x1b[27;2;13~"] == "\ue001"
assert ANSI_SEQUENCES["\x1b[27;3;13~"] == "\ue000"
4 changes: 2 additions & 2 deletions tests/ui_and_conv/test_prompt_tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def test_build_toolbar_tips_without_clipboard() -> None:
"ctrl-x: toggle mode",
"shift-tab: plan mode",
"ctrl-o: editor",
"ctrl-j: newline",
"shift-enter / ctrl-j: newline",
"/feedback: send feedback",
"/theme: switch dark/light",
"@: mention files",
Expand All @@ -152,7 +152,7 @@ def test_build_toolbar_tips_with_clipboard() -> None:
"ctrl-x: toggle mode",
"shift-tab: plan mode",
"ctrl-o: editor",
"ctrl-j: newline",
"shift-enter / ctrl-j: newline",
"/feedback: send feedback",
"/theme: switch dark/light",
"ctrl-v: paste clipboard",
Expand Down