Repository navigation
iOS: paste images into the terminal from the phone clipboard - #5546
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds end-to-end clipboard-image paste support: new Paste accessory reads clipboard images, the app sends a base64 RPC ( ChangesTerminal Image Paste Support
Sequence Diagram(s)sequenceDiagram
participant User
participant TerminalInputTextView as TerminalInputTextView
participant UIPasteboard
participant GhosttySurfaceView
participant Coordinator
participant MobileShellComposite
participant RPC as RPC Client
participant TerminalController
participant GhosttyPasteboardHelper
participant TerminalSurface
User->>TerminalInputTextView: tap paste accessory
TerminalInputTextView->>UIPasteboard: read clipboard
alt Image available
TerminalInputTextView->>GhosttySurfaceView: onPasteImage(data, format)
GhosttySurfaceView->>Coordinator: ghosttySurfaceView(_:didPasteImage:format:)
Coordinator->>MobileShellComposite: submitTerminalPasteImage(data, format)
MobileShellComposite->>RPC: terminal.paste_image (base64-encoded)
RPC->>TerminalController: v2MobileTerminalPasteImage request
TerminalController->>GhosttyPasteboardHelper: saveImageData(decoded bytes, format)
GhosttyPasteboardHelper->>GhosttyPasteboardHelper: write temp file
TerminalController->>TerminalSurface: sendInputResult(escaped path)
else Text only
TerminalInputTextView->>GhosttySurfaceView: onText(string)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (4 errors, 1 warning)
✅ Passed checks (14 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
| "workspace_id": resolved.workspace.id.uuidString, | ||
| "surface_id": terminalPanel.id.uuidString, | ||
| "queued": sendResult == .queued, | ||
| ]) |
There was a problem hiding this comment.
Missing terminal sequence in response
Medium Severity
The terminal.paste_image success payload omits terminal_seq, while the iOS client still runs handleTerminalInputResponse for that RPC. Without terminal_seq, the render-grid sync path never compares remote vs local byte sequences after an image paste, so the phone can stay behind when the Mac terminal updates heavily.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5f6e1ae. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f6e1ae78c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let maxImageBytes = 8 * 1024 * 1024 | ||
| if let png = image.pngData(), png.count <= maxImageBytes { | ||
| onPasteImage?(png, "png") |
There was a problem hiding this comment.
Cap pasted images before base64 expansion
When a clipboard image is around 6–8 MiB, this branch forwards the PNG because it is below maxImageBytes, but the request then embeds it as base64 inside JSON before MobileCoreRPCSession calls MobileSyncFrameCodec.encodeFrame, whose payload limit is 8 MiB. Base64 expands the bytes by roughly 4/3 (plus auth/JSON overhead), so these images deterministically throw frameTooLarge on the phone and never reach the Mac despite being under this check; the cap needs to account for encoded frame size or recompress further before sending.
Useful? React with 👍 / 👎.
| return .ok([ | ||
| "workspace_id": resolved.workspace.id.uuidString, | ||
| "surface_id": terminalPanel.id.uuidString, | ||
| "queued": sendResult == .queued, | ||
| ]) |
There was a problem hiding this comment.
Return terminal_seq for paste-image input
sendRemoteTerminalPasteImage feeds this response into handleTerminalInputResponse, but that path only does its catch-up/replay work when the payload contains terminal_seq; unlike v2MobileTerminalInput, this response omits it. For render-grid mobile clients that are behind after the injected path is sent, the paste-image RPC will not trigger the same resync safety net as normal terminal input, so the phone can keep showing stale terminal contents until another event or manual replay occurs.
Useful? React with 👍 / 👎.
Greptile SummaryAdds iOS clipboard image paste to the terminal: a pinned Paste button reads
Confidence Score: 4/5Functional but the paste-image path skips the accelerated terminal resync that text input uses, making the iOS display lag after paste. The terminal.paste_image response omits terminal_seq, so handleTerminalInputResponse hits its early-return guard and the post-input resync that normally keeps the iOS terminal display current after injecting input never fires. Terminal output from the TUI will arrive via background subscription polling rather than the immediate resync, producing noticeable display lag after every image paste. Sources/TerminalController.swift — v2MobileTerminalPasteImage response should include terminal_seq via MobileTerminalByteTee, matching the v2MobileTerminalInput pattern. Important Files Changed
Sequence DiagramsequenceDiagram
participant PB as UIPasteboard
participant TIV as TerminalInputTextView
participant GSV as GhosttySurfaceView
participant GSR as GhosttySurfaceRepresentable
participant MSC as MobileShellComposite
participant RPC as terminal.paste_image RPC
participant TC as TerminalController (Mac)
participant GPH as GhosttyPasteboardHelper
participant PTY as Terminal PTY
TIV->>PB: hasImages / image
PB-->>TIV: "UIImage (<=8MB PNG or JPEG)"
TIV->>GSV: onPasteImage(data, format)
GSV->>GSR: delegate.didPasteImage(data, format)
GSR->>MSC: submitTerminalPasteImage(data, format)
MSC->>RPC: "terminal.paste_image {workspace_id, surface_id, image_base64, image_format}"
RPC->>TC: v2MobileTerminalPasteImage(params)
TC->>GPH: saveImageData(imageData, fileExtension)
GPH-->>TC: "shell-escaped /tmp/clipboard-*.png path"
TC->>PTY: sendInputResult(escapedPath)
PTY-->>TC: .sent / .queued
TC-->>RPC: "ok {workspace_id, surface_id, queued}"
RPC-->>MSC: responseData
MSC->>MSC: handleTerminalInputResponse early-returns: terminal_seq absent
Reviews (3): Last reviewed commit: "iOS: paste images into the terminal from..." | Re-trigger Greptile |
| if let jpeg = image.jpegData(compressionQuality: 0.8) { | ||
| onPasteImage?(jpeg, "jpg") | ||
| return | ||
| } | ||
| if let png = image.pngData() { | ||
| onPasteImage?(png, "png") | ||
| return | ||
| } |
There was a problem hiding this comment.
Silent paste failure for large images
The JPEG fallback (lines 550–553) sends the JPEG to the Mac without checking if it fits within the Mac's 10 MB cap, and the last-resort PNG path (lines 554–557) has no size check at all. When a modern iPhone camera photo produces a JPEG still above 10 MB, the Mac's saveImageData returns nil and sends back an invalid_params error. handleTerminalInputResponse silently discards that response (the guard on terminalSeq returns early), leaving the user with no indication that the paste failed. The code comment says JPEG keeps the payload "under the Mac's 10 MB cap" but the code doesn't enforce this.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`:
- Around line 542-557: handlePasteAction forwards image bytes without
consistently enforcing the 10 MB cap; change maxImageBytes to 10 * 1024 * 1024
and ensure every branch checks data.count <= maxImageBytes before calling
onPasteImage. Specifically, inside handlePasteAction compute image.pngData()
once (reuse variable) and compute jpegData as needed, then for each non-nil data
(png or jpeg) verify data.count <= maxImageBytes and only then call
onPasteImage(data, "png"/"jpg"); if it exceeds the cap, handle (e.g., no-op or
trigger an error path) instead of forwarding oversized bytes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0c92ac7f-d8db-4a7b-afcb-6d7669f67bd1
📒 Files selected for processing (9)
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swiftPackages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swiftSources/GhosttyTerminalView.swiftSources/Mobile/MobileHostService.swiftSources/TerminalController.swiftios/cmux/Resources/Localizable.xcstrings
| private func handlePasteAction() { | ||
| let pasteboard = UIPasteboard.general | ||
| if pasteboard.hasImages, let image = pasteboard.image { | ||
| let maxImageBytes = 8 * 1024 * 1024 | ||
| if let png = image.pngData(), png.count <= maxImageBytes { | ||
| onPasteImage?(png, "png") | ||
| return | ||
| } | ||
| if let jpeg = image.jpegData(compressionQuality: 0.8) { | ||
| onPasteImage?(jpeg, "jpg") | ||
| return | ||
| } | ||
| if let png = image.pngData() { | ||
| onPasteImage?(png, "png") | ||
| return | ||
| } |
There was a problem hiding this comment.
Enforce the 10 MB upload cap before forwarding pasted images.
handlePasteAction() currently forwards JPEG/PNG bytes without a hard 10 MB check (Line 550 and Line 554 paths), so oversized images can still be sent and fail later server-side instead of being handled consistently at the source.
Suggested patch
private func handlePasteAction() {
let pasteboard = UIPasteboard.general
if pasteboard.hasImages, let image = pasteboard.image {
- let maxImageBytes = 8 * 1024 * 1024
+ let jpegSwitchThreshold = 8 * 1024 * 1024
+ let maxUploadBytes = 10 * 1024 * 1024
if let png = image.pngData(), png.count <= maxImageBytes {
- onPasteImage?(png, "png")
+ onPasteImage?(png, "png")
return
}
- if let jpeg = image.jpegData(compressionQuality: 0.8) {
+ if let jpeg = image.jpegData(compressionQuality: 0.8), jpeg.count <= maxUploadBytes {
onPasteImage?(jpeg, "jpg")
return
}
- if let png = image.pngData() {
+ if let png = image.pngData(), png.count <= maxUploadBytes {
onPasteImage?(png, "png")
return
}
}
if pasteboard.hasStrings, let string = pasteboard.string, !string.isEmpty {
onText?(string)
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private func handlePasteAction() { | |
| let pasteboard = UIPasteboard.general | |
| if pasteboard.hasImages, let image = pasteboard.image { | |
| let maxImageBytes = 8 * 1024 * 1024 | |
| if let png = image.pngData(), png.count <= maxImageBytes { | |
| onPasteImage?(png, "png") | |
| return | |
| } | |
| if let jpeg = image.jpegData(compressionQuality: 0.8) { | |
| onPasteImage?(jpeg, "jpg") | |
| return | |
| } | |
| if let png = image.pngData() { | |
| onPasteImage?(png, "png") | |
| return | |
| } | |
| private func handlePasteAction() { | |
| let pasteboard = UIPasteboard.general | |
| if pasteboard.hasImages, let image = pasteboard.image { | |
| let maxUploadBytes = 10 * 1024 * 1024 | |
| if let png = image.pngData(), png.count <= maxUploadBytes { | |
| onPasteImage?(png, "png") | |
| return | |
| } | |
| if let jpeg = image.jpegData(compressionQuality: 0.8), jpeg.count <= maxUploadBytes { | |
| onPasteImage?(jpeg, "jpg") | |
| return | |
| } | |
| if let png = image.pngData(), png.count <= maxUploadBytes { | |
| onPasteImage?(png, "png") | |
| return | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`
around lines 542 - 557, handlePasteAction forwards image bytes without
consistently enforcing the 10 MB cap; change maxImageBytes to 10 * 1024 * 1024
and ensure every branch checks data.count <= maxImageBytes before calling
onPasteImage. Specifically, inside handlePasteAction compute image.pngData()
once (reuse variable) and compute jpegData as needed, then for each non-nil data
(png or jpeg) verify data.count <= maxImageBytes and only then call
onPasteImage(data, "png"/"jpg"); if it exceeds the cap, handle (e.g., no-op or
trigger an error path) instead of forwarding oversized bytes.
| private func handlePasteAction() { | ||
| let pasteboard = UIPasteboard.general | ||
| if pasteboard.hasImages, let image = pasteboard.image { | ||
| let maxImageBytes = 8 * 1024 * 1024 | ||
| if let png = image.pngData(), png.count <= maxImageBytes { | ||
| onPasteImage?(png, "png") | ||
| return | ||
| } | ||
| if let jpeg = image.jpegData(compressionQuality: 0.8) { | ||
| onPasteImage?(jpeg, "jpg") | ||
| return | ||
| } | ||
| if let png = image.pngData() { | ||
| onPasteImage?(png, "png") | ||
| return | ||
| } | ||
| } | ||
| if pasteboard.hasStrings, let string = pasteboard.string, !string.isEmpty { | ||
| onText?(string) | ||
| } |
There was a problem hiding this comment.
No user feedback when image paste is silently dropped
handlePasteAction calls onPasteImage? after reading the clipboard (and triggering the iOS one-shot consent banner), but the entire callback chain — GhosttySurfaceRepresentable → store?.submitTerminalPasteImage — silently returns without any user-visible signal when the Mac remote is unavailable: guard remoteClient != nil else { return }. The same silent drop happens when selectedWorkspace or selectedTerminalID is nil. A user who taps Paste, grants the consent dialog, and sees nothing will have no way to know the paste was discarded. An error toast, HUD, or at minimum a logged message via a user-visible path should be shown in every early-return branch of submitTerminalPasteImage.
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!
Adds a Paste button to the mobile terminal accessory bar. An image on the system clipboard is forwarded to the paired Mac over a new terminal.paste_image RPC (base64 bytes + format hint); the Mac writes it to a temp file (reusing the existing owned-temp-image machinery) and injects the shell-escaped path as terminal input, exactly the way a local clipboard-image paste does, so a running TUI like Claude Code attaches the image. Clipboard text rides the normal input path. iOS: .paste accessory action (pinned), TerminalInputTextView.handlePasteAction reads UIPasteboard (PNG, JPEG fallback over 8MB), onPasteImage callback chained through GhosttySurfaceView -> delegate -> MobileShellComposite.submitTerminalPasteImage. Mac: v2MobileTerminalPasteImage handler + dispatch; GhosttyPasteboardHelper. saveImageData(_:fileExtension:); terminal.paste_image added to both the client token-selection and server ticket-authorization terminal groups so it is scoped exactly like terminal.input. Localized Paste label (en + ja). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5f6e1ae to
3776970
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3776970. Configure here.
| // writes a temp file and injects its path into the terminal so the | ||
| // running TUI (e.g. Claude Code) attaches it. | ||
| Task { @MainActor [weak store] in | ||
| await store?.submitTerminalPasteImage(data, format: format) |
There was a problem hiding this comment.
Paste image ignores surface ID
High Severity
Image paste is sent using selectedWorkspace and selectedTerminalID, while keyboard and text paste from the same surface use surfaceID to resolve the workspace and terminal. After switching workspaces without updating the stored terminal id, the visible surface and RPC target can diverge, so image paste may fail or target the wrong terminal while text paste still works.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3776970. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 377697021f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return .err(code: "invalid_params", message: "Image payload was empty or exceeded the size limit", data: nil) | ||
| } | ||
|
|
||
| let sendResult = terminalPanel.surface.sendInputResult(escapedPath) |
There was a problem hiding this comment.
Send image paths through the paste path
When a mobile image paste reaches a terminal that has bracketed-paste or paste-specific handling enabled (the common path for TUIs such as Claude Code to recognize pasted file paths), this writes the generated path with sendInputResult, which routes text through the normal typed-input path (ghostty_surface_text_input) instead of the paste/clipboard path used by local image paste (sendText/clipboard completion in GhosttyTerminalView.swift). In that context the app receives ordinary keystrokes rather than a paste event, so the image path may not be handled as an attachment even though local clipboard-image paste works.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift (1)
614-641:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEnforce a hard 10 MB cap before emitting pasted image bytes.
Line 624 is an 8 MB format-switch threshold, but Lines 629-635 still forward JPEG/PNG without a hard upper bound. That can push oversized payloads into
terminal.paste_imageand fail later instead of being rejected at source.Suggested fix
private func handlePasteAction() { let pasteboard = UIPasteboard.general if pasteboard.hasImages, let image = pasteboard.image { - let maxImageBytes = 8 * 1024 * 1024 - if let png = image.pngData(), png.count <= maxImageBytes { + let jpegSwitchThreshold = 8 * 1024 * 1024 + let maxUploadBytes = 10 * 1024 * 1024 + let png = image.pngData() + + if let png, png.count <= jpegSwitchThreshold, png.count <= maxUploadBytes { onPasteImage?(png, "png") return } - if let jpeg = image.jpegData(compressionQuality: 0.8) { + + if let jpeg = image.jpegData(compressionQuality: 0.8), jpeg.count <= maxUploadBytes { onPasteImage?(jpeg, "jpg") return } - if let png = image.pngData() { + + if let png, png.count <= maxUploadBytes { onPasteImage?(png, "png") return } + return } if pasteboard.hasStrings, let string = pasteboard.string, !string.isEmpty { onText?(string) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift` around lines 614 - 641, The paste handling currently uses an 8 MB threshold only to prefer PNG vs JPEG but then may emit image bytes larger than 10 MB; update handlePasteAction to enforce a hard 10 MB cap (e.g. let hardMaxBytes = 10 * 1024 * 1024) before calling onPasteImage, checking any pngData() or jpegData(...) length and rejecting (or not calling onPasteImage) if the final bytes exceed the cap; for JPEG, attempt incremental recompression (reducing compressionQuality) until under the hard cap before emitting, otherwise fall back to rejecting the paste. Ensure all branches in handlePasteAction reference this hardMaxBytes check before invoking onPasteImage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1652-1665: submitTerminalPasteImage currently reads
selectedWorkspace and selectedTerminalID at send time which allows selection
drift to route the paste to the wrong terminal; instead capture the intended
target when the paste is initiated and pass it through to the async send. Change
submitTerminalPasteImage to accept (or be called with) the originating
identifiers (workspaceID and terminalID or a single originatingSurface token) so
it no longer reads selectedWorkspace/selectedTerminalID inside the async body,
and update the delegate caller to pass the originating surface/ids into
submitTerminalPasteImage; ensure sendRemoteTerminalPasteImage is invoked with
those captured ids instead of global selection.
In `@Sources/TerminalController.swift`:
- Around line 21348-21350: The code decodes base64 into Data immediately
(Data(base64Encoded:)) which can spike memory for huge payloads; instead first
bind the base64 string via v2RawString, compute an estimated decoded byte length
from base64 (e.g. floor(base64.count * 3 / 4) minus padding) and compare it to
the image size cap used by saveImageData, returning the same "invalid_params"
error if the estimate exceeds the cap, and only then call Data(base64Encoded:)
and proceed to saveImageData; apply the same change to the other occurrence that
uses v2RawString + Data(base64Encoded:) around the saveImageData call.
---
Duplicate comments:
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift`:
- Around line 614-641: The paste handling currently uses an 8 MB threshold only
to prefer PNG vs JPEG but then may emit image bytes larger than 10 MB; update
handlePasteAction to enforce a hard 10 MB cap (e.g. let hardMaxBytes = 10 * 1024
* 1024) before calling onPasteImage, checking any pngData() or jpegData(...)
length and rejecting (or not calling onPasteImage) if the final bytes exceed the
cap; for JPEG, attempt incremental recompression (reducing compressionQuality)
until under the hard cap before emitting, otherwise fall back to rejecting the
paste. Ensure all branches in handlePasteAction reference this hardMaxBytes
check before invoking onPasteImage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a7a6adf1-1020-4e10-9bee-cdc4c6aa50fb
📒 Files selected for processing (9)
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swiftPackages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swiftSources/GhosttyTerminalView.swiftSources/Mobile/MobileHostService.swiftSources/TerminalController.swiftios/cmux/Resources/Localizable.xcstrings
| public func submitTerminalPasteImage(_ data: Data, format: String) async { | ||
| guard !data.isEmpty else { return } | ||
| guard let workspaceID = selectedWorkspace?.id, | ||
| let terminalID = selectedTerminalID else { | ||
| return | ||
| } | ||
| guard remoteClient != nil else { return } | ||
| await sendRemoteTerminalPasteImage( | ||
| data, | ||
| format: format, | ||
| workspaceID: workspaceID, | ||
| terminalID: terminalID | ||
| ) | ||
| } |
There was a problem hiding this comment.
Paste-image RPC can target the wrong terminal after selection drift.
This method derives the destination from global selection at send time. If the user switches terminals between paste capture and async dispatch, the image is injected into a different session than the one that triggered paste.
Suggested fix
- public func submitTerminalPasteImage(_ data: Data, format: String) async {
+ public func submitTerminalPasteImage(
+ _ data: Data,
+ format: String,
+ surfaceID: String
+ ) async {
guard !data.isEmpty else { return }
- guard let workspaceID = selectedWorkspace?.id,
- let terminalID = selectedTerminalID else {
+ guard let workspaceID = workspaceID(forTerminalID: surfaceID) else {
return
}
guard remoteClient != nil else { return }
await sendRemoteTerminalPasteImage(
data,
format: format,
workspaceID: workspaceID,
- terminalID: terminalID
+ terminalID: .init(rawValue: surfaceID)
)
}And call it from the delegate with the originating surface:
- await store?.submitTerminalPasteImage(data, format: format)
+ await store?.submitTerminalPasteImage(data, format: format, surfaceID: self.surfaceID)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 1652 - 1665, submitTerminalPasteImage currently reads
selectedWorkspace and selectedTerminalID at send time which allows selection
drift to route the paste to the wrong terminal; instead capture the intended
target when the paste is initiated and pass it through to the async send. Change
submitTerminalPasteImage to accept (or be called with) the originating
identifiers (workspaceID and terminalID or a single originatingSurface token) so
it no longer reads selectedWorkspace/selectedTerminalID inside the async body,
and update the delegate caller to pass the originating surface/ids into
submitTerminalPasteImage; ensure sendRemoteTerminalPasteImage is invoked with
those captured ids instead of global selection.
| guard let base64 = v2RawString(params, "image_base64"), | ||
| let imageData = Data(base64Encoded: base64), !imageData.isEmpty else { | ||
| return .err(code: "invalid_params", message: "Missing or invalid image_base64", data: nil) |
There was a problem hiding this comment.
Bound image_base64 before decoding to avoid avoidable memory spikes.
Data(base64Encoded:) allocates decoded bytes immediately. A very large payload can pressure memory before saveImageData enforces the size cap.
Suggested fix
private func v2MobileTerminalPasteImage(params: [String: Any]) -> V2CallResult {
guard let base64 = v2RawString(params, "image_base64"),
- let imageData = Data(base64Encoded: base64), !imageData.isEmpty else {
+ !base64.isEmpty else {
return .err(code: "invalid_params", message: "Missing or invalid image_base64", data: nil)
}
+ let maxBytes = 10 * 1024 * 1024
+ // Base64 expands by ~4/3; reject obviously oversized payloads before decode.
+ let estimatedDecodedBytes = (base64.utf8.count * 3) / 4
+ guard estimatedDecodedBytes <= maxBytes,
+ let imageData = Data(base64Encoded: base64),
+ !imageData.isEmpty,
+ imageData.count <= maxBytes else {
+ return .err(code: "invalid_params", message: "Image payload was empty or exceeded the size limit", data: nil)
+ }
let format = v2RawString(params, "image_format") ?? "png"Also applies to: 21367-21369
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/TerminalController.swift` around lines 21348 - 21350, The code
decodes base64 into Data immediately (Data(base64Encoded:)) which can spike
memory for huge payloads; instead first bind the base64 string via v2RawString,
compute an estimated decoded byte length from base64 (e.g. floor(base64.count *
3 / 4) minus padding) and compare it to the image size cap used by
saveImageData, returning the same "invalid_params" error if the estimate exceeds
the cap, and only then call Data(base64Encoded:) and proceed to saveImageData;
apply the same change to the other occurrence that uses v2RawString +
Data(base64Encoded:) around the saveImageData call.


Adds a Paste button to the iOS terminal accessory bar so you can paste an image (or text) from the phone clipboard straight into the terminal. Requested alongside the composer work; this is the image-paste slice.
Behavior
terminal.paste_imageRPC (base64 bytes + a format hint; PNG, JPEG fallback above 8 MB). The Mac writes it to a temp file (reusing the existing owned-temp-image machinery) and injects the shell-escaped path as terminal input, exactly the way a local clipboard-image paste does, so a running TUI like Claude Code attaches it.Touch points
GhosttyPasteboardHelper.saveImageData(_:fileExtension:);v2MobileTerminalPasteImagehandler + dispatch case inTerminalController.mobileHostHandleRPC;terminal.paste_imageadded to the client token-selection group (MobileCoreRPCClient) and the server ticket-authorization group (MobileHostService) so it is scoped exactly liketerminal.input.executionPolicyalready defaults it to the main actor..pasteaccessory action;TerminalInputTextView.handlePasteActionreadsUIPasteboard;onPasteImagecallback chainsGhosttySurfaceView→ delegate →MobileShellComposite.submitTerminalPasteImage.Notes / coordination
main; expects to land first per the "image paste first" call, with iOS: toggleable iMessage-style terminal composer (text, PR1) #5511 rebasing.Test
Needs a real device + paired Mac (mobile-host RPC round trip), so verified by dogfood: copy an image on the phone, tap Paste in a Claude Code terminal, confirm
[Image #N]attaches. A unit test of the byte→tempfile→path Mac helper is a reasonable follow-up; the cross-process RPC path is exercised end-to-end by the dogfood.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
New RPC carries large base64 payloads and writes temp files before injecting paths into the PTY; auth is aligned with terminal.input but size limits and extension sanitization are the main guardrails.
Overview
Adds Paste on the iOS terminal accessory bar so clipboard content on the phone can reach the Mac-hosted terminal. Images are sent over a new
terminal.paste_imageRPC as base64 plus a format hint; the Mac writes a temp file and injects a shell-escaped path as input (same behavior as a local clipboard-image paste for TUIs like Claude Code). Text still uses the existing terminal input path—no new RPC.On iOS,
handlePasteActionreadsUIPasteboard(images preferred over text, JPEG fallback when PNG exceeds ~8 MB).submitTerminalPasteImagein the shell composite issues the RPC with the same attach-ticket scoping asterminal.input. On Mac,GhosttyPasteboardHelper.saveImageData,v2MobileTerminalPasteImage, and auth grouping inMobileCoreRPCClient/MobileHostServicewire the handler. Localized Paste strings (en/ja) are included; unrelated TestFlight strings were removed fromLocalizable.xcstringsin the same diff.Reviewed by Cursor Bugbot for commit 3776970. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Add a Paste button to the iOS terminal accessory bar so you can paste images or text from the phone clipboard into the Mac-hosted terminal. Images go over a new
terminal.paste_imageRPC and appear as file paths so TUIs (e.g., Claude Code) can attach them..pasteaccessory button readsUIPasteboard; images prefer PNG, fall back to JPEG over 8 MB; text uses the normal input path.onPasteImageflowsTerminalInputTextView→GhosttySurfaceView→ delegate →MobileShellComposite.submitTerminalPasteImage.terminal.paste_imageadded toMobileCoreRPCClientandMobileHostServiceauth groups (scoped liketerminal.input);TerminalController.v2MobileTerminalPasteImagedecodes base64, usesGhosttyPasteboardHelper.saveImageData(_:fileExtension:)to create a temp file, then injects the shell-escaped path as input.doc.on.clipboardicon.Written for commit 3776970. Summary will update on new commits.
Summary by CodeRabbit