Skip to content

fix(cua-driver)(windows): bail on minimized window screenshot - #1974

Merged
f-trycua merged 1 commit into
mainfrom
fix/windows-screenshot-minimized-guard
Jun 22, 2026
Merged

fix(cua-driver)(windows): bail on minimized window screenshot#1974
f-trycua merged 1 commit into
mainfrom
fix/windows-screenshot-minimized-guard

Conversation

@f-trycua

@f-trycua f-trycua commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #1973.

Summary

screenshot_window_bytes_with_occlusion_unsafe (the GDI / PrintWindow + screen-region BitBlt path) only guarded w <= 0 || h <= 0. A minimized Win32 window has GetWindowRect returning the off-screen iconic position (typically (-32000, -32000, -31840, -31972) — width 160, height 28, both positive), so the check passes, PrintWindow paints nothing into the bitmap, and the result is a heavily-compressed all-black ~300-byte PNG that an upstream agent can't distinguish from a real "blank screen" capture.

The WGC sibling path at wgc.rs:58 already short-circuits iconic windows, but:

  • on a non-XAML target (most apps) WGC is never tried — the code jumps to the GDI path;
  • on an XAML target, when WGC bails the code falls through to screenshot_via_screen_region, which has the same bug ((-32000, -32000) BitBlts a black tile of the off-screen region).

Fix

Single IsIconic check at the top of screenshot_window_bytes_with_occlusion_unsafe so every downstream capture path (WGC / GDI PrintWindow / screen-region BitBlt) is guarded by one source of truth, with an error matching the WGC error shape so callers can call list_windows / raise_window and retry.

+25 / -1 on a single file.

Test plan

  • cargo check -p cua-driver clean on macOS host.
  • Manual smoke on a Windows VM: minimize Notepad, call screenshot via MCP with its window_id, observe typed error instead of a 300-byte PNG. Restore Notepad, retry, observe a valid full-window capture.
  • Downstream MCP clients (Hermes' cua_backend.py, cua-agent) propagate the error instead of caching the degenerate image.

Notes

  • This doesn't fix the sticky _active_window_id caching in MCP-client wrappers like Hermes (that's a client-side concern), but the clear error from cua-driver makes it self-correcting from the model's perspective — the next turn the model can call list_windows and re-resolve.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced screenshot capture to properly handle minimized windows. The system now detects minimized windows and returns a clear error message instead of producing black or corrupted screenshots.

`screenshot_window_bytes_with_occlusion_unsafe` (the GDI / PrintWindow
+ screen-region BitBlt path) only guarded `w <= 0 || h <= 0`. A
minimized Win32 window has `GetWindowRect` returning the off-screen
iconic position (typically `(-32000, -32000, -31840, -31972)` — width
160, height 28, both positive) so the check passes, `PrintWindow`
paints nothing into the bitmap, and the result is a heavily-compressed
all-black ~300-byte PNG that upstream agents can't distinguish from a
real 'blank screen' capture.

The sibling WGC path at `wgc.rs:58` already short-circuits iconic
windows, but on a non-XAML target (most apps) WGC is never tried — the
code goes straight to the GDI path. And on XAML targets, when WGC bails
the code falls through to `screenshot_via_screen_region` which has the
same bug.

Add a single `IsIconic` check at the top of
`screenshot_window_bytes_with_occlusion_unsafe` so every downstream
capture path (WGC / GDI PrintWindow / screen-region BitBlt) is guarded
by a single source of truth, and the error matches the shape WGC
already emits so callers can call list_windows / raise_window and
retry.

Closes #1973.
@vercel

vercel Bot commented Jun 22, 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 Jun 22, 2026 7:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90e8f67a-3254-4ebf-a7a3-9f9b675ce595

📥 Commits

Reviewing files that changed from the base of the PR and between 60e0156 and a2c4793.

📒 Files selected for processing (1)
  • libs/cua-driver/rust/crates/platform-windows/src/capture.rs

📝 Walkthrough

Walkthrough

Adds an IsIconic check at the top of screenshot_window_bytes_with_occlusion_unsafe in the Windows GDI capture path. If the target HWND is minimized/iconic, the function immediately returns an error instead of proceeding to PrintWindow or BitBlt, which would otherwise produce an all-black degenerate bitmap.

Changes

Minimized window guard in GDI capture path

Layer / File(s) Summary
IsIconic early-bail guard
libs/cua-driver/rust/crates/platform-windows/src/capture.rs
Adds IsIconic to local Windows API imports and inserts an early error return at the start of screenshot_window_bytes_with_occlusion_unsafe when the HWND is minimized, matching the guard already present in the WGC path.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related issues

Possibly related PRs

  • trycua/cua#1599: Modifies the same PrintWindow-based capture path in capture.rs to avoid returning degenerate/all-black bitmaps via a screen-region BitBlt fallback for UWP/XAML windows.
  • trycua/cua#1697: Also modifies screenshot_window_bytes_with_occlusion_unsafe in the same file, adding a different guard (DWMWA_EXTENDED_FRAME_BOUNDS cropping) to prevent visually degenerate PrintWindow output.
  • trycua/cua#1696: Modifies the same function in capture.rs, changing buffer sizing from GetClientRect to GetWindowRect in the same occlusion-aware PrintWindow path.

Poem

🐇 Hop hop, no blank screens today,
A minimized window? I bail away!
IsIconic checks before pixels flow,
No all-black PNG putting on a show.
The agents rejoice, no wasted turns remain,
This rabbit patched the GDI capture chain! 🖼️

✨ 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 fix/windows-screenshot-minimized-guard

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.

@f-trycua
f-trycua merged commit 9ab589f into main Jun 22, 2026
65 of 69 checks passed
@f-trycua
f-trycua deleted the fix/windows-screenshot-minimized-guard branch June 22, 2026 19:14
@github-actions

Copy link
Copy Markdown
Contributor

Linux visual regression artifacts

Matrix jobs now run independently. Download visual artifacts from this workflow run.
Each background-GUI job uploads a .gif of the interaction plus two annotated PNGs (<app>.png raw, <app>-atspi.png with AT-SPI element boxes); the cua-driver-linux-som-overlays artifact adds <app>-som.png cua Set-of-Marks overlays:

  • cua-driver-linux-cursor-click-gif
  • cua-driver-linux-background-terminal-gif
  • cua-driver-linux-parallel-drag-xserver
  • cua-driver-linux-background-gui-chromium
  • cua-driver-linux-background-gui-tk
  • cua-driver-linux-background-gui-gtk3-gedit
  • cua-driver-linux-background-gui-gtk3-mousepad
  • cua-driver-linux-background-gui-gtk3-scite
  • cua-driver-linux-background-gui-gtk4-characters
  • cua-driver-linux-background-gui-qt5-manuskript
  • cua-driver-linux-background-gui-qt5-klog
  • cua-driver-linux-background-gui-qt5-openambit
  • cua-driver-linux-background-gui-qt6-kate
  • cua-driver-linux-background-gui-qt6-kcalc
  • cua-driver-linux-background-gui-qt6-okular
  • cua-driver-linux-background-gui-qt6-qownnotes
  • cua-driver-linux-background-gui-electron-zettlr
  • cua-driver-linux-background-gui-electron-joplin
  • cua-driver-linux-background-gui-electron-logseq
  • cua-driver-linux-som-overlays

Open workflow run and download artifacts

f-trycua added a commit that referenced this pull request Jun 22, 2026
…1977)

Follow-up to #1974. The iconic-window bail from `screenshot_window_bytes`
correctly fires for minimized targets, but `get_window_state` swallowed
the error at impl_.rs:708:

  Err(_) => None,

So the call returned an empty response (no image, no error) and the
upstream agent had no signal that the window was minimized — it just
saw 'no screenshot' and kept retrying, exactly the loop pattern from the
original Hermes bug report.

Capture the screenshot error alongside the (optional) image, and on the
no-image branch surface it in BOTH:

- the human-readable `content` stream as 'screenshot unavailable: <err>'
  so models with content-only parsing see it next to the UIA tree they
  did get;
- `structuredContent.screenshot_error` for MCP clients that consume the
  structured side (Hermes' cua_backend.py, downstream wrappers).

Now a minimized-window `get_window_state` returns the typed bail
('cannot capture minimized window 0x… : it has no rendered content.
Restore the window first via list_windows / raise_window…'), making the
issue actionable end-to-end instead of stopping at a silent empty
response.

Single file change, +25 / -6.
f-trycua pushed a commit to biztex/cua that referenced this pull request Aug 4, 2026
Element actions against a minimized window were dispatched to the Win32
iconic position and silently did nothing, while the tool reported success.

Windows parks a minimized window at (-32000, -32000), so GetWindowRect
returns (-32000,-32000)-(-31840,-31972) -- width 160, height 28, both
positive. UIA mirrors that same sentinel into the BoundingRectangle of
every element inside the window, so the cached element center is a
sentinel point and the window rect is a sentinel rect.

point_in_window_bounds then compares one against the other. Because both
sides are poisoned identically, the sentinel center tests as *inside* the
sentinel window rect and the guard passes. The click is posted to
coordinates no monitor covers; nothing happens and no error is raised.
The guard that exists to catch off-screen centers is structurally unable
to catch this case.

Refuse it in resolve_onscreen_point_with_scroll, before the containment
check, on either signal: the resolved point being in the sentinel region,
or IsIconic on the target window. The point test is kept alongside the
authoritative IsIconic call because the reports in trycua#2015 include elements
carrying sentinel bounds while their window appeared visible on a
secondary monitor -- IsIconic alone would not cover that shape.

The error names the remedy (bring_to_front, then re-snapshot), matching
the wording the capture path already uses for the same condition. The
capture and WGC paths have refused iconic windows since trycua#1973/trycua#1974; this
brings the input path in line.

Scope: this fixes the silent no-op. get_window_state still reports the
raw sentinel bounds in elements[].frame -- suppressing or annotating
those changes the response shape, so it is left for a follow-up.

Refs trycua#2015, trycua#1979, trycua#1981
f-trycua pushed a commit to biztex/cua that referenced this pull request Aug 4, 2026
Element actions against a minimized window were dispatched to the Win32
iconic position and silently did nothing, while the tool reported success.

Windows parks a minimized window at (-32000, -32000), so GetWindowRect
returns (-32000,-32000)-(-31840,-31972) -- width 160, height 28, both
positive. UIA mirrors that same sentinel into the BoundingRectangle of
every element inside the window, so the cached element center is a
sentinel point and the window rect is a sentinel rect.

point_in_window_bounds then compares one against the other. Because both
sides are poisoned identically, the sentinel center tests as *inside* the
sentinel window rect and the guard passes. The click is posted to
coordinates no monitor covers; nothing happens and no error is raised.
The guard that exists to catch off-screen centers is structurally unable
to catch this case.

Refuse it in resolve_onscreen_point_with_scroll, before the containment
check, on either signal: the resolved point being in the sentinel region,
or IsIconic on the target window. The point test is kept alongside the
authoritative IsIconic call because the reports in trycua#2015 include elements
carrying sentinel bounds while their window appeared visible on a
secondary monitor -- IsIconic alone would not cover that shape.

The error names the remedy (bring_to_front, then re-snapshot), matching
the wording the capture path already uses for the same condition. The
capture and WGC paths have refused iconic windows since trycua#1973/trycua#1974; this
brings the input path in line.

Scope: this fixes the silent no-op. get_window_state still reports the
raw sentinel bounds in elements[].frame -- suppressing or annotating
those changes the response shape, so it is left for a follow-up.

Refs trycua#2015, trycua#1979, trycua#1981
f-trycua pushed a commit that referenced this pull request Aug 4, 2026
* fix(cua-driver/windows): refuse iconic-sentinel element actions

Element actions against a minimized window were dispatched to the Win32
iconic position and silently did nothing, while the tool reported success.

Windows parks a minimized window at (-32000, -32000), so GetWindowRect
returns (-32000,-32000)-(-31840,-31972) -- width 160, height 28, both
positive. UIA mirrors that same sentinel into the BoundingRectangle of
every element inside the window, so the cached element center is a
sentinel point and the window rect is a sentinel rect.

point_in_window_bounds then compares one against the other. Because both
sides are poisoned identically, the sentinel center tests as *inside* the
sentinel window rect and the guard passes. The click is posted to
coordinates no monitor covers; nothing happens and no error is raised.
The guard that exists to catch off-screen centers is structurally unable
to catch this case.

Refuse it in resolve_onscreen_point_with_scroll, before the containment
check, on either signal: the resolved point being in the sentinel region,
or IsIconic on the target window. The point test is kept alongside the
authoritative IsIconic call because the reports in #2015 include elements
carrying sentinel bounds while their window appeared visible on a
secondary monitor -- IsIconic alone would not cover that shape.

The error names the remedy (bring_to_front, then re-snapshot), matching
the wording the capture path already uses for the same condition. The
capture and WGC paths have refused iconic windows since #1973/#1974; this
brings the input path in line.

Scope: this fixes the silent no-op. get_window_state still reports the
raw sentinel bounds in elements[].frame -- suppressing or annotating
those changes the response shape, so it is left for a follow-up.

Refs #2015, #1979, #1981

* fix(cua-driver/windows): type minimized element refusals

Replace the fixed negative-coordinate heuristic with live virtual-desktop bounds, fail before every element click route, and expose window_minimized as a structured refusal.

Add a real minimized WPF public-driver cell with fixture, focus, z-order, cursor, and leaked-input oracles, plus evidence-validator coverage.

* fix(cua-driver): record refusals without click markers

---------

Co-authored-by: trycua-release[bot] <trycua-release[bot]@users.noreply.github.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.

cua-driver(windows): screenshot of minimized window returns ~300-byte all-black PNG instead of erroring

1 participant