Repository navigation
feat(cli): add parameter aliasing, decoupled screenshots and semantic click - #76
Conversation
hyprcat
left a comment
There was a problem hiding this comment.
None of this code is macOS-specific. All of it lives in the Node launcher template in scripts/npm/build-packages.mjs, which every platform shares, plus docs. The only macOS-flavoured part is the Chinese role names (按钮 / 单元格 / 文本) in the click scoring. No Swift changes.
Main has moved on in ways that overlap most of this PR:
- Launcher rewrite (#80). Since #80, the generated launcher is a thin shim that imports
scripts/node-repl/open-computer-use-cli.mjs(ESM). TherenderLauncherbody this PR edits, withspawnAndExit,installCommandsand thecall/mcpbranches, no longer exists. This needs a port, not a rebase. - Semantic click vs
query(#73).queryfinds controls by text/role natively, with exact-then-contains matching and stable indexes (≥ 1,000,000) that click, set_value, scroll and perform_secondary_action accept. - Screenshot decoupling vs native options. Screenshot size is now handled natively:
include_screenshot: falseonget_app_state(#101),OPEN_COMPUTER_USE_ACTION_READ_BACK=0for short action results (ComputerUseService.swift:252on main), andimage.maxLongEdgePixels/image.format(2cf3eae).
Suggestion: drop semantic click and screenshot decoupling in favour of those. If parameter aliases are still wanted, add them in ComputerUseToolDispatcher as a small separate PR with tests. That is the one place where the CLI, MCP and js all pick them up.
I rendered this PR's launcher template against a fake native binary and confirmed the issues below.
Other notes
- No tests. Main has
scripts/node-repl/open-computer-use-cli.test.mjs, where launcher behaviour like this is covered.
How I checked
Tests. swift test on the head: Executed 167 tests, with 1 test skipped and 0 failures. That is just its September base, since the PR has no Swift changes. make check-docs passes.
Conflicts. git merge-tree --write-tree origin/main review/pr-76 gives CONFLICT (content) in docs/releases/feature-release-notes.md, scripts/npm/build-packages.mjs and skills/open-computer-use/SKILL.md.
Launcher harness. I rendered the PR's renderLauncher() template with a stub platform table that points at a shell script, which logs its argv and echoes MCP lines. The fixed /tmp path was swapped for a scratch path.
--- MCP: one request on stdin, then EOF
{"jsonrpc":"2.0","id":1,...,"arguments":{"app":"X","element_index":3}}
launcher exit=142 (killed by the 5 s alarm)
1792698329 argv: mcp
1792698334 mcp: stdin EOF, exiting <- only after the launcher died
--- click {"text":"Save"} against "1 button Save As…" / "3 button Save"
argv: snapshot X
argv: call click --args {"app":"X","element_index":1}
--- --calls with a semantic click second
argv: snapshot X
argv: call --calls [...{"element_index":1},{"element_index":1}]
both results: {"type":"image_saved","path":".../shot.png",...}; file contents: BBBB
--- no match {"text":"Delete account"}
argv: call click --args {"app":"X"}
| }); | ||
|
|
||
| let stdinBuffer = ""; | ||
| process.stdin.on("data", (chunk) => { |
There was a problem hiding this comment.
When the client closes stdin, the launcher never passes that on, so the native MCP server never sees EOF and the launcher never exits. With one request piped in, the launcher was still running after 5 s. The fake native server logged mcp: stdin EOF, exiting only when the launcher was killed. On main the child inherited stdio, so EOF went straight through. This also means anything the runtime does when the connection closes, such as restoring parked windows, waits until the client kills the process. At minimum this needs process.stdin.on("end", () => child.stdin.end()).
| const role = m[2]; | ||
| const rest = m[3] || ""; | ||
| const fullLine = role + " " + rest; | ||
| if (fullLine.toLowerCase().includes(targetText)) { |
There was a problem hiding this comment.
The match is a substring test on the whole line, and candidates are ranked only by role, so ties go to the first line. With 1 button Save As… and 3 button Save in the snapshot, {"text":"Save"} was sent as element_index: 1, which clicks Save As…. With labels like "Delete", a wrong pick is a real risk. query already handles this with exact-then-contains matching.
| candidates.sort((a, b) => b.score - a.score); | ||
| argsObj.element_index = candidates[0].idx; | ||
| } | ||
| } catch (err) {} |
There was a problem hiding this comment.
When nothing matches, or the snapshot throws, the catch is empty and lines 430–433 then delete text/title/role. The native side receives {"app":"X"} and reports "click requires either element_index or x/y", which hides the actual reason (no element matched).
| } catch (e) {} | ||
| } | ||
|
|
||
| const callsIdx = args.indexOf("--calls"); |
There was a problem hiding this comment.
In --calls, every semantic click is resolved before the batch starts. The fake binary logged snapshot X and only then call --calls …. So a click that follows a navigation step in the same batch is resolved against the UI as it was before the batch.
| try { | ||
| const { execFileSync } = require("node:child_process"); | ||
| const nativeBin = resolveNativeExecutable(); | ||
| const snapshotOut = execFileSync(nativeBin, ["snapshot", argsObj.app], { |
There was a problem hiding this comment.
The index comes from a separate snapshot process and is then used in another process. In MCP mode, the synchronous execFileSync also blocks the stdin handler for up to 5 s per click.
| function handleContentItem(item) { | ||
| if (item && item.type === "image" && typeof item.data === "string") { | ||
| try { | ||
| const savePath = "/tmp/ocu_last_screenshot.png"; |
There was a problem hiding this comment.
Every image is written to the same fixed path:
- In a
--callsbatch with two images, both results pointed at one file, which held only the second image (BBBB). - Because it is a fixed name in shared
/tmp,fs.writeFileSyncfollows a symlink planted there. I checked this: the write went through the symlink and overwrote the target file. - On main the format can be jpg or webp (2cf3eae), but the file is always named
.png. - It also changes the default output of
callfor every existing consumer (imagebecomesimage_saved), so the "100% backward-compatible" claim doesn't hold.
|
|
||
| const toolName = args[1]; | ||
|
|
||
| const argsIdx = args.indexOf("--args"); |
There was a problem hiding this comment.
Only --args and --calls are normalized. --args-file and --calls-file skip the aliases.
|
|
||
| ### Screenshot Decoupling in CLI | ||
|
|
||
| By default, CLI tool calls decouple multi-megabyte base64 screenshot data from stdout and write the image to `/tmp/ocu_last_screenshot.png`. This prevents terminal buffer overflow and large LLM context overhead. The stdout JSON response summarizes the image reference: |
There was a problem hiding this comment.
This documents the fixed /tmp path. It would go with the decoupling.
Summary
This PR addresses three key developer and AI Agent experience pain points identified during live GUI automation on macOS:
Parameter Aliasing Tolerance (API Drift Protection):
indexinstead ofelement_index,buttoninstead ofmouse_button, oramountinstead ofpages, triggering errors likeinvalidArguments("click requires either element_index or x/y").renderLauncher) now normalizes these common aliases before handing off to the native dispatcher, providing seamless 100% backward-compatible execution.Decoupled Screenshot Storage (Token Explosion & Buffer Overflow Prevention):
stdoutoften causes terminal buffer truncation, pipe stalls, and massive LLM context token consumption./tmp/ocu_last_screenshot.png, and outputs a lightweight image summary descriptor in JSON.--raw-imageor setOCU_RAW_IMAGE=1to retain the original inline base64 behavior.Semantic Text Click with Heuristic Role Scoring:
clicknow supports passingtext/title(and optionalrole), allowing agents to click elements by display text (e.g.{"app": "Music", "text": "播放", "role": "button"}).button,cell) higher than generic container wrappers (row,outline), resolving the targetelement_indexautomatically.Documentation & Release Notes:
skills/open-computer-use/SKILL.mdandskills/open-computer-use/references/usage.mdwith alias specifications and macOS complex control guidelines.docs/releases/feature-release-notes.md.docs/histories/2026-09/20260919-1045-cli-enhancements-aliases-decoupled-screenshots.md.Verification
index: 40,button: "left") against live native macOS apps./tmp/ocu_last_screenshot.png(verified PNG headers and image validity).--callsand MCP stdio passthrough.make check-docs(passed).