From ec94c9465186a48efad3b373525fa178b345f702 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 31 May 2026 15:42:32 -0700 Subject: [PATCH] fix(cua-driver-rs)(macos): run the agent-cursor overlay in the serve daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay NSWindow + AppKit render loop were only wired into the in-process `mcp` arm. In the daemon-proxy setup users actually run (`mcp` relaunches `open -n -g … serve` and proxies to it for correct TCC attribution), the DAEMON performs the clicks/AX presses but never inited or ran the overlay — its main thread parked in `serve_handle.join()`. So `set_agent_cursor_enabled` flipped registry flags and clicks sent OverlayCommands, but CMD_TX was never set → every cursor command was a silent no-op and the agent cursor never appeared (confirmed: daemon main thread in pthread_join, zero tiny_skia / SkyLight / NSWindow activity). Fix: the Serve arm now builds cursor_cfg, inits the overlay channel before spawning the serve thread, and — when the cursor is enabled — parks the main thread in `overlay::run_on_main_thread()` (mirrors the Mcp arm) instead of join. It self-guards on has_graphic_access() and falls back to join when the daemon has no Window Server session, so headless serving is unaffected. Verified: with the fix the serve daemon's main thread is now in __CFRunLoopRun / -[NSApplication run] with tiny_skia + SkyLight overlay render activity, and it still serves (list_windows over the socket works). Co-Authored-By: Claude Opus 4.8 --- .../rust/crates/cua-driver/src/main.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/libs/cua-driver/rust/crates/cua-driver/src/main.rs b/libs/cua-driver/rust/crates/cua-driver/src/main.rs index 3c6b9d8dfe..41f0dc1a6f 100644 --- a/libs/cua-driver/rust/crates/cua-driver/src/main.rs +++ b/libs/cua-driver/rust/crates/cua-driver/src/main.rs @@ -259,6 +259,21 @@ fn main() { None => pip_preview::PipConfig::from_args(), }; maybe_init_pip(); + + // Agent-cursor overlay. The DAEMON is the process that actually + // performs clicks / AX presses, so the overlay NSWindow + render + // loop must run HERE — not only in the in-process `mcp` arm. In the + // daemon-proxy setup (`mcp` relaunches `open -n -g … serve` and + // proxies to it), the proxy never renders and, before this, neither + // did the daemon — so every cursor command was a silent no-op and + // the agent cursor never appeared. Init the channel before spawning + // the serve thread so `run_on_main_thread()` always finds it ready + // (mirrors the Mcp arm). + let cursor_cfg = cursor_overlay::CursorConfig::from_args(); + if cursor_cfg.enabled { + platform_macos::cursor::overlay::init(cursor_cfg.clone()); + } + let reg = Arc::new(build_macos_registry()); reg.init_self_weak(); let sp = socket.unwrap_or_else(serve::default_socket_path); @@ -320,6 +335,15 @@ fn main() { // stays up as long as the daemon does. if pip_cfg.enabled { platform_macos::pip::run_appkit_main_loop(); + } else if cursor_cfg.enabled { + // Render the agent-cursor overlay: park the main thread in the + // AppKit run loop so the overlay NSWindow draws. `run_on_main_thread` + // self-guards on `has_graphic_access()` and returns immediately + // when the daemon has no Window Server session — fall through to + // join so the daemon still serves headless. The serve thread runs + // on its background thread regardless. + platform_macos::cursor::overlay::run_on_main_thread(); + let _ = serve_handle.join(); } else { let _ = serve_handle.join(); }