Skip to content

Commit 0c10b39

Browse files
Merge pull request #27 from Factory-AI/ainesh/cli-900-desktop-control
2 parents c132162 + 99720f4 commit 0c10b39

10 files changed

Lines changed: 316 additions & 10 deletions

File tree

.skillsrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
droid-control/skills/agent-browser
55
droid-control/skills/capture
66
droid-control/skills/compose
7+
droid-control/skills/desktop-control
78
droid-control/skills/droid-cli
89
droid-control/skills/droid-control
910
droid-control/skills/pty-capture
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "droid-control",
3-
"description": "Terminal and browser automation for testing, demos, QA, and computer-use tasks",
3+
"description": "Terminal, browser, and native desktop automation for testing, demos, QA, and computer-use tasks",
44
"version": "1.0.0"
55
}

plugins/droid-control/ARCHITECTURE.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ This is the first guardrail against agent drift. The droid does not start with "
3636

3737
| Route | Question | Examples |
3838
|---|---|---|
39-
| **Target** | What are we driving? | Droid CLI, other terminal TUI, web/Electron app, raw PTY bytes |
39+
| **Target** | What are we driving? | Droid CLI, other terminal TUI, web/Electron app, native desktop app, raw PTY bytes |
4040
| **Stage** | What does the workflow need? | capture, compose, verify |
4141
| **Artifact** | Does compose need polish tools? | showcase presets, effects, keystroke overlays |
4242

@@ -48,7 +48,7 @@ Each atom skill is a self-contained surface the droid reads at a specific point
4848

4949
| Atom type | Skills | Responsibility |
5050
|---|---|---|
51-
| Driver atoms | `tuistory`, `true-input`, `agent-browser` | How to drive a class of environment. |
51+
| Driver atoms | `tuistory`, `true-input`, `agent-browser`, `desktop-control` | How to drive a class of environment. |
5252
| Target atoms | `droid-cli`, `pty-capture` | Target-specific shortcuts, launch rules, and byte-capture patterns. |
5353
| Stage atoms | `capture`, `compose`, `verify` | Lifecycle phases with explicit inputs and outputs. |
5454
| Polish atom | `showcase` | Visual presets and cinematic layer guidance. |
@@ -119,7 +119,7 @@ Terminal workflows use `bin/tctl` as the only launch/control boundary. It hides
119119

120120
`tctl` also enforces Droid CLI launch invariants. `droid-dev` sessions must provide `--repo-root`, which lets `tctl` set `DROID_DEV_REPO_ROOT` and record provenance for the captured branch and commit.
121121

122-
Browser and Electron workflows intentionally do **not** go through `tctl`; they use `agent-browser`, whose persistent Playwright-backed daemon is the right control boundary for DOM snapshots, screenshots, and CDP-connected apps.
122+
Browser/Electron and native-desktop workflows intentionally do **not** go through `tctl`. They have their own control boundaries: `agent-browser`'s persistent Playwright daemon for DOM snapshots, screenshots, and CDP-connected apps; `cua-driver`'s daemon for accessibility trees and per-`(pid, window_id)` element caches on desktop GUIs.
123123

124124
## Video composition
125125

@@ -161,6 +161,9 @@ skills/true-input/platforms/macos.md
161161
skills/pty-capture/platforms/linux.md
162162
skills/pty-capture/platforms/windows.md
163163
skills/pty-capture/platforms/macos.md
164+
skills/desktop-control/platforms/linux.md
165+
skills/desktop-control/platforms/windows.md
166+
skills/desktop-control/platforms/macos.md
164167
```
165168

166169
A Linux droid reads Linux Wayland instructions. A Windows VM byte-capture task reads Windows KVM instructions. The system does not rely on the droid to skim irrelevant sections correctly.

plugins/droid-control/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ The `render-showcase.sh` helper owns the full pipeline: `.cast` conversion via `
8989
| true-input | Windows (KVM) | `libvirt`, `qemu`, KVM VM with SSH |
9090
| true-input | macOS (QEMU) | `qemu`, `socat`, macOS VM with SSH |
9191
| agent-browser | All | `agent-browser` |
92+
| desktop-control | All | `cua-driver` |
9293
| compose | All | `ffmpeg`, `ffprobe`, `agg` |
9394
| showcase | All | Node.js (>= 18), Chrome/Chromium |
9495

@@ -98,7 +99,8 @@ pip install asciinema # terminal recording
9899
cargo install --git https://github.com/asciinema/agg # .cast -> .gif converter
99100
sudo apt-get install -y ffmpeg # video processing
100101
agent-browser install # browser automation (downloads Chromium)
102+
curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash # native desktop GUI automation
101103
cd plugins/droid-control/remotion && npm install # Remotion video rendering
102104
```
103105

104-
Only install what you need for your use case. Terminal demos need tuistory, asciinema, agg, and ffmpeg. Web/Electron automation just needs agent-browser.
106+
Only install what you need for your use case. Terminal demos need tuistory, asciinema, agg, and ffmpeg. Web/Electron automation just needs agent-browser. Native desktop GUI automation just needs cua-driver.

plugins/droid-control/skills/capture/SKILL.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ user-invocable: false
88

99
The orchestrator routed you here. This atom owns the full recording lifecycle: launch a target, execute an interaction script, collect raw outputs.
1010

11-
You should already have a **driver atom** loaded (tuistory, true-input, or agent-browser) and optionally a **target atom** (droid-cli). This atom layers the recording discipline on top.
11+
You should already have a **driver atom** loaded (tuistory, true-input, agent-browser, or desktop-control) and optionally a **target atom** (droid-cli). This atom layers the recording discipline on top.
1212

1313
## Inputs
1414

@@ -28,7 +28,7 @@ Before recording anything:
2828
- Terminal size is consistent across all sessions (`--cols 120 --rows 36`)
2929
- **Browser viewport size matches the composition layout** (see "Browser viewport sizing" below) — mismatched aspects letterbox in the final video
3030
- Branch/worktree paths and env vars are correct
31-
- Recording format matches the driver: `.cast` for tuistory, `.mp4` for true-input, screenshots for agent-browser
31+
- Recording format matches the driver: `.cast` for tuistory, `.mp4` for true-input, screenshots for agent-browser, window PNGs / `recording.mp4` for desktop-control
3232
- If comparing branches, both sessions use identical terminal / viewport dimensions and launch parameters
3333
- For `droid-dev` captures, `--repo-root` is **mandatory**`tctl` will refuse to launch without it
3434
- **Color env vars are set** (see below)
@@ -137,6 +137,7 @@ Before handing off, confirm every expected output file exists and is non-empty:
137137
| Visual rendering | Screenshots: `$TCTL -s <name> screenshot -o /tmp/proof-N.png` |
138138
| Keyboard encoding | PTY bytes: `${DROID_PLUGIN_ROOT}/scripts/capture-terminal-bytes.py --backend <terminal> --combo <keys>` |
139139
| Web/Electron | Screenshots: `agent-browser screenshot --annotate /tmp/proof-N.png` |
140+
| Native desktop GUI | Window screenshots + AX trees: `cua-driver get_window_state '{...}' --screenshot-out-file ${RUN_DIR}/proof-N.png`; video via `cua-driver recording start/stop` |
140141
| Before/after | Run the same sequence on both branches at the same capture points |
141142

142143
## Outputs
@@ -148,7 +149,7 @@ Hand these to the **compose** stage:
148149
- clips: [/tmp/before.cast, /tmp/after.cast]
149150
- screenshots: [/tmp/proof-1.png, /tmp/proof-2.png]
150151
- keys: /tmp/keys.tsv (if keystroke logging was requested)
151-
- driver: tuistory | true-input | agent-browser
152+
- driver: tuistory | true-input | agent-browser | desktop-control
152153
- terminal_size: 120x36 # for tuistory / true-input
153154
- viewport: 960x1000 # for agent-browser; report so compose knows the clip aspect
154155
```
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
name: desktop-control
3+
description: Background knowledge for droid-control workflows -- not invoked directly. Desktop-control driver mechanics for native GUI app automation via trycua cua-driver.
4+
user-invocable: false
5+
---
6+
7+
# Desktop-Control Driver
8+
9+
The orchestrator routed you here. Use these mechanics to execute your plan.
10+
11+
Drive native desktop GUI apps through upstream [trycua/cua](https://github.com/trycua/cua) `cua-driver`: enumerate apps and windows, snapshot accessibility trees, click/type/scroll by `element_index` or pixel coordinates, and verify by re-snapshot -- all without bringing the target to the foreground.
12+
13+
## When to use
14+
15+
- Automating a native desktop app (Finder, Notepad, System Settings, native editors)
16+
- Driving native dialogs and security/permission sheets that no DOM or PTY can reach
17+
- Visual QA of native UI: per-window screenshots, accessibility-tree assertions
18+
19+
If the target is a terminal TUI, use **tuistory** or **true-input**. If it is a web page or an Electron app, use **agent-browser** -- CDP beats accessibility trees for anything Chromium-based.
20+
21+
## Platform support
22+
23+
| Platform | Upstream tier | Read |
24+
|---|---|---|
25+
| macOS | Production | [platforms/macos.md](platforms/macos.md) |
26+
| Windows | Production | [platforms/windows.md](platforms/windows.md) |
27+
| Linux | Pre-release (real caveats) | [platforms/linux.md](platforms/linux.md) |
28+
29+
**Read the platform file for your target OS.** Each contains permissions, daemon launch, and platform-specific patterns and failure modes.
30+
31+
## Prerequisites
32+
33+
```bash
34+
# one-time install: per-user, no sudo/admin
35+
curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash
36+
# Windows (PowerShell):
37+
# irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex
38+
39+
cua-driver doctor # platform probes: permissions, daemon, accessibility plumbing
40+
cua-driver skills install # fetch the upstream skill pack to ~/.cua-driver/skills/cua-driver
41+
```
42+
43+
The upstream pack (`~/.cua-driver/skills/cua-driver/SKILL.md` + your platform's doc) is the deep reference -- full tool surface, window-state behavior matrix, forbidden-command lists -- and it updates with the binary. **Read it before any nontrivial workflow.** This atom owns the droid-control integration: routing, run isolation, delegation, evidence handoff.
44+
45+
## Daemon lifecycle
46+
47+
`element_index` workflows **require the daemon**. Without it each CLI invocation is a fresh process and the per-`(pid, window_id)` element cache dies between calls.
48+
49+
```bash
50+
cua-driver serve # start the daemon (macOS needs the LaunchServices form -- see platforms/macos.md)
51+
cua-driver status # daemon + socket health
52+
cua-driver stop
53+
```
54+
55+
Permissions are checked and granted through the driver, not by hand-editing system settings (macOS-only gate; a no-op surface on Windows/Linux):
56+
57+
```bash
58+
cua-driver permissions status # read-only; answers via the running daemon
59+
cua-driver permissions grant # attributed prompt flow -- the correct way to grant
60+
```
61+
62+
## Core loop
63+
64+
Tool names are `snake_case` and invoked directly: `cua-driver <tool> '<json>'`. (`cua-driver call <tool>` is legacy; do not use it.) `cua-driver list-tools` for the inventory, `cua-driver describe <tool>` for any schema.
65+
66+
Every workflow is Discover -> Observe -> Act -> Verify against an explicit `(pid, window_id)`:
67+
68+
```bash
69+
cua-driver launch_app '{"name":"TextEdit"}'
70+
# -> {pid: 844, windows: [{window_id: 10725, ...}]} # list_windows only needed for long-lived pids
71+
cua-driver get_window_state '{"pid":844,"window_id":10725}' --screenshot-out-file "${RUN_DIR}/before.png"
72+
cua-driver click '{"pid":844,"window_id":10725,"element_index":14,"session":"'"${RUN_ID}"'-desktop"}'
73+
cua-driver get_window_state '{"pid":844,"window_id":10725}' --screenshot-out-file "${RUN_DIR}/after.png"
74+
```
75+
76+
**Snapshot before AND after every action.** The pre-action `get_window_state` resolves the `element_index` you are about to use -- indices are per-snapshot, per `(pid, window_id)`, and stale ones fail with `No cached AX state`. The post-action snapshot is the evidence the action landed; without it a silent no-op looks like success.
77+
78+
Addressing-mode preference:
79+
80+
1. **`element_index`** (default) -- semantic, works on hidden and backgrounded windows, no foreground change.
81+
2. **Pixel** `click '{"pid":N,"window_id":W,"x":X,"y":Y}'` -- for surfaces the tree does not reach (canvases, custom-drawn controls). Coordinates are window-local screenshot pixels, top-left origin.
82+
3. **Keyboard** (`press_key`, `hotkey`) and platform fallbacks -- last resort; see the platform files.
83+
84+
## Run isolation (ground rule 5 -> cua sessions)
85+
86+
cua sessions are the desktop equivalent of `tctl` session prefixes: a session owns its agent cursor, config overrides, and recording scope. Declare one per run, derived from the workflow's `RUN_ID`, and pass it on every action:
87+
88+
```bash
89+
cua-driver start_session '{"session":"'"${RUN_ID}"'-desktop"}'
90+
# ... every action carries "session":"${RUN_ID}-desktop" ...
91+
cua-driver end_session '{"session":"'"${RUN_ID}"'-desktop"}'
92+
```
93+
94+
Parallel workers each declare their **own session** and pass `creates_new_application_instance: true` to `launch_app` so each gets its own window. The element cache is keyed on `(pid, window_id)` and the cursor on `session`, so isolated workers cannot collide.
95+
96+
## Delegation
97+
98+
`cua-driver` is on PATH -- workers need no `${DROID_PLUGIN_ROOT}` resolution. As with the other drivers, give capture workers **exact commands** with the parent's run scope baked in:
99+
100+
```
101+
Task prompt for a desktop capture worker:
102+
"Run these commands in order. Report screenshot paths and any errors.
103+
1. cua-driver start_session '{"session":"1712345678-42-notepad"}'
104+
2. cua-driver launch_app '{"name":"Notepad","creates_new_application_instance":true}'
105+
-> note the returned pid and window_id
106+
3. cua-driver get_window_state '{"pid":<pid>,"window_id":<wid>}' --screenshot-out-file /tmp/droid-run-1712345678-42-xxxx/before.png
107+
4. cua-driver type_text '{"pid":<pid>,"window_id":<wid>,"element_index":<text-area>,"text":"hello","session":"1712345678-42-notepad"}'
108+
5. cua-driver get_window_state '{"pid":<pid>,"window_id":<wid>}' --screenshot-out-file /tmp/droid-run-1712345678-42-xxxx/after.png
109+
6. cua-driver end_session '{"session":"1712345678-42-notepad"}'"
110+
```
111+
112+
## Evidence handoff
113+
114+
| Proof type | How to capture |
115+
|---|---|
116+
| Window state | `get_window_state ... --screenshot-out-file ${RUN_DIR}/proof-N.png` (also keeps the PNG out of the tool response) |
117+
| Full display | `cua-driver screenshot '{"out_file":"'"${RUN_DIR}"'/screen.png"}'` |
118+
| Semantic assertions | `tree_markdown` from `get_window_state` (filter with `"query":"..."`) |
119+
| Video | `cua-driver recording start` / `recording stop` -> session-scoped `recording.mp4` |
120+
121+
Hand PNG/mp4 paths to **compose** / **verify** like any other driver output. Keep raw tool output alongside screenshots whenever GUI behavior is the thing under test.
122+
123+
## Critical rules
124+
125+
1. **Never change the user's frontmost app.** If a command says activate, foreground, raise, or make key -- stop; the per-pid event paths exist precisely so you do not need it. Platform forbidden-lists live in the upstream pack.
126+
2. **Re-snapshot after every action and report what you observed**, not what you intended. An unchanged tree after an action is a finding, not a formality.
127+
3. **Destructive actions need explicit user intent.** Do not delete files, send messages, or submit forms unless the workflow asked for exactly that.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Desktop-Control: Linux
2+
3+
cua-driver on Linux enumerates windows via **X11**, walks semantic trees via **AT-SPI**, and injects input via **XSendEvent** (synthetic events targeted at a window XID -- no focus change, nothing leaks to the user's focused app). Upstream calls this tier pre-release, and it shows: the lifecycle (install, daemon, doctor, sessions, one-shot CLI), window discovery, and per-window screenshots are solid; Wayland-native enumeration, AT-SPI tree quality, and input delivery are not. Plan workflows around the reliable half.
4+
5+
## Install and daemon
6+
7+
Same installer and lifecycle as everywhere else (no sudo, `~/.cua-driver`):
8+
9+
```bash
10+
cua-driver doctor # trustworthy probes: catches missing DISPLAY, verifies X11 + AT-SPI before you waste a run
11+
cua-driver serve # required for element_index workflows
12+
cua-driver status
13+
```
14+
15+
`cua-driver permissions` is a no-op surface on Linux.
16+
17+
## The Wayland boundary
18+
19+
Window enumeration is **X11-only**. On a modern Plasma/GNOME Wayland desktop, native-Wayland windows are invisible to `list_windows` -- which is most windows.
20+
21+
- Targets running under **Xwayland** (or a plain X11 session) enumerate and screenshot fine.
22+
- To drive an app that defaults to native Wayland, force its X11 backend at launch where the toolkit allows it: `QT_QPA_PLATFORM=xcb` (Qt), `GDK_BACKEND=x11` (GTK), `--ozone-platform=x11` (Chromium/Electron).
23+
- If the target cannot be put on X11, desktop-control cannot see it -- fall back to **agent-browser** (web/Electron) or **true-input** (terminal emulators).
24+
25+
## Semantic layer (AT-SPI) reliability
26+
27+
AT-SPI trees can collapse: the registry's `GetChildren` may time out, and Qt apps can render as a single root node even with `QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1`. When `get_window_state` returns a near-empty tree:
28+
29+
```bash
30+
cua-driver config set capture_mode vision # screenshot-only snapshots
31+
```
32+
33+
and work the pixel path (`click '{"pid":N,"window_id":W,"x":X,"y":Y}'`) against the returned PNG. Don't burn turns re-snapshotting hoping the tree fills in -- on this tier, pixel-first is a legitimate default.
34+
35+
## The toolkit boundary: synthetic input is silently dropped by Qt and GTK4
36+
37+
XSendEvent marks events with the `send_event` flag, and major toolkits **ignore flagged input entirely**. Verified on v0.5.1: Qt apps (kcalc) and GTK4 apps (zenity) no-op on *every* action -- pixel clicks, `press_key`, `type_text` -- while the driver reports success. There is no error to catch; only the post-action snapshot reveals it.
38+
39+
Practical consequence: the Act stage only works against apps that honor synthetic events (verified: winit-based apps like alacritty; generally simpler/older X11 toolkits). **Probe before committing to a workflow**: send one cheap keystroke, re-snapshot, and check it rendered. If the target ignores synthetic input, desktop-control cannot act on it on this tier -- Observe (screenshots, window enumeration) still works, but route the interaction through **agent-browser** (web/Electron) or **true-input** (terminal) instead.
40+
41+
## Text input is lossy even where it lands
42+
43+
In apps that do accept synthetic input, typing drops and mangles characters: shifted symbols can inject as their unshifted key (`*` arriving as `8`), trailing characters get dropped (verified: `type_text "echo ok42"` rendered `echo ok4`), and `type_text_chars` with generous per-char delays still loses keystrokes. `hotkey` chords (including paste shortcuts) and middle-click paste do **not** land reliably, so the clipboard is not a workaround here.
44+
45+
What works: short bursts plus verification. After every `type_text`, re-snapshot, compare the rendered text against what you sent, and repair the diff (`press_key` backspace, retype the missing tail). On Linux the post-action screenshot is not a formality -- it is the only way to know what actually arrived.
46+
47+
## Failure modes
48+
49+
| Symptom | Fix |
50+
|---|---|
51+
| Expected window missing from `list_windows` | Native-Wayland target -- relaunch it on the X11 backend (`QT_QPA_PLATFORM=xcb` / `GDK_BACKEND=x11` / `--ozone-platform=x11`) |
52+
| Tree is a single root node / AT-SPI timeouts | `capture_mode vision` + pixel actions |
53+
| Every action "succeeds" but nothing changes | Toolkit drops `send_event` input (Qt, GTK4) -- target is unreachable on this tier; use agent-browser or true-input for the interaction |
54+
| Typed text arrives mangled or truncated | Verify-and-repair loop: re-snapshot, diff rendered text, backspace + retype the tail |
55+
| `doctor` reports no DISPLAY | Run from the graphical session (or export the session's `DISPLAY`/`XAUTHORITY`), not a bare TTY/SSH context |
56+
57+
Deep mechanics live in the upstream pack: `~/.cua-driver/skills/cua-driver/LINUX.md`.

0 commit comments

Comments
 (0)