diff --git a/src/kimi_cli/ui/shell/__init__.py b/src/kimi_cli/ui/shell/__init__.py index 8df1895be..5ce9fa1dd 100644 --- a/src/kimi_cli/ui/shell/__init__.py +++ b/src/kimi_cli/ui/shell/__init__.py @@ -35,7 +35,7 @@ from kimi_cli.utils.signals import install_sigint_handler from kimi_cli.utils.slashcmd import SlashCommand, SlashCommandCall, parse_slash_command_call from kimi_cli.utils.subprocess_env import get_clean_env -from kimi_cli.utils.term import ensure_new_line, ensure_tty_sane +from kimi_cli.utils.term import ensure_new_line, ensure_tty_sane, maybe_disable_kitty_keyboard_protocol from kimi_cli.wire.types import ContentPart, StatusUpdate @@ -109,6 +109,8 @@ async def _plan_mode_toggle() -> bool: return await self.soul.toggle_plan_mode_from_manual() return False + maybe_disable_kitty_keyboard_protocol() + with CustomPromptSession( status_provider=lambda: self.soul.status, model_capabilities=self.soul.model_capabilities or set(), diff --git a/src/kimi_cli/utils/term.py b/src/kimi_cli/utils/term.py index c22d12e57..ee954109a 100644 --- a/src/kimi_cli/utils/term.py +++ b/src/kimi_cli/utils/term.py @@ -6,6 +6,8 @@ import sys import time +from kimi_cli.utils.envvar import get_env_bool + def ensure_new_line() -> None: """Ensure the next prompt starts at column 0 regardless of prior command output.""" @@ -50,6 +52,33 @@ def ensure_tty_sane() -> None: termios.tcsetattr(fd, termios.TCSADRAIN, attrs) +def maybe_disable_kitty_keyboard_protocol() -> None: + """Disable kitty keyboard protocol in terminals that send CSI-u sequences. + + This is primarily a workaround for VS Code's integrated terminal, which can + emit CSI-u key sequences that prompt_toolkit doesn't parse. + """ + if sys.platform == "win32": + return + if not sys.stdout.isatty() or not sys.stdin.isatty(): + return + if not _should_disable_kitty_keyboard_protocol(): + return + + _write_escape("\x1b[ bool: + env_value = os.getenv("KIMI_CLI_DISABLE_KITTY_KEYS") + if env_value is not None: + return get_env_bool("KIMI_CLI_DISABLE_KITTY_KEYS", default=False) + return _is_vscode_terminal() + + +def _is_vscode_terminal() -> bool: + return os.getenv("TERM_PROGRAM") == "vscode" or "VSCODE_IPC_HOOK_CLI" in os.environ + + def _cursor_position_unix() -> tuple[int, int] | None: """Get cursor position (row, column) on Unix. Both are 1-indexed.""" assert sys.platform != "win32" @@ -149,6 +178,11 @@ def _write_newline() -> None: sys.stdout.flush() +def _write_escape(value: str) -> None: + sys.stdout.write(value) + sys.stdout.flush() + + def get_cursor_row() -> int | None: """Get the current cursor row (1-indexed).""" if not sys.stdout.isatty() or not sys.stdin.isatty():