From 52f8ead0f246464c4d4ff096dc4f357710fcc82d Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Sat, 9 May 2026 02:45:21 +0900 Subject: [PATCH] fix(click): wait for paint to settle so SPA renders complete before next command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes a real-world race that broke X multi-tweet thread composition (and similar SPA flows): clicking "Add post" returned immediately, inserttext fired before React had committed the new textarea, the keystroke landed on the dialog wrapper, and X interpreted the stray input as a request to dismiss the modal. After mouseReleased we now wait for two requestAnimationFrame ticks plus a microtask boundary (~33ms at 60fps, bounded). That's enough for React/Vue/Svelte to commit any state update scheduled by the click handler. Errors during the wait are swallowed — a click never fails because of post-processing. Opt out for perf-sensitive scripts that don't drive SPA UIs: AGENT_BROWSER_CLICK_WAIT_STABLE=0 --- cli/src/native/interaction.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cli/src/native/interaction.rs b/cli/src/native/interaction.rs index 0c51ec7..3375ace 100644 --- a/cli/src/native/interaction.rs +++ b/cli/src/native/interaction.rs @@ -884,6 +884,38 @@ pub async fn tap_touch( Ok(()) } +/// After a click is dispatched, give the page two animation frames + a +/// microtask boundary to let React/Vue/Svelte commit any state update +/// scheduled by the click handler. Without this wait, follow-up commands +/// (e.g. `inserttext` against the textbox the click was supposed to mount) +/// race the renderer and can land on stale or wrong elements. +/// +/// The wait is bounded to ~33ms in the common case (two RAFs at 60fps) and +/// returns immediately on any error — never an exception path. +/// +/// Set `AGENT_BROWSER_CLICK_WAIT_STABLE=0` to disable for perf-sensitive +/// scripts that don't drive SPA UIs. +async fn wait_for_paint_settled(client: &CdpClient, session_id: &str) { + if std::env::var("AGENT_BROWSER_CLICK_WAIT_STABLE").as_deref() == Ok("0") { + return; + } + let script = "new Promise(resolve => \ + requestAnimationFrame(() => \ + requestAnimationFrame(() => \ + queueMicrotask(() => resolve(true)))))"; + let _ = client + .send_command_typed::<_, Value>( + "Runtime.evaluate", + &EvaluateParams { + expression: script.to_string(), + return_by_value: Some(true), + await_promise: Some(true), + }, + Some(session_id), + ) + .await; +} + async fn dispatch_click( client: &CdpClient, session_id: &str, @@ -955,6 +987,7 @@ async fn dispatch_click( ) .await?; + wait_for_paint_settled(client, session_id).await; Ok(()) }