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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Shell: Fix Alt+Backspace not deleting a word inside modal text input (e.g. the `/btw` panel) — the global escape key binding was registered with `eager=True`, which swallowed the leading `escape` of the `escape,backspace` sequence before prompt_toolkit could match the emacs `backward-kill-word` binding; the eager flag is now disabled while a modal text input delegate is active so multi-key sequences resolve normally (fixes #1744)
- Shell: Add `/btw` side question command — ask a quick question during streaming without interrupting the main conversation; uses the same system prompt and tool definitions for prompt cache alignment; responses display in a scrollable modal panel with streaming support
- Shell: Redesign bottom dynamic area — split the monolithic `visualize.py` (1865 lines) into a modular package (`visualize/`) with dedicated modules for input routing, interactive prompts, approval/question panels, and btw modal; unify input semantics with `classify_input()` for consistent command routing
- Shell: Add queue and steer dual-channel input during streaming — Enter queues messages for delivery after the current turn; Ctrl+S injects messages immediately into the running turn's context; queued messages display in the prompt area with count indicator and can be recalled with ↑
Expand Down
1 change: 1 addition & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Shell: Fix Alt+Backspace not deleting a word inside modal text input (e.g. the `/btw` panel) — the global escape key binding was registered with `eager=True`, which swallowed the leading `escape` of the `escape,backspace` sequence before prompt_toolkit could match the emacs `backward-kill-word` binding; the eager flag is now disabled while a modal text input delegate is active so multi-key sequences resolve normally (fixes #1744)
- Shell: Add `/btw` side question command — ask a quick question during streaming without interrupting the main conversation; uses the same system prompt and tool definitions for prompt cache alignment; responses display in a scrollable modal panel with streaming support
- Shell: Redesign bottom dynamic area — split the monolithic `visualize.py` (1865 lines) into a modular package (`visualize/`) with dedicated modules for input routing, interactive prompts, approval/question panels, and btw modal; unify input semantics with `classify_input()` for consistent command routing
- Shell: Add queue and steer dual-channel input during streaming — Enter queues messages for delivery after the current turn; Ctrl+S injects messages immediately into the running turn's context; queued messages display in the prompt area with count indicator and can be recalled with ↑
Expand Down
1 change: 1 addition & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## 未发布

- Shell:修复模态文本输入(如 `/btw` 面板)中 Alt+Backspace 无法按词删除的问题——全局 escape 键绑定注册时使用了 `eager=True`,会在 prompt_toolkit 匹配 emacs `backward-kill-word` 之前吞掉 `escape,backspace` 序列里的 escape;现在当模态文本输入 delegate 处于活跃状态时禁用 eager 标志,多键序列恢复正常解析(修复 #1744)
- Shell:新增 `/btw` 侧问命令——在 streaming 期间提出快速问题,不打断主对话;使用相同的系统提示词和工具定义以对齐 Prompt 缓存;响应在可滚动的模态面板中显示,支持流式输出
- Shell:重新设计底部动态区——将单体 `visualize.py`(1865 行)拆分为模块化包(`visualize/`),包含输入路由、交互式提示、审批/提问面板和 btw 模态面板等独立模块;通过 `classify_input()` 统一输入语义,实现一致的命令路由
- Shell:新增 streaming 期间的排队和 steer 双通道输入——Enter 将消息排队,在当前轮次结束后发送;Ctrl+S 将消息立即注入到正在运行的轮次上下文中;排队消息在提示区域显示计数指示器,可通过 ↑ 键召回编辑
Expand Down
9 changes: 8 additions & 1 deletion src/kimi_cli/ui/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,9 +1369,16 @@ def _(event: KeyPressEvent) -> None:
def _(event: KeyPressEvent) -> None:
self._handle_running_prompt_key("c-d", event)

# `eager=True` ensures a plain Escape closes the modal immediately, but
# prompt_toolkit's eager matching short-circuits longer sequences such as
# `escape backspace` (Alt+Backspace → backward-kill-word). When the modal
# accepts text input, drop eager so the default Alt+X emacs bindings
# (backward-kill-word, backward-word, etc.) work against the buffer
# (fixes #1744). The standalone `escape` binding still fires when no
# longer sequence arrives, preserving the Escape-to-cancel behaviour.
@_kb.add(
"escape",
eager=True,
eager=Condition(lambda: self._active_ui_state() != PromptUIState.MODAL_TEXT_INPUT),
filter=Condition(lambda: self._should_handle_running_prompt_key("escape")),
)
def _(event: KeyPressEvent) -> None:
Expand Down
85 changes: 85 additions & 0 deletions tests/ui_and_conv/test_modal_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,3 +1272,88 @@ async def test_clear_active_approval_sink_requeues_pending_requests(
shell._pending_approval_requests.clear() # type: ignore[attr-defined]
shell._clear_active_view() # type: ignore[attr-defined]
assert len(shell._pending_approval_requests) == 0 # type: ignore[attr-defined]


# ---------------------------------------------------------------------------
# CustomPromptSession._active_ui_state — drives the escape key eager filter
# that fixes Alt+Backspace inside text-input modals (#1744).
# ---------------------------------------------------------------------------


def _make_prompt_session_stub(delegate: object | None):
"""Partially build a CustomPromptSession with just enough state to call
`_active_ui_state()`. Avoids the full `__init__` which touches filesystem
history and clipboard."""
from kimi_cli.ui.shell.prompt import CustomPromptSession

session = object.__new__(CustomPromptSession)
session._running_prompt_delegate = None # type: ignore[attr-defined]
session._modal_delegates = [] # type: ignore[attr-defined]
if delegate is not None:
session._modal_delegates.append(delegate) # type: ignore[attr-defined]
return session


def test_active_ui_state_is_modal_text_input_for_text_accepting_delegate() -> None:
"""When the active modal delegate accepts text input, the session reports
MODAL_TEXT_INPUT. The escape key binding's eager filter relies on this to
drop out of eager mode so that default emacs Alt+X sequences (notably
Alt+Backspace → backward-kill-word) reach the buffer (#1744).
"""
from kimi_cli.ui.shell.prompt import PromptUIState

class _TextInputDelegate:
modal_priority = 10

@staticmethod
def running_prompt_hides_input_buffer() -> bool:
return False

@staticmethod
def running_prompt_allows_text_input() -> bool:
return True

session = _make_prompt_session_stub(_TextInputDelegate())
assert session._active_ui_state() == PromptUIState.MODAL_TEXT_INPUT # type: ignore[attr-defined]


def test_active_ui_state_is_normal_for_non_text_input_delegate() -> None:
"""A modal that does not accept text input (e.g. question panel in
multi-choice mode) should leave the session in NORMAL_INPUT so plain
Escape closes it eagerly."""
from kimi_cli.ui.shell.prompt import PromptUIState

class _ChoiceOnlyDelegate:
modal_priority = 10

@staticmethod
def running_prompt_hides_input_buffer() -> bool:
return False

@staticmethod
def running_prompt_allows_text_input() -> bool:
return False

session = _make_prompt_session_stub(_ChoiceOnlyDelegate())
assert session._active_ui_state() == PromptUIState.NORMAL_INPUT # type: ignore[attr-defined]


def test_active_ui_state_is_modal_hidden_for_buffer_hiding_delegate() -> None:
"""A modal that hides the input buffer (approval panel, btw modal) should
report MODAL_HIDDEN_INPUT; the eager escape filter stays eager in that
state."""
from kimi_cli.ui.shell.prompt import PromptUIState

class _HiddenBufferDelegate:
modal_priority = 10

@staticmethod
def running_prompt_hides_input_buffer() -> bool:
return True

@staticmethod
def running_prompt_allows_text_input() -> bool:
return False

session = _make_prompt_session_stub(_HiddenBufferDelegate())
assert session._active_ui_state() == PromptUIState.MODAL_HIDDEN_INPUT # type: ignore[attr-defined]
Loading