From b2f1aef5fef77851d67742437ba956822e218ba5 Mon Sep 17 00:00:00 2001 From: Said Akkas Date: Wed, 13 May 2026 20:23:07 +0800 Subject: [PATCH] feat: add kill_ring_system_clipboard config option Add a configurable option to prevent emacs-style kill commands (Ctrl-W, Ctrl-K, Ctrl-U, etc.) from writing deleted text to the system clipboard. When set to false, these commands use an in-memory kill ring instead, avoiding clipboard pollution. Intentional cut/copy/paste operations (Ctrl-W with selection, Alt-W, Ctrl-Y, Ctrl-V) continue to use the system clipboard regardless of this setting. Files changed: - config.py: add kill_ring_system_clipboard field (default: true) - shell/__init__.py: pass config value to CustomPromptSession - shell/prompt.py: add eager keybinding overrides when disabled - docs: document the new option --- CHANGELOG.md | 2 + docs/en/configuration/config-files.md | 2 + docs/en/release-notes/changelog.md | 2 + docs/zh/configuration/config-files.md | 2 + docs/zh/release-notes/changelog.md | 2 + src/kimi_cli/config.py | 10 ++++ src/kimi_cli/ui/shell/__init__.py | 5 ++ src/kimi_cli/ui/shell/prompt.py | 69 ++++++++++++++++++++++++++- 8 files changed, 93 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40131bea7..8a1ae7629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Only write entries that are worth mentioning to users. ## Unreleased +- Config: Add `kill_ring_system_clipboard` option — set to `false` to keep emacs-style kill commands (`Ctrl-W`, `Ctrl-K`, `Ctrl-U`, etc.) in an in-memory kill ring instead of overwriting the system clipboard + ## 1.43.0 (2026-05-12) - Security: Bump pillow to 12.2.0 to address CVE-2026-25990 (out-of-bounds write when loading PSD images); unblocks installs in environments that gate on the older pinned version diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md index c504bdc3b..79adf581f 100644 --- a/docs/en/configuration/config-files.md +++ b/docs/en/configuration/config-files.md @@ -34,6 +34,7 @@ The configuration file contains the following top-level configuration items: | `show_thinking_stream` | `boolean` | Whether to stream the raw reasoning text in the live area as a 6-line scrolling preview and commit the full reasoning markdown to history when the block ends (defaults to `true`; set to `false` to show only the compact `Thinking ...` indicator and a one-line trace summary) | | `merge_all_available_skills` | `boolean` | Whether to merge skills from all brand directories (defaults to `true`); see [Skills configuration](../customization/skills.md) | | `telemetry` | `boolean` | Whether to enable anonymous telemetry to help improve kimi-cli (defaults to `true`; set to `false` to disable) | +| `kill_ring_system_clipboard` | `boolean` | Whether emacs-style kill commands (Ctrl-W, Ctrl-K, etc.) write to the system clipboard (defaults to `true`; set to `false` to use an in-memory kill ring) | | `providers` | `table` | API provider configuration | | `models` | `table` | Model configuration | | `loop_control` | `table` | Agent loop control parameters | @@ -54,6 +55,7 @@ theme = "dark" show_thinking_stream = true merge_all_available_skills = true telemetry = true +kill_ring_system_clipboard = true [providers.kimi-for-coding] type = "kimi" diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 6c1ec30f8..673ca61f2 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -4,6 +4,8 @@ This page documents the changes in each Kimi Code CLI release. ## Unreleased +- Config: Add `kill_ring_system_clipboard` option — set to `false` to keep emacs-style kill commands (`Ctrl-W`, `Ctrl-K`, `Ctrl-U`, etc.) in an in-memory kill ring instead of overwriting the system clipboard + ## 1.43.0 (2026-05-12) - Security: Bump pillow to 12.2.0 to address CVE-2026-25990 (out-of-bounds write when loading PSD images); unblocks installs in environments that gate on the older pinned version diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md index c5464df40..7c2dbd8c0 100644 --- a/docs/zh/configuration/config-files.md +++ b/docs/zh/configuration/config-files.md @@ -34,6 +34,7 @@ kimi --config '{"default_model": "kimi-for-coding", "providers": {...}, "models" | `show_thinking_stream` | `boolean` | 是否在 Live 区域以 6 行滚动预览方式实时展示模型的原始思考文本,并在 thinking 块结束时把完整思考内容(Markdown)写入历史记录(默认为 `true`;设为 `false` 则仅显示紧凑的 `Thinking ...` 指示器和一行 trace 总结) | | `merge_all_available_skills` | `boolean` | 是否合并所有品牌目录中的 Skills(默认为 `true`);详见 [Skills 配置](../customization/skills.md) | | `telemetry` | `boolean` | 是否启用匿名遥测以帮助改进 kimi-cli(默认为 `true`;设为 `false` 可关闭) | +| `kill_ring_system_clipboard` | `boolean` | Emacs 风格的删除命令(Ctrl-W、Ctrl-K 等)是否写入系统剪贴板(默认为 `true`;设为 `false` 则使用内存 kill ring) | | `providers` | `table` | API 供应商配置 | | `models` | `table` | 模型配置 | | `loop_control` | `table` | Agent 循环控制参数 | @@ -54,6 +55,7 @@ theme = "dark" show_thinking_stream = true merge_all_available_skills = true telemetry = true +kill_ring_system_clipboard = true [providers.kimi-for-coding] type = "kimi" diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index bbfdf575f..b00cee9ce 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -4,6 +4,8 @@ ## 未发布 +- Config:新增 `kill_ring_system_clipboard` 配置项——设为 `false` 时,Emacs 风格的删除命令(`Ctrl-W`、`Ctrl-K`、`Ctrl-U` 等)将使用内存 kill ring,不再覆盖系统剪贴板 + ## 1.43.0 (2026-05-12) - Security:将 pillow 升级到 12.2.0 以修复 CVE-2026-25990(加载 PSD 图像时存在越界写入);解除在依赖审查严格的环境下因旧版本被阻断而无法安装的限制 diff --git a/src/kimi_cli/config.py b/src/kimi_cli/config.py index 451f46810..45c32dc26 100644 --- a/src/kimi_cli/config.py +++ b/src/kimi_cli/config.py @@ -262,6 +262,16 @@ class Config(BaseModel): default=True, description="Enable anonymous telemetry to help improve kimi-cli. Set to false to disable.", ) + kill_ring_system_clipboard: bool = Field( + default=True, + description=( + "When true (default), emacs-style kill commands (Ctrl-W, Ctrl-K, etc.) " + "write deleted text to the system clipboard. When false, deleted text " + "is discarded and does not pollute the system clipboard. " + "Intentional cut/copy/paste (Ctrl-W with selection, Alt-W, Ctrl-Y, Ctrl-V) " + "continue to use the system clipboard regardless of this setting." + ), + ) @model_validator(mode="after") def validate_model(self) -> Self: diff --git a/src/kimi_cli/ui/shell/__init__.py b/src/kimi_cli/ui/shell/__init__.py index 8a91828d9..6e73b5f93 100644 --- a/src/kimi_cli/ui/shell/__init__.py +++ b/src/kimi_cli/ui/shell/__init__.py @@ -469,6 +469,11 @@ def _bg_task_counts() -> BgTaskCounts: self.soul.runtime.config.default_editor if isinstance(self.soul, KimiSoul) else "" ), plan_mode_toggle_callback=_plan_mode_toggle, + kill_ring_system_clipboard=( + self.soul.runtime.config.kill_ring_system_clipboard + if isinstance(self.soul, KimiSoul) + else True + ), ) as prompt_session: self._prompt_session = prompt_session if self._prefill_text: diff --git a/src/kimi_cli/ui/shell/prompt.py b/src/kimi_cli/ui/shell/prompt.py index b7a950b09..82f98631d 100644 --- a/src/kimi_cli/ui/shell/prompt.py +++ b/src/kimi_cli/ui/shell/prompt.py @@ -32,7 +32,7 @@ ) from prompt_toolkit.data_structures import Point from prompt_toolkit.document import Document -from prompt_toolkit.filters import Condition, has_completions, has_focus, is_done +from prompt_toolkit.filters import Condition, has_completions, has_focus, has_selection, is_done from prompt_toolkit.formatted_text import AnyFormattedText, FormattedText, to_formatted_text from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent @@ -1185,6 +1185,7 @@ def __init__( shell_mode_slash_commands: Sequence[SlashCommand[Any]], editor_command_provider: Callable[[], str] = lambda: "", plan_mode_toggle_callback: Callable[[], Awaitable[bool]] | None = None, + kill_ring_system_clipboard: bool = True, ) -> None: history_dir = get_share_dir() / "user-history" history_dir.mkdir(parents=True, exist_ok=True) @@ -1215,6 +1216,7 @@ def __init__( self._prompt_buffer_container: ConditionalContainer | None = None self._last_ui_state: PromptUIState = PromptUIState.NORMAL_INPUT self._suspended_buffer_document: Document | None = None + self._kill_ring_system_clipboard = kill_ring_system_clipboard clipboard_available = is_clipboard_available() media_clipboard_available = is_media_clipboard_available() self._tips = _build_toolbar_tips(clipboard_available or media_clipboard_available) @@ -1496,6 +1498,71 @@ def _(event: KeyPressEvent) -> None: self._insert_pasted_text(event.current_buffer, clipboard_data.text) event.app.invalidate() + # Override built-in kill bindings so they don't pollute the system + # clipboard when the user has disabled kill-ring-to-system-clipboard. + if not self._kill_ring_system_clipboard: + + @_kb.add("c-w", eager=True, filter=~has_selection) + def _(event: KeyPressEvent) -> None: + """Unix-word-rubout without system clipboard.""" + buffer = event.current_buffer + pos = buffer.document.find_start_of_previous_word(count=event.arg, WORD=True) + if pos is None: + pos = -buffer.cursor_position + if pos: + buffer.delete_before_cursor(count=-pos) + else: + event.app.output.bell() + + @_kb.add("c-k", eager=True) + def _(event: KeyPressEvent) -> None: + """Kill-line without system clipboard.""" + buff = event.current_buffer + if event.arg < 0: + buff.delete_before_cursor(count=-buff.document.get_start_of_line_position()) + else: + if buff.document.current_char == "\n": + buff.delete(1) + else: + buff.delete(count=buff.document.get_end_of_line_position()) + + @_kb.add("c-u", eager=True) + def _(event: KeyPressEvent) -> None: + """Unix-line-discard without system clipboard.""" + buff = event.current_buffer + if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: + buff.delete_before_cursor(count=1) + else: + buff.delete_before_cursor(count=-buff.document.get_start_of_line_position()) + + @_kb.add("c-delete", eager=True) + def _(event: KeyPressEvent) -> None: + """Kill-word (forward) without system clipboard.""" + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + if pos: + buff.delete(count=pos) + + @_kb.add("escape", "d", eager=True) + def _(event: KeyPressEvent) -> None: + """Meta-D: kill-word (forward) without system clipboard.""" + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + if pos: + buff.delete(count=pos) + + @_kb.add("escape", "backspace", eager=True) + def _(event: KeyPressEvent) -> None: + """Meta-Backspace: backward-kill-word without system clipboard.""" + buffer = event.current_buffer + pos = buffer.document.find_start_of_previous_word(count=event.arg) + if pos is None: + pos = -buffer.cursor_position + if pos: + buffer.delete_before_cursor(count=-pos) + else: + event.app.output.bell() + # Only use PyperclipClipboard when pyperclip actually works. # PromptSession built-in keybindings (ctrl-k, ctrl-w, ctrl-y) # use clipboard without error handling, so a broken clipboard