Skip to content

cua-driver v0.1 — initial public release - #1359

Merged
f-trycua merged 13 commits into
mainfrom
v0.1-public
Apr 23, 2026
Merged

cua-driver v0.1 — initial public release#1359
f-trycua merged 13 commits into
mainfrom
v0.1-public

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Summary

cua-driver - background computer-use driver for any agents.

A macOS driver that lets any agent (Claude, GPT, Gemini, Codex, OpenCode, custom loops) drive a real Mac app in the background while the user keeps working. Speaks MCP over stdio as a drop-in for Claude Code, Cursor, or the cua-computer-server stack, and ships a CLI where every MCP tool is a top-level subcommand.

Built around a strict no-foreground contract: the user's frontmost app never changes, the real cursor never warps, and the target never raises or switches Space. The agent drives one window; the user keeps typing in another.

This PR lands the entire libs/cua-driver subtree in a single squashed commit. Full changelog lives in the commit body.

Highlights

  • Element-indexed AX actions that work on hidden, off-Space, or occluded targets. click({pid, window_id, element_index}) fires the AX action without cursor move or focus steal.
  • Background pixel clicks via auth-signed SLEventPostToPid with a yabai-style focus-without-raise primer. Same (x, y) space as the returned screenshot; supports modifiers and click count.
  • Three capture modes: vision (PNG only, default), ax (tree only, no screen-capture cost), som (both).
  • Chromium / Electron AX via the private _AXObserverAddNotificationAndCheckRemote SPI so the tree stays populated without activating the target.
  • Pid-mandatory keyboard: every press_key / type_text routes through CGEvent.postToPid, so keys can't leak into the user's foreground app.
  • Agent-cursor overlay that glides to each target before dispatch, press-in / ripple on landing, idle-hides. Uniform across AX and pixel clicks.
  • Trajectory recording + replay: per-turn folders with app state, screenshot, action, click marker. Optional video capture with zoom-on-click render for demos.
  • ScreenCaptureKit screenshots defaulting to a 1568-long-side cap matching common multimodal input limits, so model-picked pixel coords match the tool's coordinate space.

Action-loop conventions live in libs/cua-driver/Skills/cua-driver/SKILL.md. Full technical notes (SkyLight SPIs, yabai recipe, comparison vs Codex Computer Use / Claude Computer Use) ship in the release blog post.

Try it

cd libs/cua-driver
./scripts/install.sh          # build + grant TCC
cua-driver serve &            # start the daemon
cua-driver launch_app '{"bundle_id":"com.apple.calculator"}'
cua-driver get_app_state '{"pid":<pid>}'
cua-driver click '{"pid":<pid>,"element_index":14}'

Or point your MCP client at cua-driver mcp.

Test plan

  • libs/cua-driver/scripts/install.sh on a clean Mac: builds without warnings, TCC dialogs appear for Accessibility + Screen Recording
  • cua-driver check_permissions returns both grants true
  • scripts/test.sh (Python integration suite against the built binary) passes end-to-end
  • Drive a backgrounded Calculator from Claude Code via MCP: the user's frontmost app stays frontmost, cursor doesn't move
  • cua-driver recording start <dir> → drive an app → recording stop produces session.json + per-turn folders + optional zoom-rendered recording.mp4

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added cua-driver, a new macOS background computer-use driver for controlling native apps without raising windows.
    • Added CLI with commands for app launching, clicking, typing, scrolling, and cursor control.
    • Added agent cursor visualization for on-screen feedback during automated interactions.
    • Added trajectory recording and replay capabilities for capturing and re-running interaction sequences.
    • Added MCP server integration for programmatic tool access.
  • Documentation

    • Added comprehensive guides for skill usage, web-app handling, recording workflows, and test specifications.
  • Chores

    • Added CI/CD workflows for Swift build and release processes.
    • Added configuration and permission management infrastructure.

@vercel

vercel Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Apr 23, 2026 7:16am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75c1fc86-5b07-4595-9b42-e30691645aa0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces the Cua Driver—a macOS background computer-use driver that enables Claude to interact with native applications via accessibility APIs and input synthesis. Includes complete Swift package with CLI entry points, core driver library, MCP server implementation with ~25 tools, focus/window management, input handling, cursor overlay, recording/trajectory playback, and comprehensive skill documentation.

Changes

Cohort / File(s) Summary
CI/CD Workflows
.github/workflows/cd-swift-cua-driver.yml, .github/workflows/ci-swift-cua-driver.yml
New macOS CI/CD pipeline: dedicated test/build workflow for pull requests; release workflow deriving version from tags, importing certificates, building/notarizing, uploading artifacts, and creating GitHub releases with checksums and installation instructions.
Documentation & Skills
README.md, libs/cua-driver/README.md, libs/cua-driver/Skills/cua-driver/*
Updated project README with Cua Driver card; new driver README; four comprehensive skill guides covering core behavior (SKILL.md), web-app interactions (WEB_APPS.md), recording/replay (RECORDING.md), and deterministic test specifications (TESTS.md).
Package Configuration
libs/cua-driver/Package.swift, libs/cua-driver/Package.resolved, libs/cua-driver/.gitignore, libs/cua-driver/App/CuaDriver/Info.plist
Swift package manifest defining products (CLI, libraries), dependencies, targets; lockfile pinning transitive deps; macOS-specific gitignore; app bundle metadata (CFBundle keys, version 0.0.1, min OS 14.0).
CLI Implementation
libs/cua-driver/Sources/CuaDriverCLI/*
Seven command modules: root entry point with telemetry/GUI permissions/argv rewriting; mcp bootstrapping AppKit/permissions/stdio server; serve/stop/status daemon lifecycle; call/list-tools/describe tool dispatch; config/telemetry/updates management; diagnose system inspection; recording render video post-processing; update auto-update runner.
Core Driver Library — Apps & Windows
libs/cua-driver/Sources/CuaDriverCore/Apps/*, libs/cua-driver/Sources/CuaDriverCore/Windows/*
App enumeration (running + installed) and launching with non-activating backgrounded mode; window enumeration and metadata extraction; Space migration detection; window-local-to-screen coordinate translation.
Core Driver Library — Accessibility & Focus
libs/cua-driver/Sources/CuaDriverCore/AppState/*, libs/cua-driver/Sources/CuaDriverCore/Focus/*, libs/cua-driver/Sources/CuaDriverCore/Input/AXInput.swift
AX tree snapshot engine with element indexing and caching; AX enablement assertion per-process; synthetic focus suppression (prevent foreground activation); system-focus-steal prevention via workspace observation; AX input primitives (actions, attributes, element location).
Core Driver Library — Input Synthesis
libs/cua-driver/Sources/CuaDriverCore/Input/KeyboardInput.swift, libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift, libs/cua-driver/Sources/CuaDriverCore/Input/CursorControl.swift, libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swift, libs/cua-driver/Sources/CuaDriverCore/Input/SkyLightEventPost.swift
Keyboard (hotkeys, character typing) and mouse (click, right-click, multi-click) synthesis with HID/SkyLight posting; cursor position reading/warping; SkyLight private framework wrapper for auth-signed events and focus-without-raise activation.
Core Driver Library — Capture & Cursor
libs/cua-driver/Sources/CuaDriverCore/Capture/*, libs/cua-driver/Sources/CuaDriverCore/Cursor/*
Window/display screenshot capture via ScreenCaptureKit with scaling; screen size detection; debug crosshair overlay rendering; agent cursor overlay (SVG-based gradient/arrow rendering, Bezier motion, click ripples, bloom animation, workspace pinning, idle auto-hide).
Core Driver Library — Configuration & Permissions
libs/cua-driver/Sources/CuaDriverCore/Config/*, libs/cua-driver/Sources/CuaDriverCore/Permissions/*, libs/cua-driver/Sources/CuaDriverCore/CuaDriverCore.swift
Persistent JSON config store (capture mode, max image dimension, agent cursor motion, telemetry/auto-update flags); permission status query and request flows; version constant definition.
Core Driver Library — Recording & Video
libs/cua-driver/Sources/CuaDriverCore/Recording/*
Session-scoped trajectory recording (turn-based action/app-state/screenshot capture); cursor sampler and video recorder (H.264 via AVAsset framework); click-marker rendering; zoom region computation from click telemetry with Bezier easing; MP4 re-rendering with per-frame zoom/crop/focus via CoreImage; trajectory JSON loader for playback metadata.
Core Driver Library — Telemetry
libs/cua-driver/Sources/CuaDriverCore/Telemetry/*
PostHog event client with stable distinct ID persistence, environment variable override, opt-out config, and async non-blocking submission.
Server & Daemon
libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift, libs/cua-driver/Sources/CuaDriverServer/DaemonServer.swift, libs/cua-driver/Sources/CuaDriverServer/DaemonClient.swift, libs/cua-driver/Sources/CuaDriverServer/DaemonProtocol.swift, libs/cua-driver/Sources/CuaDriverServer/AppStateRegistry.swift, libs/cua-driver/Sources/CuaDriverServer/ToolRegistry.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ImageResizeRegistry.swift
MCP server factory; Unix-domain-socket daemon with signal handling and pid file; CLI-to-daemon JSON RPC client; tool registry with dispatch routing; shared singleton registry for accessibility/focus/cursor state.
Server & Daemon Tools — Inspection
libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/GetAccessibilityTreeTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ListAppsTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ListWindowsTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/GetScreenSizeTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift
Tools querying window state (AX tree, filtered by query), app/window enumeration, screen metadata, and display screenshots (PNG/JPEG).
Server & Daemon Tools — App & Input Control
libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ClickTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/DoubleClickTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/RightClickTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/PressKeyTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/HotkeyTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextCharsTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ScrollTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/MoveCursorTool.swift
App launching with non-activating mode and focus restoration; click/double-click/right-click with AX or pixel addressing; keyboard input (hotkeys, character typing, key presses); scrolling; cursor movement.
Server & Daemon Tools — Recording & Config
libs/cua-driver/Sources/CuaDriverServer/Tools/SetRecordingTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/GetRecordingStateTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/ReplayTrajectoryTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/SetConfigTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/GetConfigTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorEnabledTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorMotionTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/GetAgentCursorStateTool.swift, libs/cua-driver/Sources/CuaDriverServer/Tools/CheckPermissionsTool.swift
Tools for recording trajectory enable/disable and state query; trajectory replay with pacing and error handling; config get/set with dotted-key paths and persistence; agent cursor enablement/motion parameter tuning; permission checking and prompting.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

The diff introduces a complete, production-grade macOS background automation driver spanning 70+ heterogeneous files with logic density in multiple complex domains: Accessibility framework integration with element indexing and caching, private SkyLight API event synthesis, AppKit focus control and window management, CoreGraphics/ScreenCaptureKit image capture, video encoding/decoding via AVAsset, Bezier animation and curve rendering, concurrent actor-based state management, MCP server protocol, CLI argument parsing, daemon IPC, and sophisticated recording/trajectory infrastructure. Each subsystem requires careful reasoning about macOS system behavior, concurrency safety, and error handling. While many tool implementations follow repetitive patterns, the underlying infrastructure (AX tree management, focus suppression, event synthesis, coordinate spaces) demands deep scrutiny due to intricate interactions between accessibility, window state, and input delivery semantics.

Suggested reviewers

  • ddupont808

🐰 A burrow of code, 'tis true—
Apps now dance 'neath cursor's blue!
Trees of AX, windows bright,
Robots click through day and night—
Background magic, pure delight!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v0.1-public

@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: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟡 Minor comments (29)
libs/cua-driver/Sources/CuaDriverCore/Permissions/Permissions.swift-37-53 (1)

37-53: ⚠️ Potential issue | 🟡 Minor

Docstring overstates what the return value represents.

AXIsProcessTrustedWithOptions returns the trust state at call time, not after the user interacts with the dialog — the prompt is async and Accessibility grants typically require a restart to take effect. Same for CGRequestScreenCaptureAccess. Right after calling these with a missing grant, the bool will almost always be false, even when the user is about to (or just did) grant access.

Consider rewording to "Returns the trust state at call time (not after user interaction)" so callers like CheckPermissionsTool (which composes a summary immediately after) don't misinterpret the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Permissions/Permissions.swift` around
lines 37 - 53, The docstrings for requestAccessibility() and
requestScreenRecording() overstate the meaning of their return values; update
the comments for these functions (requestAccessibility, which calls
AXIsProcessTrustedWithOptions, and requestScreenRecording, which calls
CGRequestScreenCaptureAccess) to state that the returned Bool is the trust/grant
state at call time (the prompt is asynchronous and grants may require restart),
e.g. reword to "Returns the trust state at call time (not after user
interaction)"; ensure callers like CheckPermissionsTool are not misled by the
comment.
libs/cua-driver/Sources/CuaDriverCore/CuaDriverCore.swift-3-5 (1)

3-5: ⚠️ Potential issue | 🟡 Minor

Version string disagrees with the "v0.1 initial release" label.

CuaDriverCore.version = "0.0.1" is what the MCP server will advertise to clients (per the summary, it's the default CuaDriverMCPServer version). The PR title, branch, and release notes all say v0.1. Since this is the first public release and several downstream consumers (MCP clients, user-facing logs, recordings) are likely to key off this string, it's worth aligning before tagging.

-    public static let version = "0.0.1"
+    public static let version = "0.1.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/CuaDriverCore.swift` around lines 3 -
5, Update the exported version string on CuaDriverCore so it matches the release
label: change CuaDriverCore.version from "0.0.1" to the intended release value
(e.g., "0.1.0" or "0.1" depending on your versioning scheme) so the MCP server
advertises the correct version to clients; ensure the literal assigned to the
public static let version is updated in the CuaDriverCore enum.
libs/cua-driver/Sources/CuaDriverCore/Input/SkyLightEventPost.swift-418-427 (1)

418-427: ⚠️ Potential issue | 🟡 Minor

Avoid force-cast on kCGWindowNumber value.

info[kCGWindowNumber as String] as! Int will trap if the CF value bridges to anything other than Int (the underlying type is SInt32/UInt32, and historically it has bridged as different NSNumber representations across OS versions). CGWindowID is UInt32, so going through NSNumber.uint32Value is both safer and more accurate. A crash here would take down the driver on an otherwise benign window enumeration.

🛡️ Suggested fix
-        return all.compactMap { info -> CGWindowID? in
-            guard (info[kCGWindowOwnerPID as String] as? Int32) == pid
-            else { return nil }
-            return CGWindowID(info[kCGWindowNumber as String] as! Int)
-        }
+        return all.compactMap { info -> CGWindowID? in
+            guard (info[kCGWindowOwnerPID as String] as? pid_t) == pid,
+                  let num = info[kCGWindowNumber as String] as? NSNumber
+            else { return nil }
+            return CGWindowID(num.uint32Value)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Input/SkyLightEventPost.swift` around
lines 418 - 427, The force-cast in windowIDs(forPid:) is unsafe: replace the
`info[kCGWindowNumber as String] as! Int` force-cast with a safe extraction that
handles NSNumber/CFNumber and uses its uint32Value to create the CGWindowID;
e.g., guard-let the value as NSNumber (or CFNumber bridged) and call
uint32Value, then return CGWindowID(uint32Value) so the code won't crash if the
underlying representation differs across OS versions.
README.md-36-36 (1)

36-36: ⚠️ Potential issue | 🟡 Minor

Broken anchor link: Cua card points to a non-existent section.

The card links to #cua---agentic-ui-automation--code-execution, but the corresponding section heading on line 68 is ## Cua - Agent-Ready Sandboxes for Any OS, whose GitHub-generated anchor is #cua---agent-ready-sandboxes-for-any-os. Clicking the Cua card will not jump to its section.

🔗 Proposed fix
-        <a href="#cua---agentic-ui-automation--code-execution">
+        <a href="#cua---agent-ready-sandboxes-for-any-os">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 36, The README has a broken anchor: the card href uses
"#cua---agentic-ui-automation--code-execution" but the actual section heading is
"## Cua - Agent-Ready Sandboxes for Any OS" (GitHub anchor
"#cua---agent-ready-sandboxes-for-any-os"); update the href in the card (the
link with anchor "#cua---agentic-ui-automation--code-execution") to the correct
anchor "#cua---agent-ready-sandboxes-for-any-os" or alternatively rename the
section heading to match the existing anchor so the two identifiers (the card
href and the section heading) match.
libs/cua-driver/Sources/CuaDriverCore/Input/CursorControl.swift-18-21 (1)

18-21: ⚠️ Potential issue | 🟡 Minor

Check the CGWarpMouseCursorPosition return value and document the Space/"no-foreground" contract.

Two things worth tightening:

  1. CGWarpMouseCursorPosition returns a CGError — silently ignoring failures (e.g., when the daemon lacks the necessary permission, or on multi-display edge cases) means move_cursor reports success even when the warp was rejected. Propagating or at least logging the error would make tool failures actionable.
  2. The PR summary emphasizes a "no-foreground contract: … the real cursor is not warped." This utility warps the real cursor. That is fine for an explicit move_cursor tool invoked by the user, but a brief doc comment clarifying that CursorControl is intentionally the exception to the no-warp rule (and must not be used from the background-click path) would help future contributors avoid wiring it into places where it breaks the contract.
♻️ Proposed fix
     public static func move(to point: CGPoint) {
-        CGWarpMouseCursorPosition(point)
-        CGAssociateMouseAndMouseCursorPosition(1)
+        let err = CGWarpMouseCursorPosition(point)
+        if err != .success {
+            // Caller should surface this; warp can fail on permission or
+            // invalid coordinates and we don't want to claim success.
+            // Consider logging here once a logger is wired in.
+        }
+        CGAssociateMouseAndMouseCursorPosition(1)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Input/CursorControl.swift` around lines
18 - 21, Update the move(to point: CGPoint) implementation to check the
CGWarpMouseCursorPosition return CGError and surface failures (either propagate
the error from move(to:) or call the process logger/OSLog with the CGError)
instead of ignoring it; keep the CGAssociateMouseAndMouseCursorPosition call but
only after a successful warp. Also add a brief doc comment on the
CursorControl/move(to:) API stating this intentionally warps the real cursor
(exception to the "no-foreground" contract) and must NOT be used from
background-click or other background paths.
libs/cua-driver/Sources/CuaDriverServer/Tools/MoveCursorTool.swift-47-57 (1)

47-57: ⚠️ Potential issue | 🟡 Minor

Dead code: point is constructed but never used.

let point = CursorPoint(x: x, y: y) on line 48 is unused — the success message interpolates x and y directly. Either drop the binding or use it to format the response (and consider including it as structured content for tool consumers, since the tool's value is returning the new position).

♻️ Proposed fix
             CursorControl.move(to: CGPoint(x: x, y: y))
-            let point = CursorPoint(x: x, y: y)
             return CallTool.Result(
                 content: [
                     .text(
                         text: "✅ Moved cursor to (\(x), \(y)).",
                         annotations: nil,
                         _meta: nil
                     )
                 ]
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/MoveCursorTool.swift` around
lines 47 - 57, The code creates an unused CursorPoint instance (let point =
CursorPoint(x: x, y: y)) after calling CursorControl.move(to:)—remove the dead
binding or use it in the returned CallTool.Result; to fix, either delete the
unused let point line or include the constructed CursorPoint in the result
(e.g., embed it in the content or metadata of CallTool.Result) so the tool
returns the new cursor position; update the MoveCursorTool/CursorControl.call
implementation to reference CursorPoint when building the response instead of
only interpolating x and y.
libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorEnabledTool.swift-51-65 (1)

51-65: ⚠️ Potential issue | 🟡 Minor

Avoid partial success when config persistence fails.

The live cursor state flips before persistence. If the config write fails, callers get an error even though the daemon state already changed and will diverge after restart.

Proposed ordering fix
-            await MainActor.run {
-                AgentCursor.shared.setEnabled(enabled)
-            }
-            // Persist through to the on-disk config so the next daemon
-            // restart boots in the same enabled/disabled state. Live
-            // state already flipped above — this is purely for the
-            // durability promise.
             do {
                 try await ConfigStore.shared.mutate { config in
                     config.agentCursor.enabled = enabled
                 }
             } catch {
                 return errorResult(
-                    "Agent cursor live state updated, but persisting to config failed: \(error.localizedDescription)"
+                    "Persisting agent cursor setting failed: \(error.localizedDescription)"
                 )
             }
+            await MainActor.run {
+                AgentCursor.shared.setEnabled(enabled)
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorEnabledTool.swift`
around lines 51 - 65, The code flips live state with
AgentCursor.shared.setEnabled(enabled) before persisting via
ConfigStore.shared.mutate, causing partial success if the mutate fails; change
the ordering so you first persist the new enabled value by awaiting
ConfigStore.shared.mutate { config in config.agentCursor.enabled = enabled } and
only after that call await MainActor.run {
AgentCursor.shared.setEnabled(enabled) }, or alternatively if you must set live
state first then catch mutation errors and call
AgentCursor.shared.setEnabled(!enabled) to roll back; update the errorResult
usage (errorResult(...)) to reflect the chosen strategy so callers don't observe
a successful live change with a failed durable write.
libs/cua-driver/Sources/CuaDriverServer/Tools/SetRecordingTool.swift-80-88 (1)

80-88: ⚠️ Potential issue | 🟡 Minor

Reject relative or whitespace-only recording directories.

The schema says output_dir is absolute or ~-rooted, but the implementation accepts relative paths and resolves them against the daemon’s working directory.

Suggested validation
-                guard let rawDir = arguments?["output_dir"]?.stringValue,
-                      !rawDir.isEmpty
+                guard let rawDir = arguments?["output_dir"]?.stringValue
                 else {
                     return errorResult(
                         "`output_dir` is required when enabling recording.")
                 }
-                let expanded = (rawDir as NSString).expandingTildeInPath
+                let trimmedDir = rawDir.trimmingCharacters(in: .whitespacesAndNewlines)
+                guard !trimmedDir.isEmpty else {
+                    return errorResult("`output_dir` must not be empty.")
+                }
+                let expanded = (trimmedDir as NSString).expandingTildeInPath
+                guard expanded.hasPrefix("/") else {
+                    return errorResult("`output_dir` must be absolute or ~-rooted.")
+                }
                 let url = URL(fileURLWithPath: expanded).standardizedFileURL
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/SetRecordingTool.swift` around
lines 80 - 88, The code currently accepts relative or whitespace-only output_dir
values; change the validation in SetRecordingTool (the guard around
arguments?["output_dir"] -> rawDir) to first trim whitespace and reject
empty/whitespace-only strings, then require that the raw (trimmed) value is
either absolute (starts with "/") or begins with "~" before calling (rawDir as
NSString).expandingTildeInPath and creating URL/standardizedFileURL; if it fails
these checks return errorResult("`output_dir` must be absolute or start with '~'
and cannot be relative or empty.").
libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextTool.swift-81-89 (1)

81-89: ⚠️ Potential issue | 🟡 Minor

window_id without element_index is silently ignored.

The current check only catches element_index without window_id. If a caller passes window_id alone (reasonable mistake, the two are paired everywhere else in the surface), it's silently dropped and the write targets whatever happens to be focused. Consider rejecting the incomplete pair symmetrically so the mistake surfaces at call time rather than landing text in an unexpected element.

🛠 Proposed fix
-            if elementIndex != nil && rawWindowId == nil {
-                return errorResult(
-                    "window_id is required when element_index is used — the "
-                    + "element_index cache is scoped per (pid, window_id). Pass "
-                    + "the same window_id you used in `get_window_state`.")
-            }
+            if elementIndex != nil && rawWindowId == nil {
+                return errorResult(
+                    "window_id is required when element_index is used — the "
+                    + "element_index cache is scoped per (pid, window_id). Pass "
+                    + "the same window_id you used in `get_window_state`.")
+            }
+            if elementIndex == nil && rawWindowId != nil {
+                return errorResult(
+                    "element_index is required when window_id is supplied; "
+                    + "without element_index the write always targets the "
+                    + "currently-focused element of the pid and window_id is ignored.")
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextTool.swift` around
lines 81 - 89, The code currently rejects element_index without window_id but
silently ignores window_id without element_index; update the validation in
TypeTextTool (the block reading arguments?["element_index"]?.intValue and
arguments?["window_id"]?.intValue) to symmetrically reject a request that
supplies window_id without element_index by returning an errorResult with a
clear message (mirror the existing message referring to element_index cache
scoping per (pid, window_id) and referencing get_window_state) so callers cannot
accidentally pass only window_id and have input routed to the focused element.
libs/cua-driver/Sources/CuaDriverServer/Tools/RightClickTool.swift-54-55 (1)

54-55: ⚠️ Potential issue | 🟡 Minor

Description promises modifier forces the CGEvent path — implementation silently drops modifiers in the element path.

The tool description says
"modifier forces the CGEvent path (AX doesn't propagate modifier keys)"
but invoke never inspects modifiers when routing: if element_index is provided, it unconditionally runs performElementRightClick and the parsed modifiers are discarded without warning. Either drop that sentence from the description, explicitly reject the combination with an error, or fall through to performPixelRightClick when modifiers.isEmpty == false. Today users asking for, say, shift+right_click on an element will see the modifier silently ignored.

Proposed fix (prefer explicit error or fallthrough)
-            if let index = elementIndex, let rawWindowId {
+            if let index = elementIndex, let rawWindowId, modifiers.isEmpty {
                 return await performElementRightClick(
                     pid: pid, windowId: UInt32(rawWindowId), index: index)
             }
+            // element_index + modifiers → fall through to the pixel path,
+            // which is the only route that propagates modifier keys.
             return await performPixelRightClick(
                 pid: pid,
                 windowId: rawWindowId.map { UInt32($0) },
                 x: x!, y: y!, modifiers: modifiers)

Note the pixel fallthrough still requires x/y; if those aren't provided with element_index + modifier, you'll want to return errorResult(...) explaining the combination isn't supported rather than force-unwrap.

Also applies to: 139-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/RightClickTool.swift` around
lines 54 - 55, The implementation of RightClickTool.invoke claims "modifier
forces the CGEvent path" but currently ignores parsed modifiers when
element_index is present; update invoke to either (A) detect non-empty modifiers
with an element_index and return an explicit error via errorResult explaining
that element+modifier is not supported, or (B) when modifiers.isEmpty == false
and element_index is present fall through to performPixelRightClick (requiring
x/y) instead of calling performElementRightClick; ensure you reference and
update the logic in RightClickTool.invoke, and handle/validate combinations of
modifiers, element_index, and x/y so you never silently drop modifiers (use
performElementRightClick, performPixelRightClick, and errorResult as
appropriate).
libs/cua-driver/Sources/CuaDriverCore/Windows/SpaceMigrator.swift-127-132 (1)

127-132: ⚠️ Potential issue | 🟡 Minor

Empty spaces array is reported as .onAnotherSpace with zero ids.

If spaceIDs(forWindowID:) returns a non-nil but empty array (SPI quirk, transient state during window creation, etc.), spaces.contains(active) is false and the function falls through to .onAnotherSpace(currentSpaceID: active, windowSpaceIDs: []), which downstream callers can't act on meaningfully. Treat empty-array the same as SPI-unresolved and return .unknown:

Proposed fix
-        guard let spaces = spaceIDs(forWindowID: primary.id)
-        else { return .unknown }
+        guard let spaces = spaceIDs(forWindowID: primary.id),
+              !spaces.isEmpty
+        else { return .unknown }
         if spaces.contains(active) { return .onCurrentSpace }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Windows/SpaceMigrator.swift` around
lines 127 - 132, The function currently treats a non-nil but empty result from
spaceIDs(forWindowID:) as .onAnotherSpace with an empty windowSpaceIDs array;
change the logic in the method that queries spaceIDs(forWindowID: primary.id) so
that after the guard unwrapping you also check if spaces.isEmpty and, if so,
return .unknown (same behavior as the nil/SPI-unresolved case) instead of
falling through to .onAnotherSpace(currentSpaceID: active, windowSpaceIDs:
spaces); reference the call to spaceIDs(forWindowID:), the variables primary.id
and active, and the enum cases .unknown and .onAnotherSpace(...) when making the
change.
.github/workflows/cd-swift-cua-driver.yml-119-135 (1)

119-135: ⚠️ Potential issue | 🟡 Minor

grep -c … || echo "0" can produce a multi-line result and break the numeric comparison.

When grep -c finds zero matches it still prints 0 to stdout and exits with status 1, so the || branch fires and appends another 0. The captured CERT_COUNT/INSTALLER_COUNT then becomes "0\n0", which causes [ "$CERT_COUNT" -eq 0 ] to error out with integer expression expected rather than succeed/fail cleanly. Use grep -c ... || true and suppress grep's own exit status, or skip the fallback entirely:

Proposed fix
-          CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || echo "0")
-          INSTALLER_COUNT=$(security find-identity -v build.keychain | grep -c "Developer ID Installer" || echo "0")
+          CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || true)
+          INSTALLER_COUNT=$(security find-identity -v build.keychain | grep -c "Developer ID Installer" || true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cd-swift-cua-driver.yml around lines 119 - 135, The
CERT_COUNT and INSTALLER_COUNT assignments can produce duplicate lines because
using "grep -c … || echo '0'" lets grep both output and fail, resulting in
values like "0\n0" which break the numeric test; change those assignments so
grep's non-zero exit doesn't append another 0 (for example, suppress grep's
failure with "|| true" or remove the fallback echo and rely on grep -c alone),
ensuring CERT_COUNT and INSTALLER_COUNT are single numeric strings before the
numeric comparisons in the script.
.github/workflows/cd-swift-cua-driver.yml-283-283 (1)

283-283: ⚠️ Potential issue | 🟡 Minor

Bump softprops/action-gh-release to the latest version.

softprops/action-gh-release@v1 is outdated and flagged by actionlint as running on a retired runner image. Upgrade to v2.6.2 (the latest stable version) to keep this step runnable on GitHub-hosted runners.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cd-swift-cua-driver.yml at line 283, The workflow step
currently uses softprops/action-gh-release@v1 which is outdated and flagged by
actionlint; update the action reference to softprops/action-gh-release@v2.6.2 in
the GitHub Actions workflow (replace the uses: softprops/action-gh-release@v1
entry) so the release step runs on supported GitHub-hosted runners and avoids
the retired runner image warning.
libs/cua-driver/Sources/CuaDriverCLI/ConfigCommand.swift-397-414 (1)

397-414: ⚠️ Potential issue | 🟡 Minor

Disabling updates leaves the running LaunchAgent loaded until logout.

run() removes the plist from ~/Library/LaunchAgents, but an already-loaded LaunchAgent keeps running until the user logs out or explicitly unloads it. Consider calling launchctl bootout gui/$(id -u)/com.trycua.cua_driver_updater (or legacy launchctl unload <plist> before removing) so "disable" is effective immediately. Also note the "will be removed" line prints even when the plist doesn't exist — small UX nit.

Sketch
         if fileManager.fileExists(atPath: plistPath) {
+            // Best-effort: unload before removing so the running agent stops now.
+            let unload = Process()
+            unload.executableURL = URL(fileURLWithPath: "/bin/launchctl")
+            unload.arguments = ["bootout", "gui/\(getuid())/com.trycua.cua_driver_updater"]
+            try? unload.run()
+            unload.waitUntilExit()
             do {
                 try fileManager.removeItem(atPath: plistPath)
                 print("Removed LaunchAgent.")
             } catch {
                 print("Note: Failed to remove LaunchAgent (you can do this manually):")
                 print("  rm \(plistPath)")
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCLI/ConfigCommand.swift` around lines 397 -
414, ConfigCommand.run currently disables auto-update and deletes the plist at
plistPath but does not unload a loaded LaunchAgent or guard the "will be
removed" message; update run() to (1) if the plist exists call launchctl to
unload/bootout the agent for the current user (e.g., "launchctl bootout gui/$(id
-u)/com.trycua.cua_driver_updater" or fallback to "launchctl unload <plist>")
before removing it, handling errors and logging them via the same print/error
path, and (2) only print "The LaunchAgent will be removed from your system." (or
similar) when the plist actually exists (use fileManager.fileExists(atPath:
plistPath) to decide); keep ConfigStore.setAutoUpdateEnabledSync(false) as-is
and ensure plistPath and the agent label "com.trycua.cua_driver_updater" are the
referenced symbols.
.github/workflows/cd-swift-cua-driver.yml-75-84 (1)

75-84: ⚠️ Potential issue | 🟡 Minor

Unreachable duplicate branch.

Both elif blocks check the same ${{ inputs.version }} expression, so the second branch is dead. workflow_dispatch and workflow_call both populate inputs.version, so a single branch suffices.

Proposed fix
           if [[ "$GITHUB_REF" == refs/tags/cua-driver-v* ]]; then
             VERSION="${GITHUB_REF#refs/tags/cua-driver-v}"
             echo "Using version from tag: $VERSION"
           elif [[ -n "${{ inputs.version }}" ]]; then
             VERSION="${{ inputs.version }}"
             echo "Using version from input: $VERSION"
-          elif [[ -n "${{ inputs.version }}" ]]; then
-            VERSION="${{ inputs.version }}"
-            echo "Using version from workflow_call input: $VERSION"
           else
             echo "Error: No version found in tag or input"
             exit 1
           fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/cd-swift-cua-driver.yml around lines 75 - 84, The second
elif checking the same `${{ inputs.version }}` is unreachable; remove the
duplicate branch and consolidate into a single branch that sets VERSION from
inputs and logs a clear message (e.g., "Using version from input: $VERSION") so
both workflow_dispatch and workflow_call cases are handled by the same
condition; update the surrounding conditional in the shell block to only test
`${{ inputs.version }}` once and keep the final else to error out if neither tag
nor input provided.
libs/cua-driver/Sources/CuaDriverCore/Windows/SpaceMigrator.swift-28-54 (1)

28-54: ⚠️ Potential issue | 🟡 Minor

Declare CopySpacesForWindowsFn with Unmanaged<CFArray>? and use takeRetainedValue() to properly handle the +1 retained return from this Copy-named SPI.

For manually declared @convention(c) function pointers, Swift cannot infer Core Foundation ownership semantics from naming conventions. The SPI SLSCopySpacesForWindows transfers ownership (Copy → +1 retained), but declaring the return type as CFArray? treats it as Swift-managed with +0 ownership, causing one reference leak per call. Since this function is invoked per window on every list_windows call, the leak accumulates over time in long-lived processes.

Proposed fix
-    private typealias CopySpacesForWindowsFn = `@convention`(c) (
-        Int32, Int32, CFArray
-    ) -> CFArray?
+    private typealias CopySpacesForWindowsFn = `@convention`(c) (
+        Int32, Int32, CFArray
+    ) -> Unmanaged<CFArray>?

At the call site (around line 90):

-        let raw = r.copySpacesForWindows(cid, 7, widArray) as? [NSNumber]
+        guard let unmanaged = r.copySpacesForWindows(cid, 7, widArray) else { return nil }
+        let raw = unmanaged.takeRetainedValue() as? [NSNumber]

Also applies to: 83-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Windows/SpaceMigrator.swift` around
lines 28 - 54, The CopySpacesForWindowsFn currently returns CFArray? which
causes a CF ownership leak for the Copy-named SPI; change its signature to
return Unmanaged<CFArray>? (e.g., CopySpacesForWindowsFn = `@convention`(c)
(Int32, Int32, CFArray) -> Unmanaged<CFArray>?) and at the call site where you
invoke resolved.copySpacesForWindows(...) unwrap the optional Unmanaged and call
takeRetainedValue() (safely using optional chaining or guard) to obtain the
CFArray with correct +0 ownership before bridging to Swift types; update any
handling of the return to account for the Unmanaged-to-CFArray conversion and
nil case.
libs/cua-driver/Sources/CuaDriverServer/Tools/ClickTool.swift-458-482 (1)

458-482: ⚠️ Potential issue | 🟡 Minor

isWindowMinimized(pid:) is dead code and should be removed.

The function defined at lines 461-482 is never called anywhere in the codebase. Based on references to FocusGuardError.windowMinimized and minimized-window pitfalls in documentation, it appears to be an incomplete advisory feature for pixel clicks (which silently no-op against minimized windows because SLEventPostToPid doesn't reach a non-rendering tree). Either complete the wiring by warning when a pixel click targets a minimized window, or delete the unused helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/ClickTool.swift` around lines
458 - 482, The helper isWindowMinimized(pid:) is dead code (never referenced) —
either wire it into the pixel-click flow or remove it; the simplest fix is to
delete the unused function to avoid dead code. Remove the private static func
isWindowMinimized(pid: Int32) implementation and any related unused imports or
comments; if you choose to keep behavior instead, call isWindowMinimized(pid:)
from the click path where SLEventPostToPid is used and raise or map to
FocusGuardError.windowMinimized before attempting a pixel click so minimized
windows are detected and handled.
libs/cua-driver/Sources/CuaDriverCore/Focus/FocusGuard.swift-70-79 (1)

70-79: ⚠️ Potential issue | 🟡 Minor

FocusGuardError.windowMinimized is never thrown — the minimized branch silently continues.

Lines 73–79 detect a minimized window and set focusState = nil, then let body proceed. The FocusGuardError.windowMinimized case at lines 126–131 — whose description provides user-facing remediation ("use type_text_chars/press_key for keyboard input … or unminimize first") — is never produced by this code. No downstream caller (TypeTextTool, SetValueTool, PressKeyTool, ScrollTool, ClickTool, RightClickTool) catches this error specifically, so users cannot access the documented guidance.

Either throw the error when the window is minimized and let downstream tools surface the remediation advice, or delete the case to reflect current behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Focus/FocusGuard.swift` around lines 70
- 79, The minimized-window branch in FocusGuard.swift currently sets focusState
= nil and proceeds silently; instead, change that branch to throw
FocusGuardError.windowMinimized so the user-facing remediation is surfaced.
Specifically, inside the async block where you compute window and
windowIsMinimized (the code that now does "if windowIsMinimized { focusState =
nil } else { focusState = await enforcer.preventActivation(...)}"), replace the
nil-assignment with throwing FocusGuardError.windowMinimized so the error
propagates to callers (TypeTextTool, SetValueTool, PressKeyTool, ScrollTool,
ClickTool, RightClickTool) and their user guidance is reachable.
libs/cua-driver/Skills/cua-driver/SKILL.md-23-42 (1)

23-42: ⚠️ Potential issue | 🟡 Minor

Document the open -n -g CuaDriver bootstrap exception.

The no-foreground section says every open invocation is forbidden, but the prerequisites and management commands later require open -n -g -a CuaDriver --args serve. Add an explicit carve-out for launching the driver daemon itself, otherwise the skill contradicts its own startup instructions.

Also applies to: 219-224, 232-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Skills/cua-driver/SKILL.md` around lines 23 - 42, Add an
explicit carve-out in the "no-foreground" section documenting the bootstrap
exception for launching the driver daemon: state that while all forms of `open`
are forbidden, invoking `open -n -g -a CuaDriver --args serve` (or equivalent
`open -n -g CuaDriver`) is allowed for the initial bootstrap/daemon start
because it launches the background driver without activating it; reference the
`launch_app` guidance and explain that this exception is narrowly scoped to the
driver bootstrap only (not general app launches) and must be used only when
starting the CuaDriver daemon, keeping the rest of the `open` prohibition
intact.
libs/cua-driver/Sources/CuaDriverServer/Tools/DoubleClickTool.swift-74-78 (1)

74-78: ⚠️ Potential issue | 🟡 Minor

Fix the window_id schema description for pixel mode.

Line 77 says window_id is ignored in the pixel path, but lines 241-245 use it to anchor window-local pixel conversion. Update the description so callers know passing window_id is useful and recommended for pixel double-clicks.

Also applies to: 241-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/DoubleClickTool.swift` around
lines 74 - 78, The schema description for the "window_id" field in
DoubleClickTool.swift incorrectly says it's ignored in the pixel path; instead
update the description to state that "window_id" is used to anchor window-local
pixel conversion and is recommended (and can be required) for pixel-based
double-clicks so callers know to pass it when performing pixel actions; apply
this same description update to the other occurrence of the "window_id" schema
(the duplicate block that governs pixel conversion/anchoring and the
get_window_state/element_index usage).
libs/cua-driver/Skills/cua-driver/SKILL.md-244-251 (1)

244-251: ⚠️ Potential issue | 🟡 Minor

Add languages to fenced code blocks.

These fences trigger MD040 and lose syntax highlighting. Mark the command examples as bash.

📝 Proposed markdown fix
-```
+```bash
 open -n -g -a CuaDriver --args serve
 cua-driver launch_app '{"bundle_id":"com.apple.calculator"}'
 # → {pid: 844, windows: [{window_id: 10725, ...}]}
 cua-driver get_window_state '{"pid":844,"window_id":10725}'
 cua-driver click '{"pid":844,"window_id":10725,"element_index":14}'
 cua-driver stop
 ```
@@
-```
+```bash
 launch_app(target)
   → pick window_id from the returned `windows` array
@@
 ```
@@
-```
+```bash
 # canonical, works in every capture mode — writes the image bytes
@@
 fi
 ```
@@
-```
+```bash
 osascript -e 'tell application "<App Name>" to activate'
 ```

Also applies to: 348-355, 441-452, 715-717

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Skills/cua-driver/SKILL.md` around lines 244 - 251, Add
explicit language annotations to the fenced code blocks that contain shell/CLI
examples in SKILL.md: change the fences around the block beginning with "open -n
-g -a CuaDriver --args serve" and the other command examples to use ```bash;
likewise update the fenced blocks that start with "launch_app(target)", the
canonical capture snippet (the block commented "# canonical, works in every
capture mode — writes the image bytes"), and the osascript activation example to
```bash as well so they pass MD040 and get proper syntax highlighting.
libs/cua-driver/Skills/cua-driver/SKILL.md-521-537 (1)

521-537: ⚠️ Potential issue | 🟡 Minor

Reconcile the pixel-click guidance.

Lines 521-537 say screenshot pixels are the click coordinate system, and lines 586-587 say the pixel path animates the agent cursor. Lines 785-792 then say never translate screenshot pixels into clicks and that pixel clicks skip the overlay. Please qualify this section to mean “prefer element_index when AX is available; use screenshot pixels for pixel-only surfaces.”

Also applies to: 586-587, 783-792

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Skills/cua-driver/SKILL.md` around lines 521 - 537, Update
the SKILL.md wording to reconcile the pixel-click guidance: state clearly that
click({pid, x, y}) (the "pixel path") uses window-local screenshot pixels from
get_window_state and is intended only for surfaces that the AX tree cannot
target (canvases, video players, WebGL, custom-drawn controls), whereas
element_index (the AX path) should be preferred whenever accessibility nodes are
available; explicitly note that pixel clicks drive the agent cursor animation
and intentionally bypass the accessibility overlay/hit-testing, and recommend
passing window_id to pin coordinate conversion to the correct screenshot. Also
apply the same clarified language/qualification to the other mentions of the
pixel path and overlay behavior (the sections that currently state the pixel
path animates the agent cursor and that screenshot pixels should never be
translated into clicks) so all references consistently say “prefer element_index
when AX is available; use screenshot pixels only for pixel-only surfaces; pixel
clicks animate the cursor and skip the overlay; pass window_id to anchor
conversion.”
libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift-88-92 (1)

88-92: ⚠️ Potential issue | 🟡 Minor

Don’t mark URL launches as idempotent.

Line 91 advertises this tool as idempotent, but urls can open documents, folders, or browser windows on repeated calls. That can mislead MCP clients into retrying or caching a mutating operation.

-                idempotentHint: true,  // relaunching a running app is a no-op
+                idempotentHint: false,  // url/document handoff can create side effects
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift` around
lines 88 - 92, The tool's annotations wrongly set idempotentHint: true in the
annotations initializer (the LaunchAppTool tool definition), which can cause
clients to retry or cache URL launches; change idempotentHint to false (or
remove it) in the annotations .init call so URL launches are not advertised as
idempotent, leaving readOnlyHint/destructiveHint/openWorldHint unchanged.
libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift-30-33 (1)

30-33: ⚠️ Potential issue | 🟡 Minor

Align the tool description with off-Space behavior.

The description says off-Space windows return isError: true, but the implementation intentionally accepts them and surfaces off_space. Update the public tool text so agents do not avoid supported background-window flows.

Also applies to: 122-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift`
around lines 30 - 33, Update the public description text in GetWindowStateTool
to reflect actual behavior: instead of saying off‑Space windows return isError:
true, document that the tool accepts window_id values that belong to the pid but
are on a different Space and will surface an off_space status (not an error),
and update both description blocks in this file (the short tool description and
the longer usage text near the bottom) to mention off_space as the returned
condition and that callers must handle background/off‑Space windows themselves.
libs/cua-driver/Sources/CuaDriverCore/Config/ConfigStore.swift-177-206 (1)

177-206: ⚠️ Potential issue | 🟡 Minor

Avoid read-modify-write races in synchronous setters.

These setters bypass the actor and rewrite the full config file, so concurrent config telemetry, config auto-update, or daemon set_config calls can lose unrelated changes. Add an interprocess file lock or route the synchronous commands through a single serialized helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Config/ConfigStore.swift` around lines
177 - 206, The synchronous setters setTelemetryEnabledSync and
setAutoUpdateEnabledSync do a read-modify-write without actor serialization,
causing lost updates; wrap the whole loadSync -> modify -> encode -> write
sequence in an interprocess exclusive file lock (or create/hold a lock file in
the same directory) so concurrent CLI/daemon invocations serialize;
specifically, before calling loadSync use a POSIX/flock-style exclusive lock on
Self.fileURL (or a dedicated lock at Self.configDirectoryURL()), perform the
modification and write to Self.fileURL atomically, then release the lock; ensure
the lock is obtained and released even on error to avoid deadlocks.
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift-368-379 (1)

368-379: ⚠️ Potential issue | 🟡 Minor

Do not catch and replace the script’s ExitCode.

The throw ExitCode(Int32(process.terminationStatus)) inside the do block is immediately caught by the catch-all below, so all script failures become ExitCode(1) and print a misleading “Error running update script”.

Proposed fix
         do {
             try process.run()
-            process.waitUntilExit()
-            
-            if process.terminationStatus != 0 {
-                print("Update check failed. See /tmp/cua_driver_updater.log for details.")
-                throw ExitCode(Int32(process.terminationStatus))
-            }
         } catch {
             print("Error running update script: \(error)")
             throw ExitCode(1)
         }
+        process.waitUntilExit()
+
+        if process.terminationStatus != 0 {
+            print("Update check failed. See /tmp/cua_driver_updater.log for details.")
+            throw ExitCode(Int32(process.terminationStatus))
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` around lines 368
- 379, The catch-all is swallowing script ExitCode and replacing it with
ExitCode(1); change the error handling so the original ExitCode from throw
ExitCode(Int32(process.terminationStatus)) is not caught and replaced: either
move the process terminationStatus check and throw outside the do-catch, or
narrow the catch to only non-ExitCode errors and rethrow if the caught error is
an ExitCode (inspect error as? ExitCode and throw it), referencing the existing
throw ExitCode(Int32(process.terminationStatus)), the do { try process.run();
process.waitUntilExit() } block, and the current catch { print("Error running
update script: \(error)"); throw ExitCode(1) } to implement the fix.
libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift-45-56 (1)

45-56: ⚠️ Potential issue | 🟡 Minor

Generate the MCP config as JSON instead of interpolating strings.

Executable paths can contain quotes or backslashes; direct interpolation can print invalid JSON. Build the object and serialize it.

Proposed fix
         // itself was invoked via a `/usr/local/bin/` symlink.
         let binary = resolvedBinaryPath()
-        let snippet = """
-        {
-          "mcpServers": {
-            "cua-driver": {
-              "command": "\(binary)",
-              "args": ["mcp"]
-            }
-          }
-        }
-        """
-        print(snippet)
+        let payload: [String: Any] = [
+            "mcpServers": [
+                "cua-driver": [
+                    "command": binary,
+                    "args": ["mcp"],
+                ]
+            ]
+        ]
+        let data = try JSONSerialization.data(
+            withJSONObject: payload,
+            options: [.prettyPrinted, .sortedKeys]
+        )
+        print(String(decoding: data, as: UTF8.self))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` around lines 45
- 56, The code currently builds JSON by interpolating resolvedBinaryPath() into
the multiline string (variable snippet) and printing it, which can produce
invalid JSON when the path contains quotes or backslashes; instead construct a
Swift Dictionary/struct representing the payload for "mcpServers" ->
"cua-driver" with keys "command" and "args", then serialize it to JSON using
JSONEncoder (or JSONSerialization.data(withJSONObject:options:)) and print the
resulting UTF-8 string, handling/propagating any encoding errors; replace the
snippet construction and print(snippet) with the serialization flow so
resolvedBinaryPath() is inserted safely as the "command" value.
libs/cua-driver/Sources/CuaDriverCore/Cursor/AgentCursor.swift-478-488 (1)

478-488: ⚠️ Potential issue | 🟡 Minor

Add guards for non-finite and zero timing values before UInt64 conversion.

The JSON schema declares bounds (glide_duration_ms ≥ 50, dwell_after_click_ms ≥ 0, idle_hide_ms ≥ 100), but the Swift invoke handler does not re-validate after extracting values. Non-finite values (NaN, infinity) are not explicitly prevented by the schema. Additionally, lines 480, 699-701 lack the defensive check present at line 670 (if dwellAfterClickSeconds > 0), creating inconsistency when converting to UInt64.

The properties are public and mutable; direct assignment bypasses schema validation. A local defensive helper ensures safety:

Defensive helper
+    private func sleepSeconds(_ seconds: CFTimeInterval) async {
+        guard seconds.isFinite, seconds > 0 else { return }
+        try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
+    }
+
@@
-        try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
+        await sleepSeconds(duration)

Apply the same pattern to lines 672 and 701.

Also applies to: 647-647, 670-672, 699-701

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Cursor/AgentCursor.swift` around lines
478 - 488, Add defensive checks to ensure timing values are finite and positive
(or non-negative where allowed) before converting to UInt64: validate
glideDurationSeconds, dwellAfterClickSeconds, and idleHideSeconds with a helper
like isFinitePositive(_:) or isFiniteNonNegative(_:) and return/skip the
Task.sleep or UInt64 conversion when invalid (NaN, ±infinity, or zero where zero
is disallowed). Apply this pattern around the animate(to:duration:options:) call
and the Task.sleep conversion (the sleep at the end of the shown block), and the
other places noted (the code paths using dwellAfterClickSeconds and
idleHideSeconds) so conversions to UInt64 only occur after the values pass the
finite/threshold checks.
libs/cua-driver/Sources/CuaDriverCore/Config/CuaDriverConfig.swift-35-39 (1)

35-39: ⚠️ Potential issue | 🟡 Minor

Doc string contradicts itself.

"Anonymous telemetry opt-out. Default true (opt-in)" mixes the two postures. With telemetryEnabled = true by default, this is an opt-out posture (telemetry on; user must disable). The (opt-in) parenthetical is the opposite and will confuse readers auditing the privacy default.

📝 Suggested wording
-    /// Anonymous telemetry opt-out. Default `true` (opt-in) to match
-    /// lume's posture. Override at run time via
+    /// Anonymous telemetry, opt-out posture. Default `true` (enabled;
+    /// user must explicitly disable) to match lume's posture. Override at run time via
     /// `CUA_DRIVER_TELEMETRY_ENABLED={0|1}` or mutate persistently via
     /// `cua-driver config telemetry {enable|disable}`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Config/CuaDriverConfig.swift` around
lines 35 - 39, The doc comment for the telemetryEnabled property is
self-contradictory; update the comment on telemetryEnabled to clearly state the
posture and default without mixing opt-in/opt-out terms (e.g., "Anonymous
telemetry opt-out. Default true (telemetry enabled)." or invert the default if
you meant opt-in), and remove the confusing "(opt-in)" parenthetical so the
comment and the property's default value are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e831430-94d5-4302-b945-683b8608f410

📥 Commits

Reviewing files that changed from the base of the PR and between 91a70dc and 9014657.

⛔ Files ignored due to path filters (14)
  • img/card-cua-bench.png is excluded by !**/*.png
  • img/card-cua-driver.png is excluded by !**/*.png
  • img/card-cua-lume.png is excluded by !**/*.png
  • img/card-cua-sandbox.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_128x128.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_128x128@2x.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_16x16.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_16x16@2x.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_256x256.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_256x256@2x.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_32x32.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_32x32@2x.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_512x512.png is excluded by !**/*.png
  • libs/cua-driver/App/CuaDriver/AppIcon.iconset/icon_512x512@2x.png is excluded by !**/*.png
📒 Files selected for processing (119)
  • .github/workflows/cd-swift-cua-driver.yml
  • .github/workflows/ci-swift-cua-driver.yml
  • README.md
  • libs/cua-driver/.gitignore
  • libs/cua-driver/App/CuaDriver/AppIcon.icns
  • libs/cua-driver/App/CuaDriver/Info.plist
  • libs/cua-driver/Package.resolved
  • libs/cua-driver/Package.swift
  • libs/cua-driver/README.md
  • libs/cua-driver/Skills/cua-driver/README.md
  • libs/cua-driver/Skills/cua-driver/RECORDING.md
  • libs/cua-driver/Skills/cua-driver/SKILL.md
  • libs/cua-driver/Skills/cua-driver/TESTS.md
  • libs/cua-driver/Skills/cua-driver/WEB_APPS.md
  • libs/cua-driver/Sources/CuaDriverCLI/CallCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/ConfigCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/DiagnoseCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/RecordingCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/RecordingRenderCommand.swift
  • libs/cua-driver/Sources/CuaDriverCLI/ServeCommand.swift
  • libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift
  • libs/cua-driver/Sources/CuaDriverCore/Apps/AppEnumerator.swift
  • libs/cua-driver/Sources/CuaDriverCore/Apps/AppInfo.swift
  • libs/cua-driver/Sources/CuaDriverCore/Apps/AppLauncher.swift
  • libs/cua-driver/Sources/CuaDriverCore/Capture/DebugCrosshair.swift
  • libs/cua-driver/Sources/CuaDriverCore/Capture/ScreenInfo.swift
  • libs/cua-driver/Sources/CuaDriverCore/Capture/WindowCapture.swift
  • libs/cua-driver/Sources/CuaDriverCore/Config/ConfigStore.swift
  • libs/cua-driver/Sources/CuaDriverCore/Config/CuaDriverConfig.swift
  • libs/cua-driver/Sources/CuaDriverCore/CuaDriverCore.swift
  • libs/cua-driver/Sources/CuaDriverCore/Cursor/AgentCursor.swift
  • libs/cua-driver/Sources/CuaDriverCore/Cursor/AgentCursorOverlayWindow.swift
  • libs/cua-driver/Sources/CuaDriverCore/Cursor/AgentCursorView.swift
  • libs/cua-driver/Sources/CuaDriverCore/Cursor/Bezier.swift
  • libs/cua-driver/Sources/CuaDriverCore/Cursor/CursorMotionPath.swift
  • libs/cua-driver/Sources/CuaDriverCore/Focus/AXEnablementAssertion.swift
  • libs/cua-driver/Sources/CuaDriverCore/Focus/FocusGuard.swift
  • libs/cua-driver/Sources/CuaDriverCore/Focus/SyntheticAppFocusEnforcer.swift
  • libs/cua-driver/Sources/CuaDriverCore/Focus/SystemFocusStealPreventer.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/AXInput.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/CursorControl.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/CursorPoint.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/FocusWithoutRaise.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/KeyboardInput.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/MouseInput.swift
  • libs/cua-driver/Sources/CuaDriverCore/Input/SkyLightEventPost.swift
  • libs/cua-driver/Sources/CuaDriverCore/Permissions/Permissions.swift
  • libs/cua-driver/Sources/CuaDriverCore/Permissions/PermissionsGate.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/ClickMarkerRenderer.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/CursorSampler.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/RecordingSession.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Render/FrameTransform.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Render/RecordingRenderer.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Render/TrajectoryLoader.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/VideoRecorder.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Zoom/CursorTelemetry.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Zoom/ZoomMath.swift
  • libs/cua-driver/Sources/CuaDriverCore/Recording/Zoom/ZoomRegion.swift
  • libs/cua-driver/Sources/CuaDriverCore/Telemetry/TelemetryClient.swift
  • libs/cua-driver/Sources/CuaDriverCore/Windows/SpaceMigrator.swift
  • libs/cua-driver/Sources/CuaDriverCore/Windows/WindowCoordinateSpace.swift
  • libs/cua-driver/Sources/CuaDriverCore/Windows/WindowEnumerator.swift
  • libs/cua-driver/Sources/CuaDriverCore/Windows/WindowInfo.swift
  • libs/cua-driver/Sources/CuaDriverServer/AppStateRegistry.swift
  • libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift
  • libs/cua-driver/Sources/CuaDriverServer/DaemonClient.swift
  • libs/cua-driver/Sources/CuaDriverServer/DaemonProtocol.swift
  • libs/cua-driver/Sources/CuaDriverServer/DaemonServer.swift
  • libs/cua-driver/Sources/CuaDriverServer/ToolRegistry.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/CheckPermissionsTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ClickTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/DoubleClickTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetAccessibilityTreeTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetAgentCursorStateTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetConfigTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetCursorPositionTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetRecordingStateTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetScreenSizeTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/HotkeyTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ImageResizeRegistry.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ListAppsTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ListWindowsTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/MoveCursorTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/PressKeyTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ReplayTrajectoryTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/RightClickTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ScreenshotTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ScrollTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorEnabledTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/SetAgentCursorMotionTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/SetConfigTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/SetRecordingTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/SetValueTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextCharsTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/TypeTextTool.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/ZoomTool.swift
  • libs/cua-driver/Tests/FocusMonitorApp/FocusMonitorApp.swift
  • libs/cua-driver/Tests/FocusMonitorApp/build.sh
  • libs/cua-driver/Tests/ZoomMathTests/ZoomMathTests.swift
  • libs/cua-driver/Tests/integration/driver_client.py
  • libs/cua-driver/Tests/integration/fixtures/interactive.html
  • libs/cua-driver/Tests/integration/test_background_focus.py
  • libs/cua-driver/Tests/integration/test_blender_background.py
  • libs/cua-driver/Tests/integration/test_check_permissions_cli.py
  • libs/cua-driver/Tests/integration/test_chrome_minimized_nav.py
  • libs/cua-driver/Tests/integration/test_click_pixel_ax.py
  • libs/cua-driver/Tests/integration/test_double_click_delivery.py
  • libs/cua-driver/Tests/integration/test_list_windows.py
  • libs/cua-driver/Tests/integration/test_pixel_click_delivery.py
  • libs/cua-driver/docs/tool-output-format.md
  • libs/cua-driver/scripts/CuaDriver.entitlements
  • libs/cua-driver/scripts/build-app.sh
  • libs/cua-driver/scripts/install-local.sh
  • libs/cua-driver/scripts/install.sh
  • libs/cua-driver/scripts/test.sh
  • libs/cua-driver/scripts/uninstall.sh

Comment on lines +88 to +101
private static let managementSubcommands: Set<String> = [
"mcp",
"mcp-config",
"call",
"list-tools",
"describe",
"serve",
"stop",
"status",
"recording",
"config",
"diagnose",
"help",
]

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 | 🔴 Critical

Keep update out of implicit tool-call rewriting.

UpdateCommand is registered, but "update" is missing from managementSubcommands, so cua-driver update rewrites to cua-driver call update and fails as an unknown tool.

Proposed fix
         "recording",
         "config",
+        "update",
         "diagnose",
         "help",
@@
         case "recording": return TelemetryEvent.recording
         case "config": return TelemetryEvent.config
+        case "update": return "cua_driver_update"
         default: return TelemetryEvent.guiLaunch

Also applies to: 182-190, 211-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` around lines 88
- 101, The managementSubcommands set is missing the "update" entry which causes
the CLI to rewrite "cua-driver update" into an implicit tool call (via the
implicit tool-call rewriting logic) and fail; add "update" to the
managementSubcommands Set definition (the same change should be mirrored in the
other managementSubcommands occurrences referenced around the UpdateCommand
registration) so that UpdateCommand is treated as a management subcommand rather
than rewritten to a tool call.

Comment on lines +24 to +31
public static func mainScreenSize() -> ScreenSize? {
guard let screen = NSScreen.main else { return nil }
let frame = screen.frame
return ScreenSize(
width: Int(frame.width),
height: Int(frame.height),
scaleFactor: screen.backingScaleFactor
)

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Apple Developer Documentation NSScreen main Swift MainActor isolation

💡 Result:

NSScreen.main is a class computed property in AppKit that returns the NSScreen? containing the window with keyboard focus (the screen the user is most likely interacting with). It is not the primary screen (menu bar screen), which is NSScreen.screens[0]. In Swift, access it as NSScreen.main (optional NSScreen?). Official Apple documentation does not mark NSScreen or its methods as @MainActor isolated. AppKit APIs are traditionally main-thread-only, so call from the main actor/queue: use @MainActor, DispatchQueue.main.sync/async, or MainActor.run. Example usage: if let mainScreen = NSScreen.main { let frame = mainScreen.frame // CGRect bounds let visibleFrame = mainScreen.visibleFrame // excluding dock/menu bar print("Main screen frame: (frame)") } Requires NSApplication.shared to exist (auto-created in Xcode apps). NSScreen.screens returns all screens; do not cache as configuration changes trigger NSApplication.didChangeScreenParametersNotification. No evidence of explicit MainActor isolation in docs or sources; safe on main thread per AppKit conventions. For concurrency, wrap nonisolated calls in Task { @MainActor in ... } if needed.

Citations:


🏁 Script executed:

# Check the actual file content
cat -n "libs/cua-driver/Sources/CuaDriverCore/Capture/ScreenInfo.swift"

Repository: trycua/cua

Length of output: 1317


🏁 Script executed:

# Find all call sites of mainScreenSize
rg "mainScreenSize" --type swift -B 2 -A 2

Repository: trycua/cua

Length of output: 1190


🏁 Script executed:

# Check for any swift concurrency settings in the project
find . -name "*.swift" -o -name "package.swift" -o -name "*.xcconfig" | head -20 | xargs grep -l "strict" 2>/dev/null || echo "No strict concurrency found in first pass"

Repository: trycua/cua

Length of output: 180


🏁 Script executed:

# Check the GetScreenSizeTool.swift file to see the context of the invoke closure
cat -n "libs/cua-driver/Sources/CuaDriverServer/Tools/GetScreenSizeTool.swift"

Repository: trycua/cua

Length of output: 1903


🏁 Script executed:

# Check if strict concurrency is configured in those files
grep -n "strict" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" -B 2 -A 2

Repository: trycua/cua

Length of output: 427


🏁 Script executed:

# Look for Package.swift or build configuration
find . -maxdepth 3 -name "Package.swift" -o -name "*.xcconfig" | xargs cat 2>/dev/null | head -100

Repository: trycua/cua

Length of output: 3517


🏁 Script executed:

# Check MCP ToolHandler definition to understand invoke signature
rg "ToolHandler" --type swift -A 5 | head -50

Repository: trycua/cua

Length of output: 4691


🏁 Script executed:

# Look for any swiftSettings that enable/disable strict concurrency
find . -name "Package.swift" -exec grep -A 20 "swiftSettings\|unsafeFlags" {} \;

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

# Check if there are any `@MainActor` annotations on invoke handlers elsewhere in the codebase
rg "invoke.*{" --type swift -B 5 -A 3 | grep -A 8 "MainActor\|async"

Repository: trycua/cua

Length of output: 135


🏁 Script executed:

# Check the actual GetScreenSizeTool.swift structure to see if invoke is declared as async
cat -n "libs/cua-driver/Sources/CuaDriverServer/Tools/GetScreenSizeTool.swift" | head -30

Repository: trycua/cua

Length of output: 1278


🏁 Script executed:

# Verify ToolHandler's invoke type signature completely
rg "invoke.*Sendable" --type swift -A 1

Repository: trycua/cua

Length of output: 472


Make AppKit screen access main-actor isolated.

NSScreen access requires the main actor; in Swift 6 strict concurrency, accessing main-thread-only AppKit APIs from a nonisolated context will fail type checking and cause runtime issues.

Proposed actor-isolation fix
-    public static func mainScreenSize() -> ScreenSize? {
+    `@MainActor` public static func mainScreenSize() -> ScreenSize? {
         guard let screen = NSScreen.main else { return nil }
         let frame = screen.frame
         return ScreenSize(

Then update the tool call site:

-            guard let size = ScreenInfo.mainScreenSize() else {
+            guard let size = await ScreenInfo.mainScreenSize() else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverCore/Capture/ScreenInfo.swift` around lines
24 - 31, The function mainScreenSize() accesses AppKit's NSScreen and must be
main-actor isolated; annotate the function with `@MainActor` (or move its body
into a `@MainActor-isolated` helper) so that ScreenInfo.mainScreenSize() is
executed on the main actor, and then update all call sites of
ScreenInfo.mainScreenSize() to call it from the main actor (e.g., mark callers
`@MainActor` or use await/Task { `@MainActor` in ... } as appropriate) to satisfy
Swift 6 strict concurrency rules.

Comment on lines +83 to +118
// Simple blocking connect — we only wait ~250ms so a dead socket
// doesn't hang the CLI.
let addrLen = socklen_t(MemoryLayout<sockaddr_un>.size)
// Set send/recv timeouts so we don't block forever on a broken daemon.
var tv = timeval(
tv_sec: Int(timeout),
tv_usec: __darwin_suseconds_t((timeout - Double(Int(timeout))) * 1_000_000)
)
_ = setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, socklen_t(MemoryLayout<timeval>.size))

let result = withUnsafePointer(to: &addr) { addrPtr in
addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
Darwin.connect(fd, sockPtr, addrLen)
}
}
if result != 0 {
close(fd)
return nil
}
return fd
}

private static func writeAll(fd: Int32, data: Data) -> Bool {
return data.withUnsafeBytes { raw -> Bool in
guard let base = raw.baseAddress else { return false }
var remaining = raw.count
var offset = 0
while remaining > 0 {
let n = write(fd, base.advanced(by: offset), remaining)
if n <= 0 { return false }
offset += n
remaining -= n
}
return true
}
}

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm no global SIGPIPE handler is installed elsewhere in the driver
# that would already mitigate this, and confirm SO_NOSIGPIPE is available
# (it is on Darwin).
rg -nP --type=swift -C2 '\bSIGPIPE\b|\bSO_NOSIGPIPE\b|\bMSG_NOSIGNAL\b|\bsignal\s*\(\s*SIGPIPE'
rg -nP --type=swift -C2 '\bsocket\s*\(\s*AF_UNIX'

Repository: trycua/cua

Length of output: 1470


🏁 Script executed:

# Check the target structure to understand which code is the CLI client
fd -e swift . libs/cua-driver/Sources/CuaDriverServer | head -20

# Check if DaemonClient is used by a CLI target
rg -l 'DaemonClient' --type=swift

# Look at the main CLI entry point to see how DaemonClient is used
find . -name 'main.swift' -o -name '*CLI*' -o -name '*cli*' | grep -i swift

Repository: trycua/cua

Length of output: 1812


🏁 Script executed:

# Check how DaemonClient is used in CLI code
rg -A5 'DaemonClient' libs/cua-driver/Sources/CuaDriverCLI/ --type=swift | head -50

Repository: trycua/cua

Length of output: 4165


Set SO_NOSIGPIPE on the client socket to prevent SIGPIPE crash on write().

Two issues in this region:

  1. SIGPIPE can crash the CLI (major). If the daemon dies or closes between connect() at line 93–97 and write() at line 111, writing to the UDS raises SIGPIPE and terminates the CLI foreground process. The daemon has a SIGPIPE handler (DaemonServer.swift line 193), but that does not protect the client process. On Darwin, set SO_NOSIGPIPE on the fd immediately after socket creation so write() returns EPIPE instead:

    let fd = socket(AF_UNIX, SOCK_STREAM, 0)
    guard fd >= 0 else { return nil }
    +
    +var on: Int32 = 1
    +_ = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, socklen_t(MemoryLayout<Int32>.size))
  2. SO_SNDTIMEO does not bound connect() (minor). The setsockopt at line 91 applies only to subsequent send/write operations, not to connect() on a blocking socket. UDS connect is typically instant-or-fail, so this is low risk; update the comment to avoid future misreading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/DaemonClient.swift` around lines 83 -
118, The client must set SO_NOSIGPIPE on the socket right after creating the fd
so that subsequent write() calls (used by writeAll(fd:data:)) return EPIPE
instead of raising SIGPIPE; add a setsockopt call for SO_NOSIGPIPE immediately
after socket() returns and before connect(fd, …) and keep the existing error
handling (close(fd)/return nil) on connect failure. Also update the comment that
the existing setsockopt(SO_SNDTIMEO) only affects send/write operations and does
not bound connect(), to avoid confusion. Reference: the socket creation site
where fd is returned/used, the connect(fd, sockPtr, addrLen) call, and the
writeAll(fd:data:) helper.

Comment on lines +184 to +195
private func installSignalHandlers() {
let handler: @convention(c) (Int32) -> Void = { _ in
// Signal handlers can't call into the actor safely. Hand off
// to a Task so the actual shutdown runs in Swift-concurrency
// land, where close(listenFD) is safe.
Task.detached { await DaemonSignal.fireShutdown() }
}
signal(SIGINT, handler)
signal(SIGTERM, handler)
signal(SIGPIPE, SIG_IGN)
DaemonSignal.register(self)
}

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Are Swift runtime calls such as Task.detached safe to call from a POSIX signal handler on Darwin?

💡 Result:

No, Swift runtime calls such as Task.detached are not safe to call from a POSIX signal handler on Darwin. POSIX signal handlers must only call async-signal-safe functions, which are limited to a specific list of low-level C functions like write(2) and sigaction(2). Higher-level Swift APIs, including those in the concurrency runtime like Task.detached, involve heap allocations, locks, and other non-async-signal-safe operations. Swift concurrency runtime is "intimately tied to threads and memory" and not designed for signal handler contexts (9). Experimental attributes like @_noLocks and @_noAllocation exist for no-lock/no-allocation code in real-time or signal-safe scenarios, but even these do not cover async runtime calls, and Task.detached creates a new top-level task requiring runtime support (2). Signal handlers in Swift should set atomic flags (using async-safe atomics if available) or use Dispatch signal sources for async handling, avoiding direct Swift runtime calls (1,13,9). Apple's Swift runtime crash handler itself uses raw POSIX signal handling without concurrency APIs (26).

Citations:


🌐 Web query:

DispatchSourceSignal Darwin Swift async signal handling best practices

💡 Result:

DispatchSourceSignal is the recommended way for async UNIX signal handling in Swift on Darwin (macOS/iOS). It avoids async-signal-safety issues of synchronous handlers by queuing signals for dispatch queue execution, allowing full Swift code in handlers. Best practices: 1. Ignore the signal first with signal(SIG, SIG_IGN) to prevent default termination. 2. Create source: DispatchSource.makeSignalSource(signal: SIGINT, queue: someQueue) 3. Set handlers: setEventHandler, setCancelHandler. Activate with activate or resume. 4. Use dedicated queue, not main if possible, to avoid blocking. 5. Cannot monitor SIGILL, SIGBUS, SIGSEGV. Not a replacement for sigaction if needing to prevent termination. 6. For Swift concurrency integration, use AsyncStream wrapping DispatchSource (like swift-service-lifecycle/UnixSignals does) or withCheckedContinuation. Example (SIGINT graceful shutdown): import Dispatch signal(SIGINT, SIG_IGN) let source = DispatchSource.makeSignalSource(signal: SIGINT, queue: .global) source.setEventHandler { print("SIGINT received, cleaning up...") // Perform async-safe cleanup exit(0) } source.setCancelHandler { // Cleanup on cancel } source.resume // Keep runloop alive, e.g., dispatchMain or loop For modern async/await servers, use swift-server/swift-service-lifecycle which wraps DispatchSourceSignal into UnixSignalsSequence: AsyncSequence. Example: import UnixSignals Task { for await signal in UnixSignalsSequence(trapping: .sigint, .sigterm) { print("Got (signal), shutting down") break // or cleanup } }

Citations:


🏁 Script executed:

# Search the codebase for any existing signal handling patterns or DispatchSource usage
rg -i "dispatchsource" --type swift
rg -i "signal.*handler" --type swift -A 3 -B 1
rg "async-signal-safe|signal.*safe" --type swift

Repository: trycua/cua

Length of output: 2599


Replace the C signal handler with DispatchSourceSignal for async-signal-safe shutdown.

Calling Task.detached from a POSIX signal handler violates async-signal-safety requirements on Darwin. The Swift runtime is not async-signal-safe; only a limited set of C functions can be called from signal handlers. Use DispatchSource.makeSignalSource() instead:

  1. Call signal(SIGINT, SIG_IGN) and signal(SIGTERM, SIG_IGN) to suppress default termination
  2. Create signal sources with DispatchSource.makeSignalSource(signal:queue:)
  3. Set handlers via setEventHandler {} to safely trigger shutdown in the dispatch queue context
  4. Alternatively, use swift-service-lifecycle's UnixSignalsSequence for native async/await integration
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/DaemonServer.swift` around lines 184
- 195, installSignalHandlers currently installs a C signal handler that calls
Task.detached (unsafe from a POSIX signal handler); replace it by first
suppressing default handlers with signal(SIGINT, SIG_IGN) and signal(SIGTERM,
SIG_IGN), then create DispatchSource signal sources via
DispatchSource.makeSignalSource(signal:queue:) for SIGINT and SIGTERM, call
setEventHandler { Task { await DaemonSignal.fireShutdown() } } (or dispatch to a
serial queue) to perform the async shutdown safely, keep SIGPIPE ignored with
signal(SIGPIPE, SIG_IGN), and continue to call DaemonSignal.register(self) as
before; update references in installSignalHandlers and ensure the dispatch
sources are retained so they remain active.

Comment on lines +106 to +109
guard let rawPid = arguments?["pid"]?.intValue else {
return errorResult("Missing required integer field pid.")
}
let pid = Int32(rawPid)

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Swift FixedWidthInteger initializer from another integer out of range trap runtime documentation

💡 Result:

In Swift, FixedWidthInteger types (like Int8, UInt16, etc.) conform to BinaryInteger. The initializer init<T: BinaryInteger>(_ source: T) creates a new instance from another binary integer. If the source value is not representable in the target type's range (e.g., converting Int(500) to Int8), it triggers a runtime trap (crash) with an overflow error. This behavior is documented in the official Apple Swift documentation for BinaryInteger: - "If the value passed as source is not representable in this type, a runtime error may occur." - Example: Int16(500) succeeds, but Int8(500) fails, "triggering a runtime error." The source code in Swift standard library (Integers.swift) includes the same documentation comments, confirming: "If the value passed as source is not representable in this type, a runtime error may occur." with examples like UInt32(-500 as Int) erroring. Alternatives to avoid trapping: - init?(exactly:): Returns nil if out of range. - init(clamping:): Clamps to min/max. - init(truncatingIfNeeded:): Truncates or sign-extends bits, no range check. The runtime trap is a deliberate safety feature, appearing as EXC_BREAKPOINT (SIGTRAP) or EXC_BAD_INSTRUCTION (SIGILL) in crash logs, as per Swift runtime error docs. This applies to FixedWidthInteger since they are fixed-size binary integers implementing these init methods with range checks.

Citations:


🏁 Script executed:

# First, let's find and examine the DoubleClickTool.swift file
fd -t f "DoubleClickTool.swift" --exec cat -n {} \;

Repository: trycua/cua

Length of output: 15776


Validate integer ranges before narrowing conversions to prevent daemon crashes.

Int32(rawPid) at line 109 and UInt32(rawWindowId) at lines 143 and 147 will trap on out-of-range values, causing the daemon to crash instead of returning isError. Malformed MCP input (negative window_id or out-of-range pid) triggers runtime traps rather than graceful error handling.

🛡️ Proposed validation
             guard let rawPid = arguments?["pid"]?.intValue else {
                 return errorResult("Missing required integer field pid.")
             }
+            guard rawPid >= Int(Int32.min), rawPid <= Int(Int32.max) else {
+                return errorResult("pid is out of Int32 range.")
+            }
             let pid = Int32(rawPid)
@@
             if elementIndex != nil && rawWindowId == nil {
                 return errorResult(
                     "window_id is required when element_index is used — the "
                     + "element_index cache is scoped per (pid, window_id). Pass "
                     + "the same window_id you used in `get_window_state`.")
             }
+            if let rawWindowId,
+               rawWindowId < 0 || rawWindowId > Int(UInt32.max)
+            {
+                return errorResult("window_id is out of UInt32 range.")
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/DoubleClickTool.swift` around
lines 106 - 109, The code narrows integers unsafely (Int32(rawPid),
UInt32(rawWindowId)) which can trap; before converting, validate that
arguments?["pid"]?.intValue and arguments?["window_id"]?.intValue are within the
target ranges (Int32.min...Int32.max for pid and 0...UInt32.max for window_id)
and return errorResult(...) if they are out of range; replace direct
Int32(rawPid) and UInt32(rawWindowId) conversions with guarded checks that only
perform the narrowing when the value is in-range (using the same symbols rawPid,
pid, rawWindowId, windowId and the existing errorResult helper).

Comment on lines +106 to +116
guard let rawPid = arguments?["pid"]?.intValue else {
return errorResult("Missing required integer field pid.")
}
guard let rawWindowId = arguments?["window_id"]?.intValue else {
return errorResult(
"Missing required integer field window_id. Use `list_windows` "
+ "to enumerate the target app's windows, or read `launch_app`'s "
+ "`windows` array.")
}
let pid = Int32(rawPid)
let windowId = UInt32(rawWindowId)

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only check for similar unchecked narrowing conversions in tool entry points.
rg -nP '\b(Int32|UInt32)\(\s*raw[A-Za-z0-9_]+\s*\)' -C2

Repository: trycua/cua

Length of output: 10448


🏁 Script executed:

cat -n libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift | sed -n '100,125p'

Repository: trycua/cua

Length of output: 1546


Use bounds-checked conversions for numeric narrowing.

The unchecked conversions Int32(rawPid) and UInt32(rawWindowId) will trap on out-of-range input, crashing the daemon instead of returning an error. This pattern is repeated across 10+ tool entry points.

Bounds-safe conversion
             guard let rawWindowId = arguments?["window_id"]?.intValue else {
                 return errorResult(
                     "Missing required integer field window_id. Use `list_windows` "
                     + "to enumerate the target app's windows, or read `launch_app`'s "
                     + "`windows` array.")
             }
-            let pid = Int32(rawPid)
-            let windowId = UInt32(rawWindowId)
+            guard let pid = Int32(exactly: rawPid) else {
+                return errorResult("pid \(rawPid) is outside the supported Int32 range.")
+            }
+            guard let windowId = UInt32(exactly: rawWindowId) else {
+                return errorResult("window_id \(rawWindowId) is outside the supported UInt32 range.")
+            }

Similar conversions occur in TypeTextCharsTool, RightClickTool, PressKeyTool, HotkeyTool, TypeTextTool, ZoomTool, ScrollTool, ClickTool, DoubleClickTool, SetValueTool, and AppEnumerator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/cua-driver/Sources/CuaDriverServer/Tools/GetWindowStateTool.swift`
around lines 106 - 116, The conversion from rawPid/rawWindowId using
Int32(rawPid) and UInt32(rawWindowId) can trap on out-of-range values; update
GetWindowStateTool to perform bounds-checked narrowing by either using
Int32(exact: rawPid)/UInt32(exact: rawWindowId) or explicitly checking
rawPid/rawWindowId against Int32.min..Int32.max and UInt32.min..UInt32.max
before assigning pid/windowId, and if out of range return errorResult with a
clear message; apply the same pattern to the other tools listed
(TypeTextCharsTool, RightClickTool, PressKeyTool, HotkeyTool, TypeTextTool,
ZoomTool, ScrollTool, ClickTool, DoubleClickTool, SetValueTool, AppEnumerator)
for all conversions from raw numeric arguments to narrower types, referencing
their respective argument keys and using their existing errorResult helper for
failures.

macOS computer-use driver that speaks the Model Context Protocol
over stdio. Drop-in backend for MCP clients (Claude Code, Cursor) or
as part of the cua-computer-server stack; also usable standalone via
a CLI where every MCP tool is a top-level subcommand.

Designed around a strict no-foreground contract: the user's frontmost
app never changes, the real cursor never warps, and the target never
raises or switches Space. You drive a backgrounded macOS app in one
window while your foreground editor keeps typing in another.

Highlights:

- Element-indexed AX actions that work on hidden, off-Space, or
  occluded targets — `get_window_state(pid, window_id)` returns a
  per-window AX tree (filtered correctly on multi-window apps),
  `click({pid, window_id, element_index})` fires the AX action
  without cursor move or focus steal.
- Backgrounded pixel clicks via auth-signed SLEventPostToPid with a
  yabai-style focus-without-raise primer — same (x, y) addressing
  space as the returned screenshot, supports modifiers and count.
- Three capture modes — `vision` (PNG only; default), `ax` (tree
  only, no screen-capture hit), `som` (both).
- Chromium / Electron AX support via the private
  `_AXObserverAddNotificationAndCheckRemote` SPI so the tree stays
  populated without activating the target.
- Pid-mandatory keyboard — every `press_key` / `type_text` routes
  through `CGEvent.postToPid` so keys can't leak into the user's
  foreground app.
- Agent-cursor overlay that glides to each target before dispatch,
  press-in/ripple on landing, idle-hides. Uniform across AX clicks
  and pixel clicks.
- Trajectory recording + replay — per-turn folders with app state,
  screenshot, action, click marker. Optional video capture with
  zoom-on-click render for demos.
- ScreenCaptureKit screenshots defaulting to a 1568-long-side cap
  that matches Anthropic's multimodal input limit, so model-picked
  pixel coords match the tool's coordinate space.

See libs/cua-driver/README.md for the full feature list plus
comparison against Codex Computer Use and Claude Computer Use, and
libs/cua-driver/Skills/cua-driver/SKILL.md for the canonical action
loop.

Co-Authored-By: Sarina Li <sarinajin.li@gmail.com>
Co-Authored-By: Dillon DuPont <ddupont@mit.edu>
Co-Authored-By: Claude <noreply@anthropic.com>
f-trycua and others added 2 commits April 22, 2026 22:32
Wrap every `Int32(raw...)` / `UInt32(raw...)` conversion in
`Int32(exactly:)` / `UInt32(exactly:)` so malformed MCP input
returns a structured error instead of trapping the daemon.

Addresses CodeRabbit #5 + #6 on PR #1359.

Co-Authored-By: Claude <noreply@anthropic.com>
Replaces the POSIX signal handler that called Task.detached (not
async-signal-safe per Apple's Swift concurrency docs) with a
DispatchSource-based pattern. SIGINT/SIGTERM routed through
dispatch sources on a user-initiated queue; SIGPIPE continues to
be SIG_IGN'd so write() returns EPIPE cleanly.

Addresses CodeRabbit #4 on PR #1359.

Co-Authored-By: Claude <noreply@anthropic.com>
som returns tree + screenshot, so element_index clicks — the
driver's primary addressing mode — work on the first
get_window_state call without any configuration. vision (PNG only)
stays available as opt-in for vision-first VLM pipelines that
specifically don't want the AX walk.

The previous vision default meant a user who installed the driver
and ran the canonical snapshot-then-click loop ended up on the
pixel-click fallback path instead of the element-indexed primary
path, which made the driver's distinguishing feature invisible out
of the box.

Co-Authored-By: Claude <noreply@anthropic.com>
f-trycua and others added 2 commits April 22, 2026 23:21
The bounds-checked-narrowing sweep caught the 11 tools CodeRabbit
called out but missed `ListWindowsTool.swift`, which had the same
`Int32($0)` trap in its `pidFilter` path (filters the window list
against a caller-supplied pid). A malformed `pid` over Int32.max
would crash the daemon instead of returning a structured error.

Adds a `private static func errorResult` alongside the existing
`summary` helper so the same pattern the other tools use is
available here.

Co-Authored-By: Claude <noreply@anthropic.com>
Adds the cua-driver docs subtree to docs/content/docs/cua-driver/,
matching the lume structure (guide + reference sections, no
examples for v0.1). Registers cua-driver in the root docs
meta.json alongside cua/cuabench/cuabot/lume.

- guide/getting-started/{introduction,installation,quickstart,comparison,faq}.mdx
- reference/{cli-reference,mcp-tools,limits}.mdx
- Fumadocs format: frontmatter + Callout imports matching the
  lume pages; no VersionHeader yet (only one shipped version).

Co-Authored-By: Claude <noreply@anthropic.com>
Matches the cuabench / lume pattern: bare `/cua-driver`, the
`/guide` section root, and the `/reference` section root each
redirect to their first content page. Before this, hitting
`/docs/cua-driver` returned a 404 — the docs tree existed but
the section landing redirects in middleware.ts hadn't been
added alongside the new content.

Co-Authored-By: Claude <noreply@anthropic.com>
…e-speed render (#1360)

**Cursor**
- AgentCursorView/AgentCursorRenderer: rewrite agent cursor rendering with
  SwiftUI Canvas + Dubins-path motion engine (arc → straight → arc)
- AgentCursor.animate(): always arrive at 45° (upper-left tip), approaching
  targets from the lower-right — consistent visual signature on every click

**Recording**
- ToolRegistry: capture CLOCK_UPTIME_RAW before handler.invoke() so the
  recorded span brackets the full animation time (t_start_ms_from_session_start)
- RecordingSession: add lastAutoRenderURL + auto-render to recording_rendered.mp4
  on stop; embed display_scale_factor in session.json
- TrajectoryLoader: add loadActionSpans() — walks all turn-*/action.json and
  extracts ActionSpan{startMs, endMs, windowBounds, clickPoint}; read
  displayScaleFactor from session.json
- ActionSpan (new): ClickPoint, FocusWaypoint; ActionSpanGenerator with
  padMs=500, fastSpeed=8×, mergeGapMs=5000; per-span focus waypoints for
  smooth camera pan on merged spans

**Video post-processing**
- RecordingRenderer: window-bbox zoom (ZoomRegion from windowBounds, letterbox
  aspect ratio, 400ms eased); variable-speed PTS remapping (1× inside spans,
  8× outside); Metal-backed CIContext for GPU frame processing; falls back to
  legacy click-zoom when no action spans present
- SetRecordingTool: report rendered path in stop confirmation message

Co-authored-by: cua <cua@cua.localdomain>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds cua-driver alongside Cua / Cua Bench / Lume / Cua-Bot in the
docs header nav. Ships Guide + Reference tabs (no Examples in
v0.1). Icon is a koala-astronaut mascot provided by Francesco,
tinted into black and white PNG variants at build time so both
light and dark themes have a readable contrast.

SVG equivalents would be nicer (crisper at high DPI, smaller
payload) — left as a design follow-up.

Co-Authored-By: Claude <noreply@anthropic.com>
"Background computer-use" reads punchier and matches the length
of the other product descriptions in the dropdown (Benchmarking
toolkit, macOS VM CLI and Framework, etc.).

Co-Authored-By: Claude <noreply@anthropic.com>
f-trycua and others added 2 commits April 22, 2026 23:58
Restore `nonisolated(unsafe)` on `capturedWriter` in RecordingRenderer;
PR #1360 dropped it and Swift 6.1 (Xcode 16.3) rejects implicit capture
of non-Sendable AVAssetWriter in the @sendable finishWriting closure.

Co-Authored-By: Claude <noreply@anthropic.com>
Add two entries to .lycheeignore to unbreak the Check External Links
(lychee) job on PR #1359: `https://openai.com/*` (openai.com returns
403 to automated checkers on URLs like /codex but loads fine in a
browser — flagged from docs/content/docs/cua-driver/guide/getting-started/comparison.mdx)
and the self-referential install.sh raw-content URL used by the CLI
reference page (docs/content/docs/cua-driver/reference/cli-reference.mdx),
which 404s only because the install script doesn't exist on main yet
and will resolve once this PR merges. The internal link check failure
(`/cua/guide/get-started/self-hosted-sandboxes` in
docs/content/docs/cua/guide/sandbox/lifecycle.mdx, introduced by
PR #1228) is pre-existing on main and out of scope for this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swift 6.1 on CI flags `NSApplication.shared`, `setActivationPolicy`,
and `.run()` as main-actor-isolated calls from a nonisolated
context. Our local Swift 6.3 toolchain was more permissive here, so
the errors only surfaced on CI. Compiler-suggested fix: annotate the
helper itself with `@MainActor`. All callers are already on the
main actor (this is the AppKit bootstrap path), so isolation is
tightened without behavior change.

Co-Authored-By: Claude <noreply@anthropic.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.

2 participants