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
100 changes: 78 additions & 22 deletions libs/cua-driver/rust/crates/cua-driver/tests/harness_web_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,65 @@ where F: FnOnce(u32, u64, &mut ChildStdin, &mut BufReader<&mut ChildStdout>) {
drop(session);
}

// ── WebView2 structural ──────────────────────────────────────────────────────
// ── WebView2 structural + page tool ─────────────────────────────────────────

#[test]
#[ignore]
fn harness_webview_window_discoverable() {
// Smoke test: WebView2 harness launches, window appears via list_windows.
// Behavioural page-tool tests are deferred until WebView2 actually opens
// its CDP listener — see the module docstring TODO.
run_with_session("webview", webview_exe(), "CuaTestHarness WebView", 9222,
|pid, wid, _stdin, _stdout| {
println!("✅ harness_webview_window_discoverable: pid={pid} wid={wid}");
});
}

#[test]
#[ignore]
fn harness_webview_page_tool() {
// Regression guard for WebView2 CDP exposure via
// CoreWebView2EnvironmentOptions.AdditionalBrowserArguments.
// Combined with the `/json` Content-Length fix in mcp-server/src/cdp.rs,
// the page tool now reaches WebView2's DOM via CDP just like Electron.
run_with_session("webview", webview_exe(), "CuaTestHarness WebView", 9222,
|pid, wid, stdin, stdout| {

let marker_resp = tools_call(stdin, stdout, 30, "page", serde_json::json!({
"pid": pid as i64, "window_id": wid, "action": "execute_javascript",
"javascript": "document.querySelector('[data-cua-id=\"page-marker\"]').textContent"
}));
let marker = marker_resp.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.get(0))
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("");
assert!(marker.contains("WEB_HARNESS_MARKER_v1"),
"WebView2 CDP execute_javascript marker fetch: {marker:?}");

// click_element via DOM selector + counter readback.
let _ = tools_call(stdin, stdout, 31, "page", serde_json::json!({
"pid": pid as i64, "window_id": wid, "action": "click_element",
"selector": "#btn-increment"
}));
std::thread::sleep(Duration::from_millis(500));

let counter_resp = tools_call(stdin, stdout, 32, "page", serde_json::json!({
"pid": pid as i64, "window_id": wid, "action": "execute_javascript",
"javascript": "document.getElementById('lbl-counter').textContent"
}));
let post = counter_resp.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.get(0))
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("");
assert!(post.contains("counter=1"),
"WebView2 counter didn't advance via page.click_element: {post:?}");
println!("✅ harness_webview_page_tool: CDP+execute_javascript+click_element green");
});
}

// ── Electron structural + page tool ──────────────────────────────────────────

#[test]
Expand Down Expand Up @@ -217,25 +262,24 @@ fn harness_electron_page_tool() {
});
}

/// Documents a separate gap in page.click_element: its CDP probe runs JS
/// that returns a JSON object (vx/vy/sx/sy/dpr) but the result is wrapped
/// as a CDP `runtime.evaluate.user_gesture` string by the time the page
/// tool's JSON parser sees it — causing "probe JSON missing required
/// field 'vx'". Fix: unwrap the CDP result's `.value` before parsing,
/// or send the probe with `returnByValue:true` in the Runtime.evaluate
/// params. Manually unwrapping the harness's verified JSON string works.
/// Regression guard for the page.click_element double-encode fix.
///
/// Originally the CDP runtime.evaluate response for the probe JS came
/// back as a JSON-encoded string containing the actual `{vx,vy,...}`
/// object. The page tool's `serde_json::from_str(&probe_json).or_else(...)`
/// only fell into the inner-decode branch on a hard parse error, but
/// `from_str` happily parses a JSON-string into a `Value::String`, so the
/// inner-decode branch never ran and `parsed.get("vx")` returned None.
/// Fix: match on Value::String and re-decode explicitly.
#[test]
#[ignore]
fn harness_electron_click_element_DOCUMENTED_wrapper_bug() {
fn harness_electron_click_element() {
run_with_session("electron", electron_exe(), "CuaTestHarness Electron", 9223,
|pid, wid, stdin, stdout| {
let resp = tools_call(stdin, stdout, 30, "page", serde_json::json!({
"pid": pid as i64, "window_id": wid, "action": "click_element",
"selector": "#btn-increment"
}));
// Safe traversal — error responses from `page` can vary in shape
// (sometimes `result.isError: true` + `content[0].text`, sometimes
// `error.message`). Don't blow up on a missing array entry.
let text = resp.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
Expand All @@ -244,12 +288,24 @@ fn harness_electron_click_element_DOCUMENTED_wrapper_bug() {
.and_then(|t| t.as_str())
.or_else(|| resp.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str()))
.unwrap_or("");
// Expect the gap: parser fails on the CDP-wrapped probe response.
let expected_gap = text.contains("probe JSON missing") || text.contains("required field");
assert!(expected_gap,
"Expected click_element probe-wrapper gap. Got: {text:?}. \
If this asserts on a success message, the wrapper-unwrap fix is in — \
flip this assertion to assert success.");
println!("⚠️ harness_electron_click_element_DOCUMENTED_wrapper_bug: probe JSON parse gap present");
assert!(!text.contains("probe JSON missing") && !text.contains("required field"),
"click_element probe parse regressed: {text:?}");
std::thread::sleep(Duration::from_millis(400));

// Verify the click actually fired in the DOM.
let counter_resp = tools_call(stdin, stdout, 31, "page", serde_json::json!({
"pid": pid as i64, "window_id": wid, "action": "execute_javascript",
"javascript": "document.getElementById('lbl-counter').textContent"
}));
let post = counter_resp.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.get(0))
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("");
assert!(post.contains("counter=1"),
"Counter didn't advance after page.click_element: {post:?}");
println!("✅ harness_electron_click_element: probe parsed, click fired, counter=1");
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,22 +326,36 @@ fn harness_winui3_radio_select() {
/// fails on RangeValuePattern-only elements. Real fix: cua-driver should
/// try RangeValuePattern.SetValue (coercing the string to a double) when
/// ValuePattern isn't found.
/// WinUI3 Slider's parent AutomationId doesn't surface in the flat UIA
/// element list (SliderAutomationPeer's children are the actionable
/// elements, not the slider itself). Still document the gap — fixing
/// the UIA enumeration to expose the slider as an indexed element is
/// orthogonal to this branch's set_value fix.
/// Regression guard for Slider element enumeration + RangeValuePattern
/// set_value. cua-driver's UIA cache now pre-fetches RangeValuePattern,
/// and `detect_cached_actions` reports `set_value` when present — so
/// Slider parents get an `[N]` flat-tree index. The `set_value` tool
/// already falls back to RangeValuePattern, so writing the value works
/// end-to-end against a WinUI3 Slider.
#[test]
#[ignore]
fn harness_winui3_slider_DOCUMENTED_unreachable() {
fn harness_winui3_slider_set_value() {
winui3_with_session(|pid, wid, stdin, stdout| {
let snap = tools_call(stdin, stdout, 20, "get_window_state",
serde_json::json!({"pid": pid as i64, "window_id": wid, "capture_mode": "ax"}));
let idx_opt = find_idx(snapshot_text(&snap), "sld-value");
assert!(idx_opt.is_none(),
"WinUI3 Slider 'sld-value' now appears in the UIA flat tree — \
cua-driver fixed the slider-element enumeration gap. Update this test.");
println!("⚠️ harness_winui3_slider_DOCUMENTED_unreachable: sld-value not in UIA flat tree");
let idx = find_idx(snapshot_text(&snap), "sld-value")
.expect("sld-value should now be in the UIA flat tree after RangeValuePattern detection fix");
let resp = tools_call(stdin, stdout, 30, "set_value", serde_json::json!({
"pid": pid as i64, "window_id": wid, "element_index": idx,
"value": "42"
}));
println!("set_value sld-value=42: {}", resp["result"]["content"][0]["text"]);
std::thread::sleep(Duration::from_millis(400));
let post = tools_call(stdin, stdout, 31, "get_window_state",
serde_json::json!({"pid": pid as i64, "window_id": wid, "capture_mode": "ax"}));
let text = snapshot_text(&post);
let advanced = text.lines().any(|l|
l.contains("slider_value=") && !l.contains("slider_value=0\""));
assert!(advanced,
"WinUI3 Slider didn't move via RangeValuePattern.SetValue. Lines: {}",
text.lines().filter(|l| l.contains("slider_value"))
.collect::<Vec<_>>().join(" / "));
println!("✅ harness_winui3_slider_set_value: value moved via UIA RangeValuePattern.SetValue");
});
}

Expand Down
58 changes: 38 additions & 20 deletions libs/cua-driver/rust/crates/cua-driver/tests/harness_wpf_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,37 +676,55 @@ fn harness_wpf_layered_popup_capture() {

#[test]
#[ignore]
fn harness_wpf_slider_drag_tool_returns() {
// Coverage note for the drag tool against a WPF Slider thumb.
fn harness_wpf_slider_drag() {
// Regression guard for the SendInput drag path. PostMessage drag
// doesn't update GetKeyState, so WPF's Thumb-drag handler (which
// polls Mouse.LeftButton via GetKeyState) never sees the button
// held — the thumb stays put. dispatch:"foreground" routes through
// send_drag_synthesized which goes via the system input queue and
// DOES update GetKeyState, so the thumb actually tracks.
//
// PostMessage WM_LBUTTONDOWN / WM_MOUSEMOVE / WM_LBUTTONUP doesn't
// update the OS keyboard/mouse state visible to GetKeyState. WPF's
// Slider Thumb relies on Mouse.LeftButton (which polls GetKeyState)
// to recognise an in-progress drag — so a PostMessage drag never
// moves a WPF thumb, even when from/to are correctly on the thumb
// in client coords. The companion `harness_wpf_slider_increase_large`
// test covers the slider via UIA Invoke on its internal IncreaseLarge
// sub-button, which is what an agent SHOULD use for slider
// manipulation on a backgrounded window.
//
// We still exercise the drag tool against the slider so its codepath
// (coord translation, dispatch policy, message synthesis) is on the
// critical-path test list — we just don't assert on the value moving.
// TODO: add a SendInput-based drag path so dispatch:"foreground" can
// drive the thumb, then enable a behavioral assertion here.
// Foreground-lock caveat: SetForegroundWindow can be rejected from
// non-UIAccess processes during the daemon-process foreground swap.
// bring_to_front first to make the harness foreground (via
// AttachThreadInput), then SendInput's own SetForegroundWindow is a
// no-op success.
with_session(|pid, wid, stdin, stdout| {
focus_harness(stdin, stdout, pid, wid);
let pre = snapshot_elements(stdin, stdout, pid, wid);
assert!(snapshot_text(&pre).contains("slider_value=0"),
"initial slider_value=0 missing");

let resp = tools_call(stdin, stdout, 30, "drag", serde_json::json!({
"pid": pid as i64, "window_id": wid,
// drag screen-coords path: send_drag_synthesized takes screen
// coords directly. The harness window is centered at
// (517, 66) with the slider track at client (50-330, 275);
// convert to screen via ClientToScreen approximation by
// offsetting by window position + non-client chrome
// (title bar + border ~30,8). screen coords here are
// re-derived in window-local form by the tool's existing
// ClientToScreen step.
"from_x": 50.0, "from_y": 275.0,
"to_x": 330.0, "to_y": 275.0,
"duration_ms": 600, "steps": 30
"duration_ms": 700, "steps": 40,
"dispatch": "foreground"
}));
let msg = resp["result"]["content"][0]["text"].as_str().unwrap_or("");
println!("drag slider: {msg}");
println!("drag slider (foreground): {msg}");
assert!(msg.starts_with("✅"),
"drag tool returned non-success: {msg}");
println!("✅ harness_wpf_slider_drag_tool_returns: PostMessage drag emitted (known no-op vs WPF Thumb)");
std::thread::sleep(Duration::from_millis(500));

let post = snapshot_elements(stdin, stdout, pid, wid);
let text = snapshot_text(&post);
let advanced = text.lines().any(|l|
l.contains("slider_value=") && !l.contains("slider_value=0\""));
assert!(advanced,
"Slider value did not advance via SendInput drag. Lines: {}",
text.lines().filter(|l| l.contains("slider_value"))
.collect::<Vec<_>>().join(" / "));
println!("✅ harness_wpf_slider_drag: thumb tracked via SendInput drag");
});
}

Expand Down
130 changes: 130 additions & 0 deletions libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,133 @@ pub fn send_click_synthesized(

Ok(())
}

/// Press-hold-move-release drag via `SendInput`. Companion to
/// [`send_click_synthesized`] for the `drag` tool's `dispatch:"foreground"`
/// path.
///
/// Why a SendInput drag is needed at all: the PostMessage drag path posts
/// `WM_LBUTTONDOWN` + `WM_MOUSEMOVE`s + `WM_LBUTTONUP` to the target HWND.
/// PostMessage does NOT update the per-thread keyboard state that
/// `GetKeyState(VK_LBUTTON)` reads, so frameworks that poll the button-held
/// state during their drag handler (WPF's Thumb.IsDragging logic does this
/// via Mouse.LeftButton, which polls GetKeyState) never observe the button
/// as down and the drag is a no-op. SendInput goes through the system
/// input queue and DOES update GetKeyState, so a WPF Slider thumb actually
/// tracks the drag.
///
/// Same UIAccess constraints as [`send_click_synthesized`] — the
/// `SetForegroundWindow` swap is rejected from non-UIAccess processes
/// when foreground-lock is active; route through `cua-driver-uia.exe`
/// for reliable operation.
pub fn send_drag_synthesized(
target: u64,
sx_from: i32, sy_from: i32,
sx_to: i32, sy_to: i32,
duration_ms: u64,
steps: 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) {
bail!(msg);
}

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

let (vd_x, vd_y, vd_w, vd_h) = unsafe {
(
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN).max(1),
GetSystemMetrics(SM_CYVIRTUALSCREEN).max(1),
)
};
let norm = |sx: i32, sy: i32| -> (i32, i32) {
let nx = ((sx - vd_x) as i64 * 65535 / vd_w as i64).clamp(0, 65535) as i32;
let ny = ((sy - vd_y) as i64 * 65535 / vd_h as i64).clamp(0, 65535) as i32;
(nx, ny)
};
let make_input = |dx: i32, dy: i32, flags| INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx, dy, mouseData: 0,
dwFlags: flags | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
time: 0, dwExtraInfo: 0,
},
},
};

let steps = steps.max(1);
let step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { 0 };

Comment on lines +445 to +447

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Show the exact section around the reported lines
echo "=== Context around $FILE:430-470 ==="
nl -ba "$FILE" | sed -n '420,480p'

# Search for step_delay_ms usage within the file
echo "=== step_delay_ms usages in $FILE ==="
rg -n "step_delay_ms" "$FILE" -n

# Find any other mention of duration_ms/steps in the file
echo "=== duration_ms and steps in $FILE ==="
rg -n "duration_ms|steps\.max" "$FILE" -n

Repository: trycua/cua

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

echo "=== Context around $FILE:430-470 ==="
cat -n "$FILE" | sed -n '420,480p'

echo "=== step_delay_ms usages in $FILE ==="
rg -n "step_delay_ms" "$FILE"

echo "=== duration_ms and steps in $FILE ==="
rg -n "duration_ms|steps\.max" "$FILE"

echo "=== Search for duration/steps/drag contract clues in repo ==="
rg -n "drag.*duration|duration.*drag|steps.*drag|step_delay_ms" libs/cua-driver/rust/crates/platform-windows/src/input libs/cua-driver/rust/crates -S || true

Repository: trycua/cua

Length of output: 7138


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

echo "=== mouse.rs: lines 90-200 ==="
cat -n "$FILE" | sed -n '80,210p'

echo "=== mouse.rs: lines 360-540 ==="
cat -n "$FILE" | sed -n '360,540p'

echo "=== macOS mouse.rs: step_delay_ms logic ==="
MAC="libs/cua-driver/rust/crates/platform-macos/src/input/mouse.rs"
cat -n "$MAC" | sed -n '220,340p'

echo "=== Linux mod.rs: step_delay_ms logic ==="
LIN="libs/cua-driver/rust/crates/platform-linux/src/input/mod.rs"
cat -n "$LIN" | sed -n '60,170p'

Repository: trycua/cua

Length of output: 23555


Honor duration_ms when steps == 1 in Windows SendInput drag.

send_drag_synthesized sets step_delay_ms to 0 when steps == 1, so the only MOVE step isn’t followed by any delay (unlike the Windows PostMessage path and macOS/Linux, which use else { duration_ms }).

🛠️ Proposed fix
-    let step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { 0 };
+    let step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { duration_ms };
📝 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 steps = steps.max(1);
let step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { 0 };
let steps = steps.max(1);
let step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { duration_ms };
🤖 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/rust/crates/platform-windows/src/input/mouse.rs` around lines
445 - 447, The send_drag_synthesized implementation currently sets step_delay_ms
to 0 when steps == 1, causing the single MOVE to have no delay; change the
calculation so step_delay_ms is duration_ms when steps == 1 (i.e., use
step_delay_ms = if steps > 1 { duration_ms / steps as u64 } else { duration_ms
}) while keeping the existing steps = steps.max(1) logic; update the
send_drag_synthesized function and any related comments so the Windows SendInput
path honors duration_ms for the single-step drag like the PostMessage and
macOS/Linux paths.

unsafe {
let prev_fg = GetForegroundWindow();
let mut prev_cursor = POINT::default();
let _ = GetCursorPos(&mut prev_cursor);

let _ = SetForegroundWindow(target);
sleep(Duration::from_millis(8));
let actual_fg = GetForegroundWindow();
if actual_fg != target {
bail!(
"Foreground swap to target HWND {:?} was rejected by Windows \
(actual foreground is HWND {:?}). Non-UIAccess processes can't \
reliably change foreground under the foreground-lock. Route the \
drag through cua-driver-uia.exe.",
target.0, actual_fg.0
);
}

// 1. Move + press at the start of the drag.
let (nfx, nfy) = norm(sx_from, sy_from);
let _ = SetCursorPos(sx_from, sy_from);
let prelude = [
make_input(nfx, nfy, MOUSEEVENTF_MOVE),
make_input(nfx, nfy, down_flag),
];
let sent = SendInput(&prelude, std::mem::size_of::<INPUT>() as i32);
if sent as usize != prelude.len() {
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
bail!("SendInput drag-prelude inserted {sent}/{} events", prelude.len());
}

// 2. Interpolate the path. SetCursorPos + MOUSEEVENTF_MOVE in lockstep
// so both the visible cursor and the system input queue track the
// same path — WPF's drag-handler watches GetKeyState during each
// move event.
for i in 1..=steps {
let t = i as f64 / steps as f64;
let x = sx_from + ((sx_to - sx_from) as f64 * t).round() as i32;
let y = sy_from + ((sy_to - sy_from) as f64 * t).round() as i32;
let (nx, ny) = norm(x, y);
let _ = SetCursorPos(x, y);
let mv = [make_input(nx, ny, MOUSEEVENTF_MOVE)];
let _ = SendInput(&mv, std::mem::size_of::<INPUT>() as i32);
if step_delay_ms > 0 {
sleep(Duration::from_millis(step_delay_ms));
}
}

// 3. Release at the end.
let (ntx, nty) = norm(sx_to, sy_to);
let release = [make_input(ntx, nty, up_flag)];
let _ = SendInput(&release, std::mem::size_of::<INPUT>() as i32);

Comment on lines +484 to +501

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

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Show relevant function/region with line numbers around 430-520
nl -ba "$FILE" | sed -n '420,525p'

# Also search for SendInput usage in this file to compare patterns
rg -n "SendInput\\(" "$FILE"

# Show definitions of any variables referenced in proposed fix (prev_cursor, prev_fg, up_flag) if present
rg -n "prev_cursor|prev_fg|up_flag" "$FILE"

Repository: trycua/cua

Length of output: 97


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Show relevant function/region with line numbers around 420-525
nl -ba "$FILE" | sed -n '420,525p'

# Search for SendInput usage patterns in this file
rg -n "SendInput\\(" "$FILE"

# Search for variables mentioned in the proposed diff
rg -n "prev_cursor|prev_fg|up_flag" "$FILE"

Repository: trycua/cua

Length of output: 97


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

nl -ba "$FILE" | sed -n '420,525p'
rg -n "SendInput\\(" "$FILE"
rg -n "prev_cursor|prev_fg|up_flag" "$FILE"

Repository: trycua/cua

Length of output: 97


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Show relevant function/region with line numbers around 420-525 (use cat -n since nl missing)
cat -n "$FILE" | sed -n '420,525p'

# Search for SendInput usage patterns in this file
rg -n "SendInput\\(" "$FILE"

# Search for variables mentioned in the proposed diff
rg -n "prev_cursor|prev_fg|up_flag" "$FILE"

Repository: trycua/cua

Length of output: 5577


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

rg -n "post_drag|duration_ms|step_delay_ms|steps > 1|drag.*duration" "$FILE"

Repository: trycua/cua

Length of output: 506


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"
cat -n "$FILE" | sed -n '90,210p'

Repository: trycua/cua

Length of output: 5747


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Find the function definition line numbers
rg -n "send_drag_synthesized" "$FILE"

# Show a broader window around its signature and doc (to understand duration_ms contract)
cat -n "$FILE" | sed -n '320,430p'

Repository: trycua/cua

Length of output: 5794


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs"

# Find call sites of send_drag_synthesized
rg -n "send_drag_synthesized\\(" -S .

# Search for mentions of duration_ms contract in code/docs near drag usage
rg -n "duration_ms" libs/cua-driver/rust/crates/platform-windows/src/input/mouse.rs
rg -n "send_drag_synthesized|post_drag" -S libs/cua-driver/rust/crates/platform-windows/src

Repository: trycua/cua

Length of output: 941


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs"
# Show around the send_drag_synthesized call and any computation of duration_ms/steps
cat -n "$FILE" | sed -n '3380,3520p'

Repository: trycua/cua

Length of output: 7918


Check SendInput return values for synthesized drag move + release

In send_drag_synthesized, the SendInput results are ignored for the interpolated move (mv, line 491) and final release (release, line 500). The drag prelude already checks for partial insertion and bails/restores state; the same sent as usize != ...len() handling should be added here to avoid cases where the release isn’t actually inserted (leaving the button logically held).

duration_ms is also not honored when steps == 1 in send_drag_synthesized (step_delay_ms becomes 0 at line 446), unlike post_drag which uses duration_ms in that case.

🤖 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/rust/crates/platform-windows/src/input/mouse.rs` around lines
484 - 501, In send_drag_synthesized, the SendInput calls for the interpolated
move (mv) and final release (release) ignore return values and therefore can
leave the button logically held; change both SendInput invocations to check the
returned sent count (e.g., compare sent as usize == mv.len() and ==
release.len()) and handle partial/zero insertions the same way the drag prelude
does (restore state or return an error), and also ensure duration_ms is honored
when steps == 1 by making step_delay_ms use duration_ms in that case (or
special-case a single-step delay to wait duration_ms before sending the release,
consistent with post_drag). Ensure you reference send_drag_synthesized, mv,
release, SendInput, steps, step_delay_ms, duration_ms and post_drag when
applying the fixes.

// Brief settle, then restore previous state.
sleep(Duration::from_millis(40));
let _ = SetCursorPos(prev_cursor.x, prev_cursor.y);
let _ = SetForegroundWindow(prev_fg);
}

Ok(())
}
Loading
Loading