Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
pub mod mouse;
pub mod keyboard;

pub use mouse::{post_click, post_click_screen};
pub use mouse::{is_chromium_target_window, post_click, post_click_screen, send_click_synthesized};
pub use keyboard::{
is_xaml_host_hwnd, post_char, post_key, post_type_text, post_type_text_with_delay,
send_key_synthesized,
Expand Down
199 changes: 197 additions & 2 deletions libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
//! (via ChildWindowFromPointEx), so the message never reaches the top-level
//! chrome that would call SetForegroundWindow in response to WM_LBUTTONDOWN.

use anyhow::Result;
use anyhow::{Result, bail};
use std::thread::sleep;
use std::time::Duration;
use windows::Win32::Foundation::{HWND, LPARAM, POINT, WPARAM};
use windows::Win32::Graphics::Gdi::{ClientToScreen, ScreenToClient};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
};
use windows::Win32::UI::WindowsAndMessaging::{
ChildWindowFromPointEx, CWP_SKIPDISABLED, CWP_SKIPINVISIBLE, CWP_SKIPTRANSPARENT,
PostMessageW, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
GetCursorPos, GetForegroundWindow, GetSystemMetrics, PostMessageW, SetCursorPos,
SetForegroundWindow, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
SM_YVIRTUALSCREEN, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
WM_MOUSEMOVE, WM_RBUTTONDOWN, WM_RBUTTONUP,
};

Expand Down Expand Up @@ -163,3 +170,191 @@ pub fn post_drag(
fn make_lparam(x: i32, y: i32) -> LPARAM {
LPARAM((((y as u16 as u32) << 16) | (x as u16 as u32)) as isize)
}

/// Returns `true` when `hwnd` is a top-level frame of a Chromium-based browser
/// — Edge, Chrome, Brave, Vivaldi, Opera, Chromium, Arc, Thorium, Iridium,
/// Yandex, or any other Chromium-derivative. Matches by window class name,
/// which is stable across versions and consistent across Chromium forks.
///
/// Chromium uses the window class `Chrome_WidgetWin_1` (or `Chrome_WidgetWin_0`
/// for in-process child frames; both should be treated the same way). Electron
/// apps that embed Chromium also use this class, so Electron app coord clicks
/// will route through the SendInput path too — that's intentional, same root
/// cause (#1623).
///
/// Cheap call: one `GetClassNameW` to a 32-char buffer + a `matches!` against
/// the known prefixes. Suitable to call inline in the click dispatch hot path.
pub fn is_chromium_target_window(hwnd: u64) -> bool {
use windows::Win32::UI::WindowsAndMessaging::GetClassNameW;
if hwnd == 0 {
tracing::debug!(target: "click", "is_chromium_target_window: hwnd=0 short-circuit");
return false;
}
let mut buf = [0u16; 64];
let n = unsafe { GetClassNameW(HWND(hwnd as *mut _), &mut buf) };
if n <= 0 {
tracing::debug!(
target: "click",
"is_chromium_target_window: GetClassNameW returned {n} for hwnd=0x{hwnd:x}"
);
return false;
}
let class_name = String::from_utf16_lossy(&buf[..n as usize]);
// Chromium-family classes. Match by prefix because Chromium suffixes a
// 0/1 digit; future Chromium forks may use other suffixes.
let is_chromium = class_name.starts_with("Chrome_WidgetWin_")
// Electron sometimes uses CefBrowserWindow or similar — be permissive.
|| class_name.starts_with("CefBrowser");
tracing::debug!(
target: "click",
"is_chromium_target_window: hwnd=0x{hwnd:x} class={class_name:?} → {is_chromium}"
);
is_chromium
}

/// Click at **screen** coordinates `(sx, sy)` via `SendInput` against the
/// system input queue, briefly focusing `target` so the click lands there.
///
/// Why this exists alongside `post_click_screen`: PostMessage(WM_LBUTTONDOWN)
/// to Chromium-based browsers' top-level frame HWND (or Chrome_RenderWidgetHostHWND
/// descendant) doesn't fire DOM `onclick` / `mousedown` handlers. Chromium's
/// input thread architecture requires events with `SendInput`-queue origin —
/// the same constraint that broke modifier-state hotkey delivery (#1614/#1618)
/// applies to coord clicks on Chromium content (#1623).
///
/// `SendInput` puts the synthetic mouse events on the **system input queue**,
/// where Chromium's input filter accepts them. The trade-off is a brief
/// foreground swap + visible cursor jump (mitigated by saving/restoring the
/// previous foreground HWND and previous cursor position after the click).
///
/// UIAccess constraint: `SetForegroundWindow` is restricted from non-UIAccess
/// processes when not driven by user input. The `cua-driver-uia` worker runs
/// at UIAccess integrity precisely so this restriction is lifted; outside the
/// worker, the foreground swap may silently fail and SendInput land on the
/// wrong window. Callers should funnel Chromium coord clicks through the
/// uia worker (the MCP proxy already prefers the uia pipe over the regular
/// pipe when both are running).
pub fn send_click_synthesized(
target: u64,
sx: i32,
sy: i32,
count: usize,
button: &str,
) -> Result<()> {
let target = HWND(target as *mut _);
if target.0.is_null() {
bail!("invalid target hwnd");
}
if let Some(msg) = crate::input::post_message_blocked_by_uipi(target.0 as u64) {
// Same UIPI defense as PostMessage path — SendInput from non-UIAccess
// would fail just as silently as PostMessage when target is at higher
// integrity. Surface the diagnostic early.
bail!(msg);
}
Comment on lines +248 to +253

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and key symbols
ls -la libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

rg -n "post_message_blocked_by_uipi|send_click_synthesized|SetForegroundWindow|SendInput|uipi|UIAccess|uiAccess|bail!|is_chromium_target_window" libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

# Show the exact snippet area around the referenced lines
sed -n '220,280p' libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

# Find where post_message_blocked_by_uipi is defined
rg -n "fn post_message_blocked_by_uipi|post_message_blocked_by_uipi\(" libs/cua-driver-rs/crates -S

# Show the implementation of post_message_blocked_by_uipi (and any helpers it calls)
ppath="$(rg -n "fn post_message_blocked_by_uipi" -S libs/cua-driver-rs/crates | head -n1 | cut -d: -f1)"
echo "Implementation file: $ppath"
sed -n '1,220p' "$ppath"

# Also inspect any send-click helpers or foreground-related logic near send_click_synthesized
rg -n "fn send_click_synthesized|send_click_synthesized\(" libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs
sed -n '1,220p' libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

Repository: trycua/cua

Length of output: 21357


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '230,420p' libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

Repository: trycua/cua

Length of output: 5265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '230,420p' libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

Repository: trycua/cua

Length of output: 5265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "cua-driver-uia|uiAccess|UIAccess|TokenUIAccess|token uiaccess|mandatory integrity|integrity.*uia" .

Repository: trycua/cua

Length of output: 10969


🌐 Web query:

UIAccess bypasses UIPI SendInput allowed across integrity levels

💡 Result:

UIAccess can bypass UIPI restrictions for cross-privilege UI interaction (including using SendInput), but only within the specific trust/launch model Microsoft defines for UIAccess assistive-technology apps. Microsoft’s position (grounded in docs) 1) UIAccess is specifically meant to bypass UIPI across privilege levels for assistive technology. - An assistive-technology app that needs access to higher integrity level (IL) processes must set the UIAccess flag in its manifest and be launched by a user with administrator privileges [1]. - If an app presents a UIAccess attribute when requesting privileges, it is stating a requirement to bypass UIPI restrictions for sending messages across privilege levels [2]. - Microsoft also describes that a process started with UIAccess rights can “drive any application window by using the SendInput function” [2]. 2) UIAccess is not “unrestricted bypass” for all IL boundaries. - Microsoft explicitly says UIAccess is not enough “for a process to move up through the IL boundary,” and it also notes that none of the described scenarios provides access to UI running under system IL [1]. Related behavior when UIAccess is not used - A Microsoft-authored forum post summarizes that SendInput can be blocked by UIPI so an app can “only send commands to applications of the same or lower elevation” [3]. So, how to interpret the query statement - Accurate in the supported sense: With UIAccess configured and launched under the required conditions, Windows allows cross-privilege UI interaction that UIPI would otherwise restrict, including driving windows via SendInput [1][2]. - Inaccurate if read as: UIAccess lets an arbitrary process bypass UIPI/IL boundaries in all cases (especially system-IL targets), which Microsoft explicitly denies [1].

Citations:


🌐 Web query:

Microsoft UIAccess lifts User Interface Privilege Isolation UIPI rules

💡 Result:

UIAccess is a mechanism in Windows that allows an application to bypass User Interface Privilege Isolation (UIPI) restrictions to interact with applications running at higher privilege levels [1][2]. UIPI is a Windows feature designed to prevent lower-privilege processes from sending messages, installing hooks, or otherwise interacting with higher-privilege processes [1][2][3]. While UIPI effectively isolates processes of different integrity levels, it can inadvertently block legitimate accessibility software—such as screen readers—that require access to these higher-privilege interfaces [1][2][4]. The UIAccess attribute is used within an application's manifest file (specifically in the requestedExecutionLevel tag) to indicate that the application requires these elevated UI capabilities [5][6]. To successfully use UIAccess and bypass UIPI, an application must meet several strict security requirements: 1. Manifest Requirement: The application's manifest must include the uiAccess="true" attribute [5][6]. 2. Code Signing: The application must be digitally signed with an Authenticode certificate [5][6]. 3. Trusted Location: By default, the application must be installed in a secure, write-protected location on the file system, such as %ProgramFiles% or %WinDir% [5][7]. This prevents unauthorized or malicious code from being injected into the elevated process [8][4]. When these conditions are met, a process started with UIAccess gains specific privileges, including the ability to set the foreground window, drive other application windows via the SendInput function, and set low-level journal hooks [4]. It is important to note that UIAccess does not provide full system-level access; for example, it cannot interact with UI running under the SYSTEM integrity level, which is often reserved for critical OS components and protected UAC prompts [5]. Security policies, such as "User Account Control: Only elevate UIAccess applications that are installed in secure locations," can be used to enforce these constraints [2][8]. Disabling such security policies may allow UIAccess to function from insecure locations, but doing so significantly reduces system security by removing the protection against unauthorized application elevation [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "UIAccess|uiAccess|ui_access|ui-access|TOKEN_UIACCESS|TokenUIAccess|RequestedExecutionLevel|GetTokenInformation\\(|TOKEN_QUERY|Token.*UIAccess" libs/cua-driver-rs/crates/platform-windows/src/input libs/cua-driver-rs/crates/platform-windows/src/tools libs/cua-driver-rs/crates -S

Repository: trycua/cua

Length of output: 7936


Stop using post_message_blocked_by_uipi for the SendInput click path.

post_message_blocked_by_uipi only compares TokenIntegrityLevel of the current process vs the target and has no UIAccess awareness. When send_click_synthesized runs in the cua-driver-uia UIAccess worker, UIPI should be bypassed for SendInput, but this precheck can still bail for higher-integrity targets before the existing SetForegroundWindow/partial SendInput UIAccess diagnostic triggers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs` around lines
248 - 253, The precheck using post_message_blocked_by_uipi should not run for
the SendInput click path (e.g., inside send_click_synthesized) because it lacks
UIAccess awareness and can incorrectly bail for UIAccess processes; remove or
guard the current if-let block that calls post_message_blocked_by_uipi so it
only executes for the PostMessage path (not for
SendInput/send_click_synthesized), and rely on the existing
SetForegroundWindow/SendInput UIAccess diagnostics for the SendInput branch.


let (down_flag, up_flag) = match button {
"right" => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
"middle" => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
_ => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
};

// Convert screen pixel coords to normalized absolute coords spanning the
// virtual desktop (0..65535 across the union of all monitors). This is
// what `MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK` expects.
//
// Without VIRTUALDESK the coords are relative to the primary monitor only;
// multi-monitor setups would misroute. Better to always use VIRTUALDESK.
let (vd_x, vd_y) = unsafe {
(
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
)
};
let (vd_w, vd_h) = unsafe {
(
GetSystemMetrics(SM_CXVIRTUALSCREEN).max(1),
GetSystemMetrics(SM_CYVIRTUALSCREEN).max(1),
)
};
let norm_x = ((sx - vd_x) as i64 * 65535 / vd_w as i64).clamp(0, 65535) as i32;
let norm_y = ((sy - vd_y) as i64 * 65535 / vd_h as i64).clamp(0, 65535) as i32;

let move_input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: norm_x,
dy: norm_y,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0,
dwExtraInfo: 0,
},
},
};
let down_input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: norm_x, dy: norm_y, mouseData: 0,
dwFlags: down_flag | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0, dwExtraInfo: 0,
},
},
};
let up_input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: norm_x, dy: norm_y, mouseData: 0,
dwFlags: up_flag | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0, dwExtraInfo: 0,
},
},
};

unsafe {
// Save previous foreground + cursor position so we can restore.
let prev_fg = GetForegroundWindow();
let mut prev_cursor = POINT::default();
let _ = GetCursorPos(&mut prev_cursor);

// Focus the target so the click lands there (mirrors send_key_synthesized).
let _ = SetForegroundWindow(target);
sleep(Duration::from_millis(8));

// Move the cursor first so the OS hover state matches before the click.
// `SetCursorPos` is the visible cursor move; the MOUSEEVENTF_MOVE input
// ensures Chromium's input filter sees a coordinated move event.
let _ = SetCursorPos(sx, sy);

let count = count.max(1);
for i in 0..count {
let events = [move_input, down_input, up_input];
let sent = SendInput(&events, std::mem::size_of::<INPUT>() as i32);
if sent as usize != events.len() {
// Partial insertion — restore foreground+cursor and bail with
// the standard "needs UIAccess worker" diagnostic.
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
bail!(
"SendInput inserted only {sent} of {} mouse events. Likely cause: \
the daemon is not at UIAccess integrity, so SetForegroundWindow was \
rejected and the events landed on the wrong window. Route Chromium \
coord clicks through the cua-driver-uia worker.",
events.len()
Comment on lines +323 to +345

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "SetForegroundWindow\(|GetForegroundWindow\(|SendInput\(|UIAccess worker|SetCursorPos\(" libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs

# Show the relevant section with line numbers
sed -n '250,430p' libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs | cat -n

Repository: trycua/cua

Length of output: 5682


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all SetForegroundWindow/GetForegroundWindow occurrences in the file
FILE="libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs"
rg -n "SetForegroundWindow\(|GetForegroundWindow\(|needs UIAccess worker|UIAccess worker" "$FILE"

# Show a wider section around the SetForegroundWindow call site
sed -n '260,380p' "$FILE" | cat -n

# Also search for SetForegroundWindow usage in the whole crate to see if there is an existing guard elsewhere
rg -n "SetForegroundWindow\(" libs/cua-driver-rs/crates/platform-windows/src/input

Repository: trycua/cua

Length of output: 5643


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver-rs/crates/platform-windows/src/input/keyboard.rs"
sed -n '260,380p' "$FILE" | cat -n

Repository: trycua/cua

Length of output: 6455


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs"
# Print function header and relevant checks preceding the SetForegroundWindow block
sed -n '240,360p' "$FILE" | cat -n

Repository: trycua/cua

Length of output: 5556


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs"
# Print more context around the function start and the checks preceding the SetForegroundWindow block
sed -n '200,360p' "$FILE" | cat -n

Repository: trycua/cua

Length of output: 7909


Fail fast when focusing target fails in send_click_synthesized (mouse.rs)

SetForegroundWindow(target) is currently ignored, and the only failure guard is SendInput returning a partial insertion. SendInput fully succeeding only means the events were queued—it doesn’t guarantee they land on target, so focus denial can cause the click to hit the current foreground window.

🛠️ Suggested guard
-        let _ = SetForegroundWindow(target);
+        if !SetForegroundWindow(target).as_bool() {
+            bail!(
+                "failed to foreground hwnd 0x{:x}; refusing to inject SendInput into the current foreground window",
+                target.0 as usize
+            );
+        }
         sleep(Duration::from_millis(8));
+        if GetForegroundWindow() != target {
+            bail!(
+                "hwnd 0x{:x} never became foreground; route Chromium coord clicks through the cua-driver-uia worker",
+                target.0 as usize
+            );
+        }

File: libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs (around lines 323-345)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let _ = SetForegroundWindow(target);
sleep(Duration::from_millis(8));
// Move the cursor first so the OS hover state matches before the click.
// `SetCursorPos` is the visible cursor move; the MOUSEEVENTF_MOVE input
// ensures Chromium's input filter sees a coordinated move event.
let _ = SetCursorPos(sx, sy);
let count = count.max(1);
for i in 0..count {
let events = [move_input, down_input, up_input];
let sent = SendInput(&events, std::mem::size_of::<INPUT>() as i32);
if sent as usize != events.len() {
// Partial insertion — restore foreground+cursor and bail with
// the standard "needs UIAccess worker" diagnostic.
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
bail!(
"SendInput inserted only {sent} of {} mouse events. Likely cause: \
the daemon is not at UIAccess integrity, so SetForegroundWindow was \
rejected and the events landed on the wrong window. Route Chromium \
coord clicks through the cua-driver-uia worker.",
events.len()
if !SetForegroundWindow(target).as_bool() {
bail!(
"failed to foreground hwnd 0x{:x}; refusing to inject SendInput into the current foreground window",
target.0 as usize
);
}
sleep(Duration::from_millis(8));
if GetForegroundWindow() != target {
bail!(
"hwnd 0x{:x} never became foreground; route Chromium coord clicks through the cua-driver-uia worker",
target.0 as usize
);
}
// Move the cursor first so the OS hover state matches before the click.
// `SetCursorPos` is the visible cursor move; the MOUSEEVENTF_MOVE input
// ensures Chromium's input filter sees a coordinated move event.
let _ = SetCursorPos(sx, sy);
let count = count.max(1);
for i in 0..count {
let events = [move_input, down_input, up_input];
let sent = SendInput(&events, std::mem::size_of::<INPUT>() as i32);
if sent as usize != events.len() {
// Partial insertion — restore foreground+cursor and bail with
// the standard "needs UIAccess worker" diagnostic.
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
bail!(
"SendInput inserted only {sent} of {} mouse events. Likely cause: \
the daemon is not at UIAccess integrity, so SetForegroundWindow was \
rejected and the events landed on the wrong window. Route Chromium \
coord clicks through the cua-driver-uia worker.",
events.len()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-windows/src/input/mouse.rs` around lines
323 - 345, In send_click_synthesized, don't ignore SetForegroundWindow(target):
check its boolean return and fail fast if it returns false so we don't proceed
sending events to the wrong window. If SetForegroundWindow(target) fails,
restore the previous cursor (prev_cursor) and previous foreground window
(prev_fg) like the existing SendInput error path, and bail! with a clear
diagnostic mentioning focus denial (same style as the existing bail! for partial
SendInput). Keep the subsequent SetCursorPos(target) and SendInput-only path for
the successful-focus case.

);
}
if i + 1 < count {
sleep(Duration::from_millis(80));
}
}

// Brief settle so the target processes the click before we restore.
sleep(Duration::from_millis(40));
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
}

Ok(())
}
49 changes: 49 additions & 0 deletions libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,55 @@ impl Tool for ClickTool {
));
}
}

// #1623: PostMessage(WM_LBUTTONDOWN) to Chromium frame HWNDs doesn't
// reach the DOM input pipeline — Chromium's input thread only accepts
// events with SendInput-queue origin. For Chromium targets, take the
// SendInput path; for everything else, take the existing PostMessage
// path (the path was added as a no-focus-steal click delivery, which
// PostMessage uniquely provides). The Chromium path moves the cursor
// visibly and briefly steals foreground — there's no Chromium-native
// alternative that gets DOM events to fire without these tradeoffs.
//
// SendInput on Chromium requires the daemon to have UIAccess integrity
// (otherwise SetForegroundWindow is rejected and the events land on the
// wrong window). The MCP proxy auto-prefers the cua-driver-uia worker
// when both pipes are up, so this path runs with UIAccess in the
// common case. When UIAccess is missing, send_click_synthesized
// surfaces an actionable error and we fall through to PostMessage —
// PostMessage won't fire DOM events on Chromium either, but the user
// gets a meaningful diagnostic instead of silent no-op.
let chromium = tokio::task::spawn_blocking(move || {
crate::input::is_chromium_target_window(hwnd)
})
.await
.unwrap_or(false);
if chromium {
let send_result = tokio::task::spawn_blocking(move || {
crate::input::send_click_synthesized(hwnd, sx as i32, sy as i32, count, &btn)
})
.await;
match send_result {
Ok(Ok(())) => {
let click_word = match count {
2 => "double-click",
3 => "triple-click",
_ => "click",
};
return ToolResult::text(format!(
"✅ Sent {click_word} via SendInput to pid {pid} (Chromium target)."
));
}
Ok(Err(e)) => {
// Bubble the actionable diagnostic ("Run through uia worker") up
// to the caller rather than silently falling to PostMessage,
// which we know doesn't work for Chromium.
return ToolResult::error(e.to_string());
}
Err(e) => return ToolResult::error(format!("Task error: {e}")),
}
}

let result = tokio::task::spawn_blocking(move || {
crate::input::post_click(hwnd, px as i32, py as i32, count, &btn)
}).await;
Expand Down
Loading