Environment
• OS: macOS (any version with screenshot drag-to-thumbnail)
• Kimi CLI version: 1.40.0
• Install method: uv tool install kimi-cli
Reproduction
- Take a macOS screenshot (Cmd+Shift+4) but don't save it — drag the floating thumbnail directly into the Kimi CLI t
inal.
- The path appears in the prompt, e.g.:
/var/folders/hx/.../TemporaryItems/NSIRD_screencaptureui_abc123/Screenshot 2026-05-07 at 5.47.51 PM.png
- Submit the message.
- Result: The agent calls ReadMediaFile and gets:
Error: File not found — /var/folders/hx/.../Screenshot ... PM.png
By the time the tool runs, macOS has already reaped the TemporaryItems/NSIRD_* directory (cleanup window is ~1–3 s
nds).
Root cause
The image path is read lazily (via ReadMediaFile tool call inside the agent loop), but the path lifecycle is eager (≤
3 seconds). The two clocks don't agree.
Claude Code CLI and Cursor solve this the same way: scan the user's input string at submit time, detect image-extensi
on paths, read them synchronously before the message goes to the model, and attach the bytes as multimodal content bl
ocks. The agent never sees a path it has to chase.
Fix
The fix is localized to the input-handling module (kimi_cli/ui/shell/placeholders.py) plus a small catch-block in the
shell event router.
What changed
File Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
kimi_cli/ui/shell/placeholders.py Added synchronous image-path scanning in PromptPlaceholderManager.resolve_comma
nd(): detects .png/.jpg/.jpeg/.webp/.gif/.heic/.bmp/.svg paths in literal text,
reads them at submit time, validates size (≤ 20 MB), sniffs MIME from magic byt
es, and attaches ImageURLPart content blocks.
kimi_cli/ui/shell/init.py Catches ImagePathResolutionError in _route_prompt_events() and prints a user-fa
cing error before resuming the prompt loop.
kimi_cli/ui/shell/prompt.py Threads model_capabilities into PromptPlaceholderManager so image attachment is
skipped when the model doesn't support image_in.
tests/ 26 tests covering drag-and-drop, spaces, quotes, Unicode, ~, relative paths, de
duplication, size cap, missing files, non-image extensions, URLs, and capabilit
y gating.
Key behaviors
• Drag macOS screenshot thumbnail → image visible to model on first turn, zero tool calls.
• Path with spaces / quotes / Unicode / ~ → all resolve correctly.
• Two images in one message → both attached as separate content blocks.
• Same path twice → deduplicated to one attachment.
• 50 MB image → rejected with inline size-cap error.
• Missing explicit path (e.g., reaped temp file) → clear error: "Image at was no longer accessible (macOS Temp
aryItems likely reaped); save to a persistent location and try again."
• .txt / .pdf / .json path → NOT auto-read; flows through normal text path.
• https://... URL → left as text, not treated as a missing local file.
• file:///path/to/image.png → stripped to /path/to/image.png and attached.
Patch
Click to expand full patch
NOTE: This patch is a summary of the exact changes applied.
The three core files are:
- kimi_cli/ui/shell/placeholders.py (~+270 lines including helpers & tests)
- kimi_cli/ui/shell/init.py (+1 import, +4 lines catch block)
- kimi_cli/ui/shell/prompt.py (+2 lines pass model_capabilities)
Because the installed wheel is not a git checkout, a literal git diff is not
available, but the implementation is a straightforward addition to
PromptPlaceholderManager.resolve_command() with these helpers:
_expand_image_path() — unescape spaces, expand ~, strip file://
_find_image_path_candidates() — regex + backward walk to detect image paths
_is_likely_text() — reject text files saved with image extensions
_read_image_sync() — size check, magic-byte MIME, base64 encode
_resolve_image_paths_in_text()— orchestrate detection → ImageURLPart
ImagePathResolutionError — user-facing exception for missing/bad files
Tests
All 26 new tests pass:
platform darwin -- Python 3.13.12, pytest-9.0.3
collected 26 items
test_placeholders.py::TestFindImagePathCandidates::test_basic_absolute PASSED
test_placeholders.py::TestFindImagePathCandidates::test_escaped_spaces PASSED
test_placeholders.py::TestFindImagePathCandidates::test_quoted_paths PASSED
test_placeholders.py::TestFindImagePathCandidates::test_no_candidates PASSED
test_placeholders.py::TestFindImagePathCandidates::test_path_with_spaces PASSED
test_placeholders.py::TestExpandImagePath::test_unescape_spaces PASSED
test_placeholders.py::TestExpandImagePath::test_tilde PASSED
test_placeholders.py::TestReadImageSync::test_valid_png PASSED
test_placeholders.py::TestReadImageSync::test_svg PASSED
test_placeholders.py::TestReadImageSync::test_size_cap PASSED
test_placeholders.py::TestReadImageSync::test_fake_png PASSED
test_placeholders.py::TestReadImageSync::test_missing_file PASSED
test_placeholders.py::TestResolveImagePathsInText::test_valid_png PASSED
test_placeholders.py::TestResolveImagePathsInText::test_two_images PASSED
test_placeholders.py::TestResolveImagePathsInText::test_duplicate_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_missing_explicit_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_missing_simple_filename_treated_as_text PASSED
test_placeholders.py::TestResolveImagePathsInText::test_text_not_auto_read PASSED
test_placeholders.py::TestResolveImagePathsInText::test_quoted_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_escaped_spaces PASSED
test_placeholders.py::TestResolveImagePathsInText::test_relative_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_unicode_filename PASSED
test_placeholders.py::TestResolveImagePathsInText::test_url_left_as_text PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_resolve_command_with_image PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_resolve_command_with_pasted_text_and_image PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_capability_gating_no_image_in PASSED
============================== 26 passed in 0.51s ==============================
Prior art
• Claude Code CLI ships this exact behavior. Drag a macOS screenshot thumbnail directly into the prompt and it works
the first turn — no ReadMediaFile, no TemporaryItems race.
• Cursor implements the same for Finder drag-and-drop.
Impact
This is table-stakes UX for any terminal-based coding agent on macOS. The screenshot-thumbnail workflow is too common
to lose to a 1-second cleanup race.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I'm happy to open this as a proper PR if the maintainers prefer — just let me know and I'll fork + branch.
Environment
• OS: macOS (any version with screenshot drag-to-thumbnail)
• Kimi CLI version: 1.40.0
• Install method: uv tool install kimi-cli
Reproduction
inal.
/var/folders/hx/.../TemporaryItems/NSIRD_screencaptureui_abc123/Screenshot 2026-05-07 at 5.47.51 PM.png
Error: File not found — /var/folders/hx/.../Screenshot ... PM.png
By the time the tool runs, macOS has already reaped the TemporaryItems/NSIRD_* directory (cleanup window is ~1–3 s
nds).
Root cause
The image path is read lazily (via ReadMediaFile tool call inside the agent loop), but the path lifecycle is eager (≤
3 seconds). The two clocks don't agree.
Claude Code CLI and Cursor solve this the same way: scan the user's input string at submit time, detect image-extensi
on paths, read them synchronously before the message goes to the model, and attach the bytes as multimodal content bl
ocks. The agent never sees a path it has to chase.
Fix
The fix is localized to the input-handling module (kimi_cli/ui/shell/placeholders.py) plus a small catch-block in the
shell event router.
What changed
File Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
kimi_cli/ui/shell/placeholders.py Added synchronous image-path scanning in PromptPlaceholderManager.resolve_comma
nd(): detects .png/.jpg/.jpeg/.webp/.gif/.heic/.bmp/.svg paths in literal text,
reads them at submit time, validates size (≤ 20 MB), sniffs MIME from magic byt
es, and attaches ImageURLPart content blocks.
kimi_cli/ui/shell/init.py Catches ImagePathResolutionError in _route_prompt_events() and prints a user-fa
cing error before resuming the prompt loop.
kimi_cli/ui/shell/prompt.py Threads model_capabilities into PromptPlaceholderManager so image attachment is
skipped when the model doesn't support image_in.
tests/ 26 tests covering drag-and-drop, spaces, quotes, Unicode, ~, relative paths, de
duplication, size cap, missing files, non-image extensions, URLs, and capabilit
y gating.
Key behaviors
• Drag macOS screenshot thumbnail → image visible to model on first turn, zero tool calls.
• Path with spaces / quotes / Unicode / ~ → all resolve correctly.
• Two images in one message → both attached as separate content blocks.
• Same path twice → deduplicated to one attachment.
• 50 MB image → rejected with inline size-cap error.
• Missing explicit path (e.g., reaped temp file) → clear error: "Image at was no longer accessible (macOS Temp
aryItems likely reaped); save to a persistent location and try again."
• .txt / .pdf / .json path → NOT auto-read; flows through normal text path.
• https://... URL → left as text, not treated as a missing local file.
• file:///path/to/image.png → stripped to /path/to/image.png and attached.
Patch
Click to expand full patch
NOTE: This patch is a summary of the exact changes applied.
The three core files are:
- kimi_cli/ui/shell/placeholders.py (~+270 lines including helpers & tests)
- kimi_cli/ui/shell/init.py (+1 import, +4 lines catch block)
- kimi_cli/ui/shell/prompt.py (+2 lines pass model_capabilities)
Because the installed wheel is not a git checkout, a literal git diff is not
available, but the implementation is a straightforward addition to
PromptPlaceholderManager.resolve_command() with these helpers:
_expand_image_path() — unescape spaces, expand ~, strip file://
_find_image_path_candidates() — regex + backward walk to detect image paths
_is_likely_text() — reject text files saved with image extensions
_read_image_sync() — size check, magic-byte MIME, base64 encode
_resolve_image_paths_in_text()— orchestrate detection → ImageURLPart
ImagePathResolutionError — user-facing exception for missing/bad files
Tests
All 26 new tests pass:
platform darwin -- Python 3.13.12, pytest-9.0.3
collected 26 items
test_placeholders.py::TestFindImagePathCandidates::test_basic_absolute PASSED
test_placeholders.py::TestFindImagePathCandidates::test_escaped_spaces PASSED
test_placeholders.py::TestFindImagePathCandidates::test_quoted_paths PASSED
test_placeholders.py::TestFindImagePathCandidates::test_no_candidates PASSED
test_placeholders.py::TestFindImagePathCandidates::test_path_with_spaces PASSED
test_placeholders.py::TestExpandImagePath::test_unescape_spaces PASSED
test_placeholders.py::TestExpandImagePath::test_tilde PASSED
test_placeholders.py::TestReadImageSync::test_valid_png PASSED
test_placeholders.py::TestReadImageSync::test_svg PASSED
test_placeholders.py::TestReadImageSync::test_size_cap PASSED
test_placeholders.py::TestReadImageSync::test_fake_png PASSED
test_placeholders.py::TestReadImageSync::test_missing_file PASSED
test_placeholders.py::TestResolveImagePathsInText::test_valid_png PASSED
test_placeholders.py::TestResolveImagePathsInText::test_two_images PASSED
test_placeholders.py::TestResolveImagePathsInText::test_duplicate_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_missing_explicit_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_missing_simple_filename_treated_as_text PASSED
test_placeholders.py::TestResolveImagePathsInText::test_text_not_auto_read PASSED
test_placeholders.py::TestResolveImagePathsInText::test_quoted_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_escaped_spaces PASSED
test_placeholders.py::TestResolveImagePathsInText::test_relative_path PASSED
test_placeholders.py::TestResolveImagePathsInText::test_unicode_filename PASSED
test_placeholders.py::TestResolveImagePathsInText::test_url_left_as_text PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_resolve_command_with_image PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_resolve_command_with_pasted_text_and_image PASSED
test_placeholders.py::TestPromptPlaceholderManager::test_capability_gating_no_image_in PASSED
============================== 26 passed in 0.51s ==============================
Prior art
• Claude Code CLI ships this exact behavior. Drag a macOS screenshot thumbnail directly into the prompt and it works
the first turn — no ReadMediaFile, no TemporaryItems race.
• Cursor implements the same for Finder drag-and-drop.
Impact
This is table-stakes UX for any terminal-based coding agent on macOS. The screenshot-thumbnail workflow is too common
to lose to a 1-second cleanup race.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I'm happy to open this as a proper PR if the maintainers prefer — just let me know and I'll fork + branch.