Skip to content

fix(cua-driver-rs): 3 cli.rs bugs — BOM-in-stdin, daemon image merge, --socket plumbing - #1661

Merged
f-trycua merged 4 commits into
mainfrom
fix/cua-driver-rs-cli-bom-image-socket
May 23, 2026
Merged

fix(cua-driver-rs): 3 cli.rs bugs — BOM-in-stdin, daemon image merge, --socket plumbing#1661
f-trycua merged 4 commits into
mainfrom
fix/cua-driver-rs-cli-bom-image-socket

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Summary

Three small fixes to cua-driver call discovered during overnight dogfooding on the Windows VM (cua-driver-rs v0.2.18). All are real bugs with concrete repros + tests; none changes documented behavior — they fix paths that should have worked but didn't.

1. UTF-8 BOM in stdin payloads silently rejected (5f033aed)

PowerShell 5.1's Set-Content -Encoding utf8 writes a 3-byte BOM (`EF BB BF`) at file start. When that file is piped into `cua-driver call `, the cli's `read_stdin_json` failed to parse and the tool returned the misleading error "Missing required integer field pid" (because every field looked missing).

Fix: strip the BOM in `read_stdin_json` before serde_json::from_str. Two unit tests guard the behavior (BOM present + BOM absent).

Repro before fix:
```powershell
Set-Content -Path .\req.json -Value '{"pid":99999}' -Encoding utf8 # writes BOM
Get-Content .\req.json -Raw | cua-driver call list_windows

→ "Missing required integer field pid."

```

2. Daemon-forwarded screenshot dropped the image bytes (cf8f3ca9)

`cua-driver call screenshot` over a daemon socket returned `{"format": "png", "width": ..., "height": ...}` and silently discarded the `screenshot_png_b64` field. The in-process path was correct; only the daemon-forwarding branch in `run_call` was missing the image merge into structuredContent.

Fix: in the daemon-forwarding branch, walk the response `content` array, capture any `type: image` item, and merge it into `structuredContent` as `screenshot_png_b64` + `screenshot_mime_type` — same shape as the in-process path produces.

3. `--socket` flag wasn't wired for `Command::Call` (d375e053)

The cli-reference docs document `--socket` for `cua-driver call`, but `Command::Call` didn't actually accept it. Added the `socket` field to `Command::Call` (parsed in both call sites), added a `socket_override` param to `run_call`, and added an integration regression test (`test_call_screenshot_via_daemon_emits_b64`).

The new test is `@unittest.skipIf(sys.platform == "win32")` because the subprocess-spawned daemon on Windows CI runs in Session 0 (no graphics → BitBlt returns `0x80070006`). It exercises the regression on macOS + Linux runners.

Diff

  • `crates/cua-driver/src/cli.rs` — BOM strip + image merge + `socket_override` param
  • `crates/cua-driver/src/main.rs` — `Command::Call` socket field plumbed
  • `tests/integration/test_cli.py` — `test_call_screenshot_via_daemon_emits_b64`

~140 LOC across 3 files. All three commits independently revertable.

Docs

No docs change needed — `cli-reference.mdx` already documents `--socket` for `call` (the docs were aspirational; this PR aligns implementation with documentation). `mcp-tools.mdx` is auto-generated from Swift sources and these are internal cli plumbing fixes that don't change Swift's tool surface.

Test plan

  • `python3 -m unittest test_cli` → 17/17 passing on macOS
  • Verified live on Windows VM (cuademo Session 9) — see OVERNIGHT_HANDOFF_CHECKS.md section 2
  • CI green

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --socket parameter to override daemon socket path for cua-driver call commands
    • Screenshot image data now automatically included in daemon call responses
  • Bug Fixes

    • Improved JSON input parsing to handle UTF-8 BOM characters
  • Tests

    • Added integration test verifying daemon socket functionality with screenshot retrieval

Review Change Stack

f-trycua and others added 3 commits May 23, 2026 14:20
`read_stdin_json` was feeding the buffer directly to serde_json::from_str
after trimming whitespace, but PowerShell 5.1's
`Set-Content -Encoding utf8` silently prepends a UTF-8 BOM
(`EF BB BF` / `U+FEFF`) to the file. When that file is fed to cua-driver
via `Start-Process -RedirectStandardInput`, the BOM stays at the front
of stdin, serde_json fails (a BOM isn't a valid JSON start character),
and the call falls through to default args — producing surprising errors
like "Missing required integer field pid" despite a valid-looking payload.

Surfaced live during the 2026-05-23 overnight Windows dogfood:

    $ Set-Content -Path tmp.json -Value '{"pid":8608}' -Encoding utf8
    $ cua-driver call hotkey < tmp.json
    Missing required integer field pid.

After fix:

    $ cua-driver call hotkey < tmp.json
    ✅ Pressed ctrl+s on pid 8608 via PostMessage (Win32 target).

Implementation: strip a single leading `'\u{feff}'` character from the
trimmed buffer before handing to serde_json. The PS5.1 BOM is a 3-byte
sequence in UTF-8 (`EF BB BF`) but a single char in str terms, so a
`strip_prefix('\u{feff}')` is the right primitive.

Tests in `stdin_bom_tests`:
- BOM-prefixed payload parses successfully
- Plain JSON unchanged (no-op when no BOM)

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

`cua-driver call screenshot` was silently dropping the image bytes when
a daemon was running on the named pipe. The output looked like:

    $ cua-driver call screenshot
    {"format": "png", "height": 949, "width": 1512}

— metadata only, no `screenshot_png_b64`. The image was 7.4 MB and the
caller had no way to get it back.

Root cause: the CLI in `run_call` has two code paths.

The **in-process path** (no daemon listening) loops `result.content`,
captures any Content::Image into `image_b64`, then merges that into the
JSON-printed `structuredContent`:

    if let Some(sc) = &result.structured_content {
        let mut obj = sc.clone();
        if out_path.is_none() {
            if let Some((b64, mime)) = image_b64 {
                ... insert("screenshot_png_b64", b64) ...
            }
        }
        println!("{pretty}");
    }

The **daemon-forwarding path** (daemon listening at default pipe) parsed
the daemon's response, looped `content`, but only handled the image when
`--screenshot-out-file` was set — otherwise the image was just thrown
away:

    for item in content {
        if item.get("type") == Some("image") {
            if let Some(ref path) = screenshot_out_file {
                ... write to file ...
            }
            // ← else: silently drop
        }
    }
    if let Some(sc) = result.get("structuredContent") {
        let pretty = serde_json::to_string_pretty(sc)...;
        println!("{pretty}");
    }

Tonight on the cuademo VM (cua-driver-serve task registered, daemon at
\\.\pipe\cua-driver listening), every `cua-driver call screenshot`
invocation went through the daemon path and lost the image. From a
non-daemon shell it worked fine.

Fix: in the daemon-forwarding path, stash the image into `image_b64`
during the content walk (when no out-path is set), then merge it into
the structuredContent object before the pretty-print — mirroring the
in-process path's behavior 1:1. Same final `screenshot_png_b64` +
`screenshot_mime_type` keys.

Empirical verification:

  before: cua-driver call screenshot | out-string  →  61 chars
  after:  cua-driver call screenshot | out-string  →  7,654,802 chars

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

`--socket <path>` was a CLI-level flag (parsed in cli.rs line 142,
listed in VALUE_FLAGS line 93) and threaded into `Command::Serve` /
`Command::Stop` / `Command::Status` / `Command::Mcp` / `Command::Config`
/ `Command::Recording` — but **not into `Command::Call`**.

That meant `cua-driver call --socket X screenshot` silently ignored the
--socket override and used `default_socket_path()`. The flag worked for
every long-running command but not for the per-call invocation.

This made it impossible to write a daemon-forwarding regression test
that hits a tempfile-socketed test daemon (the existing
ServeDaemonTests pattern): the call always routed to the default
socket, never to the test's `--socket /tmp/cua-test-XXX.sock` daemon.

Changes:
- `cli::Command::Call` gains `socket: Option<String>` field
- Both parse sites (line 249 + 310) thread `socket` through
- `cli::run_call` takes a `socket_override: Option<String>` parameter
- When Some, skip the platform default + uia-worker-preferred resolution
  and route directly to that path
- Both main.rs callsites pass the socket through

Added a regression test for the 2026-05-23 screenshot fix:

`test_call_screenshot_via_daemon_emits_b64` — starts a tempfile-socketed
daemon, invokes `cua-driver call --socket X screenshot`, asserts that
`screenshot_png_b64` appears in the JSON output. This catches the bug
where the daemon-forwarding path in `run_call` was silently dropping
the image (only printing `structuredContent` metadata: format / width /
height) — a class of bug the in-process-only `test_call_screenshot_returns_b64_image`
test couldn't catch because it never exercised the daemon code path.

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

vercel Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 23, 2026 2:04pm

Request Review

@coderabbitai

coderabbitai Bot commented May 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: 895046b8-f31c-4120-98f0-98ecd7cf3348

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

This PR extends cua-driver call to accept an explicit --socket override for daemon forwarding, updates socket selection logic to prefer the override, refactors daemon response handling to extract and merge image data into structured output, wires the parameter through both platform dispatchers, adds BOM stripping to stdin JSON parsing for PowerShell compatibility, and validates the feature with a new integration test.

Changes

Daemon Socket Override Feature

Layer / File(s) Summary
Extend Call command with socket parameter
libs/cua-driver-rs/crates/cua-driver/src/cli.rs
Command::Call gains a socket: Option<String> field; explicit and implicit CLI parsing branches both extract and propagate the --socket flag into the command variant.
Route daemon calls to override socket and merge responses
libs/cua-driver-rs/crates/cua-driver/src/cli.rs
run_call() accepts socket_override, selects the daemon socket (preferring override), and extracts image fields (screenshot_png_b64, screenshot_mime_type) from the daemon response to merge into structuredContent for consistent output whether the call is in-process or forwarded.
Wire socket parameter through main() dispatchers
libs/cua-driver-rs/crates/cua-driver/src/main.rs
macOS and non-macOS main() branches match on socket and forward it to run_call() for both explicit and implicit call paths.
Strip BOM from PowerShell-piped JSON input
libs/cua-driver-rs/crates/cua-driver/src/cli.rs
read_stdin_json() detects and removes leading UTF-8 BOM before JSON parsing; documentation and unit tests cover both BOM and non-BOM cases to handle PowerShell 5.1 behavior.
Integration test for daemon socket forwarding
libs/cua-driver-rs/tests/integration/test_cli.py
New test starts a daemon on a temporary socket and verifies cua-driver call --socket <sock> screenshot returns screenshot_png_b64 and screenshot_mime_type in the response; Windows-skipped and includes cleanup via finally block.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • trycua/cua#1604: Both PRs modify daemon socket selection in cua-driver call on Windows; this PR adds explicit --socket override + response merging, while the other PR changes the default pipe preference.

Poem

🐰 A socket, now chosen with care,
Daemon calls routed just here, just there,
Images merge through the structured flow,
BOM stripping cleans what PowerShell did throw.
Tests verify all works just right—
A call is a call, whether day or night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the three main bug fixes addressed in the PR: BOM-in-stdin handling, daemon image merge, and socket plumbing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cua-driver-rs-cli-bom-image-socket

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/cua-driver-rs/tests/integration/test_cli.py (1)

145-151: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast if the daemon never comes up.

This helper returns proc even when status --socket never succeeds. Because run_call(..., socket) falls back in-process on an unreachable socket, test_call_screenshot_via_daemon_emits_b64 can still pass without ever exercising the daemon path.

Proposed fix
     def _start_daemon(self):
         import subprocess, time
         proc = subprocess.Popen(
             [self.binary, "serve", "--socket", self._sock_file],
             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         )
         # Wait for daemon to bind.
         for _ in range(30):
             time.sleep(0.1)
             r = _run([self.binary, "status", "--socket", self._sock_file])
             if r.returncode == 0:
-                break
-        return proc
+                return proc
+
+        proc.terminate()
+        proc.wait(timeout=3)
+        self.fail(f"daemon did not become ready on {self._sock_file}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/tests/integration/test_cli.py` around lines 145 - 151, The
helper currently returns proc even when the daemon never responds to `status
--socket`, which masks daemon-start failures; update the loop that checks `r =
_run([self.binary, "status", "--socket", self._sock_file])` so that if no
successful `r.returncode == 0` is observed after the retry window you
terminate/cleanup `proc` (e.g., proc.kill() or proc.terminate()), wait for it to
exit, and raise an exception (or assert) instead of returning `proc`, ensuring
tests such as `test_call_screenshot_via_daemon_emits_b64` cannot silently fall
back to the in-process path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs`:
- Around line 771-775: The daemon path currently discards std::fs::write result
when writing decoded bytes for screenshot_out_file, so write failures are
hidden; change the Ok(bytes) arm inside the base64 decode match to check the
Result from std::fs::write(path, &bytes) and surface errors exactly like the
decode Err branch (e.g., eprintln!("--screenshot-out-file: {e}") or otherwise
return/propagate a non-zero/Err), ensuring screenshot_out_file write errors are
reported and cause the same failure behavior as the in-process branch.

---

Outside diff comments:
In `@libs/cua-driver-rs/tests/integration/test_cli.py`:
- Around line 145-151: The helper currently returns proc even when the daemon
never responds to `status --socket`, which masks daemon-start failures; update
the loop that checks `r = _run([self.binary, "status", "--socket",
self._sock_file])` so that if no successful `r.returncode == 0` is observed
after the retry window you terminate/cleanup `proc` (e.g., proc.kill() or
proc.terminate()), wait for it to exit, and raise an exception (or assert)
instead of returning `proc`, ensuring tests such as
`test_call_screenshot_via_daemon_emits_b64` cannot silently fall back to the
in-process path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0dd76d0d-d0de-4bec-b10c-ddddf8b2187f

📥 Commits

Reviewing files that changed from the base of the PR and between e37687c and d375e05.

📒 Files selected for processing (3)
  • libs/cua-driver-rs/crates/cua-driver/src/cli.rs
  • libs/cua-driver-rs/crates/cua-driver/src/main.rs
  • libs/cua-driver-rs/tests/integration/test_cli.py

Comment thread libs/cua-driver-rs/crates/cua-driver/src/cli.rs Outdated
…mon path

The daemon-forwarding branch was silently discarding the result of
`std::fs::write`, so a failed write (e.g. permission denied, disk full,
parent dir missing) would exit 0 with no indication. The in-process
branch already reports these. This commit aligns the daemon branch with
the same error-reporting shape, with separate messages for the base64
decode vs. file-write failure paths.

Spotted by CodeRabbit on PR #1661.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@f-trycua
f-trycua merged commit 43e51b4 into main May 23, 2026
5 checks passed
@f-trycua
f-trycua deleted the fix/cua-driver-rs-cli-bom-image-socket branch May 23, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant