Skip to content

Support pasting clipboard images in terminal#562

Open
josemasri wants to merge 4 commits intomanaflow-ai:mainfrom
josemasri:feat/clipboard-image-paste
Open

Support pasting clipboard images in terminal#562
josemasri wants to merge 4 commits intomanaflow-ai:mainfrom
josemasri:feat/clipboard-image-paste

Conversation

@josemasri
Copy link

@josemasri josemasri commented Feb 26, 2026

Summary

  • When the clipboard contains only image data (e.g. from Cmd+Ctrl+Shift+4 screenshot), Cmd+V in the terminal now saves the image as a temporary PNG and pastes the file path
  • Enables CLI tools like Claude Code to receive pasted screenshots
  • Normal text paste is unaffected — only intercepts when clipboard has image data and no text/HTML/RTF

Test plan

  • Take a screenshot to clipboard (Cmd+Ctrl+Shift+4, select area)
  • In a terminal pane running Claude Code, press Cmd+V
  • Verify the file path (e.g. /var/folders/.../clipboard-2026-02-26-120000.png) is pasted
  • Copy text normally (Cmd+C), paste (Cmd+V) — should paste text as before
  • Copy from a web page, paste in terminal — should paste text as before

Closes #457

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Clipboard image paste on macOS: paste images from the clipboard as temporary PNG files (10 MB size limit) and insert the image file path into the terminal for command-line use.
  • Improvements

    • Maintains existing text paste behavior and access checks; falls back to prior paste when no image is available.
  • Debug

    • Adds debug logging for image paste operations.
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>
@vercel
Copy link

vercel bot commented Feb 26, 2026

Someone is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@lawrencecchen
Copy link
Contributor

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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".

@lawrencecchen
Copy link
Contributor

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 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>
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Clipboard image paste
Sources/GhosttyTerminalView.swift
Add maxClipboardImageSize (10 MB), clipboardHasImageOnly() detection, and saveClipboardImageIfNeeded() to convert/save clipboard images as temporary PNGs. Extend paste(_:) to handle image-only clipboard content and fall back to existing text paste. Add DEBUG logging paths for image paste operations.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes and paste with glee,
TIFF becomes PNG, a temp-file for thee.
Cmd+V hops, the terminal sings,
Screenshots tumble on rabbit wings. 📸

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support pasting clipboard images in terminal' directly and clearly summarizes the main change: adding clipboard image paste support to the Ghostty terminal view.
Linked Issues check ✅ Passed The PR implements all key requirements from #457: detect image-only clipboard content, convert to PNG, save temporarily, and paste the file path so CLI tools can access it.
Out of Scope Changes check ✅ Passed All changes are directly related to the clipboard image paste feature. No unrelated modifications to other systems or out-of-scope additions were introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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-*.png files (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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0bf4ef and f18de56.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift
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>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 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 like private static func clipboardHasText() -> Bool would 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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18de56 and a65c341.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)

2677-2687: Consider extracting shared text-type detection logic.

The hasText check on lines 2682-2684 duplicates the logic in clipboardHasImageOnly() (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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a65c341 and ad32f0f.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift
@lawrencecchen
Copy link
Contributor

Hm I'm not sure if we should handle this since ctrl+v should already handle pasting image-data only cases.

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

Labels

None yet

2 participants