Skip to content

fix(tui): send pasted image and PDF paths to local servers - #59833

Merged
rekram1-node merged 1 commit into
devfrom
tui-attachment-path
Oct 28, 2026
Merged

rekram1-node merged 1 commit into
devfrom
tui-attachment-path

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When I drag an image into the TUI, or paste a path to an image or PDF, the TUI reads the bytes and sends a data: URI. The path is gone by the time the model sees it, so it can't open the file again or hand it to a tool.

Core already does the right thing for file:// attachments: it reads the file, keeps source.uri, and puts Attached file: <path> in front of the image. The TUI just never sent one.

Change

The composer still stores the data: URI, so previews, [Image N] labels, dedup, history and drafts work like before. It now also remembers the local path. On submit, each attachment picks its URI:

for each attached file
  if it came from a local path
     and the server runs on this machine
     and the file still exists
    send file://<path>        (core reads it, model sees "Attached file: <path>")
  else
    send the data: bytes from paste time
  • "Runs on this machine" means the CLI started or reused the managed server, or used --standalone. With an explicit --server I keep sending bytes, since the server could be anywhere. That's the same rule the CLI already uses to decide whether to forward the terminal's environment.
  • If the file disappeared before submit (macOS screenshot thumbnail drags, [FEATURE]: TUI should capture dropped/pasted files at drop time when the source path is ephemeral #46664), the bytes captured at paste time still go out.
  • Clipboard bitmaps have no path and stay data:. SVG paste-as-text and @-mentioned files don't change.
 packages/cli/src/commands/handlers/default.ts   # server.local = no --server
 packages/tui/src/app.tsx                        # passes it into the terminal environment
 packages/tui/src/context/runtime.tsx            # localServer flag
 packages/tui/src/prompt/history.tsx             # PromptFile gets an optional path
 packages/tui/src/component/prompt/
+  local-attachment.ts   # resolvePastedAttachments returns path, promptFilesForServer picks the URI
   index.tsx             # stores path on paste, maps files on prompt and command submit

Out of scope

  • opencode run mini mode still sends bytes. It has its own prompt types and host wiring.
  • Editing or forking a message sent this way puts back a file:// URI. That's how @-mentioned images already behave: no thumbnail in the composer, and core reads the file again on resend.
  • Pasting a path as plain text (TUI: pasted image paths force attachments in V2 #57298) is still a separate request. This doesn't change which pastes get turned into attachments.

Verification

  • bun test test/prompt in packages/tui: 78 pass. The new test covers PNG, JPEG and PDF with spaces, unicode and quotes, a local vs remote server, a deleted file, and a pathless clipboard image keeping its order.
  • bun run test in packages/tui: 1550 pass. The one failure, tab-pulse.test.tsx, also fails now and then on dev.
  • bun typecheck in packages/tui and packages/cli: clean.
  • Drove the dev TUI with bracketed pastes in a real PTY:
    • a shell-escaped path with spaces and unicode, a quoted JPEG path, a file:// URL to a PDF, and a PNG I deleted before submit. The stored message had source.uri for the first three and inline for the deleted one, with [Image 1] [Image 2] [PDF 1] [Image 3] in order.
    • two images and a PDF in one prompt. The model listed all three paths next to the right labels.
    • against serve through --server. The image went inline.
  • Core coverage was already there: session-prompt.test.ts "materializes local image content before admission" and session-runner-message.test.ts "exposes admitted local image source paths before provider media".

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
  <style>
    .card { animation: drop 4s ease-in-out infinite; }
    .path { animation: path 4s ease-in-out infinite; }
    @keyframes drop {
      0%, 8% { transform: translate(-46px, -26px); opacity: 0; }
      22% { opacity: 1; }
      38%, 100% { transform: translate(0, 0); opacity: 1; }
    }
    @keyframes path {
      0%, 45% { opacity: 0; transform: translateX(-6px); }
      58%, 92% { opacity: 1; transform: translateX(0); }
      100% { opacity: 0; transform: translateX(0); }
    }
    @media (prefers-reduced-motion: reduce) { .card, .path { animation: none; } }
  </style>
  <rect x="8" y="22" width="224" height="42" rx="6" stroke-opacity="0.35"/>
  <line x1="8" y1="22" x2="8" y2="64" stroke-width="3" stroke-opacity="0.6"/>
  <g class="card">
    <rect x="18" y="31" width="20" height="24" rx="3" stroke-opacity="0.9"/>
    <path d="M21 50l5-6 4 4 3-3 3 5" stroke-opacity="0.7"/>
    <circle cx="32" cy="37" r="1.6" fill="currentColor" stroke="none" fill-opacity="0.7"/>
  </g>
  <g class="path" fill="currentColor" stroke="none" font-family="ui-monospace, monospace">
    <text x="46" y="41" font-size="9" fill-opacity="0.9">[Image 1]</text>
    <text x="46" y="55" font-size="8" fill-opacity="0.55">file:///Users/me/shot.png</text>
  </g>
</svg>

Added by the opencode bot (AI-generated).

Fixes #48190
Fixes #41454

Related: #46664, #57298

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found nothing that should block this. A path pasted into a TUI using a local server is sent as file://, which core already reads and labels for the model with Attached file: <path>. Remote servers (--server), files deleted before submit, and clipboard bitmaps still send the bytes captured at paste time. Only the pasted-path flow changes; @-mentions and SVG text are untouched.

I ran bun test test/prompt in packages/tui (78 pass) and the two core tests the description cites (both pass). I couldn't check it end to end because our TUI driver can't send a pasted path. bun typecheck won't run in this environment on either branch, but tsc reported no errors in the changed files.

Public API is unchanged. TuiInput.server.local and TuiTerminalEnvironment.localServer are optional fields inside the TUI. Saved prompt history entries gain an optional path, and existing history still loads.

Fixes #48190
Fixes #41454
Related: #46664, #57298

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pasting an image file loses the original path — model only gets an inline data: URL tui: attached image path is omitted from model context

1 participant