Skip to content

fix(clipboard): paste Finder-copied screenshot files as images on macOS - #23635

Open
OrcaWin wants to merge 2 commits into
mainfrom
fix/scan29-mac-clipboard-copied-screenshot-file-paste
Open

OrcaWin wants to merge 2 commits into
mainfrom
fix/scan29-mac-clipboard-copied-screenshot-file-paste

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​348 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​348
Prod 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​320 $\color{#cf222e}{\Huge{\mathbf{−}}}$​167 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​153

Description

On macOS, pasting a copied screenshot into an Orca terminal (or the native chat composer) could paste the file's name, or nothing useful, instead of the image. A user reported "I can't paste a screenshot from my clipboard."

There are two causes:

  1. The terminal paste looks for text before it looks for an image. pasteTerminalClipboard pastes any text it finds and only asks for an image when the clipboard has no text. When you copy a screenshot file in Finder (Cmd+C), the clipboard holds the file's URL and its name as plain text, so the name wins. Screenshot tools that put both a PNG and a path string on the clipboard hit the same rule.
  2. There is no macOS way to turn a copied file into an image. The main-process handler clipboard:saveImageAsTempFile already converts a copied image file into a PNG, but only on Windows (readWindowsClipboardImageFileAsPng). On macOS, Electron's clipboard.readImage() for a Finder-copied file returns the 1024x1024 generic file icon, not the screenshot.

What Changed

  • New main-process handler clipboard:saveCopiedImageFileAsTempFile (macOS only; returns null on Windows, on Linux, and in the dashboard popout window). It reads public.file-url first and returns early if the clipboard has no file:// URL, so a normal text paste costs one small extra read. If there is a URL, it accepts it only when all of the following are true:

    • exactly one file is copied (checked with NSFilenamesPboardType);
    • the file is a .png, .jpg or .jpeg at an absolute path that exists;
    • any clipboard text is the full path, the file name, or the file name without its extension (Finder hides screenshot extensions). Both sides are converted to the composed Unicode form (NFC) before comparing, because macOS file URLs decode to the decomposed form (NFD). Without this, names like "Capture d'écran" or Korean screenshot names would never match.

    The file is then loaded with the existing size, dimension and pixel-bomb checks and saved with the existing target-aware saver, so SSH and remote-runtime pastes upload it the same way image pastes already do.

  • Shared file reader. The file-to-PNG reader and its safety limits moved unchanged from clipboard-windows-image-file.ts into clipboard-image-file-read.ts. The Windows path and the new macOS path both use it.

  • Terminal paste (terminal-clipboard-paste.ts). Before reading text, it asks the new handler whether the clipboard holds a single copied image file. If so, it pastes the image path, just like a normal image paste. Otherwise the old text-first behaviour is unchanged. All three entry points pass the handler through: keyboard / paste-event, paste listener, and app menu.

  • Native chat composer (use-native-chat-composer-paste.ts). It tries the copied-file handler first and falls back to the old saveClipboardImageAsTempFile, so a Finder-copied screenshot is attached as the screenshot instead of the file icon. The SSH connectionId is passed through.

  • Preload and web stub. The preload exposes the new method. The web client stub returns null, so the web client keeps its old behaviour.

Why

Alternative considered: always prefer an image over text. This would break the case where copying rich text also puts an image on the clipboard. On macOS it would still paste the file icon for Finder copies, because that is what readImage() returns.

What this PR does instead. It only changes the result when the clipboard text is just the name of the one copied image file. That is how Finder copies a file. Any other text (real content) still wins, so normal text paste is not affected.

Why reuse the Windows reader and target-aware saver. They already solve "copied image file becomes a pasted image" safely: they enforce size limits and handle SSH and runtime targets. Reusing them avoids writing a second version of that logic.

Fix evidence

1. Real macOS pasteboard probe (Electron 43.7.0, the same version as v1.4.215; main process only, no window, ORCA_BACKGROUND_LAUNCH=1).

A Swift helper wrote four clipboard shapes for a 640x400 PNG named Screenshot 2026-09-28 at 09.30.00.png. The probe then applied the v1.4.215 decision logic:

Clipboard shape What v1.4.215 pastes
Finder-style: file URL + filename text + icon TIFF text Screenshot 2026-09-28 at 09.30.00.png (and readImage() is the 1024x1024 file icon)
PNG + path text (CleanShot/Shottr style) the path text; the image is ignored
File URL only image path (works)
PNG only (Cmd+Ctrl+Shift+4 control) image path (works)

2. Unit tests: fail before, pass after.

Command (renderer paste logic reverted to origin/main, new tests kept):

git checkout origin/main -- src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts src/renderer/src/components/native-chat/use-native-chat-composer-paste.ts
pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx

Before:

 × pastes a Finder-copied screenshot file as an image instead of its filename text
 × reports a failed copied-image-file save instead of pasting the filename
 × attaches a Finder-copied image file instead of the clipboard icon image
 Test Files  2 failed (2)
      Tests  3 failed | 33 passed (36)

After, on this branch, adding the main-process tests:

pnpm exec vitest run --config config/vitest.config.ts \
  src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts \
  src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx \
  src/main/window/clipboard-mac-image-file.test.ts \
  src/main/window/clipboard-mac-copied-image-ipc.test.ts \
  src/main/window/clipboard-dashboard-popout-access.test.ts
 Test Files  5 passed (5)
      Tests  56 passed (56)

These main-process tests cover the matcher and the handler:

  • reads the image file behind a Finder Cmd+C (file URL + filename text)
  • accepts the extension-hidden Finder display name as text
  • matches an NFD file URL against NFC filename text (uses a real file with a decomposed-form name)
  • ignores a copied image file that no longer exists
  • a check that unrelated text is still rejected
  • saves a Finder-copied image file for the paste target on macOS
  • skips the text read when no file URL is on the clipboard
  • a dashboard popout test showing the popout gets null

Removing the NFC normalization and the extension-less match makes 2 of these tests fail; this was verified in review.

3. Broader runs. pnpm test src/main/window/ src/renderer/src/components/native-chat/ src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts gave 2405 passed and 1 skipped. pnpm tc, oxlint on the changed files, and pnpm run check:code-quality:changed are all clean.

ELI5

When you copy a screenshot file in Finder, the Mac clipboard holds a note that says "this file" plus a label with the file's name. Orca read the label first and typed the name, instead of opening the file and handing over the picture. Now, if the label is just the name of that one copied picture, Orca opens the picture and pastes it like a normal image. If the clipboard holds real text, Orca still pastes the text.

Trade-offs

  • Plain shell prompt behaviour changes. Pasting a Finder-copied .png/.jpg into a plain shell (no agent running) now inserts a temporary file path such as /var/folders/.../orca-paste-*.png instead of the file name.
  • Copies are always PNG. A copied JPEG is re-encoded as PNG, so the temporary file can be larger.
  • SSH uploads. Over SSH, the image file is uploaded by SFTP on every paste, the same as existing image pastes.
  • Oversized images show an error. If a Finder-copied image is over the existing size limits, the terminal now shows an error and pastes nothing, where it used to paste the file name.
  • Looser name match. Clipboard text equal to the file name without its extension also counts as "just the file name." This only applies when a single copied image file URL is present, which is Finder's own pattern.
  • Composer preview. On the native chat composer's menu-paste path, the preview chip can briefly show the file icon, but the file actually attached is the real screenshot.
  • Not verified against a real Finder copy. The Finder clipboard was built to match Finder's types, not captured from a real Finder Cmd+C, because driving Finder needs desktop focus. If Finder ever puts a file-reference URL (file:///.file/id=…) in public.file-url, the extension check fails and paste falls back to the old text behaviour. That is no worse than today.
  • Linux and Windows are unchanged. Windows already had its own file fallback.

Adversarial review

2 loops, clean on loop 2.

  • Loop 1: not clean, 5 findings.

    1. Finder hides screenshot extensions, so the clipboard text may lack .png. Fixed: the matcher also accepts the name without its extension.
    2. The Unicode form did not match: file URLs decode to NFD while filename text is NFC, which the reviewer confirmed with real Electron 43.7.0. Fixed: both sides are converted to NFC.
    3. The native chat composer would still attach the file icon. Fixed: it tries the copied-file handler first.
    4. The handler read the clipboard text eagerly on every paste. Fixed: it reads the file URL first and returns early.
    5. Temp path, PNG re-encode and SFTP upload trade-offs. Documented above.
  • Loop 2: clean, no defects. The reviewer checked:

    • all three terminal paste paths plus the composer;
    • that the popout, Windows, Linux and web get null;
    • that the file-URL-first early return works;
    • the multi-file rejection, extension rules and NFC matching;
    • that the moved Windows reader is unchanged;
    • that reverting each part of the fix makes its tests fail.

    pnpm tc, oxlint and the code-quality gate are clean, and the branch merges cleanly with main.

Linked crash reports

Linked Issue

None. This came from a crash/diagnostic report.

Visual Proof

N/A. Showing the change on screen would need a real Finder Cmd+C and a foreground window, which this background-only validation cannot do. The behaviour is shown by the pasteboard probe and the unit tests above.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Platforms: macOS, using unit tests and a real-pasteboard probe with Electron 43.7.0. Windows and Linux paths return null from the new handler and are covered by the existing tests.

Notes

  • Security: the new handler is restricted to the trusted main window, and the dashboard popout gets null.
  • Cross-platform: the new behaviour is macOS only.
  • SSH and runtime: these reuse the existing target-aware saver.
  • Web client: the stub returns null.
  • Performance: a normal text paste makes one extra small read of the file URL.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

m4air added 2 commits September 28, 2026 07:14
…cover native chat paste

- Accept Finder's filename text without its extension (screenshots hide it).
- Compare NFC-normalized text and path; NSURL file URLs decode to NFD.
- Read public.file-url first and skip the text read when no file was copied.
- Native chat composer tries the copied image file before readImage (file icon).
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ae0fb6b9-d114-4aa1-abe6-94281e17f4df

📥 Commits

Reviewing files that changed from the base of the PR and between aedb930 and 17b1b1e.

📒 Files selected for processing (17)
  • src/main/window/clipboard-dashboard-popout-access.test.ts
  • src/main/window/clipboard-image-file-read.ts
  • src/main/window/clipboard-ipc-handlers.ts
  • src/main/window/clipboard-mac-copied-image-ipc.test.ts
  • src/main/window/clipboard-mac-image-file.test.ts
  • src/main/window/clipboard-mac-image-file.ts
  • src/main/window/clipboard-windows-image-file.ts
  • src/preload/api/ui-bridge-clipboard-and-window-controls.ts
  • src/preload/api/ui-window-api.ts
  • src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-composer-paste.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-paste.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts
  • src/renderer/src/web/preload-api/web-ui-api.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The change adds bounded image-file reading, macOS clipboard file validation, and an IPC and preload API for saving copied clipboard images. Windows clipboard image reading now uses the shared reader. Native chat and terminal paste flows try the copied-file API before existing clipboard handling. The web API returns null.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 17b1b

The copied-image paste change is mergeable after normal checks. A real Finder copy has not been tested manually, but no specific failure is established.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 17b1b

Copied image files can now be read and pasted into local or remote sessions. Platform, window-trust, file-size, and image checks limit the new path, but its reliance on clipboard-provided file locations warrants review.

Retained concerns

  • Medium · security · inferred: A clipboard-provided file URL becomes authority for the main process to open a local image and, on a targeted paste, save its contents locally or remotely. Unlike the neighboring clipboard file operation, this read does not resolve an authorized path. Whether clipboard selection alone is the intended authorization for this transfer needs an explicit boundary decision; this is not a verified exfiltration path.
Security review details

Security Blast Radius

  • inferred — The new independently selectable source is a local image readable by the main process at a clipboard-supplied path; a paste can then place converted bytes in a local, SSH, or runtime destination. The observed callers initiate this through paste handling, not an arbitrary path argument.

Security Findings and Attack Paths

  • inferred — A party able to place a suitable file URL and matching clipboard formats could cause a subsequent trusted-window paste to read and transfer an image accessible to the app process. The evidence does not establish that such a party can trigger the paste, read a file inaccessible to that party, or obtain the saved output.

Trust Boundaries and Controls

  • observed — Sender and macOS checks exclude other platforms and renderers that lack trusted clipboard authority. File-type, byte-length, and dimension checks constrain what reaches image decoding and saving; they do not establish authorization to read the selected path.

Resilience and Maintainability Implications

  • inferred — The URL, filename list, and text are read separately, so a single clipboard-generation invariant is not demonstrated. No observed failure proves that mixed generations can pass validation. Once a file is opened, one-handle reading and exact-size acceptance reduce ordinary replacement and partial-read races.

Hardening Proposals

  • proposed — Define explicitly whether copying a file is sufficient authorization to read it through the main process and transfer its image bytes to the selected destination; use that policy to decide whether any further path or destination control is appropriate without breaking Finder paste.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: supporting Finder-copied screenshot files as images on macOS.
Description check ✅ Passed The description is comprehensive and covers the user impact, implementation, rationale, testing, platform behavior, security, performance, and visual proof. It states that no linked issue exists and l…
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one coverage gap and two small robustness notes.

Reviewed changes

  • macOS copied-file probe: new clipboard:saveCopiedImageFileAsTempFile IPC reads public.file-url first, validates a single copied image file, and reuses the bounded target-aware saver (SSH/runtime included); returns null on Windows/Linux/popout/web.
  • Shared reader: the Windows file→PNG logic moved verbatim to clipboard-image-file-read.ts; Windows now delegates to it.
  • Matcher: clipboard-mac-image-file.ts accepts the full path, basename, or extensionless Finder display name (NFC-normalized on both sides) and rejects multi-selections.
  • Terminal paste: probes the copied file before the text read across all three entry points; pastes the returned path as an image.
  • Native chat composer: tries the copied-file handler, then falls back to saveClipboardImageAsTempFile.
  • Preload/web: method exposed; web stub returns null.

The public.file-url and "readImage() is the file icon" premises are confirmed against Chromium's macOS clipboard source.

ℹ️ The markdown editor's image paste still misses Finder copies

rich-markdown-paste-image.ts is a third image-paste surface and still calls only saveClipboardImageAsTempFile, so a Finder-copied screenshot pasted into a note will not get the new behavior. If that path is reachable for a Finder copy (it gates on an image item in the ClipboardEvent), the same icon-vs-file problem remains there.

Technical details
# Finder-copied image files in the markdown editor

## Affected sites
- `src/renderer/src/components/editor/rich-markdown-paste-image.ts:78` — calls
  `window.api.ui.saveClipboardImageAsTempFile({ connectionId })`, which on macOS
  returns the clipboard icon for a Finder copy.
- `src/renderer/src/components/editor/rich-markdown-paste-handler.ts:29` — routes
  the paste here before the text fallbacks.

## Required outcome
- Either route this path through `saveCopiedClipboardImageFileAsTempFile` first
  (same fallback shape as the composer), or state explicitly that the markdown
  editor is out of scope for this fix.

## Open questions for the human
- Is the markdown-editor paste surface in scope for this bug? The report names
  the terminal/composer, but the root cause is identical.

ℹ️ Nitpicks

  • src/main/window/clipboard-ipc-handlers.ts:164 — clipboard.read('public.file-url') (and 'NSFilenamesPboardType' at line 171) use raw pasteboard types with no /, which the published clipboard.read(format) contract calls invalid; the macOS backend accepts them today and Electron's own spec reads NSFilenamesPboardType, so this is only a future-upgrade risk. A short comment noting the reliance would help a future reader.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

return null
}
// Why: public.file-url exposes only the first item of a Finder multi-selection.
if ((filenamesPlist.match(/<string>/g)?.length ?? 0) > 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The multi-file guard counts <string> occurrences in NSFilenamesPboardType, but that pasteboard type has been deprecated since macOS 10.14 and Chromium no longer reads it; if Finder stops writing it (or writes a binary plist instead of XML), the guard goes inert and a multi-selection would fall through to the text check. Since public.file-url is documented as first-item-only, it is worth confirming on a real multi-select that the text fallback genuinely rejects it (i.e. the clipboard text is the newline-joined list of all names), or counting the file URLs instead. The ignores a multi-file selection test only covers the XML representation.

@oliver-mee

Copy link
Copy Markdown
Contributor

This still reproduces in the macOS desktop app on 1.4.224, paired to a Linux Orca Server with Codex in terminal mode. Cmd+V after copying one screenshot file in Finder inserts its filename rather than an image. A read-only native pasteboard check found one item with public.file-url, filename text, TIFF and icon formats.

I refreshed this PR's copied-file-first fix against main at 88a82c0a7d7e: commit 5d4efb3c2, with a downloadable patch. This is based on OrcaWin's existing contribution here and is ready to cherry-pick. It preserves #27346's native image detection and the current runtime upload routing. Native chat now has its own copied-file intake, so the refresh retains that newer implementation and scopes the change to terminal paste. It also preserves the newer Windows generic copied-file reader.

The refreshed patch passes 207 clipboard and terminal paste tests (one existing benchmark skipped), the full typecheck, the changed-code quality gate, and the Electron Vite build on macOS ARM64 with Node 24. Three screenshot regression cases fail against unpatched main and pass with the refresh, including using a paired runtime's returned image path. The remote upload checks are automated; a live two-machine paste and rendered desktop verification have not been performed.

Could the existing PR be refreshed with this patch so it can land alongside #27346 and #27605? I would prefer to help this PR land rather than open a competing one.

@oliver-mee

oliver-mee commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

I refreshed the existing adaptation again against main ba6599eab, preserving OrcaWin's contribution here: branch at 4bce59273. This is still support for this PR, not a competing submission.

The refresh retains main's upload-cleanup subscription and passes its new attachment-store lifecycle options through the copied-image callback. It also extracts clipboard file helpers to stay within the 300-line limit. A new regression showed that an opaque filename list could otherwise be treated as one image; the copied-image path now declines that case instead of silently taking only the first file.

On this adaptation, 60 focused tests, Node typecheck and scoped lint pass. Renderer typecheck passed before the subsequent main-process helper extraction and filename-list changes. A fresh app/CLI build and a real Electron preload-to-main IPC test pass: a selected PNG is saved with the expected dimensions, and a two-file XML list is refused. That test replaces the clipboard read functions and does not change the user's clipboard. Separately, a native named scratch pasteboard confirmed one/two NSURL items produce one/two XML filename entries. This is not a real Finder Cmd+V or live paired-machine acceptance claim, and it does not establish support for binary filename plists. The original PR head has not been retested by this run.

The adaptation now includes paired application acceptance in e6f52be12. A hidden desktop client pairs to an isolated headless host. Shipping preload/main IPC reads a synthetic selected PNG, uploads it through the real chunked pairing transport and writes identical PNG bytes into a dedicated host-side temp directory. XML multi-selection, opaque binary filename data and multiple newline-separated names without a legacy plist all return null without creating another host file. No direct SSH attempt occurs. Clipboard read functions alone are substituted; the user's OS clipboard is untouched.

The first new fixture run failed before upload because Playwright's main-process evaluation does not support the dynamic import used to create a test directory. Moving directory creation into the test runner fixed setup; the same destination/byte/refusal assertions then pass with retries disabled. Scoped lint passes, and the failed trace is retained. The existing 82 native-chat tests pass on the adaptation: current main's generic copied-file intake supersedes the original chat-specific probe, so this does not add a duplicate chat implementation.

This is support for the existing PR, not a competing submission. The adaptation is a credited source port without original-head ancestry, and its results are not acceptance of the original head. Opaque binary lists are refused, not decoded. Two paired application processes on one Mac do not establish physical two-machine or real Finder Cmd+V acceptance; native-focus testing still requires an isolated desktop.

Machine-readable acceptance receipt.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants