Skip to content

[FEATURE]: TUI should capture dropped/pasted files at drop time when the source path is ephemeral #46664

Description

@chrishalltu

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Searched existing issues; several are adjacent, none covers this. Specifically:

Prior art, same root cause in another harness: anthropics/claude-code#65181 — a file shared via the macOS shared pasteboard lands on a path the agent cannot read (EPERM); the requested remedy is to copy it somewhere readable before substituting the path into the prompt.

Describe the enhancement you want to request

Summary

Dropping a non-image file into the TUI works from Finder and silently fails from every other application. The cause is not the file type — it's that the dropped path is ephemeral, and the TUI resolves it lazily, long after it has been reaped.

Request: classify the path at drop time and, when it is ephemeral or unreadable, capture the file immediately rather than storing a reference that is already dying.

Root cause

There is no drag-and-drop code in the TUI. Dropping a file on a terminal is handled by the emulator, which types the path into the tty; it arrives as an ordinary bracketed paste. Drop and paste therefore share one path: onPaste (packages/tui/src/component/prompt/index.tsx:1396) → pasteInputTextreadLocalAttachment.

Raw bytes never arrive. A terminal injects text and nothing else. Any fix that hopes to receive the dropped bytes in the TUI cannot work — the only input is a path, and the only question is whether it still resolves when a tool finally reads it.

What differs between file types is when the bytes are read:

Drop source Behavior Why
Image, any app works bytes read at drop time into a data: URL (index.tsx:1195-1201); the source path expiring never matters
Non-image, Finder works rejected as an attachment, so the path falls through as literal text (index.tsx:1215); Finder paths are durable, so Read resolves it later
Non-image, non-Finder silently fails same fall-through, but the path is a promise/pasteboard file that is gone or EPERM by the time the agent reads it

The rejection is one line, packages/tui/src/component/prompt/local-attachment.ts:44:

if (!mime.startsWith("image/") && mime !== "application/pdf") return

MIME comes from an 8-entry extension map with no sniffing (local-attachment.ts:25-34). #46173 addresses that half; this issue is about the other half.

Steps to reproduce

  1. From a non-Finder source — an email attachment, Slack, an AirDropped file, a Safari download drag — drag a .csv, .yaml, or .txt into the TUI prompt.
  2. A path like the following is inserted as literal text:
/Users/<me>/Library/Group Containers/group.com.apple.coreservices.useractivityd/shared-pasteboard/items/<uuid>/data.csv
  1. Ask the agent to read it.

Observed: the read fails — ENOENT once the promise file is reaped, or EPERM on the sandboxed shared-pasteboard path.
Expected: readable, exactly as the identical drop of a .png already is.

Verified on macOS (Darwin 25.x, Terminal.app / iTerm2). The $TMPDIR/TemporaryItems and shared-pasteboard directories are the two macOS sources I confirmed. Linux (XDG document portal) and Windows (INetCache) are likely equivalent but I have not verified them, so I'm not claiming them.

Proposed behavior

At drop/paste time, once a path is recognized:

  1. Durable and readable — Finder, a project file, anything outside the pasteboard/TemporaryItems/portal dirs that passes an access() probe → keep current behavior, insert the real path. This is strictly better than copying: the agent can edit the original in place, and there's nothing to clean up.
  2. Ephemeral or unreadable → capture the bytes immediately and attach the captured copy.

Detection should be location plus an access probe, never extension.

For step 2's destination, the managed store in #46173 is the natural home rather than a second parallel mechanism — but it currently expects upload at submit, and a drop can precede submit by minutes, which the source file will not survive. Failing that, a session-scoped temp dir referenced by file:// works today: packages/opencode/src/session/prompt.ts:808-889 already reads a file: FilePart through the Read tool with extra: { bypassCwdCheck: true } (line 822), so a path outside the project is explicitly permitted, and ?start=/?end= ranges already work. @file autocomplete builds exactly this shape (autocomplete.tsx:247-267).

Prefer file:// over data: for captured non-images: a data: URL inflates the prompt store, prompt-history.jsonl, and the wire payload by 4/3 for a file the model would rather read on demand.

Implementation notes

  • pasteAttachment (index.tsx:1224-1270) needs generalizing: virtualText is hardcoded to [PDF n]/[Image n] (line 1233) and the URL to data: (line 1251). A [File n] variant plus a matching counter predicate at lines 1228-1232.
  • Temp lifetime is the one genuinely new problem. The two existing TUI temp writes (clipboard.ts:50, editor.ts:49) both delete in a finally; a captured attachment must outlive submit, because the server reads it lazily. Session-scoped, cleaned on session end/exit.
  • History replay. PromptInfo.parts is persisted to prompt-history.jsonl, and restoreExtmarksFromParts (index.tsx:658-700) will restore a placeholder pointing at a file deleted days ago. Recall should degrade visibly rather than silently attach nothing.
  • The canary test is packages/tui/test/prompt/local-attachment.test.ts:32, which asserts text/plainundefined; it should fail the moment the gate widens.

Why this is worth doing separately from #46173

The failure is silent and misattributed. The path lands in the prompt, the prompt submits, and the agent reports it cannot find a file the user is looking at on their own screen. Nothing errors at drop time, and nothing in the UI distinguishes the durable path from the doomed one. Users conclude drag-and-drop "only works for images" — when in fact it works for everything from Finder and nothing else from anywhere else.

#46173 will make the file types work. Without capture-at-drop-time in the TUI, this specific case still fails afterward, because by the time anything uploads, the file is gone.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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