computer-use · diff
git:20260918.b7543ae to git:20260923.b2a6af5
176 added, 218 removed. Audit A to A.
---
name: computer-use
description: Control local desktop applications through Computer Use for tasks that require reading or operating app UI. Prefer purpose-built connectors, APIs, or CLIs when available.
---
## node_repl + @qwen-code/cua-sdk (Computer Use)
- Use `node_repl` (JavaScript) for all Computer Use actions.
- Do not use other technologies besides `node_repl` for computer interactions, unless specifically requested by the user (e.g. AppleScript, `osascript`, JXA, System Events, synthesized input).
- Prefer a dedicated plugin or skill when it can complete the task; use Computer Use for app interactions that are not exposed through a more specific interface.
- `node_repl` state is persistent across calls.
- For text output, use `nodeRepl.write(...)`. It takes a string; use `JSON.stringify(...)` only for textual metadata. For an observation, write its `.text` and emit its screenshots as images; do not stringify an observation or driver result containing image bytes.
- Omit `yield_time_ms` for ordinary UI calls to use the default 10-second wait. A shorter yield does not speed up the action and can add a `node_repl_wait` round. Use a shorter yield only when you need control back before completion; if a cell is still running, collect its result with `node_repl_wait` before issuing dependent actions.
## Forwarding results in Codex code mode
When calling `node_repl` through `tools.*` inside Codex's outer `functions.exec`,
forward each returned `content` block by its type. `nodeRepl.emitImage(...)`
produces an MCP image block; the outer script must pass that block to `image()`
for the model to receive an image. Use the tool name exposed by your MCP server:
```js
const result = await tools.mcp__node_repl__node_repl({ code });
for (const block of result.content ?? []) {
if (block.type === 'text') {
text(block.text);
} else if (block.type === 'image') {
image(block);
}
}
```
Here `code` is the JavaScript to run in the persistent Node REPL. Keep the
forwarding loop in the outer code-mode script, outside that `code` string.
Apply the same loop to `node_repl_wait` results: images may arrive only when a
running cell completes. Forward text blocks too, including running-cell IDs and
errors. Direct MCP tool calls do not need this outer forwarding loop.
Do not use `text(result)`, `text(block)`, or `JSON.stringify(result)` to forward
an MCP result containing images: this turns image base64 into text, consuming
context without showing the image. `image()` accepts one image block, not the
whole result, so do not use `image(result)` either.
## Bootstrap
If `node_repl` is unavailable, run:
```bash
qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.6
- npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.9
+ npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.11
```
Tell the user to restart Qwen Code, then stop. If only the SDK import is missing,
run the second command and retry.
Reuse an existing `computer` connected to the intended desktop. Otherwise import
- the `ComputerUse` API once per fresh `node_repl` session. Combine initialization
- and connected-platform discovery in one call. Linux uses the complete workflow below
- in this file. macOS and Windows also read their selected resource in that call;
- set `skillBase` to the absolute Skill base directory shown by the skill loader
- or the file you just read:
+ once per fresh `node_repl` session. The same App workflow below applies to macOS,
+ Linux and Windows; no additional platform resource is needed:
```js
globalThis.computer = await (
await import('@qwen-code/cua-sdk/computer-use')
).ComputerUse.create();
var platform = await computer.getPlatform();
- var reference = {
- macos: 'macos.md',
- windows: 'windows-linux.md',
- }[platform];
- if (!reference && platform !== 'linux') {
- throw new Error('Unsupported connected platform');
- }
nodeRepl.write(`Connected platform: ${platform}`);
- if (reference) {
- var skillBase = '/absolute/path/to/computer-use';
- nodeRepl.write(
- await (
- await import('node:fs/promises')
- ).readFile(`${skillBase}/references/${reference}`, 'utf8'),
- );
- }
```
- If the returned platform is `macos` and the task already identifies an
- unambiguous app, append its initial observation to that same initialization
- call, after printing the resource:
-
- ```js
- if (platform === 'macos') {
- var app = await computer.getApp('App named by the task');
- nodeRepl.write((await app.getState()).text);
- }
- ```
-
- Replace the example app name with the task's app. This only binds the app and
- reads its current state; `getState()` can open that app if stopped. Read both
- the returned platform workflow and initial state before any editing or input.
- If the app is unknown or ambiguous, omit this block and follow the selected
- resource's discovery steps. Do not guess an app or use the host platform.
-
- ## Select the target platform workflow
-
- Use this returned platform, not the CLI or Node host operating system. A connected
- driver may control a different machine. If the platform cannot be determined,
- resolve the reported driver/SDK error before continuing; do not guess a platform.
-
- For Linux, use the workflow below directly; no additional skill file is needed.
- For macOS or Windows, initialization reads exactly one resource. If filesystem
- imports are unavailable, use the following fallback before any UI work.
- Read exactly one resource with `read_file`, resolving its absolute path from the
- Skill base directory shown above:
-
- - `macos`: read `references/macos.md` for the App workflow and text operations.
- - `windows`: read `references/windows-linux.md` for the exact-window workflow.
-
- On macOS and Windows, read the selected resource before taking actions. Once it
- has been printed in the initialization result, do not read it again. After
- changing the connected desktop, query its platform again and follow that
- platform’s workflow. Resource files remain on the machine hosting this Skill;
- do not look for them on the controlled desktop.
-
- ## Linux Computer Use
-
- Use this workflow when `computer.getPlatform()` returns `linux`. The bootstrap
- above has already initialized `computer`. This workflow is complete in this
- `computer-use/SKILL.md` file.
-
- ### Targeting and input
+ Use the connected platform for shortcuts, not the CLI or Node host operating system.
+ When the task identifies an app, combine initialization with `computer.getApp()`
+ and its first `getState()` in the same call. Read that state before editing or input.
- Keep the observed process ID, window ID and element tokens. Input delivery is
- managed by the runtime: it chooses a semantic action where available and prepares
- the exact window's focus when native keyboard or pointer input requires it.
+ ## API surface
```ts
- type WindowTarget = { pid: number; windowId: number };
- type ElementTarget = { pid: number; windowId?: number; elementToken: string };
- type CoordinateTarget = WindowTarget & { x: number; y: number };
- type PointOrElementTarget = CoordinateTarget | ElementTarget;
- type ExactActionTarget = WindowTarget | ElementTarget;
-
+ type Point = number | { x: number; y: number };
type ComputerUse = {
- listApps(): Promise<
- Array<{
- name?: string;
- bundle_id?: string;
- pid?: number;
- running?: boolean;
- }>
- >;
- listWindows(args: { pid: number; onScreenOnly?: boolean }): Promise<
- Array<{
- window_id: number;
- title?: string;
- is_on_screen?: boolean;
- }>
+ getPlatform: () => Promise<'macos' | 'linux' | 'windows'>;
+ getApp: (nameOrIdentifierOrPath: string) => Promise<App>;
+ listApps: () => Promise<
+ Array<{ id: string; displayName: string; isRunning: boolean }>
>;
- observeWindow(
- args: WindowTarget & {
- disableDiff?: boolean;
- includeScreenshot?: boolean;
- maxTextChars?: number;
- },
- ): Promise<WindowObservation>;
- click(
- args: PointOrElementTarget & {
- button?: 'left' | 'right' | 'middle';
- count?: number;
- },
- ): Promise<object>;
- doubleClick(args: PointOrElementTarget): Promise<object>;
- rightClick(
- args: PointOrElementTarget & { modifier?: string[] },
- ): Promise<object>;
- drag(
- args: WindowTarget & {
- fromX: number;
- fromY: number;
- toX: number;
- toY: number;
- },
- ): Promise<object>;
- scroll(
- args: PointOrElementTarget & {
- direction: 'up' | 'down' | 'left' | 'right';
- amount?: number;
+ close: () => Promise<void>;
+ };
+ type App = {
+ getState: (options?: {
+ disableDiff?: boolean;
+ includeScreenshot?: boolean;
+ maxTextChars?: number;
+ }) => Promise<State>;
+ click: (
+ point: Point,
+ options?: { button?: 'left' | 'right' | 'middle'; count?: number },
+ ) => Promise<object>;
+ doubleClick: (point: Point) => Promise<object>;
+ rightClick: (
+ point: Point,
+ options?: { modifier?: string[] },
+ ) => Promise<object>;
+ setValue: (element: number, value: string) => Promise<object>;
+ performSecondaryAction: (element: number, action: string) => Promise<object>;
+ typeText: (text: string) => Promise<object>;
+ paste: (
+ text: string,
+ options?: { format?: 'text' | 'md' | 'html'; signal?: AbortSignal },
+ ) => Promise<object>;
+ selectText: (
+ element: number,
+ text: string,
+ options?: {
+ prefix?: string;
+ suffix?: string;
+ selection?: 'text' | 'cursor_before' | 'cursor_after';
+ signal?: AbortSignal;
},
- ): Promise<object>;
- pressKey(
- args: ExactActionTarget & { key: string; modifiers?: string[] },
- ): Promise<object>;
- hotkey(args: ExactActionTarget & { keys: string[] }): Promise<object>;
- typeText(args: ExactActionTarget & { text: string }): Promise<object>;
- setValue(args: ElementTarget & { value: string }): Promise<object>;
- performSecondaryAction(
- args: ElementTarget & { action: string },
- ): Promise<object>;
- close(): Promise<void>;
+ ) => Promise<object>;
+ pressKey: (
+ key: string,
+ options?: { modifiers?: string[] },
+ ) => Promise<object>;
+ hotkey: (keys: string[]) => Promise<object>;
+ scroll: (
+ point: Point,
+ options: { direction: 'up' | 'down' | 'left' | 'right'; amount?: number },
+ ) => Promise<object>;
+ drag: (options: {
+ fromX: number;
+ fromY: number;
+ toX: number;
+ toY: number;
+ }) => Promise<object>;
};
-
- type WindowObservation = {
- pid: number;
- windowId: number;
+ type State = {
+ app: string;
+ window: string;
mode: 'full' | 'diff' | 'no_change';
text: string;
- elements: Array<{
- element_token?: string;
- role?: string;
- label?: string;
- value?: unknown;
- actions?: string[];
- }>;
screenshot?: { images: Array<{ mimeType: string; dataBase64: string }> };
};
```
- ### Discover and observe
+ ## Workflow
- Filter the app named by the task, then select its intended window from the
- observed titles. If either is ambiguous, print the candidates before choosing.
- Do not assume the first window is the task window.
+ `paste` and `selectText` currently require macOS. On Linux/Windows, use the shared
+ `typeText`, `setValue`, `pressKey` and observed actions. Unsupported text methods
+ fail with `unsupported_platform`; do not retry them as window failures.
+ ### 1. Initialize
+
+ If initialization already bound the task's app and returned its state, reuse
+ that `app` and observation. Otherwise, bind the app named by the task, then read its state.
+ `getApp()` binds identity; `getState()` can open a discovered stopped app. Combine these
+ steps in one Node REPL call:
+
```js
- var apps = await computer.listApps();
- nodeRepl.write(JSON.stringify(apps.filter((app) => app.name === 'Target App')));
- // Use the matching app's observed PID.
- var windows = await computer.listWindows({
- pid: targetPid,
- onScreenOnly: true,
- });
- nodeRepl.write(JSON.stringify(windows));
- // Use the selected window's observed ID.
- var target = { pid: targetPid, windowId: selectedWindowId };
- var state = await computer.observeWindow(target);
- nodeRepl.write(state.text);
+ var app = await computer.getApp('Microsoft Excel');
+ nodeRepl.write((await app.getState()).text);
```
- Combine discovery, selection and observation in one cell when the target is
- unambiguous. If the app is unknown, print `await computer.listApps()` first.
- Reuse `computer` and the selected target across calls.
+ The app handle tracks its current window and dialog. Read the returned window
+ title to confirm the intended document. If the app is unknown or its name is
+ ambiguous, discover applications with `computer.listApps()` and use a matching
+ application `id` when it distinguishes the app. Two running instances may have
+ the same ID; retrying that ID cannot resolve the ambiguity. Ask the user to keep
+ only the intended instance open rather than guessing a target or retrying it.
- Observations default to text diffs. `state.elements` is the full current
- captured element list even when `.text` reports a diff or no change. Use tokens
- from that list; unchanged tokens remain usable across observations. After a
- capture read failure, use only tokens issued by the latest observation.
+ AX text uses short numeric IDs, such as `[37] TextField "Name"`. Use IDs from
+ the current observation for element actions. IDs can change when the app's
+ window or session changes. Disabled and static-text rows are observation-only.
- Text rows omit default enabled/unselected states and the primary click action.
- `disabled` and `selected` mark non-default states; `actions` lists secondary
- actions. Editable content appears separately from its label as `value`, including
- an empty value after clearing a field. The element list retains all actions.
- Empty layout containers and duplicate labels are omitted; adjacent static text
- may share a row. Window, dialog, list and table context, focused/selected state,
- and actionable elements remain visible. Use element tokens, not row positions.
- Linux observations omit virtual children when an app manages their lifetime
- (`managed_descendants_omitted`). The returned tree is bounded; use a screenshot
- for sheet or canvas content that is not present in the tree.
- Linux also omits hidden native menu branches (`hidden_menu_subtrees_omitted`).
- Open a menu and observe again to read its displayed commands. A collection
- timeout returns the completed portion with an incomplete-capture indication;
- it does not establish that missing controls are absent.
+ For token efficiency, the accessibility tree will be returned
+ as a diff when appropriate. Prefer this default diff output. A full state
+ replaces the previous state; a diff updates it; no-change preserves it. If you
+ need a full replacement, use `disableDiff: true` only when the previous state
+ is unavailable or no longer useful. Do not disregard the text and then assume
+ that a subsequent diff will reproduce the information you skipped.
- Text is limited to 12,000 characters by default. Filter the elements for controls
- you need, or request `disableDiff: true` with a larger `maxTextChars` (minimum
- 512). An omitted row does not prove absence. If you discarded earlier text,
- request a full tree the next time you read accessibility text.
+ Returned text defaults to at most 12,000 characters. Set `maxTextChars` (minimum 512) to adjust the limit. A truncation notice means some captured rows were
+ omitted; request `app.getState({ disableDiff: true, maxTextChars: 24000 })` when
+ you need more full text. An omitted row does not prove an element is absent.
+ Traversal-limited captures cover only the captured nodes: identical captures
+ can return no-change, while changes return full captured state. Use current
+ captured IDs; after a read failure, use only IDs from the latest observation.
- ### Act and verify
+ ### 2. Actions using app
- Prefer current element tokens. Use screenshot coordinates for controls whose
- accessibility actions or text are unavailable. Coordinates are pixels in the
- PNG for that exact window, measured from its top-left corner. Accessibility
- `frame` values are screen-space logical points; do not use them as PNG pixels.
+ After performing one or more UI actions, call `app.getState()` before deciding
+ what to do next. Batch actions whose target remains the same, then print only
+ the state needed for the next decision:
- Batch only actions whose target remains the same, then observe:
+ ```js
+ await app.click(37);
+ await app.hotkey([platform === 'macos' ? 'super' : 'ctrl', 'a']);
+ await app.typeText('hello');
+ await app.pressKey('Return');
+ nodeRepl.write((await app.getState()).text);
+ ```
+ Use the actual ID from your observation; `37` is only an example.
+
+ An observation is a decision boundary. When the current state already identifies
+ the controls and the next actions are known, combine those actions and saving
+ in the same call. Read state after the batch. End the batch at a new dialog,
+ menu, changed target or uncertain result; use that state before choosing the
+ next action. Do not split a known sequence merely to put each action in its own
+ call.
+
+ - Prefer element IDs to coordinates. `setValue(id, value)` changes a writable control, and `performSecondaryAction(id, action)` invokes a secondary action listed for that element. Use an observed action name rather than guessing.
+ - When an action opens or closes a dialog, sheet or menu, end the batch and call `app.getState()` to read the new window and IDs before continuing.
+ - `No open application window.` means the app is still running without a document window. If closing it completed the task, finish instead of retrying actions; otherwise open the intended file or window first.
+ - An action error can occur after the UI already changed. Read state before deciding whether to retry. Partial, unconfirmed or cancelled actions must not be blindly repeated.
+ - Coordinate actions use pixels in this app's current screenshot, with `(0, 0)` at its top-left. Every App observation refreshes that frame internally. Request `includeScreenshot: true` when you need to inspect the image, especially after a window change. Do not infer coordinates from another window or desktop screenshot.
+ - `pressKey` sends one key, optionally with modifiers. `hotkey` sends a combination such as `['super', 's']`. Use the connected platform's appropriate shortcut: macOS generally uses `super`; Linux/Windows generally use `ctrl`.
+ - App input manages any required activation internally and restores the previous focus, unless the user has moved it elsewhere. If the platform cannot confirm the target or restore focus, the action reports an error. There is no delivery-mode choice and no automatic replay after an uncertain result.
+ - On Linux, some compositor sessions cannot confirm an exact App target. An `app_window_unavailable` result can mean the session does not support this workflow; ask the user to use a supported desktop session instead of retrying input.
+ - Literal `\n` or `\r` in `typeText` sends Return. In a composer or form this may submit rather than insert a newline.
+ - If AX is incomplete or does not explain the interface, request a screenshot and inspect it. Only currently captured actionable IDs can be used for element actions.
+
+ ## Paste and select text
+
+ These App methods are available on macOS only. Read current state first and use
+ an observed element ID for selection.
+
```js
- await computer.click({ pid: target.pid, elementToken });
- await computer.typeText({ ...target, text: 'hello' });
- nodeRepl.write((await computer.observeWindow(target)).text);
+ await app.selectText(37, 'draft', { prefix: 'Status: ' });
+ nodeRepl.write((await app.getState()).text);
```
- When the controls and next actions are already known, combine the actions and
- saving in the same cell, then observe. A new observation is a decision boundary;
- do not split a known sequence into one call per action.
+ Use a real ID and text from your observation; the example assumes that the
+ selected element contains exactly one matching `draft` immediately after
+ `Status: `.
- End the batch when opening a dialog or menu. For a dialog, list windows and
- observe the matching window before typing. For a menu in the same window,
- refresh its observation and use current menu tokens. Never guess a window ID.
+ After confirming the selection, replace it with plain text:
- The runtime may retry focus preparation before sending input. An action error,
- cancellation, `suspected_noop` or `unverifiable` result does not establish that
- nothing happened. Observe the current windows and state before deciding to
- repeat an action. Do not blindly replay the previous batch or token.
+ ```js
+ await app.paste('ready');
+ nodeRepl.write((await app.getState()).text);
+ ```
- `pressKey` accepts one key and optional modifiers; `hotkey` accepts a combination
- such as `{ ...target, keys: ['ctrl', 'c'] }`. `performSecondaryAction` requires an
- action actually exposed by that element. Newlines in `typeText` can submit forms
- or send messages instead of inserting a line break.
+ - `selectText(element, text)` selects one exact, case-sensitive match. Optional
+ `prefix` and `suffix` must be immediately adjacent to that match. Missing or
+ ambiguous matches fail; provide enough observed context to identify one.
+ - `selection` defaults to `'text'`. Use `'cursor_before'` or `'cursor_after'` to
+ place the insertion point at a match boundary instead of selecting the text.
+ The element must support a writable text selection; static labels may not.
+ - `paste(text)` defaults to plain text. Use `{ format: 'md' }` for Markdown or
+ `{ format: 'html' }` for HTML; the receiving app determines which supplied
+ format it accepts. Use plain text when markup should remain literal.
+ - Paste uses the clipboard temporarily. It restores the previous clipboard only
+ while it still owns that transaction; a newer external clipboard change is
+ preserved.
+ - Both methods accept an optional `signal`. A completed dispatch, error or
+ cancellation does not by itself prove what changed. Observe state before
+ deciding whether to retry; do not blindly repeat unconfirmed text operations.
- ### Read screenshots
+ ## Reading screenshots
- Request the screenshot explicitly, print only the observation's text, and emit
- each image:
+ Every App observation captures the current screenshot internally, independent
+ of whether AX returns full state, a diff or no-change. The default return omits
+ the image. `includeScreenshot: true` exposes it when visual inspection is needed.
+ Prefer the text-only default when it identifies the controls and confirms the
+ requested change. Request an image to resolve missing or ambiguous information,
+ choose coordinates or verify an appearance that AX does not describe.
```js
- var state = await computer.observeWindow({
- ...target,
- includeScreenshot: true,
- });
+ var state = await app.getState({ includeScreenshot: true });
nodeRepl.write(state.text);
for (const image of state.screenshot?.images ?? []) {
await nodeRepl.emitImage(`data:${image.mimeType};base64,${image.dataBase64}`);
}
```
- Do not stringify the whole observation or a raw driver result containing image
- bytes. In outer code mode, also forward each returned image block with `image()`
- as shown in the shared entrypoint, including images from `node_repl_wait`.
+ Include connection cleanup at the end of the call that emits the final
+ verification. Inspect that result before reporting success; reconnect and
+ continue if it reveals unfinished work. A separate cleanup-only model turn is
+ unnecessary:
- Include `await computer.close()` and clearing `globalThis.computer` at the end
- of the cell that emits final verification. Inspect that result before reporting
- success; reconnect if it reveals unfinished work. Avoid a separate cleanup-only
- call. Reset the REPL only when no other persistent state is needed.
+ ```js
+ await computer.close();
+ globalThis.computer = undefined;
+ ```
+
+ Reset the Node REPL only when no other persistent state is needed.