Skip to content

fix(computer-use): win32 backend reports PowerShell failures truthfully - #5903

Merged
Hmbown merged 1 commit into
mainfrom
fix/win-cu-ps-failure-5896-v2
Sep 6, 2026
Merged

fix(computer-use): win32 backend reports PowerShell failures truthfully#5903
Hmbown merged 1 commit into
mainfrom
fix/win-cu-ps-failure-5896-v2

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #5896.

The verified issue named three defects in the Windows backend:

  1. create() registered the User32 C# type in a one-shot bootstrap PowerShell process, but every action spawns a fresh powershell.exe — so actions ran without the type definition they depended on.
  2. withUser32() returned raw ps() results, so left_mouse_down, mouse_move, scroll, key, clickAt, write_clipboard and friends returned {action_sent:true} even on exit≠0 or spawn failure (reproduced with a fake powershell.exe exiting 1).
  3. In left_mouse_down, ternary/concatenation precedence put the mouse_event(LEFTDOWN) expression in the no-target branch — a targeted mouse-down moved the cursor and never pressed.

Fix: every User32-backed invocation is now self-contained (full Add-Type prelude per action); psOk/psJson throw ExecError on timeout, nonzero exit, and spawn failure; left_mouse_down now always emits the press after the optional move; list_windows parses via psJson; bootstrap process removed.

Tests (tests/win32-input.test.mjs, 6 new): a fake powershell.exe on PATH decodes and captures each -EncodedCommand — exit≠0 → error; spawn failure → error; success → success; targeted down = SetCursorPos + LEFTDOWN in one self-contained command; per-action self-containment for all 10 User32 actions; cursor_position JSON parsing.

Evidence: node --test tests/win32-input.test.mjs → 6/6 pass, exit 0. Full plugin suite 42/43 on macOS with the single failure reproduced identically at baseline (pre-existing ssh-probe test that requires AppleEvents and asserts process.platform === 'linux' in its error branch — unrelated to this change, not weakened). Native Windows desktop acceptance remains a separate required receipt per the issue.


Note

Medium Risk
Changes Windows desktop automation input and error semantics; failures now surface as errors instead of silent success, which is correct but may affect callers that ignored errors.

Overview
Fixes the Windows computer-use backend so mouse/keyboard actions no longer claim success when PowerShell fails, and so User32 input actually runs in each spawned process.

Self-contained PowerShell invocations: The one-shot bootstrap that registered User32 in a separate powershell.exe is removed. Every User32-backed action (and cursor_position) now prefixes scripts with an inline Add-Type prelude (USER32_PRELUDE), because each action still spawns a fresh process.

Truthful errors: New psOk throws ExecError on timeout, nonzero exit, and spawn failure; psJson builds on that. withUser32, write_clipboard, and list_windows use these paths so callers get errors instead of { action_sent: true } when PowerShell fails.

left_mouse_down bug: Optional cursor move and LEFTDOWN are composed explicitly so a targeted press both moves and presses (fixes ternary/concatenation precedence).

Tests: Adds win32-input.test.mjs with a fake powershell.exe on PATH to assert encoded scripts, per-action self-containment, error propagation, and the targeted mouse-down behavior—without a Windows GUI.

Reviewed by Cursor Bugbot for commit ebb706f. Bugbot is set up for automated code reviews on this repo. Configure here.

Every win32 action now runs self-contained in its own PowerShell process: the User32 Add-Type definition travels with each invocation instead of a bootstrap process whose type registration could never reach the fresh powershell.exe spawned per action (issue #5896 defect 1).

withUser32()/psJson() now go through psOk(), which rejects nonzero exits, timeouts, and spawn failures, so input methods can no longer return action_sent:true after a failed subprocess (defect 2). left_mouse_down's ternary no longer swallows the LEFTDOWN press when a target is supplied - a targeted press both moves and presses (defect 3). list_windows now parses its JSON via psJson instead of mapping the raw run result, and write_clipboard fails truthfully on error.

Tests: fake powershell.exe on PATH captures decoded -EncodedCommand payloads; covers exit!=0 -> error, spawn failure -> error, success -> success, and targeted mouse-down emitting SetCursorPos + LEFTDOWN in one self-contained command.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Copilot AI lite review requested due to automatic review settings September 6, 2026 00:06
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T00:13:54.868838Z ebb706f PR opened
ℹ️ About Codex in GitHub

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

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

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e18b73ce-e4f5-411a-84af-5fb28c6487d9)

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 29s —— View job


Review complete

  • Gather context (PR body, diff, changed files)
  • Read crates/tui/plugins/computer-use/src/backends/win32.mjs diff in full
  • Read new test file tests/win32-input.test.mjs
  • Check correctness: Add-Type prelude duplication, psOk/psJson error handling, left_mouse_down precedence fix
  • Check provider/model route safety implications (not applicable to this file)
  • Check for reuse/simplification opportunities
  • Check test coverage adequacy
  • Post final review

Summary

This correctly fixes all three defects named in #5896, and the fix logic checks out against the code, not just the PR description.

1. Bootstrap-process race (fixed correctly). Every withUser32() call now inlines USER32_PRELUDE ahead of the action script, so the type definition travels with the same powershell.exe process that uses it (win32.mjs:80-88). The prior design (create() firing an unawaited bootstrap ps() while every action spawned its own process) could never guarantee ordering — this removes the race entirely rather than papering over it with a longer timeout.

2. Silent failures (fixed correctly). psOk (win32.mjs:21-27) throws ExecError on timedOut, nonzero exit, and (via run()'s child.on("error", ...)code: -1) spawn failure. withUser32 now returns psOk(...) instead of raw ps(...), so every left_mouse_down/mouse_move/scroll/key/clickAt/etc. caller gets a rejected promise instead of a silently-successful {action_sent:true} on failure. psJson composes on top of psOk, so JSON-returning actions inherit the same truthfulness. Verified run()'s spawn-failure path (exec.mjs:22-25,48-51) does produce code:-1, matching the "spawn failure" test case.

3. left_mouse_down ternary bug (fixed correctly). Confirmed the original bug: target ? move : "" + press parses as target ? move : ("" + press) — ternary binds looser than +, so a targeted press selected only the move branch and dropped the press entirely (matches the PR's "moved and never pressed" description). The fix builds move as its own string and always concatenates the press: `${move}[User32]::mouse_event(...)` (win32.mjs:265-271). Correct.

Minor — reuse/simplification (non-blocking):

  • psOk (win32.mjs:21-27) duplicates the timeout/nonzero-exit-throw pattern already implemented by runOk in exec.mjs:57-62. Since ps() is just run() with fixed argv construction, psOk could likely delegate to runOk("powershell.exe", [...], opts) (or ps could return early via runOk-shaped call) instead of re-implementing the same two checks. Small, but it's the second copy of one rule.
  • After this PR, psOk exists but open_application, screenshot, zoom, and type (win32.mjs:169-171, 221-222, 241-242, 311-312) still call raw ps() and hand-roll the same if (r.code !== 0) throw ... check inline. They weren't buggy before (already truthful), so this is cosmetic, but now that psOk is the established truthful primitive, folding these in would remove four near-identical duplicated error-throwing blocks.
  • Pre-existing (not introduced by this diff, but now colliding): probe() at win32.mjs:96 declares a local const psOk = await ps(...).then(...) — a boolean — with the exact same name as the new module-level psOk() function. It's harmless (function-scoped shadow, and probe() never calls the module-level psOk), but worth a rename in a follow-up to avoid confusing the two.

Tests: the 6 new tests in win32-input.test.mjs are well constructed — a real spawned fake powershell.exe (shell shim → node) rather than a mocked child_process, decoding the actual -EncodedCommand payload. Coverage matches the three defects: single-process assertion (no bootstrap dependency), nonzero-exit → ExecError across three action types, spawn-failure → ExecError, targeted left_mouse_down asserts both SetCursorPos and LEFTDOWN appear in one script (would have caught the ternary bug directly), per-action self-containment across all 10 User32 actions, and cursor_position JSON parsing. package.json's test script (node --test tests/*.test.mjs) picks the new file up automatically. I confirmed the test file's own logic is internally consistent (env var teardown in t.after, capture file cleanup); I wasn't able to execute node --test in this sandbox to re-run the suite myself, so I'm relying on static review plus the PR's reported 6/6 pass, exit 0.

Provider/model route safety: not applicable — this diff is confined to the Windows computer-use backend and its input plumbing, no model/provider selection logic touched.

No security concerns: scripts still travel exclusively via -EncodedCommand base64, argument interpolation into PowerShell strings uses the existing quote-escaping conventions already present in the file, and no new shell-injection surface is introduced.

Copilot AI 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.

🟢 Approval recommended

The core behavioral fixes match the issue’s defects and are backed by focused tests, with only a minor naming clarity nit noted.

Pull request overview

This PR fixes correctness in the computer-use Win32 backend so PowerShell failures no longer surface as { action_sent: true }, and User32-backed input works reliably by making each PowerShell invocation self-contained.

Changes:

  • Replace the one-shot “bootstrap” PowerShell type registration with a per-action USER32_PRELUDE so every spawned powershell.exe has the needed User32 type.
  • Add psOk() (truthful PowerShell execution) and route key call sites (withUser32, write_clipboard, cursor_position, list_windows) through it / psJson().
  • Add a Node-only test suite that fakes powershell.exe to assert self-containment, error propagation, and the targeted left_mouse_down behavior.
File summaries
File Description
crates/tui/plugins/computer-use/src/backends/win32.mjs Makes PowerShell/User32 invocations self-contained and fail truthfully; fixes targeted left_mouse_down press logic.
crates/tui/plugins/computer-use/tests/win32-input.test.mjs Adds regression tests using a fake powershell.exe to verify error semantics and per-action self-containment.
Review details

Suppressed comments (1)

crates/tui/plugins/computer-use/src/backends/win32.mjs:100

  • In probe(), the boolean psOk variable now shares a name with the new psOk() helper, which makes the code harder to read and can lead to confusion during future edits. Renaming the boolean to something like powershellOk keeps intent clear.
      const psOk = await ps("Write-Output 'ok'").then((r) => r.code === 0).catch(() => false);
      return {
        platform: "win32",
        powershell: psOk,
        capabilities: { screenshot: psOk, accessibility_tree: psOk, clipboard: psOk, recording: ffmpeg, raw_input: psOk },
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebb706fc29

ℹ️ About Codex in GitHub

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

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

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

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

const mjs = path.join(dir, "ps-fake.mjs");
fs.writeFileSync(mjs, FAKE_PS_MJS);
const bin = path.join(dir, "powershell.exe");
fs.writeFileSync(bin, `#!/bin/sh\nexec node ${JSON.stringify(mjs)} "$@"\n`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the fake PowerShell executable portable to Windows

On native Windows—the platform this backend targets—child_process.spawn("powershell.exe", ...) requires a Windows executable, but this fixture writes a POSIX /bin/sh script and chmod cannot make it executable there. Consequently the five onPath: true tests fail with a spawn/invalid-executable error rather than exercising the backend; the repository's computer-use plugin CI job runs only on Ubuntu, so this escapes CI but breaks npm test for Windows contributors. Use an injectable process runner or create a platform-appropriate launcher.

Useful? React with 👍 / 👎.

async function psOk(script, opts = {}) {
const r = await ps(script, opts);
if (r.timedOut) throw new ExecError(`powershell timed out after ${opts.timeoutMs ?? 25_000}ms`, r);
if (r.code !== 0) throw new ExecError(`powershell.exe exited ${r.code}: ${(r.stderr || r.stdout).trim().slice(0, 300)}`, r);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop treating non-terminating PowerShell errors as success

When Add-Type fails—for example because constrained-language policy rejects dynamic compilation—the new prelude suppresses that failure with -ErrorAction SilentlyContinue; subsequent missing-User32 errors can continue to each action's unconditional Write-Output '{"ok": true}'. PowerShell's documented Continue behavior “displays the error message and continues executing”, so the process can exit 0 and this check still returns action_sent: true although no input was sent. Set $ErrorActionPreference = 'Stop', stop suppressing prelude failures, or explicitly validate the operation before emitting success.

Useful? React with 👍 / 👎.

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale review

The PR correctly makes Windows User32-backed actions self-contained and truthful by adding psOk/psJson error propagation, removing the ineffective bootstrap process, and fixing left_mouse_down target press logic. Tests cover key paths with a fake powershell.exe.

Findings

  • [INFO] list_windows and write_clipboard error paths are not covered by the new tests (crates/tui/plugins/computer-use/tests/win32-input.test.mjs)
    The PR description states list_windows now parses via psJson and write_clipboard now uses psOk, but the new win32-input.test.mjs only checks nonzero exit for left_mouse_down, mouse_move, and key, and never exercises list_windows or write_clipboard. A regression in either path would go unnoticed.
  • [WARNING] Fake powershell.exe may truncate stdout/stderr by calling process.exit immediately (crates/tui/plugins/computer-use/tests/win32-input.test.mjs:27)
    In the fake PowerShell helper, process.stdout.write and process.stderr.write are asynchronous when writing to a pipe, but process.exit is called immediately after. This can truncate captured output and make tests flaky. Setting process.exitCode allows Node to flush pending stdio before exiting.

Suggestions

  • crates/tui/plugins/computer-use/tests/win32-input.test.mjs:27 — Use process.exitCode instead of process.exit so pending stdout/stderr writes flush before the fake process exits, preventing flaky test output truncation.

    process.exitCode = Number(process.env.CU_FAKE_PS_EXIT || 0);
    

Assessment

The PR is correct and materially improves error truthfulness and Windows input reliability. The new tests are well-designed, though coverage could be extended to list_windows and write_clipboard. The suggested test reliability fix is minor and high-confidence.


Advisory review by Codewhale (codewhale review --pr 5903 --post, head ebb706fc2947a3391ffc62294c83663cdfb2ead4). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

}
if (process.env.CU_FAKE_PS_STDOUT) process.stdout.write(process.env.CU_FAKE_PS_STDOUT);
if (process.env.CU_FAKE_PS_STDERR) process.stderr.write(process.env.CU_FAKE_PS_STDERR);
process.exit(Number(process.env.CU_FAKE_PS_EXIT || 0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] Fake powershell.exe may truncate stdout/stderr by calling process.exit immediately

In the fake PowerShell helper, process.stdout.write and process.stderr.write are asynchronous when writing to a pipe, but process.exit is called immediately after. This can truncate captured output and make tests flaky. Setting process.exitCode allows Node to flush pending stdio before exiting.

}
if (process.env.CU_FAKE_PS_STDOUT) process.stdout.write(process.env.CU_FAKE_PS_STDOUT);
if (process.env.CU_FAKE_PS_STDERR) process.stderr.write(process.env.CU_FAKE_PS_STDERR);
process.exit(Number(process.env.CU_FAKE_PS_EXIT || 0));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Use process.exitCode instead of process.exit so pending stdout/stderr writes flush before the fake process exits, preventing flaky test output truncation.

Suggested change
process.exit(Number(process.env.CU_FAKE_PS_EXIT || 0));
process.exitCode = Number(process.env.CU_FAKE_PS_EXIT || 0);

@Hmbown
Hmbown merged commit fef7c37 into main Sep 6, 2026
37 checks passed
@Hmbown
Hmbown deleted the fix/win-cu-ps-failure-5896-v2 branch September 6, 2026 03:24
ysm827 pushed a commit to ysm827/DeepSeek-TUI that referenced this pull request Sep 6, 2026
… exec })

Follow-up to the closed Hmbown#5910, per the maintainer's invitation after the
bug itself was fixed and merged in Hmbown#5903. This PR is refactor-only: it
keeps main's merged behavior and adds a cleaner test seam.

Previously the win32 backend's PowerShell runner (ps/psOk/psJson) lived at
module scope, so backend tests had to fake powershell.exe on PATH (or run
on Windows). Move the runner helpers into create() as instance-local
functions that default to the production runner but can be overridden with
create({ exec: { run } }) — the same injection pattern the harmonyos
backend already uses.

tests/win32.test.mjs now exercises the win32 backend on any host through
the injected runner: actions run without powershell, nonzero exits reject
(fail-truthful), and targeted mouse-down is verified to move + press with
the User32 type defined in-process. Verified on macOS with no Windows
involved: node --test tests/win32.test.mjs (4/4) and
tests/backends.test.mjs (10/10, no regression).

Refs Hmbown#5903, Hmbown#5910.

Signed-off-by: EvanProgramming <evangonggyf@gmail.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.

Windows computer-use reports input success after PowerShell failure

2 participants