Skip to content

fix(standalone): run clipboard-read commands async to avoid main-thread freeze - #321

Merged
nedtwigg merged 2 commits into
mainfrom
fix/clipboard-commands-async
Jul 28, 2026
Merged

fix(standalone): run clipboard-read commands async to avoid main-thread freeze#321
nedtwigg merged 2 commits into
mainfrom
fix/clipboard-commands-async

Conversation

@dormouse-bot

Copy link
Copy Markdown
Collaborator

Problem

standalone/src-tauri/src/lib.rs documents an INVARIANT above request_from_sidecar_timeout: every #[tauri::command] that reaches the two blocking sidecar helpers must be declared #[tauri::command(async)], because Tauri runs a plain-sync command on the main thread — where the helper's recv_timeout blocks the webview from painting for the whole round trip.

The async port converted ~19 commands, but three clipboard reads were missed and remained plain sync while still calling request_from_sidecar_timeout on the non-Windows path:

  • read_clipboard_file_paths — blocks up to 5s
  • read_clipboard_image_as_file_path — blocks up to 10s
  • read_clipboard_text — blocks up to 5s

All three are registered invoke handlers, so on macOS/Linux a clipboard read runs on the main thread and freezes the UI for the sidecar round trip — an image paste against a slow/hung sidecar can hang the window for up to 10s.

Fix

Declare the three commands #[tauri::command(async)], matching every other sidecar-reaching command. (async) moves the same blocking body onto a runtime worker so the UI keeps rendering; the JS invoke contract is unchanged (it already returns a Promise). The Windows-native clipboard path (clipboard_win) does no sidecar round trip and is unaffected.

Regression test

A runtime test can't assert Tauri's threading, so this adds a source-scanning guard, sidecar_commands_are_async: it parses lib.rs, finds every #[tauri::command] whose body reaches request_from_sidecar, and asserts each is (async)/async fn. Verified it fails on the pre-fix source (flagging exactly the three clipboard commands) and passes after — so the invariant, previously enforced only by a comment, now can't silently regress.

The tauri crate needs GTK/glib system libs that this checkout's sandbox lacks, so I validated the parser logic against the real source out-of-band rather than via cargo test locally; CI compiles and runs it on a full runner.

…ad freeze

Three clipboard reads (read_clipboard_file_paths, read_clipboard_image_as_file_path,
read_clipboard_text) were left as plain sync #[tauri::command] while still calling
request_from_sidecar_timeout on the non-Windows path, violating the invariant
documented above request_from_sidecar_timeout. On macOS/Linux they ran on the main
thread and froze the webview for the sidecar round trip (up to 10s for image paste).

Declare them #[tauri::command(async)] to match every other sidecar-reaching command,
and add a source-scanning guard test (sidecar_commands_are_async) enforcing the
invariant so it can't silently regress.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1c594f4
Status: ✅  Deploy successful!
Preview URL: https://5379e470.mouseterm.pages.dev
Branch Preview URL: https://fix-clipboard-commands-async.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The fix is right — the three clipboard commands call request_from_sidecar_timeout directly on the non-Windows path, so plain-sync they'd block the main thread, and (async) matches every other sidecar-reaching command. No concerns there.

One gap in the regression guard, which is the part meant to keep the invariant from silently regressing: it only catches direct callers of request_from_sidecar. The 8 agent-browser commands (agent_browser_command, agent_browser_open, agent_browser_screenshot, …) reach the blocking helper transitively through the agent_browser_forward wrapper (lib.rs fn agent_browser_forward), so their bodies contain agent_browser_forward, not request_from_sidecar. If one of them were ever declared plain sync, sidecar_commands_are_async would pass while the UI froze — and this is the family with the longest timeout (AGENT_BROWSER_TIMEOUT = 30s), so it's the worst case to miss. Inline suggestion below extends the reachability check to the known wrapper; all agent-browser commands are already (async), so it won't fire today.

Secondary, non-blocking: the body-extraction comment claims "Rust requires balanced braces, so a plain char count over valid source returns to depth 0 exactly at the fn's closing brace." That's not quite true — a lone brace inside a string or char literal (e.g. '{'/'}', which already appear in this test's own body, or an error string like "missing }") throws off the count. It happens to hold across today's scanned command bodies, but a future command with a lone-brace literal would silently miscount. Worth softening the comment so the assumption isn't oversold.

Comment thread standalone/src-tauri/src/lib.rs Outdated
… wrapper

The sidecar_commands_are_async guard only matched direct callers of
request_from_sidecar. The 8 agent-browser commands reach the blocking
helper transitively through agent_browser_forward, so a plain-sync one
would have slipped past — and that family carries the longest timeout
(AGENT_BROWSER_TIMEOUT = 30s). Extend the reachability check to the
wrapper, and soften the brace-counting comment which oversold the
balanced-braces assumption (a lone brace in a string/char literal would
miscount).
@nedtwigg
nedtwigg merged commit fb671f7 into main Jul 28, 2026
6 checks passed
@nedtwigg
nedtwigg deleted the fix/clipboard-commands-async branch July 28, 2026 14:10
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