Skip to content

fix(desktop): paste clipboard images on Linux - #865

Merged
alphaxiv-bot[bot] merged 4 commits into
alphaXiv:mainfrom
sBaydin:feat/desktop-clipboard-paste
Nov 4, 2026
Merged

alphaxiv-bot[bot] merged 4 commits into
alphaXiv:mainfrom
sBaydin:feat/desktop-clipboard-paste

Conversation

@sBaydin

@sBaydin sBaydin commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Closes #864

Summary

  • Add a Linux-only native GTK clipboard fallback for image pastes where WebKit exposes an empty types list or a zero-byte File.
  • Write the image to a temporary PNG and hand it to the existing composer attachment flow.
  • Keep browser and macOS/Windows behavior unchanged.

Test plan

  • npx --yes pnpm@12.16.1 test - 573 passed
  • cargo test --locked --features desktop - 1353 passed
  • cargo clippy --all-targets --locked --features desktop -- -D warnings
  • cargo build --release --locked --features desktop
  • Manual: copy a screenshot and press Ctrl+V in the desktop composer on Linux

@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 2/5

[Medium impact] The change reaches image pastes in the Linux desktop chat composer.

Fix the unsafe temporary-file writes and wrong-chat attachment behavior before merging.

Findings

  1. P1 Security Clipboard writes can overwrite files ▶
  2. P1 Images reach the wrong chat ▶
  3. P2 Late replies leave screenshots behind ▶
Fix with agent prompt
### Issue 1
src/commands/app.rs:1473-1476
If the temporary directory is shared with other local users, another user can create `openresearch-clipboard` first and plant a `<pid>-1.png` symlink to a file the desktop user can write. The next image paste follows that symlink and replaces the target with PNG bytes. With a `022` umask, new clipboard files are also readable by other local users.

Use a private, randomly named directory with owner-only permissions and create each file exclusively.

**How this was verified:** The paste request reaches `std::fs::write` through an existing shared directory without checking its owner, rejecting symlinks, or creating the destination exclusively.

### Issue 2
ui/src/components/ChatPanel.tsx:5047-5054
Switching chats while the native request or image download is pending can attach the image to the newly selected chat. `addFiles` reads `composerScopeRef.current` only after both awaits, so its later check accepts that new chat. The callback still holds the old render's `stashKey`, which can also disagree with the captured scope.

Capture the originating composer before the first await and keep the image tied to it through the download and file read.

### Issue 3
ui/src/desktopClipboard.ts:40-42
A native reply that arrives after the three-second timeout leaves its PNG behind. The timeout removes the pending request, so this handler returns before the file can reach the normal download-and-delete flow.

Delete the returned temporary file when a late reply has no waiting request. The startup sweep only runs on another launch and skips files less than an hour old, so it does not reliably clean up these failed pastes.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds a Linux-only GTK fallback when WebKit cannot provide clipboard image bytes.

  • Linux desktop chat can paste images when WebKit returns no usable file.

Diagram

sequenceDiagram
    participant Composer
    participant Bridge
    participant GTK
    participant Temp as Temporary PNG
    participant API as Local file API
    Composer->>Bridge: Request native image
    Bridge->>GTK: Read clipboard
    GTK->>Temp: Write PNG
    GTK-->>Bridge: Return request ID and path
    Bridge-->>Composer: Resolve payload
    Composer->>API: Fetch PNG
    API-->>Composer: Image bytes
    Composer->>GTK: Delete temporary PNG
    Composer->>Composer: Add attachment
Loading

Reviews (1) · Last reviewed commit: "fix(desktop): paste clipboard images on ..." · Reviewed by Greptile

Comment thread src/commands/app.rs Outdated
Comment on lines +1473 to +1476
let dir = std::env::temp_dir().join("openresearch-clipboard");
std::fs::create_dir_all(&dir).map_err(|error| error.to_string())?;
let path = dir.join(format!("{}-{id}.png", std::process::id()));
std::fs::write(&path, &bytes).map_err(|error| error.to_string())?;

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.

P1 security Clipboard writes can overwrite files

If the temporary directory is shared with other local users, another user can create openresearch-clipboard first and plant a <pid>-1.png symlink to a file the desktop user can write. The next image paste follows that symlink and replaces the target with PNG bytes. With a 022 umask, new clipboard files are also readable by other local users.

Use a private, randomly named directory with owner-only permissions and create each file exclusively.

How this was verified: The paste request reaches std::fs::write through an existing shared directory without checking its owner, rejecting symlinks, or creating the destination exclusively.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/app.rs
Line: 1473-1476

Comment:
**Clipboard writes can overwrite files**

If the temporary directory is shared with other local users, another user can create `openresearch-clipboard` first and plant a `<pid>-1.png` symlink to a file the desktop user can write. The next image paste follows that symlink and replaces the target with PNG bytes. With a `022` umask, new clipboard files are also readable by other local users.

Use a private, randomly named directory with owner-only permissions and create each file exclusively.

**How this was verified:** The paste request reaches `std::fs::write` through an existing shared directory without checking its owner, rejecting symlinks, or creating the destination exclusively.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread ui/src/components/ChatPanel.tsx Outdated
Comment on lines +5047 to +5054
void (async () => {
const payload = await requestDesktopClipboardPaste();
if (!payload) {
return;
}
const file = await fileFromDesktopClipboardPayload(payload);
if (file) {
addFiles([file]);

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.

P1 Images reach the wrong chat

Switching chats while the native request or image download is pending can attach the image to the newly selected chat. addFiles reads composerScopeRef.current only after both awaits, so its later check accepts that new chat. The callback still holds the old render's stashKey, which can also disagree with the captured scope.

Capture the originating composer before the first await and keep the image tied to it through the download and file read.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/components/ChatPanel.tsx
Line: 5047-5054

Comment:
**Images reach the wrong chat**

Switching chats while the native request or image download is pending can attach the image to the newly selected chat. `addFiles` reads `composerScopeRef.current` only after both awaits, so its later check accepts that new chat. The callback still holds the old render's `stashKey`, which can also disagree with the captured scope.

Capture the originating composer before the first await and keep the image tied to it through the download and file read.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread ui/src/desktopClipboard.ts Outdated
Comment on lines +40 to +42
const pending = pendingPastes.get(response.id);
if (!pending) {
return;

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.

P2 Late replies leave screenshots behind

A native reply that arrives after the three-second timeout leaves its PNG behind. The timeout removes the pending request, so this handler returns before the file can reach the normal download-and-delete flow.

Delete the returned temporary file when a late reply has no waiting request. The startup sweep only runs on another launch and skips files less than an hour old, so it does not reliably clean up these failed pastes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/desktopClipboard.ts
Line: 40-42

Comment:
**Late replies leave screenshots behind**

A native reply that arrives after the three-second timeout leaves its PNG behind. The timeout removes the pending request, so this handler returns before the file can reach the normal download-and-delete flow.

Delete the returned temporary file when a late reply has no waiting request. The startup sweep only runs on another launch and skips files less than an hour old, so it does not reliably clean up these failed pastes.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@paperclover

paperclover commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

comment written by Claude Sonnet 5.5

thanks for chasing this down, the diagnosis is right: on WebKitGTK 2.50/2.52 a pasted image arrives with clipboardData.types empty. but the native bridge isn't needed. navigator.clipboard.read() called from inside the paste handler returns the PNG on every build we tried, and it's refused outside a gesture (NotAllowedError), so a page script can't read the clipboard silently.

tried with a separate process owning a real image/png clipboard, then a real Ctrl+V:

WebKitGTK session clipboardData on paste read() in handler read() no gesture
2.50.4 (the AppImage's lib) Wayland types=[] image/png NotAllowedError
2.52.6 Wayland, X11 types=[] image/png NotAllowedError
2.54.1 Wayland Files, non-empty File image/png NotAllowedError

blocking: this is a lot of surface (the GTK clipboard path, temp dir, ipc messages, desktopClipboard.ts) for a paste fix. can you try it in onComposerPaste instead? when no File has a nonzero size and types is empty or includes Files, preventDefault(), await navigator.clipboard.read(), take the first image/* item, wrap its blob in a File, and pass it to addFiles with the scope and stashKey captured before the await. your target param on addFiles is worth keeping for that. does #864 still reproduce with it?

non-blocking: on 2.54.1 the existing getAsFile() path already returns a non-empty File, so the zero-byte File from the report didn't reproduce for us. what does your setup look like? distro and version, Wayland or X11, the exact WebKitGTK version, and whether you're running the AppImage or a distro/source build. also which app put the image on the clipboard (screenshot tool, browser, file manager), and what e.clipboardData.types and items look like in the paste event. that would tell us whether the zero-byte case is a different bug.

not verified: we only tested in VMs, and the 2.50.4 run used the AppImage's bundled WebKit but the host's GTK 3.24, not the image's 22.04 GTK. so we're trusting your own test on real hardware and in the real AppImage. plain-text clipboards weren't measured on 2.50/2.52 either, so check the clipboard before preventDefault() so text paste still works.

@paperclover

Copy link
Copy Markdown
Contributor

tldr pls simplify the patch but i also am really curious on the environment you hit this in.

Use a Linux-only native GTK clipboard fallback when WebKit exposes an image paste with empty types or a zero-byte file.
Store clipboard images in the user cache with private permissions and exclusive file creation. Keep pending pastes tied to their originating composer and clean up late native replies.
Read clipboard images with navigator.clipboard.read() inside the composer paste handler and remove the Linux GTK bridge, IPC messages, and temporary files. Capture the target composer scope before the async read so a paste cannot land in a different chat.
@sBaydin
sBaydin force-pushed the feat/desktop-clipboard-paste branch from 77a6514 to 4ffbf0a Compare November 4, 2026 08:14
@sBaydin

sBaydin commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

For sonnet comments:
I removed the native bridge and used navigator.clipboard.read() inside onComposerPaste instead. target is still there; scope and stashKey are captured before the await. Patch is now 3 UI files.

Environment: CachyOS, KDE Plasma Wayland, official AppImage 0.2.24 (bundled WebKitGTK 2.50.4, host GTK 3.24.52). Also tested system WebKitGTK 2.54.2. The clipboard image came from Spectacle. On 2.50.4, screenshot + Ctrl+V attaches, so #864 doesn't reproduce. On 2.54.2, a trusted WebDriver paste had types: ["Files"], one image/png item, and getAsFile() was 286941 bytes; no zero-byte file. read() outside the gesture still throws NotAllowedError. I didn't log the 2.50.4 event details, only confirmed that the image attaches.

Text-only paste still works normally. Image+text is treated as an image. Copying a PNG file from the file manager still pastes its file:// path, since that clipboard has a URI instead of image bytes.

UI tests, build, clippy and CI are green.


For @paperclover
tldr: native bridge removed, clipboard read now happens in the paste handler.
Tested on CachyOS/KDE Wayland, AppImage with WebKitGTK 2.50.4 and system 2.54.2.
Screenshot paste works, #864 doesn't reproduce.
Ekran 1

@paperclover paperclover added the auto-merge Queue for automatic merge once all checks pass label Nov 4, 2026
@alphaxiv-bot
alphaxiv-bot Bot merged commit 937aeee into alphaXiv:main Nov 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Queue for automatic merge once all checks pass

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: Ctrl+V cannot paste clipboard images on Linux

2 participants