Skip to content

Bug: macOS screenshot thumbnails dropped into terminal fail to attach (TemporaryItems race) #2182

Description

@abm9111

Environment
• OS: macOS (any version with screenshot drag-to-thumbnail)
• Kimi CLI version: 1.40.0
• Install method: uv tool install kimi-cli
Reproduction

  1. Take a macOS screenshot (Cmd+Shift+4) but don't save it — drag the floating thumbnail directly into the Kimi CLI t
    inal.
  2. 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
  3. Submit the message.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions