Skip to content

Commit e223099

Browse files
committed
fix: prevent Ctrl+V crash when clipboard contains non-text data
Two-layer fix for TypeError crash on paste: 1. _SafePyperclipClipboard: wraps PyperclipClipboard.get_data() to treat None/non-string payloads as empty text, protecting all clipboard access paths in prompt_toolkit (not just our Ctrl+V handler). Covers MoonshotAI#1750. 2. _try_paste_media() return value: when images are detected but the model does not support image_in, the paste event is now consumed (return True) instead of falling through to the text paste path.
1 parent a8f09bc commit e223099

File tree

2 files changed

+42
-5
lines changed

2 files changed

+42
-5
lines changed

src/kimi_cli/ui/shell/prompt.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from prompt_toolkit import PromptSession
2222
from prompt_toolkit.application.current import get_app_or_none
2323
from prompt_toolkit.buffer import Buffer
24+
from prompt_toolkit.clipboard.base import ClipboardData
2425
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
2526
from prompt_toolkit.completion import (
2627
CompleteEvent,
@@ -81,6 +82,24 @@
8182
PROMPT_SYMBOL_PLAN = "📋"
8283

8384

85+
class _SafePyperclipClipboard(PyperclipClipboard):
86+
"""Treat non-text clipboard payloads as empty text instead of crashing prompt_toolkit."""
87+
88+
@override
89+
def get_data(self) -> ClipboardData:
90+
try:
91+
data = super().get_data()
92+
except TypeError as exc:
93+
logger.debug(
94+
"Ignoring non-text clipboard payload in text paste handler: {error}",
95+
error=exc,
96+
)
97+
return ClipboardData()
98+
if not isinstance(data.text, str):
99+
return ClipboardData()
100+
return data
101+
102+
84103
class SlashCommandCompleter(Completer):
85104
"""
86105
A completer that:
@@ -1488,7 +1507,7 @@ def _(event: KeyPressEvent) -> None:
14881507
self._insert_pasted_text(event.current_buffer, clipboard_data.text)
14891508
event.app.invalidate()
14901509

1491-
clipboard = PyperclipClipboard()
1510+
clipboard = _SafePyperclipClipboard()
14921511
else:
14931512
clipboard = None
14941513

@@ -1846,6 +1865,7 @@ def _try_paste_media(self, event: KeyPressEvent) -> bool:
18461865
return False
18471866

18481867
parts: list[str] = []
1868+
unsupported_images = False
18491869

18501870
# 1. Insert file paths (videos, PDFs, etc.)
18511871
if result.file_paths:
@@ -1859,6 +1879,7 @@ def _try_paste_media(self, event: KeyPressEvent) -> bool:
18591879
# 2. Insert images via cache.
18601880
if result.images:
18611881
if "image_in" not in self._model_capabilities:
1882+
unsupported_images = True
18621883
console.print(
18631884
"[yellow]Image input is not supported by the selected LLM model[/yellow]"
18641885
)
@@ -1877,7 +1898,7 @@ def _try_paste_media(self, event: KeyPressEvent) -> bool:
18771898
if parts:
18781899
event.current_buffer.insert_text(" ".join(parts))
18791900
event.app.invalidate()
1880-
return bool(parts)
1901+
return bool(parts) or unsupported_images
18811902

18821903
def set_prefill_text(self, text: str) -> None:
18831904
"""Pre-fill the input buffer with the given text.

tests/ui_and_conv/test_prompt_clipboard.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from PIL import Image
99
from prompt_toolkit.key_binding import KeyPressEvent
10+
from prompt_toolkit.selection import SelectionType
1011

1112
if TYPE_CHECKING:
1213
from prompt_toolkit.buffer import Buffer
@@ -184,7 +185,7 @@ def test_paste_single_image(monkeypatch) -> None:
184185
assert buffer.inserted[0].startswith("[image:")
185186

186187

187-
def test_paste_image_unsupported_model(monkeypatch, capsys) -> None:
188+
def test_paste_image_unsupported_model_consumes_paste(monkeypatch, capsys) -> None:
188189
img = Image.new("RGB", (10, 10))
189190
monkeypatch.setattr(
190191
shell_prompt,
@@ -199,9 +200,11 @@ def test_paste_image_unsupported_model(monkeypatch, capsys) -> None:
199200

200201
result = ps._try_paste_media(cast(KeyPressEvent, event))
201202

202-
# No image placeholder inserted, returns False so caller can fall back to text paste
203-
assert result is False
203+
# Media was recognized, so the paste event should be consumed even though the
204+
# model cannot accept image input.
205+
assert result is True
204206
assert buffer.inserted == []
207+
assert app.invalidated is True
205208

206209

207210
# --- Mixed content tests ---
@@ -270,6 +273,19 @@ def test_paste_returns_false_when_no_media(monkeypatch) -> None:
270273
assert buffer.inserted == []
271274

272275

276+
def test_safe_pyperclip_clipboard_treats_none_as_empty_text(monkeypatch) -> None:
277+
monkeypatch.setattr(
278+
"prompt_toolkit.clipboard.pyperclip.pyperclip.paste",
279+
lambda: None,
280+
)
281+
282+
clipboard = shell_prompt._SafePyperclipClipboard()
283+
data = clipboard.get_data()
284+
285+
assert data.text == ""
286+
assert data.type == SelectionType.CHARACTERS
287+
288+
273289
def test_insert_pasted_text_placeholderizes_long_text_in_agent_mode() -> None:
274290
ps = _make_prompt_session(PromptMode.AGENT)
275291
buffer = _DummyBuffer()

0 commit comments

Comments
 (0)