feat(cua-driver-rs)(windows): bookmark-URL UIA bypass for page.execute_javascript - #1667
Conversation
…_javascript Adds tools/page_bookmark.rs implementing try_bookmark_exec() — a zero-config path for running JavaScript in a Chromium-family browser on Windows. The primitive UIA-edits a `cua-driver-eval` bookmark's URL field to a javascript: bookmarklet, invokes the bookmark via InvokePattern, and reads the result back from document.title. Wired into WindowsPageBackend::execute_javascript as a prefix on the existing CDP path: bookmark exec is tried first, any failure logs at DEBUG and falls through to the CUA_DRIVER_CDP_PORT-based CDP fallback unchanged. A process-wide mutex serialises bookmark-exec calls so concurrent invocations don't race on the single bookmark URL. The wrap_javascript() helper produces a try/catch IIFE that JSON.stringifies the result and restores the original tab title 500 ms after execution. Three unit tests cover the wrapper shape, newline stripping, and the title-suffix parser. Why bookmark exec: Chromium scrubs `javascript:` URLs from the omnibox but not from bookmarks, because bookmarklets are a documented Web-platform feature. Mechanism empirically validated on Edge 148.0.3967.70. Auto-creation of the bookmark (drive omnibox to edge://favorites → click Add favorite → fill dialog) is not yet wired up; users create the bookmark manually for now (any URL — the driver overwrites it). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… exec Adds a Windows-flavored section to WINDOWS.md describing the four `page` actions and the two-tier execute_javascript dispatch (bookmark-URL UIA bypass first, CDP fallback second). Spells out the requirements for the bookmark path (a pre-existing `cua-driver-eval` bookmark on the Favorites bar) and the concurrency rule (process-wide serialisation on the bookmark URL). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…+ Chrome) Extend the bookmark-URL UIA bypass to work on Chrome 148 alongside Edge 148, based on an empirical UIA-tree comparison battery run this session. Patches: 1. `find_bookmark` — accept ListItem OR Button ControlType (OrCondition). Edge surfaces favorites-bar entries as ListItem; Chrome surfaces them as Button with an autogenerated AutomationId. Filter by Name afterward so we don't pick up the overflow chevron / "Add favorite" entry. 2. `wait_for_window` + `wait_for_menu_item` — accept a slice of names instead of a single name, with case-insensitive + substring + ellipsis-tolerant matching. Edge's right-click menu shows "Edit"; Chrome shows "Edit...". Edge's edit dialog is "Edit favorite"; Chrome's is "Edit bookmark". 3. Edit-dialog URL field selector — match `"Favorite URL"` (Edge), `"Bookmark URL"` (Chrome), or "URL" (Chrome's chrome://bookmarks add-flow Edit, which has Name="URL" and AutomationId="input" — Name wins because find_edit_in_dialog's matching is case-insensitive + substring). Build clean (0 warnings on x86_64-pc-windows-msvc); the 3 page_bookmark unit tests still pass. Auto-creation of the cua-driver-eval bookmark stays out of scope — covered by the existing actionable error message. Manual one-time setup: any browser, any URL, name it `cua-driver-eval`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR implements a Windows-specific JavaScript execution bypass for the CUA driver: when CDP is unavailable, code first attempts to execute JavaScript via a UI Automation–driven bookmarklet in Edge/Chromium, falling back to CDP when bookmark execution fails. The change includes full UIA integration, result marshaling, mutex serialization for concurrency safety, and comprehensive documentation. ChangesWindows JavaScript Execution via UI Automation Bookmarklet
🎯 4 (Complex) | ⏱️ ~45 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/platform-windows/src/tools/page_bookmark.rs`:
- Around line 735-736: The current marker extraction uses after.find(" - ")
which stops at the first " - " and corrupts payloads that legitimately contain
that sequence; change the logic in the page bookmark parsing to locate the
separator from the end (use after.rfind(" - ") or equivalent) so only the final
delimiter is used when computing payload_end, and then return
after[..payload_end].to_owned() as before; update the code handling payload_end
and the variable after in the function that performs marker extraction to use
the last-occurrence search.
In `@libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.md`:
- Around line 618-620: The docs list "select" as supported for query_dom but the
backend mapping is missing; either remove "select" from WINDOWS.md or add a
mapping in the backend by updating control_type_for_tag in page.rs to map the
"select" tag to the appropriate UIA control type (e.g., ComboBox/List
equivalent), and update any unit tests or docs to keep behavior and
documentation consistent.
🪄 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: 4f916caf-b8f0-4b4f-932b-ed086ecfa63c
📒 Files selected for processing (4)
libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.mdlibs/cua-driver-rs/crates/platform-windows/src/tools/mod.rslibs/cua-driver-rs/crates/platform-windows/src/tools/page.rslibs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs
| var __r = (function(){{ {user_js} }})();\ | ||
| try {{ __r = JSON.stringify(__r); }} catch(e) {{ __r = String(__r); }}\ | ||
| document.title = 'CUA:' + (__r === undefined ? '' : __r);\ |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JavaScript, what is the return value of (function(){ 1 + 1 })()and how does it differ fromeval("1 + 1")?
💡 Result:
Return value of (function(){ 1 + 1 })(): 2 What eval("1 + 1") returns: 2 (as the completion value of evaluating the expression) How they differ: 1) Evaluation mechanism/inputs - The IIFE is a function call where the code 1 + 1 is part of the already-parsed program; execution is straightforward and returns the function’s return value. - eval("1 + 1") takes source code as a string, parses it as a script, executes it, and returns its completion value [1]. 2) Scope/privileges (important practical difference) - eval() executes the given string with the privileges of the caller and can access the scope where it was invoked (i.e., it is “dangerous” and differs from simply running already-written code) [2]. - The IIFE runs like normal JavaScript code in its own function scope; it doesn’t require parsing arbitrary strings at runtime. So for these particular inputs, both evaluate the expression and produce the numeric result 2, but eval does runtime parsing/execution from a string and has special scope/security behavior, while an IIFE is just a normal function that returns a value [1][2].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
- 2: https://developer.mozilla.org.cach3.com/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval$revision/1089617
🏁 Script executed:
#!/bin/bash
node -e "console.log('(function(){ 1+1 })() =', (function(){ 1+1 })()); console.log('eval(\"1+1\") =', eval('1+1')); console.log('(function(){ 1+1; })() =', (function(){ 1+1; })()); console.log('(function(){ return 1+1 })() =', (function(){ return 1+1 })());"Repository: trycua/cua
Length of output: 172
🌐 Web query:
JavaScript function body without return last expression return value (function(){1+1})() returns undefined
💡 Result:
Result: (function{1+1}) returns undefined. Explanation (what happens in JS): 1) The function body contains the expression 1+1, but it is not preceded by a return statement. 2) If execution of a function reaches the end without encountering a return statement (or without a return expression), JavaScript returns undefined by default [1]. 3) The ECMAScript specification defines that a return statement with no expression (i.e., return;) yields a return value of undefined [2]. In your case there is no return statement at all, so execution “falls off the end” and the effective return value to the caller is undefined [1].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions
- 2: https://tc39.es/ecma262/2022/multipage/ecmascript-language-statements-and-declarations.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact wrapper code around the referenced lines
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs"
if [ -f "$FILE" ]; then
echo "---- $FILE (lines 110-170) ----"
nl -ba "$FILE" | sed -n '110,170p'
fi
# Find marker extraction + wrapper helpers and show their definitions
echo "---- Searching for extract_marker / wrap_javascript ----"
rg -n "extract_marker|wrap_javascript|try_bookmark_exec|user_js" "$FILE" || true
# Locate unit tests in the repo and show the relevant ones
echo "---- Searching for tests referencing bookmark/page_bookmark ----"
rg -n "page_bookmark|wrap_javascript|extract_marker" libs/cua-driver-rs/crates/platform-windows/src/tools -S || true
# If tests are in a separate file/module, find them by name
rg -n "extract_marker" libs/cua-driver-rs -S || trueRepository: trycua/cua
Length of output: 211
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs"
echo "---- $FILE (lines 120-170) ----"
if [ -f "$FILE" ]; then
# line numbers without nl: use awk
awk 'NR>=120 && NR<=170 {printf "%5d:%s\n", NR, $0}' "$FILE"
fi
echo "---- $FILE (around user_js_json / wrap_javascript) ----"
if [ -f "$FILE" ]; then
rg -n "extract_marker|wrap_javascript|try_bookmark_exec|user_js" "$FILE" || true
fi
echo "---- Searching for tests referencing page_bookmark/wrapper helpers ----"
# Limit search to this crate first
rg -n "page_bookmark|wrap_javascript|extract_marker" libs/cua-driver-rs/crates/platform-windows -S || true
echo "---- Searching for JS wrapper string patterns ----"
rg -n "CUA:|JSON.stringify|document.title|function\\s*\\(\\s*\\{\\s*\\$\\{user_js\\}|function\\s*\\(\\s*\\{\\s*\\{user_js\\}\\s*\\}" libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs -S || trueRepository: trycua/cua
Length of output: 8154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs"
echo "---- extract_marker implementation (lines 700-790) ----"
awk 'NR>=700 && NR<=790 {printf "%5d:%s\n", NR, $0}' "$FILE"
echo "---- wrap_javascript tests (lines ~730-790) ----"
awk 'NR>=730 && NR<=790 {printf "%5d:%s\n", NR, $0}' "$FILE"Repository: trycua/cua
Length of output: 6309
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs"
echo "---- try_bookmark_exec docs (lines 60-120) ----"
awk 'NR>=60 && NR<=140 {printf "%5d:%s\n", NR, $0}' "$FILE"Repository: trycua/cua
Length of output: 4271
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page.rs"
echo "---- Searching for try_bookmark_exec call sites ----"
rg -n "try_bookmark_exec\\(" libs/cua-driver-rs/crates/platform-windows/src/tools -S
echo "---- Inspect page.rs around the call ----"
# show a reasonable window around the match
rg -n "try_bookmark_exec\\(" "$FILE" -S | cut -d: -f1 | head -n 5 | while read -r ln; do
start=$((ln-20)); end=$((ln+40));
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%5d:%s\n", NR, $0}' "$FILE"
doneRepository: trycua/cua
Length of output: 3677
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find all call sites of execute_javascript (to see whether callers add `return`)
rg -n "execute_javascript\\(" libs/cua-driver-rs -S
# If there are JS evaluation request handlers building the JS string, inspect them too
rg -n "cdp\\.runtime\\.evaluate|runtime\\.evaluate|evaluate\\(.*javascript|bookmark_exec" libs/cua-driver-rs -S
# Also search for tests asserting behavior for expression vs statements
rg -n "1\\s*\\+\\s*1|return 1\\s*\\+\\s*1|eval\\(\"1\\s*\\+\\s*1\"|expression" libs/cua-driver-rs -SRepository: trycua/cua
Length of output: 6367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.md"
echo "---- WINDOWS.md around wrapper contract (620-690) ----"
awk 'NR>=620 && NR<=700 {printf "%5d:%s\n", NR, $0}' "$FILE"
FILE2="libs/cua-driver-rs/Skills/cua-driver-rs/WEB_APPS.md"
echo "---- WEB_APPS.md around execute javascript 1+1 (300-360) ----"
awk 'NR>=300 && NR<=360 {printf "%5d:%s\n", NR, $0}' "$FILE2"Repository: trycua/cua
Length of output: 7048
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs"
echo "---- try_bookmark_exec tail / return path (lines ~650-760) ----"
awk 'NR>=650 && NR<=760 {printf "%5d:%s\n", NR, $0}' "$FILE"
echo "---- try_bookmark_exec around title polling loop (lines ~180-260) ----"
awk 'NR>=180 && NR<=260 {printf "%5d:%s\n", NR, $0}' "$FILE"Repository: trycua/cua
Length of output: 8778
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="libs/cua-driver-rs/crates/mcp-server/src/page.rs"
# show around the execute_javascript handling
rg -n "execute_javascript\\(|let js\\s*=|javascript\\s*:\\s*|Runtime\\.evaluate|bookmark_exec" "$FILE" -S
echo "---- snippet around line 220-270 ----"
awk 'NR>=210 && NR<=270 {printf "%5d:%s\n", NR, $0}' "$FILE"Repository: trycua/cua
Length of output: 3388
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect macOS execute_javascript backend
FILE="libs/cua-driver-rs/crates/platform-macos/src/tools/page.rs"
echo "---- macOS execute_javascript (around execute_javascript) ----"
rg -n "async fn execute_javascript" "$FILE"
awk 'NR>=70 && NR<=140 {printf "%5d:%s\n", NR, $0}' "$FILE"
# Inspect Linux execute_javascript backend
FILE="libs/cua-driver-rs/crates/platform-linux/src/tools/page.rs"
echo "---- Linux execute_javascript (around execute_javascript) ----"
rg -n "async fn execute_javascript|execute_javascript" "$FILE"
awk 'NR>=90 && NR<=170 {printf "%5d:%s\n", NR, $0}' "$FILE"
# Inspect CDP evaluate usage (common semantics)
FILE="libs/cua-driver-rs/crates/mcp-server/src/cdp.rs"
echo "---- cdp.rs evaluate helper (around Runtime.evaluate) ----"
rg -n "Runtime\\.evaluate|expression" "$FILE"
awk 'NR>=1 && NR<=120 {printf "%5d:%s\n", NR, $0}' "$FILE"Repository: trycua/cua
Length of output: 12344
Fix Windows bookmark JS wrapper completion semantics and prevent marker truncation for " - " in results
wrap_javascriptsetsvar __r = (function(){ {user_js} })();with noreturn, so expression-only JS (e.g.1+1) yieldsundefined, producingdocument.title = 'CUA:'and an empty payload—unlike CDP/macOSexecute_javascriptbehavior.extract_markercuts the payload at the first" - "afterCUA:/CUA_ERR:; if the JSON-stringified result contains" - "(common in strings), it will be truncated before the Chromium suffix. Current tests only cover Chromium’s title suffix, not payloads containing" - ".
Suggested fix (for completion values)
- var __r = (function(){{ {user_js} }})();
+ var __r = (0, eval)({user_js_json});Pass user_js_json as a JSON-escaped JS string literal from Rust (e.g., via serde_json::to_string(user_js)), so both return ... blocks and bare expressions are handled consistently.
| let payload_end = after.find(" - ").unwrap_or(after.len()); | ||
| after[..payload_end].to_owned() |
There was a problem hiding this comment.
Marker extraction truncates valid payloads containing -.
At Line 735-Line 736, trimming at the first " - " will corrupt results like CUA:"a - b" (or any JSON string containing that delimiter). This can silently return wrong JS results.
Suggested fix
fn extract_marker(s: &str, prefix: &str) -> String {
let start = s.find(prefix).unwrap_or(0);
let after = &s[start..];
- // Trim trailing " - <browser>" if Chromium appended it.
- let payload_end = after.find(" - ").unwrap_or(after.len());
- after[..payload_end].to_owned()
+ // Trim only a trailing browser suffix, not payload content.
+ if let Some((left, right)) = after.rsplit_once(" - ") {
+ let right_lc = right.to_ascii_lowercase();
+ let looks_like_browser =
+ right_lc.contains("edge")
+ || right_lc.contains("chrome")
+ || right_lc.contains("chromium")
+ || right_lc.contains("brave")
+ || right_lc.contains("opera");
+ if looks_like_browser {
+ return left.to_owned();
+ }
+ }
+ after.to_owned()
}📝 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.
| let payload_end = after.find(" - ").unwrap_or(after.len()); | |
| after[..payload_end].to_owned() | |
| fn extract_marker(s: &str, prefix: &str) -> String { | |
| let start = s.find(prefix).unwrap_or(0); | |
| let after = &s[start..]; | |
| // Trim only a trailing browser suffix, not payload content. | |
| if let Some((left, right)) = after.rsplit_once(" - ") { | |
| let right_lc = right.to_ascii_lowercase(); | |
| let looks_like_browser = | |
| right_lc.contains("edge") | |
| || right_lc.contains("chrome") | |
| || right_lc.contains("chromium") | |
| || right_lc.contains("brave") | |
| || right_lc.contains("opera"); | |
| if looks_like_browser { | |
| return left.to_owned(); | |
| } | |
| } | |
| after.to_owned() | |
| } |
🤖 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_bookmark.rs` around
lines 735 - 736, The current marker extraction uses after.find(" - ") which
stops at the first " - " and corrupts payloads that legitimately contain that
sequence; change the logic in the page bookmark parsing to locate the separator
from the end (use after.rfind(" - ") or equivalent) so only the final delimiter
is used when computing payload_end, and then return
after[..payload_end].to_owned() as before; update the code handling payload_end
and the variable after in the function that performs marker extraction to use
the last-occurrence search.
| - **`query_dom`** — CSS-selector → UIA `ControlType` match. Supports | ||
| simple tag selectors (`a`, `button`, `input`, `h1`-`h6`, `img`, | ||
| `li`, `p`, `span`, `select`), `tag#id`, `[role=…]`. **Does not** |
There was a problem hiding this comment.
select is documented as supported, but the backend does not implement it.
Line 619-Line 620 list select as supported for query_dom, but control_type_for_tag in libs/cua-driver-rs/crates/platform-windows/src/tools/page.rs currently has no select mapping. Please either remove select from this list or add the mapping in code so docs and behavior match.
🤖 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/Skills/cua-driver-rs/WINDOWS.md` around lines 618 - 620,
The docs list "select" as supported for query_dom but the backend mapping is
missing; either remove "select" from WINDOWS.md or add a mapping in the backend
by updating control_type_for_tag in page.rs to map the "select" tag to the
appropriate UIA control type (e.g., ComboBox/List equivalent), and update any
unit tests or docs to keep behavior and documentation consistent.
Three findings, all valid:
1. wrap_javascript silently returned undefined for CDP-style callers.
The IIFE wrapper required users to write `return X` themselves —
plain `document.title` got executed as an expression statement
inside the function body, the IIFE returned undefined, and the
readback channel emitted `CUA:` with an empty payload. CDP's
`Runtime.evaluate` semantics (which the macOS page tool matches
via Apple Events) treat the user JS as a script — last expression
is the value. Switched the wrapper to `eval('<user_js>')`:
- user JS is embedded as a single-quoted JS string literal
- backslashes + single-quotes escaped before embedding
- newlines still pre-replaced with spaces for one-line URL
Now matches CDP semantics exactly.
2. extract_marker silently corrupted payloads containing ` - `.
The previous `find(" - ")` truncation killed any payload with that
sequence (e.g. CUA:"a - b" returned CUA:"a). Switched to
`rsplit_once(" - ")` keyed on a known browser-name suffix list
(edge / chrome / chromium / brave / arc / vivaldi / opera) — only
strips the trailing browser tag, never touches payload content.
3. control_type_for_tag was missing `select`. WINDOWS.md listed it
as supported but page.rs had no mapping, so `select` queries
would fall through `parse_selector`'s "unparseable" guard and
reject with a misleading error. Added `"select" =>
UIA_ComboBoxControlTypeId` — correct HTML→UIA mapping for
`<select>` elements, which Chromium surfaces as ComboBox in its
UIA tree.
Test changes:
- Replaced wrap_javascript_emits_expected_shape's input from a
function-body string to an expression, asserting eval('...') is in
the output.
- Added wrap_javascript_escapes_single_quotes and
wrap_javascript_escapes_backslashes for the new escaping path.
- Renamed extract_marker_handles_chromium_title_suffix →
extract_marker_strips_chromium_suffix; added
extract_marker_preserves_dash_in_payload covering the bug class.
Build clean (0 warnings); 6/6 page_bookmark unit tests pass; 28/28
mcp_protocol_test pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Three commits. Adds a zero-config, no-launch-flag primary path for
page.execute_javascripton Windows, exploiting an empirically validated UIA bypass in Chromium's bookmark URL fields. Falls through to the existing CDP path (env-var-discovered port) on any failure.This is the durable answer to a problem that surfaced after PR #1666 merged:
--remote-debugging-portagainst the user's real browser profile (cookies / sessions / extensions invalidated unless--user-data-dirpoints at a throwaway dir) — so the CDP-via-env-var path PR feat(cua-driver-rs): unify browser_eval + page into one cross-platform tool #1666 shipped is effectively broken for real-world use on modern Chromium.The mechanism
Empirically confirmed on Edge 148.0.3967.70 AND Chrome 148.0.7778.179 during a PowerShell battery this session:
Why this works: Chromium's
OmniboxView::OnAfterPossibleChangestripsjavascript:schemes only when a "came from user input" flag is set (paste / type / drop). UIA programmatic writes don't trip the flag — but more importantly here, the bookmark URL field doesn't even live on that code path. It's part of the bookmark-edit dialog which preserves the literal URL because bookmarklets (javascript:URLs as bookmarks) have been a documented Web feature since ~1995. Closing this would break a real feature, not just a security accident.Result-readback channel
The injected JS wraps user code in:
document.titlemutations propagate to UIA'sNamePropertyIdon the window root within ~50ms; poll, strip theCUA:/CUA_ERR:prefix, JSON.parse the rest. Title is restored after 500ms.Cross-Chromium support
Empirically validated on Chrome 148 (UIA AutomationId comparison vs Edge 148):
view_NNNNautogen idfind_bookmarkmatches both viaOrCondition(ListItem, Button)wait_for_menu_itemdoes ellipsis-tolerant matchwait_for_windowaccepts a slice of namesfind_edit_in_dialogaccepts a slice of names (matches by Name + AutomationId fallback)find_favorites_baraccepts both via case-insensitive matchNo browser-specific code paths; one unified implementation that detects via UIA Name / ControlType variants.
Implementation
crates/platform-windows/src/tools/page_bookmark.rs(new, ~800 lines) —try_bookmark_execdriver +wrap_javascriptURL builder + supporting UIA walkers + 3 unit tests on the wrapper shape.crates/platform-windows/src/tools/page.rs—execute_javascriptnow callstry_bookmark_execfirst, falls through to the existingCUA_DRIVER_CDP_PORTenv-var CDP path on any failure. Error message updated to mention both paths.crates/platform-windows/src/tools/mod.rs— module registration.Skills/cua-driver-rs/WINDOWS.md— documents the new behaviour + the one-time bookmark-creation user gesture (see Out-of-scope below).Out-of-scope follow-ups (deliberate)
cua-driver-evalbookmark. First use requires the user to manually create a bookmark named exactlycua-driver-eval(any URL, gets overwritten on first call). The error message explains. Auto-creation requires UIA-driving a new tab →edge://favorites/chrome://bookmarks→ Add-bookmark button → fill the Add dialog → close the tab. This is the most fragile part of the spec (multiple dialog states, browser-specific entry points) and was deferred to keep this PR focused.Test plan
cargo build --release -p cua-driverclean (0 warnings) on Windowscargo test --release -p platform-windows page_bookmarkgreen (3/3 new tests)cargo test --release -p cua-driver --test mcp_protocol_testgreen (28/28)cua-driver-evalbookmarkcua-driver-evalbookmarkcargo checkformcp_protocol_test's expectations)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation