Skip to content

feat(cua-driver-rs)(windows): no-foreground audit v2 — hotkey Chromium + Invoke restore + click polling - #1669

Merged
f-trycua merged 3 commits into
mainfrom
windows-no-foreground-audit-v2
May 24, 2026
Merged

feat(cua-driver-rs)(windows): no-foreground audit v2 — hotkey Chromium + Invoke restore + click polling#1669
f-trycua merged 3 commits into
mainfrom
windows-no-foreground-audit-v2

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Summary

Three commits. Closes the remaining tractable no-foreground violations the user identified after PR #1668. Overnight autonomous work; user signed off in advance.

PR #1668 already shipped: launch_app FocusRestoreGuard + bookmark Ctrl+Shift+B bail. The "still has transient activation" table in #1668's docs listed four remaining items. This PR closes three of them and re-frames the fourth as an unavoidable irreducible minimum.

Changes

1. hotkey Chromium routing (tools/impl_.rs)

The non-XAML Win32 branch previously routed all modifier+key combos through send_key_synthesized (SendInput + foreground swap) because TranslateAccelerator needs GetKeyState updated, which PostMessage can't do. But Chromium doesn't use TranslateAcceleratorBrowser::HandleKeyboardEvent reads modifier state from WM_KEYDOWN LPARAM bits directly. PostMessage works on Chromium with no swap.

Routing: is_chromium_target_window(hwnd)post_key even with modifiers. Non-Chromium Win32 + modifiers continues through send_key_synthesized (real constraint).

Coverage: every Chromium browser AND every Electron app uses the Chrome_WidgetWin_* class — Chrome, Edge, Brave, Arc, Vivaldi, Slack, VS Code, Discord, Teams, Notion.

2. page.execute_javascript Ctrl+Shift+B auto-toggle restored (tools/page_bookmark.rs)

PR #1668 removed the auto-toggle because it used the focus-stealing send_key_synthesized. With the hotkey routing change in this PR's commit 1, the same accelerator can now be delivered via post_key — no foreground swap. If the bar is still hidden after the keystroke (browser policy override / Ctrl+Shift+B remapped), the same actionable bail error from PR #1668 fires.

3. Bookmark InvokePattern.Invoke polling foreground restore (tools/page_bookmark.rs)

Chromium activates the browser window when a bookmark is clicked — WebContents::OpenURL treats it as user-initiated navigation. PR #1668 documented this as "not closable without forking Chromium." Re-examined: same pattern launch_app's FocusRestoreGuard uses solves it.

New helper restore_foreground_after_browser_activation(prev_fg, browser_pid) polls for ~600 ms in 50 ms steps; once Chromium grabs foreground, SetForegroundWindow(prev) the user's window back. Gated on GetWindowThreadProcessId(fg_now) == browser_pid so user Alt-Tabs are respected. Synchronous (not tokio::spawn) because the poll_for_marker step that reads the result title via UIA must see the restored foreground.

When Windows' foreground lock denies the restore (we lack UIAccess), browser dwell is bounded to the 600 ms poll budget instead of "until next user action."

4. Chromium pixel-click polling restore (tools/impl_.rs)

click({pid, x, y}) on Chromium uses send_click_synthesized (SendInput + brief foreground swap) — required by Chromium's input thread queue-origin filter. The synchronous SetForegroundWindow(prev) inside send_click_synthesized ~40 ms after the click covers the immediate swap, but Chromium's async re-activation (focus().activate() in the renderer event handler, 100-500 ms later) was uncovered.

Wraps the call with a tokio::spawn(restore_foreground_polling_best_effort(...)) — the same helper PR #1668's launch_app FocusRestoreGuard already uses. Catches the async case.

5. Audit findings (no code change needed)

The new no-foreground status table

Tool path Status
launch_app (any target) ✓ FocusRestoreGuard (PR #1668)
press_key ✓ PostMessage
hotkey no-modifier ✓ PostMessage
hotkey modifier + Chromium target PostMessage (this PR)
hotkey modifier + classic Win32 (LibreOffice etc.) foreground swap — irreducible (TranslateAccelerator needs SendInput-queue origin)
click({element_index}) ✓ UIA Invoke
click({pid, x, y}) UWP/WinUI3 ✓ UIA Invoke at point
click({pid, x, y}) non-Chromium Win32 ✓ PostMessage
click({pid, x, y}) Chromium swap + polling restore (this PR) — irreducible (Chromium input thread queue-origin filter)
type_text ✓ PostMessage WM_CHAR
set_value ✓ UIA ValuePattern
scroll ✓ per-pid PostMessage
drag ✓ post_drag PostMessage
page.execute_javascript Ctrl+Shift+B auto-toggle PostMessage (this PR)
page.execute_javascript bookmark Invoke activation + polling restore (this PR) — Chromium-side activation suppressed
page.get_text / query_dom ✓ UIA reads

The only paths that still touch foreground:

  • hotkey with modifiers on classic Win32 — TranslateAccelerator architectural constraint
  • click({pid, x, y}) on Chromium when UIA Invoke misses — Chromium input thread architectural constraint

Both are mitigated where possible (polling restores catch async re-activations) but the synchronous foreground swap itself is unavoidable without UIAccess / Chromium fork.

Verification

Test plan

  • Build clean on Windows
  • All existing tests pass
  • Reviewer: smoke cua-driver hotkey '{"pid":<edge_pid>, "keys":["ctrl","t"]}' against an Edge window — opens a new tab WITHOUT the caller's foreground app losing focus
  • Reviewer: smoke cua-driver page '{"pid":<edge_pid>, "window_id":<W>, "action":"execute_javascript", "javascript":"document.title"}' — runs JS, returns the title, the only visible foreground activity is a brief (~150-600 ms) bookmark-click visit that the polling restore catches
  • Reviewer: smoke cua-driver click '{"pid":<edge_pid>, "x":100, "y":200}' — clicks the (x,y) point, no persistent foreground steal

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved hotkey and click handling for Chromium-based applications with better foreground window restoration.
    • Enhanced JavaScript execution via bookmarks with automatic Favorites bar visibility management.
  • Documentation

    • Updated documentation describing Chromium/Electron-specific automation behavior and focus management mitigations.

Review Change Stack

f-trycua and others added 3 commits May 23, 2026 22:43
…k polling restore

Two changes that close two of the focus-stealing paths PR #1668
documented as "still has transient activation":

1. **`hotkey` Chromium routing.** The non-XAML Win32 branch in
   `HotkeyTool::invoke` previously routed modifier+key combos through
   `send_key_synthesized` (SendInput + SetForegroundWindow(target) swap)
   because `TranslateAccelerator`-based apps (LibreOffice, FAR, classic
   Notepad) need the system modifier state updated, which PostMessage
   can't do. **But Chromium doesn't use TranslateAccelerator** — it
   dispatches accelerators in `Browser::HandleKeyboardEvent` which
   reads modifier state from the WM_KEYDOWN LPARAM bits directly.
   PostMessage works for Chromium with no foreground swap.

   Routing now: `is_chromium_target_window(hwnd)` → `post_key` even
   with modifiers. Non-Chromium Win32 with modifiers continues to use
   `send_key_synthesized` (the TranslateAccelerator constraint). The
   no-foreground win covers every Chromium browser AND every Electron
   app (Slack/VS Code/Discord/Teams/Notion — they all use
   `Chrome_WidgetWin_*` class).

2. **Chromium pixel-click polling restore guard.** `click({pid, x, y})`
   on Chromium falls through to `send_click_synthesized` because
   Chromium's input thread filters by queue-origin and PostMessage
   clicks don't fire DOM events. `send_click_synthesized` does its own
   synchronous SetForegroundWindow(prev) ~40ms after the click — but
   Chromium's reaction to the click can include an async window
   re-activation 100-500ms later (focus().activate() in the renderer
   event handler). The pre-existing restore misses that.

   Wrapping the call with a `tokio::spawn(restore_foreground_polling_best_effort(...))`
   — same helper PR #1668's `launch_app` `FocusRestoreGuard` uses,
   already gated on `GetWindowThreadProcessId(fg_now) == pid` so user
   Alt-Tabs are respected. Catches the async re-activation case.

Build clean (0 warnings, x86_64-pc-windows-msvc). Tests:
- platform-windows: 32/32 pass
- mcp_protocol_test: 28/28 pass

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

Two changes to the bookmark-based JS exec path:

1. **Auto-toggle the Favorites bar via PostMessage** (was: bail).
   PR #1668 removed the auto-toggle because the old `send_key_synthesized`
   call did a SetForegroundWindow swap. With the hotkey-on-Chromium
   routing change in the previous commit, the same Ctrl+Shift+B
   accelerator can now be delivered via `post_key` — no foreground swap.

   If the bar is still hidden after the keystroke (browser policy
   override / locked-down profile / Ctrl+Shift+B remapped), the call
   falls back to the same actionable error PR #1668 introduced, telling
   the user to enable the bar manually.

2. **Polling foreground restore around the bookmark `InvokePattern.Invoke`.**
   Same pattern PR #1668's `launch_app` `FocusRestoreGuard` uses, adapted
   for the Chromium URL-load activation case. Capture `prev_fg`
   immediately before Invoke; after Invoke, poll for ~600 ms in 50 ms
   steps for "Chromium grabbed foreground" and SetForegroundWindow(prev)
   the user's window back. Gated on
   `GetWindowThreadProcessId(fg_now) == browser_pid` so user Alt-Tabs
   are respected. Synchronous (not tokio::spawn) because the
   `poll_for_marker` step that reads the title via UIA must see the
   restored foreground state.

   When Windows' foreground lock denies the restore (we lack UIAccess),
   browser dwell is bounded to the 600 ms poll budget instead of "until
   next user action". Logged at trace.

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 May 23, 2026 10:44pm

Request Review

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds foreground-restore polling guards to Windows Chromium tool operations and updates bookmark execution to ensure Favorites bar visibility. HotkeyTool now routes modifier hotkeys to Chromium targets via PostMessage instead of SendInput. ClickTool and bookmark invocation capture prior foreground state and restore it after Chromium async re-activation. Favorites bar visibility is toggled via PostMessage Ctrl+Shift+B when hidden.

Changes

Chromium foreground-restore mitigations and bookmark handling

Layer / File(s) Summary
Chromium hotkey and click behavior documentation
libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.md
Documents Chromium-specific routing for hotkey modifiers via PostMessage(WM_KEYDOWN/UP) avoiding foreground swap, and click pixel-coordinate dispatch with foreground-restore polling guard for async re-activation.
HotkeyTool Chromium modifier routing
libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
HotkeyTool non-XAML path distinguishes Chromium-family targets: modifiers on Chromium route via PostMessage, while non-Chromium targets continue using SendInput for system-wide modifier state.
ClickTool Chromium foreground-restore polling
libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
Pixel-click path on Chromium targets captures prior foreground HWND before dispatch and asynchronously spawns foreground-restore polling to handle delayed re-activation.
Bookmark execution documentation
libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.md
Documents bookmark execute_javascript behavior including Favorites bar visibility toggle via PostMessage Ctrl+Shift+B and foreground-restore polling mitigation.
Bookmark Favorites bar visibility toggle
libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs
When Favorites bar is hidden, bookmark path synthesizes Ctrl+Shift+B via PostMessage, re-checks visibility, and errors with fallback-to-CDP guidance if still unavailable.
Bookmark invocation foreground-restore mitigation
libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs
Captures pre-invocation foreground window handle, invokes bookmark, and applies new restore_foreground_after_browser_activation helper that polls for Chromium foreground, validates prior window handle, and restores user's original window with trace/debug logging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • trycua/cua#1667: Directly builds on bookmark-URL UIA bypass by extending page_bookmark/execute_javascript with Favorites bar visibility toggle and foreground-restore polling.
  • trycua/cua#1625: Modifies Windows Chromium (x,y) click dispatch path in impl_.rs with post-click foreground-restore polling behavior.
  • trycua/cua#1611: Modifies HotkeyTool::invoke dispatch logic in platform-windows/src/tools/impl_.rs for how modifier hotkeys route across different host types.

🐰 Click and hotkey dance through Chromium's async waltz,
PostMessage whispers secrets, no foreground exalts.
Bookmark favorites bar shines via Ctrl+Shift+B's might,
Foreground restoration guards the user's rightful light!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main changes: Chromium-targeted hotkey routing, bookmark invocation restore, and click polling—all key audit items addressing no-foreground violations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch windows-no-foreground-audit-v2

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: 1

🤖 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/impl_.rs`:
- Around line 1713-1716: The restore task currently spawned with
restore_foreground_polling_best_effort(prev_fg_addr, pid) uses only pid to
decide whether to restore focus; change it to guard by the clicked target HWND
(or its root owner) instead: capture the clicked HWND (or compute its root
owner) at click time, pass that HWND (e.g. clicked_hwnd or clicked_root_owner)
into the restore task instead of or in addition to pid, and in
restore_foreground_polling_best_effort compare the current foreground HWND/root
owner (via GetForegroundWindow/GetAncestor/RootOwner) with the captured HWND
before restoring; update the spawn call and the restore function signature (and
any callers) to accept this HWND and use it for the focus check rather than
PID-only logic.
🪄 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: a4879e45-83de-4884-a14f-6ba4ba00e85f

📥 Commits

Reviewing files that changed from the base of the PR and between 7436ba8 and 907f8fb.

📒 Files selected for processing (3)
  • libs/cua-driver-rs/Skills/cua-driver-rs/WINDOWS.md
  • libs/cua-driver-rs/crates/platform-windows/src/tools/impl_.rs
  • libs/cua-driver-rs/crates/platform-windows/src/tools/page_bookmark.rs

Comment on lines +1713 to +1716
// Kick off the polling restore regardless of click result
// — even a partially-inserted click might have started an
// async activation Chromium can't undo.
tokio::spawn(restore_foreground_polling_best_effort(prev_fg_addr, pid));

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

Guard async restore by the activated target HWND, not PID-only.

This restore task can steal focus from a user who Alt-Tabs to another window in the same browser process, because PID match alone still passes. Gate on the clicked target HWND (or root owner) for the click path before restoring.

Proposed direction
- tokio::spawn(restore_foreground_polling_best_effort(prev_fg_addr, pid));
+ let target_hwnd_addr = hwnd as usize;
+ tokio::spawn(restore_foreground_polling_after_click_best_effort(
+     prev_fg_addr,
+     pid,
+     target_hwnd_addr,
+ ));
// Click-specific guard idea:
// restore only when foreground is the same HWND we clicked (or same root owner),
// not just any window owned by `pid`.
🤖 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/impl_.rs` around lines
1713 - 1716, The restore task currently spawned with
restore_foreground_polling_best_effort(prev_fg_addr, pid) uses only pid to
decide whether to restore focus; change it to guard by the clicked target HWND
(or its root owner) instead: capture the clicked HWND (or compute its root
owner) at click time, pass that HWND (e.g. clicked_hwnd or clicked_root_owner)
into the restore task instead of or in addition to pid, and in
restore_foreground_polling_best_effort compare the current foreground HWND/root
owner (via GetForegroundWindow/GetAncestor/RootOwner) with the captured HWND
before restoring; update the spawn call and the restore function signature (and
any callers) to accept this HWND and use it for the focus check rather than
PID-only logic.

@f-trycua
f-trycua merged commit 53ac662 into main May 24, 2026
5 checks passed
@f-trycua
f-trycua deleted the windows-no-foreground-audit-v2 branch May 24, 2026 10:01
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