Skip to content

feat(cua-driver-rs): unify browser_eval + page into one cross-platform tool - #1666

Merged
f-trycua merged 7 commits into
mainfrom
page-tool-unification
May 23, 2026
Merged

feat(cua-driver-rs): unify browser_eval + page into one cross-platform tool#1666
f-trycua merged 7 commits into
mainfrom
page-tool-unification

Conversation

@f-trycua

@f-trycua f-trycua commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Six logical commits. Drops the duplicate browser_eval tool, promotes page to cross-platform via a new PageBackend trait, ships Windows (UIA) + Linux (AT-SPI) backends. macOS keeps its Apple Events / Electron-CDP / AX-WebView routing — strict refactor for callers.

Informed by a 3-agent research spike + an empirical Windows-Edge test battery (see follow-up below).

Why drop browser_eval

macOS Windows Linux Swift driver
browser_eval (pre-PR) ✅ registered (redundant alongside page) ✅ registered (sole option) ✅ registered (sole option) never existed
page (pre-PR) ✅ registered (4 actions, 4 backends) ✅ canonical

browser_eval only ever existed in the Rust port — it was CDP-only, single-action (Runtime.evaluate), and on macOS shipped alongside the strictly-more-capable page. This PR removes the duplicate, brings Windows + Linux up to page's 3-action surface, and re-syncs Rust ↔ Swift parity.

What each platform now does for page

Action macOS Windows Linux
get_text Apple Events → document.body.innerText (BrowserJs); CDP for Electron; AX walk for WKWebView UIA TextPattern.DocumentRange.GetText(-1) on the web Document element — no --remote-debugging-port needed AT-SPI text walk
query_dom Apple Events querySelectorAll JS UIA FindAll(TreeScope_Subtree, …) with CSS→ControlType mapping (a[href]→Hyperlink, button→Button, input→Edit, h1-h6→Heading, li→ListItem, img→Image, [role=*]→ControlType). #id/.class/[role=*] post-filter via AutomationId. [data-*] errors with a pointer at execute_javascript. AT-SPI role-based query
execute_javascript Apple Events JS; Electron→CDP CDP via the new shared mcp_server::cdp helper, discovers port via $CUA_DRIVER_CDP_PORT env var. Returns an actionable error when not set. CDP via the same shared helper
enable_javascript_apple_events full impl "not supported on this platform" via the trait's default impl same

The Windows UIA path is research-validated: same pattern NVDA, pywinauto, and AccessKit-consumers all use. Chrome 138+ (June 2025) exposes native UIA on by default; before that it was IA2→UIA proxy — both work.

Architecture

  • crates/mcp-server/src/page.rs (new, 240 lines) — PageBackend trait + cross-platform PageTool. Tool name, schema, and action dispatch live here; each platform supplies a backend.
  • crates/mcp-server/src/cdp.rs (new, extracted from browser_eval.rs) — shared raw-TCP CDP Runtime.evaluate helper. Reused by Windows + Linux JS-exec paths.
  • crates/mcp-server/src/browser_eval.rsdeleted.
  • crates/platform-macos/src/tools/page.rs — rewritten as MacOsPageBackend impl, all 4 actions preserved.
  • crates/platform-windows/src/tools/page.rs (new, 449 lines) — WindowsPageBackend with UIA + shared CDP.
  • crates/platform-linux/src/tools/page.rs (new, 159 lines) — LinuxPageBackend with AT-SPI + shared CDP.
  • Each platform's tool-registration entry point constructs and registers its PageTool with the right backend.

Verification

  • cargo build --release -p cua-driver on x86_64-pc-windows-msvc0 warnings, 0 errors
  • cargo test --release -p cua-driver --test mcp_protocol_test28/28 passed
  • cargo test --release -p platform-windows16/16 passed
  • macOS + Linux compile-checks deferred to reviewer host (no Mac/Linux VM this session). The modules are cfg-gated cleanly; behaviour-equivalent to the previous macOS-only page tool plus new platform impls.

Explicit follow-ups (deliberately deferred)

  1. CDP-port discovery on Windows + Linux currently reads $CUA_DRIVER_CDP_PORT. Richer discovery (parsing --remote-debugging-port from the live browser's command line — NtQueryInformationProcess / PEB read on Windows, /proc/<pid>/cmdline on Linux) is a clean follow-up. Today's error message points users at the env var.
  2. 🎯 UIA-JS-exec primary path on Windows. A separate empirical Edge test battery this session confirmed that UIA ValuePattern::SetValue on the address-bar Edit element with javascript:try{document.title=…} then InvokePattern Enter executes arbitrary JS in the existing tab's existing DOM context — no --remote-debugging-port, no extension, no driver. Chromium's omnibox javascript: paste-strip is gated on user-input tracking and UIA writes route through EditModel::SetUserText() without setting the flag. This will land in a follow-up PR that upgrades execute_javascript on Windows from CDP-required to UIA-primary + CDP-fallback. Tracked separately so this PR's review surface stays focused on the unification.
  3. #id selector uses AutomationId on Windows — works for Chromium's common case but isn't universally guaranteed. Full IA2 attributes-string parse (via LegacyIAccessiblePattern.GetIAccessible()QI(IAccessible2)) is a richer follow-up.
  4. [role=*] ARIA mapping is the common subset (button, link, textbox, heading, image, listitem). Extending to the full ARIA taxonomy is mechanical.
  5. macOS BrowserJs::CdpClient is its own internal CDP client, predating the new shared mcp_server::cdp helper. Could be deduped — small win, not load-bearing.
  6. Linux page backend isn't runtime-tested. No Linux VM available this session. Builds clean; assumes AT-SPI roles match the implementer's mapping table. Small risk of role-name skew across desktops/distros.

Test plan

  • cargo build --release -p cua-driver clean on Windows
  • cargo test --release -p cua-driver --test mcp_protocol_test green (28/28)
  • cargo test --release -p platform-windows green (16/16)
  • macOS reviewer: cargo check -p platform-macos to confirm the migrated page.rs compiles
  • Linux reviewer: cargo check -p platform-linux to confirm the new backend compiles
  • End-to-end smoke on each platform: cua-driver page '{"pid":N,"window_id":W,"action":"get_text"}' against Chrome / Edge / Firefox

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced cross-platform page tool for interacting with application pages across macOS, Linux, and Windows.
    • Added support for JavaScript execution via Chrome DevTools Protocol on Linux and Windows.
  • Bug Fixes & Removals

    • Removed deprecated browser_eval tool; replaced with page tool.
  • Documentation

    • Updated guidance to reference page tool instead of browser_eval.
    • Clarified platform-specific backend behavior and requirements (e.g., CUA_DRIVER_CDP_PORT for JavaScript execution).
  • Tests

    • Updated integration tests to verify page tool behavior.

Review Change Stack

f-trycua and others added 6 commits May 23, 2026 18:09
…red CDP helper

Step (a) of the page-tool unification. Adds two new modules to mcp-server
without touching any existing code paths yet:

- `page` — cross-platform `PageTool` MCP tool. Holds an `Arc<dyn PageBackend>`
  the host platform supplies at registration time; preserves the existing
  `page` MCP schema (name, description, input_schema, action set) verbatim so
  callers see no behavior change once each platform wires in a backend.
- `cdp` — self-contained `Runtime.evaluate` client (raw TCP, no extra crates).
  Will be used by Windows + Linux `execute_javascript` once `browser_eval` is
  retired.

The `PageBackend` trait keeps `enable_javascript_apple_events` macOS-specific
via a default impl that returns an unsupported-platform error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…end impl

Step (b) of the page-tool unification. The macOS `PageTool` is now a thin
`MacOsPageBackend` impl of `mcp_server::page::PageBackend`; the MCP tool
definition itself (name, schema, action dispatch) lives in mcp-server and is
registered from `tools::register_all` via the shared `PageTool` wrapper.

Routing logic — Apple Events for Chromium/Safari, CDP for Electron, AX-tree
fallback for WKWebView/Tauri — is preserved verbatim. The macOS-only
`enable_javascript_apple_events` action is implemented via the trait's
overridable method, so non-macOS callers still get a clear unsupported-platform
error.

Callers see no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step (c) of the page-tool unification. Adds `WindowsPageBackend` implementing
`mcp_server::page::PageBackend` and registers the cross-platform `page` tool.

- `get_text` — locate the descendant Document UIA control under the HWND and
  read `TextPattern.DocumentRange.GetText(-1)`. Falls back to TextPattern on
  the root, then to a name-concatenation walk for the rare case where neither
  is supported.
- `query_dom` — maps common CSS selectors to UIA ControlType conditions
  (`a` → Hyperlink, `button` → Button, `input`/`textarea` → Edit, `h1`-`h6` →
  Header, `li` → ListItem, `img` → Image, `[role=...]` → ControlType) and
  runs `FindAll(TreeScope_Subtree, condition)` rooted at the web Document.
  Post-filters `#id` against `AutomationId`. Rejects `[data-*]` selectors
  with an actionable error pointing at `execute_javascript`.
- `execute_javascript` — uses the shared `mcp_server::cdp` helper. CDP port
  is discovered from `CUA_DRIVER_CDP_PORT`; when missing, returns an error
  telling the caller to relaunch with `--remote-debugging-port=N` (parsing
  the live browser's command line via PEB is a separate follow-up).

Builds clean (zero warnings) for x86_64-pc-windows-msvc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step (d) of the page-tool unification. Adds `LinuxPageBackend` implementing
`mcp_server::page::PageBackend` and registers the cross-platform `page` tool
from `tools::register_all`.

- `get_text` / `query_dom` reuse the existing `crate::atspi::walk_tree`
  infrastructure. `query_dom` maps common CSS tag selectors to AT-SPI role
  names (a → link, button → push button, h1-h6 → heading, etc.) and filters
  the walked nodes by role. `[data-*]` selectors error out with a pointer at
  `execute_javascript`.
- `execute_javascript` calls the shared `mcp_server::cdp` helper; CDP port is
  read from `CUA_DRIVER_CDP_PORT` (same convention as the Windows backend).

Not runtime-tested — no Linux VM on hand — but the workspace builds cleanly
with the rest of the tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step (e) of the page-tool unification.

`browser_eval` was a hand-rolled, Chromium-only `Runtime.evaluate` tool. Its
functionality is now part of the cross-platform `page` tool's
`execute_javascript` action on every platform; the raw-TCP CDP client moved
into `mcp_server::cdp` in step (a).

- Delete `crates/mcp-server/src/browser_eval.rs`.
- Drop `pub mod browser_eval;` from `mcp-server/src/lib.rs`.
- Remove `BrowserEvalTool` from `ToolRegistry::register_recording_tools`.
- Update `mcp_protocol_test.rs` and `test_api_parity.py` to expect `page`
  instead of `browser_eval` in the tools/list and to drop the now-obsolete
  Rust-only / Swift-only matrix rows.

`browser_eval` never made it to the Swift driver and was not a stable public
API, so it is removed outright with no alias / deprecation shim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…val refs

Step (f) of the page-tool unification.

- `PARITY.md` — remove the `browser_eval` row from the parity gaps table
  and the entry in the example `tools/list` payload; note that `page` is
  now cross-platform (Apple-Events on macOS, UIA+CDP on Windows,
  AT-SPI+CDP on Linux).
- `SKILL.md` — replace the "macOS-only" note about browser JS primitives
  with the cross-platform routing summary.
- `MACOS.md` — add a leading note that `page` is cross-platform and this
  section documents the macOS Apple-Events routing specifically.
- `WEB_APPS.md` — preserve the macOS-only scope of this doc but flag the
  `page` tool itself as cross-platform.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel

vercel Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 23, 2026 6:43pm

Request Review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d996fabc-c5ee-42cb-9b48-5d50616f592c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces the MCP browser_eval tool with a cross-platform page tool. It refactors CDP evaluation into a reusable helper, defines a platform-agnostic PageBackend trait, and implements platform-specific backends for Linux, macOS, and Windows. Documentation, tests, and tool registry are updated throughout.

Changes

Cross-Platform Page Tool Migration

Layer / File(s) Summary
Documentation & Test Philosophy Updates
libs/cua-driver-fixtures/README.md, libs/cua-driver-rs/PARITY.md, libs/cua-driver-rs/Skills/cua-driver-rs/MACOS.md, libs/cua-driver-rs/Skills/cua-driver-rs/SKILL.md, libs/cua-driver-rs/Skills/cua-driver-rs/WEB_APPS.md, libs/cua-driver-rs/tests/integration/v2/PHILOSOPHY.md
Documentation is updated to describe page as a cross-platform tool with platform-specific routing (Apple Events/AX on macOS, UIA/AT-SPI on Windows/Linux, CDP for JavaScript). The browser_eval tool is removed from documentation and parity tables. Test philosophy is clarified to forbid internal mechanisms like page.execute_javascript.
CDP Helper Refactoring & Page Tool Abstraction
libs/cua-driver-rs/crates/mcp-server/src/cdp.rs, libs/cua-driver-rs/crates/mcp-server/src/page.rs
The CDP evaluation logic is extracted into a reusable pub async fn evaluate(port, expression, await_promise) that performs tab discovery via HTTP, WebSocket upgrade and handshake, and Runtime.evaluate protocol exchange. A new PageBackend trait abstracts platform-specific implementations with methods for get_text, query_dom, execute_javascript, and macOS-only enable_javascript_apple_events. PageTool wraps the backend and dispatches MCP tool actions.
MCP Server Module Wiring & Registry
libs/cua-driver-rs/crates/mcp-server/src/lib.rs, libs/cua-driver-rs/crates/mcp-server/src/tool.rs
Module exports for cdp and page are added to the public crate API. BrowserEvalTool is removed from tool registry; only recording/replay tools remain registered via ToolRegistry::register_recording_tools().
Platform-Specific Page Backend Implementations
libs/cua-driver-rs/crates/platform-linux/src/tools/mod.rs, libs/cua-driver-rs/crates/platform-linux/src/tools/page.rs, libs/cua-driver-rs/crates/platform-linux/src/tools/impl_.rs, libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs, libs/cua-driver-rs/crates/platform-macos/src/tools/page.rs, libs/cua-driver-rs/crates/platform-windows/src/tools/mod.rs, libs/cua-driver-rs/crates/platform-windows/src/tools/page.rs, libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
Linux: LinuxPageBackend uses AT-SPI walk_tree for text/DOM extraction and CDP for JavaScript. macOS: MacOsPageBackend routes through JavaScript (when supported), Electron detection, and AX-tree fallback; implements macOS-only enable_javascript_apple_events. Windows: WindowsPageBackend uses UI Automation for text/DOM extraction and CDP for JavaScript. Each platform backend is registered via the tool registry.
Integration Tests & API Parity Validation
libs/cua-driver-rs/crates/cua-driver/tests/mcp_protocol_test.rs, libs/cua-driver-rs/tests/integration/test_api_parity.py
MCP protocol tests now expect page tool in tool registry and validate unknown-action error handling. API parity matrix marks page as cross-platform (Rust available, not in Swift). browser_eval is removed from Rust-only tools list and all related tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • trycua/cua#1387: Introduces the consent-gating feature (user_has_confirmed_enabling) that this PR integrates into the page tool's enable_javascript_apple_events action.
  • trycua/cua#1619: Adds the gesture-panel fixture workflow and documentation that this PR updates to use page tool's execute_javascript instead of browser_eval.
  • trycua/cua#1624: Adds gesture-panel HTML and fixture docs; this PR migrates the verification approach in the same fixture README to use page tool instead of browser_eval.

🐰 A browser's heart, once fragmented and lone,
Now beats cross-platform, in unified tone.
CDP, AT-SPI, UIA, AX—
One page tool steers them on their tracks!
No more eval ghosts in the machinery,
Just execute_javascript poetry!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: unifying browser_eval and page into a single cross-platform tool, which aligns with the comprehensive refactoring described in the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch page-tool-unification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 Prompt for all review comments with 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.

Inline comments:
In `@libs/cua-driver-rs/crates/mcp-server/src/cdp.rs`:
- Around line 39-40: Wrap the unbounded CDP discovery and socket reads in a
timeout: use tokio::time::timeout (e.g., Duration::from_secs(30)) around the
call to cdp_list_pages(port).await and around any socket/read loop used for
page.execute_javascript so the function returns a Timeout error instead of
hanging; update error handling to propagate or convert the timeout into the
existing Result error path. Locate the cdp_list_pages invocation and the
socket/read logic used by page.execute_javascript and apply the timeout wrapper
consistently, returning an appropriate error when timeout occurs.

In `@libs/cua-driver-rs/crates/mcp-server/src/page.rs`:
- Around line 103-107: The JSON RPC handler currently enforces pid and window_id
for every "action" (making "enable_javascript_apple_events" fail), so remove
pid/window_id from the global "required" list and change the pre-dispatch
validation to be action-specific: leave "action" required in the schema but make
"pid" and "window_id" optional, then in the dispatch logic (the block that
checks pid/window_id before calling the action) match on the parsed action
string/enum (e.g. "enable_javascript_apple_events" /
Action::EnableJavascriptAppleEvents) and only require/unwrap pid/window_id for
the actions that actually need them, skipping that check for
enable_javascript_apple_events; apply this change in both validation sites
referenced in the review.
- Around line 151-157: The code narrows JSON numbers with unchecked casts for
pid and window_id causing silent truncation; instead, after extracting the
i64/u64 from args (the same args.get(...).and_then(...) calls), attempt a safe
conversion using TryFrom/TryInto (e.g., i32::try_from(pid_i64) and
u32::try_from(window_id_u64>) and handle the Result; if conversion fails return
ToolResult::error with an explanatory message like "Invalid parameter: pid out
of range" or "Invalid parameter: window_id out of range" rather than "missing",
and keep the variables named pid and window_id for downstream use so other code
(in page.rs) continues to compile.

In `@libs/cua-driver-rs/crates/platform-linux/src/tools/page.rs`:
- Around line 62-69: The filter closure using role_for_selector(&selector)
currently treats None as a match-all (None => true), causing unsupported
selectors to return every node; change those fallbacks to None => false so that
when role_for_selector returns None the filter excludes nodes instead of
including them. Update the three occurrences where role_for_selector(&selector)
is used in the closures filtering result.nodes.iter() (the closures around
role.as_deref() that currently use None => true) to use None => false so
unsupported selectors don't match; keep the rest of the filter logic intact.

In `@libs/cua-driver-rs/crates/platform-windows/src/tools/page.rs`:
- Around line 396-399: The selector parsing currently treats unsupported forms
(e.g., ".class") as parseable because tag.find('#') fallback sets tag_clean and
id_filter to empty, causing query_dom_blocking to scan the full subtree and
return false positives; update the parsing around the tag handling (the code
that sets tag_clean and id_filter from tag.find('#')) to detect and reject
unsupported selectors (e.g., if tag starts with '.' or contains other unexpected
chars) instead of returning empty values, and propagate that "unparseable"
result into query_dom_blocking so it fails fast (return no matches) unless the
selector is the universal "*" — apply the same change to the other parsing sites
you mentioned (the blocks around lines 401-419 and 146-207) and ensure
functions/query paths using tag_clean and id_filter check for an explicit
parse-failure sentinel before performing subtree queries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9720ac57-ecc2-4e40-bcec-0ae10b0460b4

📥 Commits

Reviewing files that changed from the base of the PR and between c52d850 and bda1d2b.

📒 Files selected for processing (20)
  • libs/cua-driver-fixtures/README.md
  • libs/cua-driver-rs/PARITY.md
  • libs/cua-driver-rs/Skills/cua-driver-rs/MACOS.md
  • libs/cua-driver-rs/Skills/cua-driver-rs/SKILL.md
  • libs/cua-driver-rs/Skills/cua-driver-rs/WEB_APPS.md
  • libs/cua-driver-rs/crates/cua-driver/tests/mcp_protocol_test.rs
  • libs/cua-driver-rs/crates/mcp-server/src/cdp.rs
  • libs/cua-driver-rs/crates/mcp-server/src/lib.rs
  • libs/cua-driver-rs/crates/mcp-server/src/page.rs
  • libs/cua-driver-rs/crates/mcp-server/src/tool.rs
  • libs/cua-driver-rs/crates/platform-linux/src/tools/impl_.rs
  • libs/cua-driver-rs/crates/platform-linux/src/tools/mod.rs
  • libs/cua-driver-rs/crates/platform-linux/src/tools/page.rs
  • libs/cua-driver-rs/crates/platform-macos/src/tools/mod.rs
  • libs/cua-driver-rs/crates/platform-macos/src/tools/page.rs
  • libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
  • libs/cua-driver-rs/crates/platform-windows/src/tools/mod.rs
  • libs/cua-driver-rs/crates/platform-windows/src/tools/page.rs
  • libs/cua-driver-rs/tests/integration/test_api_parity.py
  • libs/cua-driver-rs/tests/integration/v2/PHILOSOPHY.md
💤 Files with no reviewable changes (1)
  • libs/cua-driver-rs/crates/mcp-server/src/tool.rs

Comment on lines 39 to 40
let pages = cdp_list_pages(port).await?;
if pages.is_empty() {

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

Add a timeout for CDP /json discovery.

Line 96–110 runs an unbounded socket read before the 30s timeout in Line 60–66 is applied. A stalled local endpoint can hang page.execute_javascript indefinitely.

💡 Proposed fix
 async fn cdp_evaluate(
     port: u16,
     expression: &str,
     await_promise: bool,
 ) -> anyhow::Result<Value> {
-    let pages = cdp_list_pages(port).await?;
+    let pages = tokio::time::timeout(
+        std::time::Duration::from_secs(10),
+        cdp_list_pages(port),
+    )
+    .await
+    .map_err(|_| anyhow::anyhow!("CDP /json discovery timed out after 10 s"))??;

Also applies to: 60-66, 96-110

🤖 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/mcp-server/src/cdp.rs` around lines 39 - 40, Wrap
the unbounded CDP discovery and socket reads in a timeout: use
tokio::time::timeout (e.g., Duration::from_secs(30)) around the call to
cdp_list_pages(port).await and around any socket/read loop used for
page.execute_javascript so the function returns a Timeout error instead of
hanging; update error handling to propagate or convert the timeout into the
existing Result error path. Locate the cdp_list_pages invocation and the
socket/read logic used by page.execute_javascript and apply the timeout wrapper
consistently, returning an appropriate error when timeout occurs.

Comment on lines +103 to +107
"required": ["pid", "window_id", "action"],
"properties": {
"pid": { "type": "integer", "description": "Target process ID." },
"window_id": { "type": "integer", "description": "Target window ID from list_windows." },
"action": {

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

enable_javascript_apple_events is incorrectly gated by pid/window_id.

Line 151–158 enforces pid and window_id before action dispatch, but Line 165–188 doesn’t use them. This makes the enable action fail unless callers pass irrelevant placeholder values.

💡 Proposed fix
     async fn invoke(&self, args: Value) -> ToolResult {
-        let pid = match args.get("pid").and_then(|v| v.as_i64()) {
-            Some(v) => v as i32,
-            None => return ToolResult::error("Missing required parameter: pid"),
-        };
-        let window_id = match args.get("window_id").and_then(|v| v.as_u64()) {
-            Some(v) => v as u32,
-            None => return ToolResult::error("Missing required parameter: window_id"),
-        };
         let action = match args.get("action").and_then(|v| v.as_str()) {
             Some(v) => v.to_owned(),
             None => return ToolResult::error("Missing required parameter: action"),
         };

         match action.as_str() {
             "enable_javascript_apple_events" => {
                 // existing branch unchanged
             }

             "execute_javascript" | "get_text" | "query_dom" => {
+                let pid = match args.get("pid").and_then(|v| v.as_i64()) {
+                    Some(v) => v as i32,
+                    None => return ToolResult::error("Missing required parameter: pid"),
+                };
+                let window_id = match args.get("window_id").and_then(|v| v.as_u64()) {
+                    Some(v) => v as u32,
+                    None => return ToolResult::error("Missing required parameter: window_id"),
+                };
                 // action logic...
             }

Also applies to: 150-158, 165-188

🤖 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/mcp-server/src/page.rs` around lines 103 - 107, The
JSON RPC handler currently enforces pid and window_id for every "action" (making
"enable_javascript_apple_events" fail), so remove pid/window_id from the global
"required" list and change the pre-dispatch validation to be action-specific:
leave "action" required in the schema but make "pid" and "window_id" optional,
then in the dispatch logic (the block that checks pid/window_id before calling
the action) match on the parsed action string/enum (e.g.
"enable_javascript_apple_events" / Action::EnableJavascriptAppleEvents) and only
require/unwrap pid/window_id for the actions that actually need them, skipping
that check for enable_javascript_apple_events; apply this change in both
validation sites referenced in the review.

Comment on lines +151 to +157
let pid = match args.get("pid").and_then(|v| v.as_i64()) {
Some(v) => v as i32,
None => return ToolResult::error("Missing required parameter: pid"),
};
let window_id = match args.get("window_id").and_then(|v| v.as_u64()) {
Some(v) => v as u32,
None => return ToolResult::error("Missing required parameter: window_id"),

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 and display the relevant portion of the file
sed -n '120,190p' libs/cua-driver-rs/crates/mcp-server/src/page.rs | cat -n

Repository: trycua/cua

Length of output: 3372


Validate integer bounds before narrowing casts (pid/window_id)

page.rs currently narrows JSON numbers with unchecked as casts (i64 -> i32 and u64 -> u32). Out-of-range values will be silently truncated and could target the wrong process/window.

💡 Proposed fix
-        let pid = match args.get("pid").and_then(|v| v.as_i64()) {
-            Some(v) => v as i32,
+        let pid = match args
+            .get("pid")
+            .and_then(|v| v.as_i64())
+            .and_then(|v| i32::try_from(v).ok())
+        {
+            Some(v) => v,
             None => return ToolResult::error("Missing required parameter: pid"),
         };
-        let window_id = match args.get("window_id").and_then(|v| v.as_u64()) {
-            Some(v) => v as u32,
+        let window_id = match args
+            .get("window_id")
+            .and_then(|v| v.as_u64())
+            .and_then(|v| u32::try_from(v).ok())
+        {
+            Some(v) => v,
             None => return ToolResult::error("Missing required parameter: window_id"),
         };

Also adjust the error message so it doesn’t claim the parameter is “missing” when it’s present but out of range.

📝 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 pid = match args.get("pid").and_then(|v| v.as_i64()) {
Some(v) => v as i32,
None => return ToolResult::error("Missing required parameter: pid"),
};
let window_id = match args.get("window_id").and_then(|v| v.as_u64()) {
Some(v) => v as u32,
None => return ToolResult::error("Missing required parameter: window_id"),
let pid = match args
.get("pid")
.and_then(|v| v.as_i64())
.and_then(|v| i32::try_from(v).ok())
{
Some(v) => v,
None => return ToolResult::error("Missing required parameter: pid"),
};
let window_id = match args
.get("window_id")
.and_then(|v| v.as_u64())
.and_then(|v| u32::try_from(v).ok())
{
Some(v) => v,
None => return ToolResult::error("Missing required parameter: window_id"),
};
🤖 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/mcp-server/src/page.rs` around lines 151 - 157, The
code narrows JSON numbers with unchecked casts for pid and window_id causing
silent truncation; instead, after extracting the i64/u64 from args (the same
args.get(...).and_then(...) calls), attempt a safe conversion using
TryFrom/TryInto (e.g., i32::try_from(pid_i64) and u32::try_from(window_id_u64>)
and handle the Result; if conversion fails return ToolResult::error with an
explanatory message like "Invalid parameter: pid out of range" or "Invalid
parameter: window_id out of range" rather than "missing", and keep the variables
named pid and window_id for downstream use so other code (in page.rs) continues
to compile.

Comment on lines +62 to +69
let role = role_for_selector(&selector);
let lines: Vec<String> = result
.nodes
.iter()
.filter(|n| match role.as_deref() {
Some(r) => n.role.eq_ignore_ascii_case(r),
None => true,
})

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

Avoid “match everything” fallback for unsupported selectors.

At Line 66-69, None => true causes query_dom to return all nodes when the selector is not one of the small mapped tag set. That makes selectors like .foo / #id / div > a behave incorrectly.

💡 Suggested fix
-        let role = role_for_selector(&selector);
+        let role = role_for_selector(&selector);
+        let selector_is_wildcard = selector.trim().is_empty() || selector.trim() == "*";
+        if role.is_none() && !selector_is_wildcard {
+            anyhow::bail!(
+                "Selector '{selector}' is not supported by Linux AT-SPI role mapping. \
+                 Use a simple tag selector (e.g. button, input, a) or `execute_javascript`."
+            );
+        }
         let lines: Vec<String> = result
             .nodes
             .iter()
             .filter(|n| match role.as_deref() {
                 Some(r) => n.role.eq_ignore_ascii_case(r),
-                None => true,
+                None => true, // only wildcard path reaches here
             })

Also applies to: 85-89, 117-137

🤖 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-linux/src/tools/page.rs` around lines 62 -
69, The filter closure using role_for_selector(&selector) currently treats None
as a match-all (None => true), causing unsupported selectors to return every
node; change those fallbacks to None => false so that when role_for_selector
returns None the filter excludes nodes instead of including them. Update the
three occurrences where role_for_selector(&selector) is used in the closures
filtering result.nodes.iter() (the closures around role.as_deref() that
currently use None => true) to use None => false so unsupported selectors don't
match; keep the rest of the filter logic intact.

Comment on lines +396 to +399
let (tag_clean, id_filter) = match tag.find('#') {
Some(i) => (&tag[..i], tag[i + 1..].to_owned()),
None => (tag, String::new()),
};

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

Selector parsing currently over-matches for .class/unsupported forms.

At Line 396-399, only #id is parsed. For .class or other unsupported selectors, control_type and id_filter are both empty, so Line 143 + Line 146 query the full subtree and return broad false positives.

💡 Suggested fix
 struct ParsedSelector {
     control_type: Option<UIA_CONTROLTYPE_ID>,
     id_filter: String,
+    class_filter: String,
     is_data_attr: bool,
 }

 fn parse_selector(sel: &str) -> anyhow::Result<ParsedSelector> {
@@
-    let (tag_clean, id_filter) = match tag.find('#') {
-        Some(i) => (&tag[..i], tag[i + 1..].to_owned()),
-        None => (tag, String::new()),
-    };
+    let (tag_clean, id_filter, class_filter) = if let Some(i) = tag.find('#') {
+        (&tag[..i], tag[i + 1..].to_owned(), String::new())
+    } else if let Some(i) = tag.find('.') {
+        (&tag[..i], String::new(), tag[i + 1..].to_owned())
+    } else {
+        (tag, String::new(), String::new())
+    };
@@
     Ok(ParsedSelector {
         control_type: ct,
         id_filter,
+        class_filter,
         is_data_attr: false,
     })
 }

And in query_dom_blocking, fail fast when nothing parseable is present (except *) instead of defaulting to full subtree:

+    let wildcard = selector.trim().is_empty() || selector.trim() == "*";
+    if parsed.control_type.is_none() && parsed.id_filter.is_empty() && parsed.class_filter.is_empty() && !wildcard {
+        anyhow::bail!("Unsupported selector '{selector}' for Windows UIA path; use `execute_javascript`.");
+    }

Also applies to: 401-419, 146-207

🤖 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/tools/page.rs` around lines
396 - 399, The selector parsing currently treats unsupported forms (e.g.,
".class") as parseable because tag.find('#') fallback sets tag_clean and
id_filter to empty, causing query_dom_blocking to scan the full subtree and
return false positives; update the parsing around the tag handling (the code
that sets tag_clean and id_filter from tag.find('#')) to detect and reject
unsupported selectors (e.g., if tag starts with '.' or contains other unexpected
chars) instead of returning empty values, and propagate that "unparseable"
result into query_dom_blocking so it fails fast (return no matches) unless the
selector is the universal "*" — apply the same change to the other parsing sites
you mentioned (the blocks around lines 401-419 and 146-207) and ensure
functions/query paths using tag_clean and id_filter check for an explicit
parse-failure sentinel before performing subtree queries.

Five findings, all valid:

1. cdp.rs — added a 10 s timeout around cdp_list_pages() so the /json
   discovery can't hang forever on a half-open localhost socket
   (the 30 s timeout further down only wraps the WebSocket call, not
   the HTTP discovery + read_to_end). The browser_eval port still
   gets its end-to-end 30 s budget on the WS leg.

2. page.rs — `enable_javascript_apple_events` was failing schema
   validation because the top-level `required` array listed `pid` and
   `window_id` for every action, but that action targets a browser
   bundle, not a running pid. Moved per-action enforcement into
   `invoke`: only `execute_javascript`/`get_text`/`query_dom` resolve
   pid+window_id; the apple-events action skips both.

3. page.rs — replaced unchecked `as i32` / `as u32` narrowing of
   pid/window_id with `i32::try_from` / `u32::try_from`. Out-of-range
   JSON numbers now surface "pid X out of i32 range" instead of
   silently truncating and targeting the wrong process / window.

4. platform-linux/page.rs — `role_for_selector` returns `None` for
   both `*` (match-all) and unrecognised selectors. The filter
   treated both as match-all, so `.foo` / `#bar` / `div > a` etc.
   silently dumped every node. Disambiguate at the call site: keep
   `None => true` only when the selector is actually wildcard;
   bail with an actionable error otherwise.

5. platform-windows/page.rs — same class of bug. `parse_selector`
   accepted `.class` and other unsupported forms as parseable, then
   built a `CreateTrueCondition()` that scanned the whole subtree and
   returned false-positive matches. Detect `.class` early with a
   clear "use tag / tag#id / [role=…] / execute_javascript" hint;
   also bail when neither control_type nor id_filter is resolved (so
   `foo` alone, where `foo` isn't in the mapping, fails fast).
   Wildcard `*` / empty selector continue to match everything; that
   path is now explicit.

Build: `cargo build --release -p cua-driver` clean (0 warnings).
Tests: `cargo test --release -p cua-driver --test mcp_protocol_test`
green (28/28).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@f-trycua
f-trycua merged commit 94f23d0 into main May 23, 2026
5 checks passed
f-trycua added a commit that referenced this pull request May 24, 2026
…up audit #C)

Adds the shared `ArgsExt` trait on `serde_json::Value` that every
platform crate uses to pull pid / window_id / element_index / etc.
out of inbound MCP tool args. Replaces ~200 hand-written
`match args.get("name").and_then(|v| v.as_X())` blocks with one
consistent surface.

Accessor families:
- `require_*` — bails with `ToolResult::error` on missing/wrong type;
  narrowing casts (i64→i32, u64→u32) go through `try_from` so
  out-of-range JSON numbers surface as an actionable range error
  instead of silently truncating. Matches the CodeRabbit fix landed
  on PR #1666's page tool.
- `opt_i32`/`opt_u32` — Result<Option<T>> with range check.
- `opt_*` (u64/i64/f64/str/bool) — plain Option<T> for callers with
  defaults handled elsewhere.
- `*_or` — default-fallback variants (the most common pattern).
- `str_array` — drains an array of strings, skipping non-strings.

Error wording is canonical: `"Missing required {kind} field: {name}"`
so MCP clients can pattern-match. Per-tool error strings with custom
helper text (`get_window_state`'s window_id helper, `kill_app`'s
range message, page.rs's CodeRabbit-vetted wording) are preserved
as-is.

15 unit tests cover happy path, missing-field, wrong-type, and
out-of-range cases. `cargo test -p mcp-server` green.

See `libs/cua-driver-rs/docs/dedup-audit.md` for the audit trail.
f-trycua added a commit that referenced this pull request May 24, 2026
…/linux tools (dedup audit #C)

Threads the new `mcp_server::tool_args::ArgsExt` trait through every
tool's `invoke()`. Replaces ~200 hand-written args.get/.and_then
chains with consistent typed accessors.

Per-platform refactor counts:
- platform-windows/tools/impl_.rs — 15 tools refactored
- platform-macos/tools/* — 20 of 26 files
- platform-linux/tools/impl_.rs — 84 of 93 patterns

Skipped (preserved verbatim per audit rules):
- `kill_app`/`debug_window_info` — custom range + bespoke wording
- `get_window_state` window_id branch — helper text directs to
  list_windows
- `hotkey` keys-array — needs raw `as_array()` for inline filter
- `set_agent_cursor_style` gradient_colors/bloom_color — per-element
  hex validation distinct from `str_array` silent-skip
- page.rs — CodeRabbit-vetted per-action wording from PR #1666

Bonus correctness wins (uncovered while refactoring):
- Linux `get_window_state` and `scroll` were silently truncating
  u64→u32 for pid. `require_u32` now range-checks.

cargo build/test green on Windows (target available locally). macOS
and Linux targets aren't installed on this Windows host — covered
by CI on those platforms.

Roughly -125 lines net across the three platforms.
f-trycua added a commit that referenced this pull request May 24, 2026
…pring + ArgsExt (#A + #1b + #C) (#1670)

* feat(cua-driver-rs)(mcp-server): introduce image_utils shared module

Pure-image-processing helpers that lived as near-identical copies in
`platform-{macos,windows,linux}/src/capture.rs`. Each platform's
capture.rs still owns its native screenshot primitive (CGImage on
macOS / BitBlt+PrintWindow on Windows / XGetImage + ImageMagick
`import` on Linux). Everything DOWNSTREAM of "I have RGBA pixels" —
PNG encoding, JPEG encoding, downscaling to a max long edge, drawing
a crosshair, reading width/height from an IHDR — now lives here.

Functions extracted:
- png_bytes_to_jpeg(png_bytes, quality)
- resize_png_if_needed(png_bytes, max_dim)
- write_crosshair_png(png_bytes, cx, cy, path)
- crosshair_png_bytes(png_bytes, cx, cy)
- png_dimensions(data)
- encode_rgba_to_png(rgba, w, h)
- encode_bgra_to_png(bgra, w, h)

8 unit tests cover round-trip dimensions, resize semantics
(no-op when fits, no-op when max_dim=0, downscale to long edge),
JPEG signature, crosshair shape, and BGRA→RGBA channel swap.

Per-platform capture.rs swaps follow in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(cua-driver-rs)(platform): route capture.rs through mcp_server::image_utils

Replace the file-local PNG/JPEG/resize/crosshair helpers in each
platform's capture.rs with thin re-exports of the shared
`mcp_server::image_utils::*` introduced in the previous commit.

The three platforms previously carried near-identical copies of:
  - png_bytes_to_jpeg
  - resize_png_if_needed
  - crosshair_png_bytes / write_crosshair_png (macOS-only path)
  - png_dimensions / png_dimensions_pub
Plus Windows + Linux each carried a hand-rolled
`write_uncompressed_png` + `write_png_chunk` + `zlib_store` +
`adler32` + `crc32_ieee` (~110 lines per platform) to convert raw RGBA
bytes from BitBlt / XGetImage to PNG. All of that is replaced by
`mcp_server::image_utils::encode_rgba_to_png` /
`encode_bgra_to_png` which go through the `image` crate's PNG encoder
— already a workspace dep, produces ~5x smaller files than the
uncompressed-store path that the hand-rolled code emitted.

Each platform's capture.rs keeps:
  - screenshot_window_bytes / screenshot_display_bytes (native:
    CGImage / BitBlt+PrintWindow / XGetImage)
  - screenshot_window / screenshot_display wrapper that returns
    base64+dimensions
  - public re-export wrappers that call into mcp_server::image_utils
    so existing callers (`tools/*.rs`) keep compiling without churn

Build: clean (0 warnings) on x86_64-pc-windows-msvc.
Tests: 32/32 platform-windows pass, 28/28 mcp_protocol_test pass,
8/8 new image_utils unit tests pass.

Diffstat: +89 / -488 lines across the three platform capture.rs files.
macOS and Linux compile-checks not run on this VM — reviewer should
`cargo check -p platform-macos` / `-p platform-linux` to confirm.
Structurally the substitutions are uniform: every public function in
the platform crate now delegates to the same `mcp_server::image_utils`
function with the same arguments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(cua-driver-rs)(cursor-overlay): extract Spring physics struct (dedup audit #1b)

The 4-field `struct Spring { ox, oy, vx, vy }` was duplicated
verbatim across all three platform `overlay.rs` files (with
`#[derive(Clone, Copy)]` and identical field names/types). Moved
to `cursor_overlay::Spring` (re-exported from `lib.rs` alongside
`MotionConfig`) and each platform crate now imports the shared
type via `use cursor_overlay::Spring;`.

Fields are now `pub` (were private when the struct was per-module);
the access pattern stays identical because Spring is now imported
into the same scope where it was previously declared.

Tiny extraction — about 30 lines removed total — but proves the
extraction pattern for the larger overlay dedup (PR #B in the audit
doc: RenderState + tick + apply_command + render_frame +
draw_default_arrow, ~1800 lines across the 3 platforms).

Build: clean (0 warnings) on Windows. Tests: 4/4 cursor-overlay,
8/8 image_utils, 32/32 platform-windows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(cua-driver-rs): cross-platform code-duplication audit report

Document at `libs/cua-driver-rs/docs/dedup-audit.md` enumerates
duplication candidates across platform-{macos,windows,linux}/src,
ranks by ROI vs refactoring risk, and outlines three follow-up PRs:

- PR #A (this branch) — image_utils + Spring extraction (~430 lines)
- PR #B — overlay.rs RenderState + render pipeline (~1800 lines, deferred)
- PR #C — mcp-server::tool_args helper trait (~600 lines, deferred)

Also captures the explicit list of things NOT to dedupe (per-platform
tool descriptions, FFI bindings, AppsFolder enumerations) and the
existing shared crates' scope (mcp-server, cursor-overlay).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(cua-driver-rs)(mcp-server): tool_args::ArgsExt helper trait (dedup audit #C)

Adds the shared `ArgsExt` trait on `serde_json::Value` that every
platform crate uses to pull pid / window_id / element_index / etc.
out of inbound MCP tool args. Replaces ~200 hand-written
`match args.get("name").and_then(|v| v.as_X())` blocks with one
consistent surface.

Accessor families:
- `require_*` — bails with `ToolResult::error` on missing/wrong type;
  narrowing casts (i64→i32, u64→u32) go through `try_from` so
  out-of-range JSON numbers surface as an actionable range error
  instead of silently truncating. Matches the CodeRabbit fix landed
  on PR #1666's page tool.
- `opt_i32`/`opt_u32` — Result<Option<T>> with range check.
- `opt_*` (u64/i64/f64/str/bool) — plain Option<T> for callers with
  defaults handled elsewhere.
- `*_or` — default-fallback variants (the most common pattern).
- `str_array` — drains an array of strings, skipping non-strings.

Error wording is canonical: `"Missing required {kind} field: {name}"`
so MCP clients can pattern-match. Per-tool error strings with custom
helper text (`get_window_state`'s window_id helper, `kill_app`'s
range message, page.rs's CodeRabbit-vetted wording) are preserved
as-is.

15 unit tests cover happy path, missing-field, wrong-type, and
out-of-range cases. `cargo test -p mcp-server` green.

See `libs/cua-driver-rs/docs/dedup-audit.md` for the audit trail.

* refactor(cua-driver-rs)(platform): adopt ArgsExt across windows/macos/linux tools (dedup audit #C)

Threads the new `mcp_server::tool_args::ArgsExt` trait through every
tool's `invoke()`. Replaces ~200 hand-written args.get/.and_then
chains with consistent typed accessors.

Per-platform refactor counts:
- platform-windows/tools/impl_.rs — 15 tools refactored
- platform-macos/tools/* — 20 of 26 files
- platform-linux/tools/impl_.rs — 84 of 93 patterns

Skipped (preserved verbatim per audit rules):
- `kill_app`/`debug_window_info` — custom range + bespoke wording
- `get_window_state` window_id branch — helper text directs to
  list_windows
- `hotkey` keys-array — needs raw `as_array()` for inline filter
- `set_agent_cursor_style` gradient_colors/bloom_color — per-element
  hex validation distinct from `str_array` silent-skip
- page.rs — CodeRabbit-vetted per-action wording from PR #1666

Bonus correctness wins (uncovered while refactoring):
- Linux `get_window_state` and `scroll` were silently truncating
  u64→u32 for pid. `require_u32` now range-checks.

cargo build/test green on Windows (target available locally). macOS
and Linux targets aren't installed on this Windows host — covered
by CI on those platforms.

Roughly -125 lines net across the three platforms.

* fix(cua-driver-rs): address CodeRabbit findings on dedup audit PRs

CodeRabbit review on PR #1670 commit 921dcdc (image_utils routing)
flagged two issues:

1. **CRITICAL** — Linux compile error.
   `platform-linux/src/capture.rs:30` still called the local
   `png_dimensions()` after the dedup extraction removed it. Route
   through `mcp_server::image_utils::png_dimensions` like the other
   two platforms. (CI on Windows hadn't caught this because the
   Linux target isn't installed on the VM.)

2. **Edge case** — `write_crosshair_png` tilde-expansion silently
   produced `/foo` instead of `~/foo` when both `HOME` and
   `USERPROFILE` are missing/empty. Now bails with a clear error
   referencing the missing env vars instead of writing to root.

Skipped: nitpick to fold `bgra.to_vec() + swap` into a single-pass
flat_map. CodeRabbit itself marked it `⚖️ Poor tradeoff` — not
worth churning the BGRA path for a microbench.

* refactor(cua-driver-rs)(mcp-server): generic ElementCacheCore + per-platform wrappers (dedup audit #2)

Extracts the locked-HashMap plumbing shared by all three platform
element caches into a generic `ElementCacheCore<K, S>` in mcp-server.

Each platform keeps its own:
- `CacheKey` (i32 pid + u32 window_id on macOS, u32 pid + u64 hwnd on
  Windows, u32 pid + u64 xid on Linux)
- `CachedSnapshot` with its native Drop impl (CFRelease for AX,
  COM Release for UIA, none for AT-SPI)
- Specialised accessors (`get_element_ptr`, `get_element_center` on
  Windows, `get_element_key` on Linux)

What moved: HashMap+Mutex insert/lookup/count. ~85 lines net.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(cua-driver-rs)(cursor-overlay): extract RenderStateCore + render pipeline (dedup audit #B)

Lifts ~950 lines of duplicated render state and animation logic out of
the three platform `overlay.rs` files into the shared `cursor-overlay`
crate.  The three platforms previously held byte-near-identical copies
of the animation state, tick physics, OverlayCommand dispatch, bloom +
arrow rasteriser, and palette/gradient/bloom-override plumbing —
maintaining them in three places meant every cursor change had to land
three times and stay in sync.

What moved to `cursor-overlay::render_state`:
- `RenderStateCore` — the platform-agnostic animation fields (`cfg`,
  `palette`, `motion`, `pos`, `heading`, `path`, `dist`, `spring`,
  `spring_tgt`, `click_t`, `shape`, `visible`, `idle_secs`,
  `idle_alpha`, `pinned_wid`, `gradient_colors`, `bloom_override`).
- `RenderStateCore::tick_motion(dt)` — speed-profile + spring physics
  + click pulse + idle fade using runtime `MotionConfig` (Windows / Linux).
- `RenderStateCore::tick_swift_constants(dt) -> bool` — the macOS
  variant that uses the hardcoded Swift reference constants
  (peakSpeed=900, springK=400, overshoot=0.8) and returns whether the
  path just ended (so the caller can fire its arrival oneshot).
- `RenderStateCore::apply_command_base(cmd, snap_move, snap_click)` —
  the 8 OverlayCommand match arms.  Two booleans select the macOS-only
  sentinel-snap behaviour for MoveTo + ClickPulse.
- `render_frame(core, w, h, origin_x, origin_y, focus_rect)` — the
  tiny-skia bloom + click-pulse + arrow paint, parametrised by pixmap
  dimensions and an origin offset (Windows passes virt_x/y; macOS +
  Linux pass 0,0).  An optional `FocusRect` is drawn on top — only
  macOS supplies one.
- `draw_default_arrow(...)` — gradient arrow rasteriser, now with the
  `gradient_override` argument all three platforms wanted.

What stays per-platform:
- macOS (`platform-macos/src/cursor/overlay.rs`): AppKit window + GCD
  render thread + `dispatch_set_layer_contents` (CGImage) + the
  focus_rect/focus_rect_t state (macOS-only post-arrival element
  highlight) + the win_w/win_h NSScreen dims.
- Windows (`platform-windows/src/overlay.rs`): Win32 message loop +
  `UpdateLayeredWindow` (BGRA DIB) + virt_x/y/w/h virtual-screen
  geometry + last_tick wall-clock stamp for the WM_TIMER dt.
- Linux (`platform-linux/src/overlay.rs`): X11 override-redirect
  window + XPutImage (BGRA ZPixmap) + scr_w/scr_h.

Each platform's RenderState is now a thin wrapper that holds
`core: cursor_overlay::RenderStateCore` plus its platform-specific
extras.  `tick` and `apply_command` forward to the shared core,
with macOS layering its focus-rect fade on top of the shared tick
and intercepting ShowFocusRect in apply_command.

Behaviour: byte-identical.  The two `tick` variants preserve the
existing per-platform constants exactly (macOS still uses the Swift
hardcoded values; Windows/Linux still use the runtime MotionConfig).
The smootherstep speed profile `30·u²·(1-u)²/1.875` and
`16·u²·(1-u)²` are algebraically equivalent (both peak at 1.0 at
u=0.5); macOS keeps the 30/1.875 form for parity with the Swift ref.

Net diff: 4 files changed, +132 / -1077 in the platform files plus
+734 in the new shared module = -213 net lines, and from now on
animation tweaks land in one place instead of three.

Verified on Windows: `cargo build --release -p cua-driver` clean
(0 warnings, 0 errors).  `cargo test --release -p cursor-overlay
-p platform-windows -p mcp-server` all green.  macOS + Linux not
compile-checked locally (only x86_64-pc-windows-msvc target installed);
CI on those platforms will catch any issues.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(cua-driver-rs): mark dedup audit complete + record what estimates got wrong

All five ranked candidates shipped on this branch:
- #A image_utils    ✓ (close to estimate)
- #1b Spring        ✓
- #B render_state   ✓ (less line-savings than estimated; tick was NOT
                       byte-for-byte identical)
- #C tool_args      ✓ (less line-savings; trait body costs add up)
- #2 element_cache  ✓ (+14 lines; audit was optimistic)

Logged the three places the audit estimates were off so future audits
can calibrate against this.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant