Skip to content

iOS image paste: deliver path via bracketed paste so Claude Code attaches [Image #N] - #5602

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
feat-ios-image-paste-bracketed
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
feat-ios-image-paste-bracketed

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Pasting a phone-clipboard image into the iOS terminal injected the image's file path as a readable string, but Claude Code did not attach it as [Image #N]. It just looked like a pasted path.

Root cause

The terminal.paste_image RPC (TerminalController.v2MobileTerminalPasteImage) writes the forwarded clipboard image to a temp file on the Mac, shell-escapes the path, then injected it with sendInputResult → ghostty_surface_text_input. Per Ghostty's own API docs, that call "is treated like typed text, not a paste ... bracketed paste mode is not used."

Claude Code only runs its pasted-image-path detection from its bracketed-paste handler (the text framed by ESC[200~ … ESC[201~). Its paste handler strips the bracketed-paste artifacts, splits the pasted text on path boundaries, unescapes shell escaping (so a shell-escaped path is accepted), matches /\.(png|jpe?g|gif|webp)$/i, reads the file, and attaches it as [Image #N]. When the path arrives as ordinary typed text (no bracketing), that handler never fires, so the path lands as a literal string and no image is attached. (Verified against the Claude Code binary's input layer: handlePaste → X(Z) path-detection vs the raw-key path, which only runs the detector for single key events over 800 chars.)

The escaping/format was never the problem; the delivery route was. The desktop drop/paste path already produces [Image #N] because it delivers through ghostty_surface_complete_clipboard_request (the paste path, which respects bracketed paste mode).

Fix

Route the mobile image-paste injection through Ghostty's paste path (ghostty_surface_text), the same route a desktop clipboard paste or file drop takes. This wraps the bytes in a bracketed paste when the foreground program has enabled bracketed paste mode (DECSET 2004), so Claude Code's image-path detection runs.

  • Adds TerminalSurface.sendTextResult(_:), a paste-path twin of sendInputResult that preserves the rich InputSendResult queue/availability error mapping (and the cold-surface .pasteText enqueue path, which already flushes via the paste route).
  • v2MobileTerminalPasteImage now calls sendTextResult instead of sendInputResult.

Non-Claude shells are unaffected: a single-line escaped path has no newline and no ESC[201~, so it passes Ghostty paste protection and pastes the path exactly as before.

The change is entirely Mac-side (the iOS app forwards the image bytes; the Mac does the injection). Stacks on / relates to the merged image-paste work (#5546).

Testing

A meaningful automated test is not practical here: the bug is in the delivery primitive (ghostty_surface_text vs ghostty_surface_text_input), which is a C-API call on a live Ghostty surface, and the user-visible outcome ([Image #N]) requires Claude Code running with auth. Per the repo test-quality policy, no source-shape regression test was added. Verification is on-device dogfood.

Dogfood (on device): copy an image to the iPhone clipboard, open a cmux terminal running claude, paste the image. Previously: the prompt showed a literal file path. Now: the prompt shows [Image #N].

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes terminal input delivery for mobile image paste and adds a new paste-path API; scope is narrow (one RPC + surface helper) but affects how bytes reach the PTY.

Overview
Fixes iOS-forwarded clipboard images not becoming [Image #N] in Claude Code by changing how the Mac injects the temp file path, not how the path is escaped.

Adds sendTextResult(_:) on the terminal surface as a paste-path counterpart to sendInputResult: it delivers UTF-8 through Ghostty’s paste API (ghostty_surface_text via writeTextData), so bracketed paste (DECSET 2004) applies when the foreground program has it enabled. It keeps the same InputSendResult semantics (queue when the surface is cold, surface/process errors, etc.), including enqueueing .pasteText for background surfaces.

v2MobileTerminalPasteImage now calls sendTextResult instead of sendInputResult, matching desktop clipboard paste / file drop so TUIs like Claude Code run paste-time image-path detection instead of treating the path as typed text.

Reviewed by Cursor Bugbot for commit c626c22. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes iOS image paste so Claude Code attaches images as [Image #N] by sending the temp file path via Ghostty’s paste path (bracketed paste) instead of typed text. Other shells paste the escaped path unchanged.

  • Bug Fixes
    • Added TerminalSurface.sendTextResult(_:) to deliver via ghostty_surface_text (uses bracketed paste when DECSET 2004 is enabled) while preserving InputSendResult semantics and queueing.
    • Switched v2MobileTerminalPasteImage to use sendTextResult rather than sendInputResult, matching desktop paste/drop behavior.

Written for commit c626c22. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a new text input API with enhanced delivery status reporting, distinguishing between successfully sent, queued, and failed delivery states.
  • Improvements

    • Enhanced image paste operations with bracketed-paste protocol support for improved terminal application compatibility.

…ches [Image #N]

The iOS image-paste RPC (terminal.paste_image) wrote the clipboard image to a
temp file on the Mac and injected the shell-escaped path with sendInputResult,
which routes through ghostty_surface_text_input. That API treats the bytes as
typed text and explicitly does NOT use bracketed paste mode.

Claude Code only runs its pasted-image-path detection from its bracketed-paste
handler (the text framed by ESC[200~ ... ESC[201~). With the path arriving as
typed text, that handler never fires, so the path lands as a literal string and
the image is never attached as [Image #N].

Route the injection through the paste path instead (ghostty_surface_text, the
same route a desktop clipboard paste or file drop takes), which wraps the bytes
in a bracketed paste when the foreground program has enabled DECSET 2004. This
matches the desktop drop behavior that already produces [Image #N]. Non-Claude
shells are unaffected: a single-line escaped path has no newline, so it passes
Ghostty paste protection and pastes the path as before.

Adds TerminalSurface.sendTextResult(_:), a paste-path twin of sendInputResult
that keeps the rich InputSendResult queue/availability error mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 8, 2026 3:27am
cmux-staging Building Building Preview, Comment Jun 8, 2026 3:27am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97fd56a1-d59c-4dd8-ab1b-a42f211af04c

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0ed95 and c626c22.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalController.swift

📝 Walkthrough

Walkthrough

This change introduces a new TerminalSurface API method sendTextResult(_:) that delivers text via bracketed-paste framing and returns detailed delivery status, then integrates this new method into the mobile image paste flow with updated documentation explaining the bracketed-paste requirement.

Changes

Paste-path text delivery and mobile image paste integration

Layer / File(s) Summary
Paste-path text delivery API
Sources/GhosttyTerminalView.swift
New TerminalSurface.sendTextResult(_:) -> InputSendResult method delivers text via ghostty_surface_text with bracketed-paste framing. Supports queuing when the runtime surface is not yet created, validates write availability, checks process state, and returns .sent, .queued, .surfaceUnavailable, .inputQueueFull, or .processExited based on delivery outcome.
Mobile image paste flow update
Sources/TerminalController.swift
Documentation for terminal.paste_image is expanded to clarify that the escaped image path must be delivered via bracketed-paste so the TUI's paste handler detects and attaches the image. The v2MobileTerminalPasteImage method is updated to call sendTextResult(...) instead of sendInputResult(...) for escaped-path delivery.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • manaflow-ai/cmux#5546: Directly implements the image paste flow using the new TerminalSurface.sendTextResult(_:) API for escaped image path delivery in the v2MobileTerminalPasteImage path.

Poem

🐰 A paste path springs forth, result-rich and bright,
Delivering text with bracketed delight,
Image paths now flow where they should belong,
Via bracketed paste, the terminal's song. 📬


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error New socket.terminal error strings in Resources/Localizable.xcstrings lack translations for 14 of 16 supported locales (only en, ja provided). Add translations for socket.terminal.* entries in all 16 supported locales: ar, bs, da, de, en, es, fr, it, ja, ko, nb, pl, ru, th, tr, uk.
Cmux Source Artifacts ❌ Error PR adds .claude/scheduled_tasks.lock (local tool session metadata) and .claude/commands/*.md files to source control. These are hidden workspace artifacts that should not be committed. Remove .claude/scheduled_tasks.lock and .claude/commands/*.md files from the PR, or add .claude/ pattern to .gitignore instead of committing workspace tool artifacts.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: delivering iOS image paths via bracketed paste so Claude Code attaches images, which directly addresses the root cause of the bug.
Description check ✅ Passed The description fully addresses all template sections: detailed Problem/Root cause/Fix explanation, Testing section with dogfood verification, and Checklist items documented. The author explains the technical reasoning comprehensively.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New sendTextResult method is explicitly @MainActor, matching sendInputResult. Return type InputSendResult is a pure Equatable enum (Sendable). Called from @MainActor context. No isolation violations.
Cmux Swift Blocking Runtime ✅ Passed New sendTextResult() API and its usage in v2MobileTerminalPasteImage introduce no blocking/timing primitives: no semaphores, Task.sleep, main.sync, or manual locks in the new code path.
Cmux No Hacky Sleeps ✅ Passed PR's core changes are in Swift files (out-of-scope), which are explicitly excluded by the check that covers only TypeScript, JavaScript, shell, and non-Swift build/runtime scripts.
Cmux Algorithmic Complexity ✅ Passed PR contains no nested loops, full-collection scans, rescans, or unbounded algorithms; sendTextResult performs O(1) queue append and O(n) text encoding without iterating over scalable collections.
Cmux Swift Concurrency ✅ Passed New sendTextResult() uses @MainActor-isolated patterns and result enums for error handling, mirroring sendInputResult—no legacy Dispatch, Combine, completion handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed New sendTextResult function is synchronous and correctly marked @MainActor for UI-bound work, called synchronously from @MainActor context. No @concurrent annotation needed or misused.
Cmux Swift File And Package Boundaries ✅ Passed Minimal focused bug fix adding 39 lines total. sendTextResult is app-dependent Ghostty bridge code mirroring sendInputResult, allowed by focused-fix exception.
Cmux Swift Logging ✅ Passed No logging violations detected. The PR adds no print, debugPrint, dump, NSLog, or file-scoped Logger statements; existing DEBUG-guarded logging unchanged.
Cmux User-Facing Error Privacy ✅ Passed User-facing error messages use pre-existing localized strings that are generic and privacy-compliant with no sensitive information exposed.
Cmux Swiftui State Layout ✅ Passed PR adds sendTextResult method to existing TerminalSurface class with no new SwiftUI state patterns, GeometryReader, or render-time state mutations.
Cmux Architecture Rethink ✅ Passed New sendTextResult API is a clean, purpose-built addition that mirrors existing sendInputResult with no timing/blocking repairs, new state owners, or split lifecycle patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds input handling method (sendTextResult) with no NSWindow/NSPanel/NSWindowController/SwiftUI Window creation or modification; existing headlessStartupWindow is unmodified.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-image-paste-bracketed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes iOS image paste not being recognized by Claude Code by switching the delivery route for the image file path from ghostty_surface_text_input (typed text, no bracketed paste) to ghostty_surface_text (paste path, bracketed paste), which is the same route desktop clipboard pastes take and the one Claude Code's image-path detector expects.

  • GhosttyTerminalView.swift: Adds sendTextResult(_:), a paste-path counterpart to sendInputResult that uses ghostty_surface_text and returns the richer InputSendResult enum for precise error mapping at the call site.
  • TerminalController.swift: v2MobileTerminalPasteImage now calls sendTextResult instead of sendInputResult, routing the shell-escaped image path through the bracketed-paste path on both live and cold (queued) surfaces.

Confidence Score: 4/5

The one-line call-site change is safe and the new helper is a straightforward paste-path wrapper with correct error mapping.

The fix correctly identifies and addresses the root cause and handles both live and cold-surface paths. The sendTextResult body duplicates sendText entirely, which is the one issue worth tidying before the change drifts.

The duplicated logic between sendText and sendTextResult in GhosttyTerminalView.swift is worth a second look.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds sendTextResult, a paste-path twin of sendInputResult returning InputSendResult. Logic is correct but duplicates the existing sendText method body; sendText could delegate to it.
Sources/TerminalController.swift One-line change from sendInputResult to sendTextResult in v2MobileTerminalPasteImage. Call-site logic, error mapping, and queued payload are all preserved correctly.

Sequence Diagram

sequenceDiagram
    participant iOS as iOS App
    participant TC as TerminalController
    participant TS as TerminalSurface
    participant G as Ghostty C API

    iOS->>TC: terminal.paste_image (base64)
    TC->>TC: saveImageData
    TC->>TS: sendTextResult(escapedPath)
    alt surface live
        TS->>G: ghostty_surface_text()
        G-->>TS: bracketed paste delivered
        TS-->>TC: .sent
        TC->>TS: forceRefresh
    else surface cold
        TS->>TS: enqueuePendingSocketInput(.pasteText)
        TS-->>TC: .queued
    end
    TC-->>iOS: "ok {queued: Bool}"
Loading

Comments Outside Diff (1)

  1. Sources/GhosttyTerminalView.swift, line 7232-7252 (link)

    P2 sendTextResult is a near-exact copy of the existing sendText method — both route through writeTextData → ghostty_surface_text and enqueue via .pasteText. The only difference is the return type. Future changes to the paste-path logic (new guard conditions, queue behaviour, etc.) will need to be made in both places. Implementing sendText as a thin wrapper over sendTextResult keeps the two in sync automatically.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "iOS image paste: deliver path via bracke..." | Re-trigger Greptile

This branch was successfully deployed

1 active deployment
Preview – cmux — c626c226 Deployed Jun 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants