Skip to content

fix(cua-driver-rs)(macos): surface display-asleep state in capture errors and action results - #2104

Closed
zqchris wants to merge 1 commit into
trycua:mainfrom
zqchris:fix/macos-display-asleep-honesty
Closed

fix(cua-driver-rs)(macos): surface display-asleep state in capture errors and action results#2104
zqchris wants to merge 1 commit into
trycua:mainfrom
zqchris:fix/macos-display-asleep-honesty

Conversation

@zqchris

@zqchris zqchris commented Jul 3, 2026

Copy link
Copy Markdown

Problem

When the macOS main display is asleep (idle sleep, lid closed), the driver goes blind but never says so:

  • screencapture fails for every window → get_window_state / zoom / debug_image_out return opaque errors: screencapture failed for window N, No content produced (neither AX tree nor screenshot succeeded).
  • When the AX walk still succeeds, the screenshot failure is silent (a tracing::warn only) — and the escalation block even tells the agent to "act by pixel (x,y) off the screenshot in this response" when no screenshot is in the response.
  • Pointer/keyboard actions still return success (✅ Posted click … confirm via screenshot) — but the app may never render the event, and there is no screenshot to confirm with.

An agent with no display-asleep signal does the worst possible thing: it assumes its coordinates are wrong, re-derives them, and keeps re-posting clicks into the void until its budget is gone.

Real-world repro (how we hit this): an agent was driving an Electron app while the user stepped away; the display slept mid-session. get_window_state degraded to "No content produced", zoom failed for every window on the system, and click kept reporting success. The agent spent the rest of the run second-guessing perfectly correct coordinates.

Fix (macOS only)

  • New display_state moduleCGDisplayIsAsleep(CGMainDisplayID()) FFI + shared hint/suffix strings; suffix logic split into a pure helper with unit tests.

  • capture.rs — window/display screencapture failures append the asleep hint when the display is asleep (checked only on the failure path; zero happy-path overhead).

  • get_window_state — new screenshot_error field (structured + text content) so a missing screenshot is never silent; the No content produced double-failure error carries the asleep hint.

  • Action tools (click, double_click, drag, type_text, press_key, hotkey, scroll) — success texts append an explicit warning while asleep:

    😴 Main display is ASLEEP: the event was posted, but the app may never render it and no screenshot can verify it. Wake the display (user presence or caffeinate -u -t 1) before retrying or verifying — do NOT re-derive coordinates from this failure.

Verification

  • All 113 platform-macos tests pass; 2 new unit tests for the suffix helper.

  • Live-tested against a genuinely sleeping Retina display (built daemon on a temp socket):

    screenshot_error = screencapture failed for window 17554: the main display
    is asleep, so macOS cannot capture windows. Wake it (user presence or
    `caffeinate -u -t 1`) and retry
    
  • Awake path unchanged (suffix is "", capture errors identical to before).

Possible follow-up (not in this PR)

A structured display_asleep: true flag on action results would make the state machine-checkable in structured-only consumers (the CLI call output prints structured only); left out to keep this change text-first and minimal.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved macOS capture error messages to better indicate when the main display is asleep.
    • Screenshot-based window state results now surface capture failures instead of hiding them, including a clear warning when images are unavailable.
    • Several desktop action responses now include an additional status note when the display is asleep, making outcomes easier to interpret.

…rors and action results

When the main display is asleep (idle sleep / lid closed), every
screencapture fails and posted CGEvents may never render — but the
driver reported opaque errors ('screencapture failed for window N',
'No content produced') and cheerful action results ('Posted click …
confirm via screenshot'), while get_window_state's escalation even
told the agent to act off a screenshot that silently failed to
capture. Agents burn their whole budget re-deriving coordinates and
re-posting clicks into the void with no signal that the display is
the problem.

Observed in a real session: an agent drove an Electron app while the
user stepped away; the display slept mid-run, get_window_state
degraded to 'No content produced', zoom failed for every window, and
click kept returning success.

Fix (macOS):
- new display_state module: CGDisplayIsAsleep(CGMainDisplayID()) FFI
  + shared hint/suffix strings, unit-tested via a pure helper
- capture.rs: window/display screencapture failures append the
  asleep hint when the display is asleep
- get_window_state: NEW screenshot_error field (structured + text
  content) so a missing screenshot is never silent; the
  'No content produced' double-failure error carries the asleep hint
- click / double_click / drag / type_text / press_key / hotkey /
  scroll success texts append an explicit '😴 Main display is
  ASLEEP … do NOT re-derive coordinates' warning while asleep

Verified live against a sleeping Retina display: screenshot_error =
'screencapture failed for window 17554: the main display is asleep,
so macOS cannot capture windows. Wake it (user presence or
`caffeinate -u -t 1`) and retry'. All 113 platform-macos tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@zqchris is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a macOS display_state module that detects whether the main display is asleep via CoreGraphics FFI, provides a capture hint constant and a suffix helper. Wires this into capture failure messages and appends the suffix to success text across multiple input tools. Also adds screenshot-error surfacing in get_window_state.

Changes

Asleep display detection and messaging

Layer / File(s) Summary
Display state detection module
libs/cua-driver/rust/crates/platform-macos/src/display_state.rs, libs/cua-driver/rust/crates/platform-macos/src/lib.rs
New module with FFI to CGMainDisplayID/CGDisplayIsAsleep, main_display_asleep(), ASLEEP_CAPTURE_HINT, asleep_suffix(), unit tests, and module registration.
Capture failure hint integration
libs/cua-driver/rust/crates/platform-macos/src/capture.rs
Window and display screenshot failure paths now check asleep state and include ASLEEP_CAPTURE_HINT in the bail message when applicable.
Tool success message suffixes
libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs, double_click.rs, drag.rs, hotkey.rs, press_key.rs, scroll.rs, type_text.rs
Success messages across click, double-click, drag, hotkey, press key, scroll, and type text tools append asleep_suffix() to the formatted output text.
Screenshot error surfacing
libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs
Adds screenshot_error tracking, appends a warning note to response text, and includes screenshot_error in the structured output when capture fails.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

  • trycua/cua#1531: Both PRs modify success-message formatting in the same tool invoke paths (click, drag, hotkey, press_key, scroll, type_text).
  • trycua/cua#1977: Both PRs update GetWindowStateTool::invoke to surface screenshot capture failures via a screenshot_error field.
  • trycua/cua#2056: Both PRs modify platform-macos/src/tools/click.rs, though for different purposes.

Poem

A rabbit checked the screen, so bright and wide,
"Is it asleep?" it asked with pride.
If dark it dozes, a hint we send,
Click, drag, and type—suffix to the end.
🐇💤 Wake up, display, our work's not done!

🚥 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 captures the macOS display-asleep signaling added to capture errors and action results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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

🧹 Nitpick comments (3)
libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs (1)

270-278: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Final error discards the captured screenshot_error detail.

screenshot_error is populated specifically to surface why the screenshot failed (permissions, WindowServer refusal, etc.), but the content.is_empty() branch never includes it — only the generic message plus, conditionally, the asleep hint. The actual root cause captured a few lines earlier is thrown away right when it would matter most (total capture failure).

♻️ Proposed fix to include the captured detail
         if content.is_empty() {
+            let detail = screenshot_error
+                .as_deref()
+                .map(|e| format!(" (screenshot error: {e})"))
+                .unwrap_or_default();
             if crate::display_state::main_display_asleep() {
                 return ToolResult::error(format!(
-                    "No content produced (neither AX tree nor screenshot succeeded) — {}",
-                    crate::display_state::ASLEEP_CAPTURE_HINT
+                    "No content produced (neither AX tree nor screenshot succeeded){} — {}",
+                    detail, crate::display_state::ASLEEP_CAPTURE_HINT
                 ));
             }
-            return ToolResult::error("No content produced (neither AX tree nor screenshot succeeded)");
+            return ToolResult::error(format!(
+                "No content produced (neither AX tree nor screenshot succeeded){}",
+                detail
+            ));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs`
around lines 270 - 278, The final fallback in get_window_state currently drops
the captured screenshot_error, so total capture failures lose the real root
cause. Update the content.is_empty() error path in get_window_state to include
the stored screenshot_error detail alongside the existing generic message, while
still preserving the asleep-case hint from display_state::ASLEEP_CAPTURE_HINT
when main_display_asleep() is true. Keep the fix scoped to the existing
screenshot capture/result assembly logic so the final ToolResult::error surfaces
the actual failure reason.
libs/cua-driver/rust/crates/platform-macos/src/display_state.rs (1)

15-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse core-graphics's existing bindings instead of hand-rolled FFI.

core-graphics 0.24.0 is already a dependency in this crate and publicly exports CGMainDisplayID and CGDisplayIsAsleep from core_graphics::display with matching signatures. Re-declaring these extern "C" symbols here duplicates vetted bindings and risks subtle drift (e.g., the declared return type u32 here vs. upstream's boolean_t).

♻️ Suggested refactor
-use std::os::raw::c_uint;
-
-extern "C" {
-    fn CGMainDisplayID() -> c_uint;
-    fn CGDisplayIsAsleep(display: c_uint) -> u32;
-}
+use core_graphics::display::{CGDisplayIsAsleep, CGMainDisplayID};

 pub fn main_display_asleep() -> bool {
     unsafe { CGDisplayIsAsleep(CGMainDisplayID()) != 0 }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-macos/src/display_state.rs` around lines
15 - 20, The display sleep helpers are duplicating FFI bindings that already
exist in core_graphics::display. Update display_state.rs to stop declaring
CGMainDisplayID and CGDisplayIsAsleep in the local extern block and instead use
the existing core-graphics bindings directly from core_graphics::display,
keeping the same call sites in display_state logic but relying on the upstream
signatures to avoid drift.
libs/cua-driver/rust/crates/platform-macos/src/capture.rs (1)

29-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated asleep-check-and-bail logic across two functions.

Both screenshot_window_bytes and screenshot_display_bytes repeat the same "check main_display_asleep(), then bail with ASLEEP_CAPTURE_HINT" pattern. Extracting a small helper (e.g., in display_state.rs) would avoid drift if the hint format changes later.

♻️ Suggested helper
// in display_state.rs
pub fn asleep_aware_bail(context: &str) -> String {
    if main_display_asleep() {
        format!("{context}: {ASLEEP_CAPTURE_HINT}")
    } else {
        context.to_string()
    }
}
-    if !status.success() {
-        if crate::display_state::main_display_asleep() {
-            anyhow::bail!(
-                "screencapture failed for window {window_id}: {}",
-                crate::display_state::ASLEEP_CAPTURE_HINT
-            );
-        }
-        anyhow::bail!("screencapture failed for window {window_id}");
-    }
+    if !status.success() {
+        anyhow::bail!(crate::display_state::asleep_aware_bail(
+            &format!("screencapture failed for window {window_id}")
+        ));
+    }

Also applies to: 67-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-macos/src/capture.rs` around lines 29 -
37, The asleep-check-and-bail pattern is duplicated in both screenshot capture
paths, so consolidate it into a shared helper instead of repeating the
`main_display_asleep()` branch in `screenshot_window_bytes` and
`screenshot_display_bytes`. Add a small utility in `display_state` (for example
an `asleep_aware_bail` helper) that returns the appropriate error message using
`ASLEEP_CAPTURE_HINT`, then call that helper from the capture functions so both
window and display failures stay consistent.
🤖 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/rust/crates/platform-macos/src/tools/double_click.rs`:
- Around line 250-253: The AXOpen fast-path in ax_double_click() is still
returning a plain success message without the display-asleep hint. Update the
AXOpen success string to append crate::display_state::asleep_suffix(), matching
the existing double-click success path in double_click() so openable-element
clicks also surface the asleep warning.

---

Nitpick comments:
In `@libs/cua-driver/rust/crates/platform-macos/src/capture.rs`:
- Around line 29-37: The asleep-check-and-bail pattern is duplicated in both
screenshot capture paths, so consolidate it into a shared helper instead of
repeating the `main_display_asleep()` branch in `screenshot_window_bytes` and
`screenshot_display_bytes`. Add a small utility in `display_state` (for example
an `asleep_aware_bail` helper) that returns the appropriate error message using
`ASLEEP_CAPTURE_HINT`, then call that helper from the capture functions so both
window and display failures stay consistent.

In `@libs/cua-driver/rust/crates/platform-macos/src/display_state.rs`:
- Around line 15-20: The display sleep helpers are duplicating FFI bindings that
already exist in core_graphics::display. Update display_state.rs to stop
declaring CGMainDisplayID and CGDisplayIsAsleep in the local extern block and
instead use the existing core-graphics bindings directly from
core_graphics::display, keeping the same call sites in display_state logic but
relying on the upstream signatures to avoid drift.

In `@libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs`:
- Around line 270-278: The final fallback in get_window_state currently drops
the captured screenshot_error, so total capture failures lose the real root
cause. Update the content.is_empty() error path in get_window_state to include
the stored screenshot_error detail alongside the existing generic message, while
still preserving the asleep-case hint from display_state::ASLEEP_CAPTURE_HINT
when main_display_asleep() is true. Keep the fix scoped to the existing
screenshot capture/result assembly logic so the final ToolResult::error surfaces
the actual failure reason.
🪄 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: 3559dd32-3c97-429f-b315-c62bd5a46a5d

📥 Commits

Reviewing files that changed from the base of the PR and between 73fe822 and 56ddf51.

📒 Files selected for processing (11)
  • libs/cua-driver/rust/crates/platform-macos/src/capture.rs
  • libs/cua-driver/rust/crates/platform-macos/src/display_state.rs
  • libs/cua-driver/rust/crates/platform-macos/src/lib.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/double_click.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/press_key.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs

Comment on lines +250 to +253
Ok(format!(
"✅ Double-clicked element [{idx}] at ({cx:.1}, {cy:.1}).{}",
crate::display_state::asleep_suffix()
))

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend the asleep hint to the AXOpen fast-path.

ax_double_click() still returns a plain AXOpen performed... string, so successful openable-element double-clicks won't include the new display-asleep warning.

Diff
 if err == kAXErrorSuccess {
-    return Ok(format!("AXOpen performed on element [{idx}]."));
+    return Ok(format!(
+        "AXOpen performed on element [{idx}].{}",
+        crate::display_state::asleep_suffix()
+    ));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/rust/crates/platform-macos/src/tools/double_click.rs` around
lines 250 - 253, The AXOpen fast-path in ax_double_click() is still returning a
plain success message without the display-asleep hint. Update the AXOpen success
string to append crate::display_state::asleep_suffix(), matching the existing
double-click success path in double_click() so openable-element clicks also
surface the asleep warning.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56ddf518a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

changes.result_suffix()
not driver-verified — confirm via screenshot).{}{}",
changes.result_suffix(),
crate::display_state::asleep_suffix()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add asleep warning to AX click successes

Adding the suffix here only covers the x/y CGEvent success path; the default click({pid, window_id, element_index}) AX branch still returns ToolResult::text(msg) after perform_ax_click without appending display_state::asleep_suffix(). When the display is asleep, the primary element-indexed click therefore reports a normal success with no sleep marker, so callers can keep retrying or re-deriving coordinates despite this commit's new warning contract.

Useful? React with 👍 / 👎.

@zqchris

zqchris commented Aug 5, 2026

Copy link
Copy Markdown
Author

Closing this one — no longer pursuing it on my side. Feel free to pick up the diff if the display-asleep surfacing is still wanted.

@zqchris zqchris closed this Aug 5, 2026
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