fix(standalone): run clipboard-read commands async to avoid main-thread freeze - #321
Conversation
…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.
Deploying mouseterm with
|
| Latest commit: |
1c594f4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5379e470.mouseterm.pages.dev |
| Branch Preview URL: | https://fix-clipboard-commands-async.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
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.
… 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).
Problem
standalone/src-tauri/src/lib.rsdocuments an INVARIANT aboverequest_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'srecv_timeoutblocks 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_timeouton the non-Windows path:read_clipboard_file_paths— blocks up to 5sread_clipboard_image_as_file_path— blocks up to 10sread_clipboard_text— blocks up to 5sAll 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 JSinvokecontract 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 parseslib.rs, finds every#[tauri::command]whose body reachesrequest_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 testlocally; CI compiles and runs it on a full runner.