Support pasting clipboard images in terminal#562
Support pasting clipboard images in terminal#562josemasri wants to merge 4 commits intomanaflow-ai:mainfrom
Conversation
When the macOS clipboard contains only image data (e.g. from Cmd+Ctrl+Shift+4 screenshot) and no text, Cmd+V now saves the image as a temporary PNG file and pastes the file path into the terminal. This allows CLI tools like Claude Code to receive pasted images. The pasteboard heuristic only intercepts when there is image data (TIFF/PNG) and no text/HTML/RTF, so normal text paste is unaffected. Images over 10 MB are skipped and fall through to default behavior. Closes manaflow-ai#457 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c86b262b2
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c86b262b2
ℹ️ 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".
…ext validation - Add UUID suffix to temp filenames to prevent overwrites when pasting images multiple times in the same second - Only enable Paste menu (not Paste as Plain Text) for image-only clipboard, since pasteAsPlainText has no image-path handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds clipboard image paste support to the macOS Ghostty terminal view: when the clipboard contains only image data, the view converts it to PNG, writes a temporary file (size-limited to 10 MB), and pastes the temp file path into the terminal; text paste behavior is unchanged. Changes
Sequence DiagramsequenceDiagram
actor User
participant Clipboard as Clipboard
participant GhosttyView as Ghostty Terminal View
participant FileSystem as File System
participant Terminal as Terminal Surface
User->>GhosttyView: Paste (Cmd+V)
GhosttyView->>Clipboard: Query content
alt Clipboard contains only image data
Clipboard-->>GhosttyView: Image data (TIFF/PNG)
GhosttyView->>GhosttyView: Validate image size (<= 10 MB)
GhosttyView->>FileSystem: Convert to PNG & write temp file
FileSystem-->>GhosttyView: Temp file path
GhosttyView->>Terminal: Paste temp file path
else Clipboard has string/text
Clipboard-->>GhosttyView: String/text
GhosttyView->>Terminal: Paste text (existing flow)
end
Terminal-->>User: CLI receives pasted input
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)
2704-2717: Add lifecycle cleanup for generated clipboard PNG files.These temporary screenshots can accumulate and retain sensitive data. Consider pruning old
clipboard-*.pngfiles (e.g., on save or app startup) to bound disk usage and exposure window.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2704 - 2717, Add lifecycle cleanup for temporary clipboard PNGs created by the clipboard image writer (the code that returns the path inside GhosttyTerminalView's method that builds "clipboard-\(timestamp)-\(UUID().uuidString.prefix(8)).png"). Implement a small cleanup routine (e.g., pruneClipboardPNGs or cleanupClipboardTempFiles) and call it at a safe point such as app startup and/or when saving/terminating; the routine should scan NSTemporaryDirectory() for files matching the "clipboard-*.png" pattern, remove files older than a retention threshold (e.g., 24h) or beyond a max count, and log failures. Ensure the new function is referenced from the existing image write flow so files are bounded in lifetime.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2723-2728: Pasted image paths are sent raw; update the branch that
handles Self.saveClipboardImageIfNeeded() so the path is shell-escaped before
calling terminalSurface?.sendText(path). Locate the paste handling in
GhosttyTerminalView (the call to Self.saveClipboardImageIfNeeded() and
terminalSurface?.sendText) and apply the same shell-escaping routine used for
drag/drop paths (or add a small helper like shellEscapePath and call it) so any
shell-special characters are quoted/escaped before sending to the terminal.
---
Nitpick comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2704-2717: Add lifecycle cleanup for temporary clipboard PNGs
created by the clipboard image writer (the code that returns the path inside
GhosttyTerminalView's method that builds
"clipboard-\(timestamp)-\(UUID().uuidString.prefix(8)).png"). Implement a small
cleanup routine (e.g., pruneClipboardPNGs or cleanupClipboardTempFiles) and call
it at a safe point such as app startup and/or when saving/terminating; the
routine should scan NSTemporaryDirectory() for files matching the
"clipboard-*.png" pattern, remove files older than a retention threshold (e.g.,
24h) or beyond a max count, and log failures. Ensure the new function is
referenced from the existing image write flow so files are bounded in lifetime.
Use escapeDropForShell on the clipboard image temp path, consistent with how drag/drop paths are escaped, to avoid issues with shell-special characters in the path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)
2674-2692: Consider extracting the text-type check to reduce duplication.The text-type detection logic (lines 2681-2684) duplicates the check in
clipboardHasImageOnly()(lines 2668-2670). While acceptable for this feature size, extracting a shared helper likeprivate static func clipboardHasText() -> Boolwould keep the logic DRY and make future maintenance easier.♻️ Optional refactor to extract shared helper
+ private static func clipboardHasText() -> Bool { + let pb = NSPasteboard.general + let types = pb.types ?? [] + return types.contains(.string) || types.contains(.html) + || types.contains(.rtf) || types.contains(.rtfd) + } + /// Quick check: does the clipboard have image data and no text? private static func clipboardHasImageOnly() -> Bool { let pb = NSPasteboard.general let types = pb.types ?? [] - let hasText = types.contains(.string) || types.contains(.html) - || types.contains(.rtf) || types.contains(.rtfd) - if hasText { return false } + if clipboardHasText() { return false } return types.contains(.tiff) || types.contains(.png) } /// When the clipboard contains only image data (no text/HTML), saves it as /// a temporary PNG file and returns the file path. Returns nil if the /// clipboard contains text or no image. private static func saveClipboardImageIfNeeded() -> String? { - let pb = NSPasteboard.general - let types = pb.types ?? [] - - // If pasteboard has text/HTML, this is a normal copy — let Ghostty handle it. - let hasText = types.contains(.string) || types.contains(.html) - || types.contains(.rtf) || types.contains(.rtfd) - if hasText { return nil } + if clipboardHasText() { return nil } + let pb = NSPasteboard.general + let types = pb.types ?? [] // Check for image types (TIFF from screenshots, PNG from some tools). guard types.contains(.tiff) || types.contains(.png) else { return nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2674 - 2692, Extract the duplicated text-type detection into a single helper: add a private static func clipboardHasText() -> Bool that retrieves NSPasteboard.general.types and returns true if it contains .string, .html, .rtf, or .rtfd; then replace the inline checks in saveClipboardImageIfNeeded() and clipboardHasImageOnly() to call clipboardHasText() (negating where needed) so both functions share the same logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2674-2692: Extract the duplicated text-type detection into a
single helper: add a private static func clipboardHasText() -> Bool that
retrieves NSPasteboard.general.types and returns true if it contains .string,
.html, .rtf, or .rtfd; then replace the inline checks in
saveClipboardImageIfNeeded() and clipboardHasImageOnly() to call
clipboardHasText() (negating where needed) so both functions share the same
logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)
2677-2687: Consider extracting shared text-type detection logic.The
hasTextcheck on lines 2682-2684 duplicates the logic inclipboardHasImageOnly()(lines 2668-2670). While the duplication is minor and clipboard reads are cheap, extracting a shared helper would improve maintainability.♻️ Optional: Extract shared helper
+ private static func pasteboardHasTextContent(_ pb: NSPasteboard) -> Bool { + let types = pb.types ?? [] + return types.contains(.string) || types.contains(.html) + || types.contains(.rtf) || types.contains(.rtfd) + } + /// Quick check: does the clipboard have image data and no text? private static func clipboardHasImageOnly() -> Bool { let pb = NSPasteboard.general let types = pb.types ?? [] - let hasText = types.contains(.string) || types.contains(.html) - || types.contains(.rtf) || types.contains(.rtfd) - if hasText { return false } + if pasteboardHasTextContent(pb) { return false } return types.contains(.tiff) || types.contains(.png) } private static func saveClipboardImageIfNeeded() -> String? { let pb = NSPasteboard.general - let types = pb.types ?? [] - - // If pasteboard has text/HTML, this is a normal copy — let Ghostty handle it. - let hasText = types.contains(.string) || types.contains(.html) - || types.contains(.rtf) || types.contains(.rtfd) - if hasText { return nil } + // If pasteboard has text/HTML, this is a normal copy — let Ghostty handle it. + if pasteboardHasTextContent(pb) { return nil } + let types = pb.types ?? [] // Check for image types (TIFF from screenshots, PNG from some tools). guard types.contains(.tiff) || types.contains(.png) else { return nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2677 - 2687, The text-only/type detection logic duplicated between saveClipboardImageIfNeeded() and clipboardHasImageOnly() should be extracted into a small shared helper (e.g., clipboardHasTextTypes() or pasteboardHasText()) and both functions should call that helper; update saveClipboardImageIfNeeded() to call the new helper instead of repeating the types.contains(.string/.html/.rtf/.rtfd) checks, and adjust clipboardHasImageOnly() to use the same helper so the logic is centralized and maintainable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2677-2687: The text-only/type detection logic duplicated between
saveClipboardImageIfNeeded() and clipboardHasImageOnly() should be extracted
into a small shared helper (e.g., clipboardHasTextTypes() or
pasteboardHasText()) and both functions should call that helper; update
saveClipboardImageIfNeeded() to call the new helper instead of repeating the
types.contains(.string/.html/.rtf/.rtfd) checks, and adjust
clipboardHasImageOnly() to use the same helper so the logic is centralized and
maintainable.
|
Hm I'm not sure if we should handle this since ctrl+v should already handle pasting image-data only cases. |
Summary
Cmd+Ctrl+Shift+4screenshot),Cmd+Vin the terminal now saves the image as a temporary PNG and pastes the file pathTest plan
Cmd+Ctrl+Shift+4, select area)Cmd+V/var/folders/.../clipboard-2026-02-26-120000.png) is pastedCmd+C), paste (Cmd+V) — should paste text as beforeCloses #457
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Debug