Skip to content

Commit 6ddfbf2

Browse files
authored
feat: add agent-friendly simulator CLI controls
Adds project default simulator selection, agent-oriented CLI aliases, unified simulator actions, native-AX-first query paths, interactive AX pruning, benchmark coverage, and reliability fixes for transient AX snapshots.
1 parent e6591bc commit 6ddfbf2

43 files changed

Lines changed: 10573 additions & 5889 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Private simulator behavior is implemented locally in:
7979

8080
The current repo uses the private boot path, private display bridge, and private accessibility translation bridge directly. The browser streams frames from that bridge, injects touch and keyboard events through the same native session layer, inspects accessibility through `AccessibilityPlatformTranslation`, and renders device chrome from `cli/XCWChromeRenderer.*`.
8181
CoreSimulator service contexts resolve the active developer directory from `DEVELOPER_DIR`, then `xcode-select -p`, then `/Applications/Xcode.app/Contents/Developer`. The display bridge prefers direct CoreSimulator screen IOSurface callbacks and activates the SimulatorKit offscreen renderable view only if direct callbacks are unavailable.
82-
Accessibility recovery may use simulator launchctl UIKit application state plus hit-tested translations to recover candidate foreground pids; the returned tree must still be rooted at tokenized `AXPTranslator` application objects, because `translationApplicationObjectForPid:` can omit the bridge delegate token after private display lifecycle changes. Full-tree snapshots merge those recovered roots with the private frontmost application translation. When multiple candidate application roots are discovered, serialize all of them in preferred order: non-extension app roots first, then largest translated roots, with `.appex`/PlugIns processes de-prioritized so SpringBoard and Safari app roots stay primary while widgets and WebContent roots remain debuggable. Widget renderer extension roots may report local frames; normalize those roots and children against matching SpringBoard widget placeholder frames before returning the snapshot.
82+
Accessibility recovery may use simulator launchctl UIKit application state plus hit-tested translations to recover candidate foreground pids; the returned tree must still be rooted at tokenized `AXPTranslator` application objects, because `translationApplicationObjectForPid:` can omit the bridge delegate token after private display lifecycle changes. Full-tree snapshots merge those recovered roots with the private frontmost application translation. Shallow snapshots with `maxDepth <= 2` use the tokenized frontmost application translation directly when it is available, and only run the expensive recovery sweep if frontmost lookup fails, so agent-oriented describe loops avoid launchctl and hit-test recovery overhead. Interactive-only snapshots also prune non-actionable native AX leaves during Objective-C serialization before the Rust-side compacting pass; keep this native pruning conservative so selector taps still retain actionable rows plus their ancestors. When multiple candidate application roots are discovered, serialize all of them in preferred order: non-extension app roots first, then largest translated roots, with `.appex`/PlugIns processes de-prioritized so SpringBoard and Safari app roots stay primary while widgets and WebContent roots remain debuggable. Widget renderer extension roots may report local frames; normalize those roots and children against matching SpringBoard widget placeholder frames before returning the snapshot.
8383
Physical chrome button support uses DeviceKit `chrome.json` input geometry for browser hit targets. Volume, action, mute, Apple Watch digital crown, Watch side button, and Watch left-side button dispatch through `IndigoHIDMessageForHIDArbitrary` with consumer/telephony/vendor HID usage pairs from the device chrome metadata; home, lock, and app-switcher remain on the existing SimulatorKit button paths. Apple Watch Digital Crown rotation dispatches through `IndigoHIDMessageForDigitalCrownEvent` when SimulatorKit exposes it, with `IndigoHIDMessageForScrollEvent(..., target=0x34)` as the fallback. tvOS simulators do not support direct screen touch; browser/API tap maps to Enter, swipe maps to arrow keys, and the native bridge rejects tvOS touch packets before they reach guest `SimulatorHID`. watchOS/tvOS skip dynamic pointer/mouse service warm-up because those guest runtimes abort on unsupported virtual services. Apple TV and Apple Watch simulators are fixed-orientation devices, so client and server rotation paths must not expose or dispatch device rotation for those families.
8484
Two-point multi-touch dispatch prefers the current SimulatorKit/Indigo packet constructor and falls back to SimDeck's manual Indigo packet adapter. On Xcode 26 SimulatorKit, the constructor expects pixel-space points and stable two-finger movement requires sending `LeftMouseDown` for both `began` and `moved`, then `LeftMouseUp` for `ended`/`cancelled`; using `LeftMouseDragged` for multi-touch moves only advances one contact in UIKit. Do not coalesce multi-touch move packets in the WebSocket or WebRTC control paths, because gesture recognizers need the intermediate two-contact samples.
8585
WebKit inspection uses the simulator `webinspectord` Unix socket named `com.apple.webinspectord_sim.socket` and WebKit's binary-plist Remote Inspector selectors. It lists only WebKit content that the runtime exposes as inspectable. For app-owned `WKWebView` on iOS 16.4 and newer, the app must set `isInspectable = true`.
@@ -137,32 +137,43 @@ Useful direct commands:
137137

138138
```sh
139139
./build/simdeck list
140+
./build/simdeck use <udid>
140141
./build/simdeck boot <udid>
141-
./build/simdeck shutdown <udid>
142-
./build/simdeck erase <udid>
143-
./build/simdeck install <udid> /path/to/App.app
144-
./build/simdeck uninstall <udid> com.example.App
145-
./build/simdeck open-url <udid> https://example.com
146-
./build/simdeck launch <udid> com.apple.Preferences
147-
./build/simdeck pasteboard set <udid> "hello"
148-
./build/simdeck pasteboard get <udid>
149-
./build/simdeck screenshot <udid> --output screen.png
150-
./build/simdeck screenshot <udid> --with-bezel --output screen-bezel.png
151-
./build/simdeck record <udid> --seconds 5 --output screen-recording.mp4
152-
./build/simdeck describe <udid>
153-
./build/simdeck tap <udid> 120 240
154-
./build/simdeck tap <udid> --label "Continue" --wait-timeout-ms 5000
155-
./build/simdeck swipe <udid> 200 700 200 200
156-
./build/simdeck gesture <udid> scroll-down
157-
./build/simdeck pinch <udid> --start-distance 160 --end-distance 80
158-
./build/simdeck rotate-gesture <udid> --radius 100 --degrees 90
159-
./build/simdeck key-sequence <udid> --keycodes h,e,l,l,o
160-
./build/simdeck key-combo <udid> --modifiers cmd --key a
161-
./build/simdeck type <udid> "hello"
162-
./build/simdeck button <udid> lock --duration-ms 1000
163-
./build/simdeck home <udid>
142+
./build/simdeck shutdown
143+
./build/simdeck erase
144+
./build/simdeck install /path/to/App.app
145+
./build/simdeck uninstall com.example.App
146+
./build/simdeck open-url https://example.com
147+
./build/simdeck launch com.apple.Preferences
148+
./build/simdeck pasteboard set "hello"
149+
./build/simdeck pasteboard get
150+
./build/simdeck screenshot --output screen.png
151+
./build/simdeck screenshot --with-bezel --output screen-bezel.png
152+
./build/simdeck record --seconds 5 --output screen-recording.mp4
153+
./build/simdeck describe --format agent --max-depth 4 -i
154+
./build/simdeck wait-for --label "Welcome" --timeout-ms 5000
155+
./build/simdeck tap 120 240
156+
./build/simdeck tap --label "Continue" --wait-timeout-ms 5000
157+
./build/simdeck tap "Continue"
158+
./build/simdeck tap --id com.apple.settings.screenTime --expect-id BackButton
159+
./build/simdeck back
160+
./build/simdeck swipe 200 700 200 200
161+
./build/simdeck gesture scroll-down
162+
./build/simdeck pinch --start-distance 160 --end-distance 80
163+
./build/simdeck rotate-gesture --radius 100 --degrees 90
164+
./build/simdeck key-sequence --keycodes h,e,l,l,o
165+
./build/simdeck key-combo --modifiers cmd --key a
166+
./build/simdeck type "hello"
167+
./build/simdeck button lock --duration-ms 1000
168+
./build/simdeck home
164169
```
165170

171+
Most simulator commands accept `[<udid>]`; when it is omitted, SimDeck uses
172+
`--device`, `SIMDECK_DEVICE`, `SIMDECK_UDID`, the saved project default, or the
173+
only booted simulator, in that order. For agent navigation, prefer
174+
`describe -i`, `wait-for`, `tap --id/--label`, `tap "Text"`, `back`, and
175+
`batch` over coordinate-only loops.
176+
166177
## Expectations For Future Changes
167178

168179
- If you add an API route, add the matching client affordance or document why it stays CLI-only.

README.md

Lines changed: 74 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -94,131 +94,89 @@ CLI commands automatically use the same warm daemon:
9494

9595
```sh
9696
simdeck list
97-
simdeck tap <udid> 0.5 0.5 --normalized
98-
simdeck describe <udid> --format agent --max-depth 2
99-
```
100-
101-
## Daemon
102-
103-
Manage the project daemon explicitly when needed:
104-
105-
```sh
106-
simdeck daemon start
107-
simdeck daemon restart
108-
simdeck daemon status
109-
simdeck daemon stop
110-
simdeck daemon killall
111-
```
112-
113-
`simdeck daemon` manages the normal per-project warm process. `daemon killall`
114-
stops SimDeck daemons across all workspaces.
115-
116-
Use software H.264 when the hardware encoder is unavailable, busy, or starved
117-
by screen recording:
118-
119-
```sh
120-
simdeck daemon start --video-codec software
121-
```
122-
123-
Restart the CoreSimulator service layer when `simctl` reports a stale service
124-
version or the live display gets stuck before the first frame:
125-
126-
```sh
127-
simdeck core-simulator restart
128-
```
129-
130-
You can also start or stop the CoreSimulator service layer explicitly:
131-
132-
```sh
133-
simdeck core-simulator start
134-
simdeck core-simulator shutdown
97+
simdeck use <udid>
98+
simdeck tap 0.5 0.5 --normalized
99+
simdeck tap "Continue"
100+
simdeck describe --format agent --max-depth 2 --interactive
101+
simdeck press @e3
102+
simdeck snapshot --format agent --max-depth 2 -i
103+
simdeck --device <other-udid> describe --format agent --max-depth 2
135104
```
136105

137106
## CLI
138107

139108
```sh
140109
simdeck list
110+
simdeck use <udid>
141111
simdeck boot <udid>
142-
simdeck shutdown <udid>
143-
simdeck erase <udid>
144-
simdeck install <udid> /path/to/App.app
145-
simdeck install <udid> /path/to/App.ipa
112+
simdeck shutdown
113+
simdeck erase
114+
simdeck install /path/to/App.app
115+
simdeck install /path/to/App.ipa
146116
simdeck install android:<avd-name> /path/to/app.apk
147-
simdeck uninstall <udid> com.example.App
148-
simdeck open-url <udid> https://example.com
149-
simdeck launch <udid> com.apple.Preferences
150-
simdeck toggle-appearance <udid>
151-
simdeck pasteboard set <udid> "hello"
152-
simdeck pasteboard get <udid>
153-
simdeck screenshot <udid> --output screen.png
154-
simdeck screenshot <udid> --with-bezel --output screen-bezel.png
155-
simdeck record <udid> --seconds 5 --output screen-recording.mp4
156-
simdeck stream <udid> --frames 120 > stream.h264
157-
simdeck describe <udid>
158-
simdeck describe <udid> --format agent --max-depth 4
159-
simdeck describe <udid> --point 120,240
160-
simdeck wait-for <udid> --label "Welcome" --timeout-ms 5000
161-
simdeck assert <udid> --id login.button --source auto --max-depth 8
162-
simdeck tap <udid> 120 240
163-
simdeck tap <udid> --label "Continue" --wait-timeout-ms 5000
164-
simdeck swipe <udid> 200 700 200 200
165-
simdeck gesture <udid> scroll-down
166-
simdeck pinch <udid> --start-distance 160 --end-distance 80
167-
simdeck rotate-gesture <udid> --radius 100 --degrees 90
168-
simdeck touch <udid> 0.5 0.5 --phase began --normalized
169-
simdeck touch <udid> 120 240 --down --up --delay-ms 800
170-
simdeck key <udid> enter
171-
simdeck key-sequence <udid> --keycodes h,e,l,l,o
172-
simdeck key-combo <udid> --modifiers cmd --key a
173-
simdeck type <udid> "hello"
174-
simdeck type <udid> --file message.txt
175-
simdeck button <udid> lock --duration-ms 1000
176-
simdeck button <udid> volume-up
177-
simdeck button <udid> action --duration-ms 1000
178-
simdeck button <udid> digital-crown
179-
simdeck crown <udid> --delta 50
180-
simdeck button <udid> left-side-button
181-
simdeck batch <udid> --step "tap --label Continue" --step "type 'hello'" --step "wait-for --label hello"
182-
simdeck dismiss-keyboard <udid>
183-
simdeck button <udid> software-keyboard
184-
simdeck home <udid>
185-
simdeck app-switcher <udid>
186-
simdeck rotate-left <udid>
187-
simdeck rotate-right <udid>
188-
simdeck chrome-profile <udid>
189-
simdeck logs <udid> --seconds 30 --limit 200
190-
simdeck processes <udid>
191-
simdeck stats <udid> --watch
192-
simdeck sample <udid> --seconds 3
117+
simdeck uninstall com.example.App
118+
simdeck open-url https://example.com
119+
simdeck launch com.apple.Preferences
120+
simdeck toggle-appearance
121+
simdeck pasteboard set "hello"
122+
simdeck pasteboard get
123+
simdeck screenshot --output screen.png
124+
simdeck screenshot --with-bezel --output screen-bezel.png
125+
simdeck record --seconds 5 --output screen-recording.mp4
126+
simdeck stream --frames 120 > stream.h264
127+
simdeck describe
128+
simdeck describe --format agent --max-depth 4
129+
simdeck describe --format agent --max-depth 4 --interactive
130+
simdeck snapshot --format agent --max-depth 4 -i
131+
simdeck describe --point 120,240
132+
simdeck wait-for --label "Welcome" --timeout-ms 5000
133+
simdeck wait --label "Welcome" --timeout-ms 5000
134+
simdeck assert --id login.button --source auto --max-depth 8
135+
simdeck tap 120 240
136+
simdeck tap --label "Continue" --wait-timeout-ms 5000
137+
simdeck tap --id com.apple.settings.screenTime --expect-id BackButton
138+
simdeck tap "Continue"
139+
simdeck press @e3
140+
simdeck back
141+
simdeck swipe 200 700 200 200
142+
simdeck gesture scroll-down
143+
simdeck pinch --start-distance 160 --end-distance 80
144+
simdeck rotate-gesture --radius 100 --degrees 90
145+
simdeck touch 0.5 0.5 --phase began --normalized
146+
simdeck touch 120 240 --down --up --delay-ms 800
147+
simdeck key enter
148+
simdeck key-sequence --keycodes h,e,l,l,o
149+
simdeck key-combo --modifiers cmd --key a
150+
simdeck type "hello"
151+
simdeck type --file message.txt
152+
simdeck button lock --duration-ms 1000
153+
simdeck button volume-up
154+
simdeck button action --duration-ms 1000
155+
simdeck button digital-crown
156+
simdeck crown --delta 50
157+
simdeck button left-side-button
158+
simdeck batch --step "tap --label Continue --expect-label Done" --step "type 'hello'" --step "back"
159+
simdeck dismiss-keyboard
160+
simdeck button software-keyboard
161+
simdeck home
162+
simdeck app-switcher
163+
simdeck rotate-left
164+
simdeck rotate-right
165+
simdeck chrome-profile
166+
simdeck logs --seconds 30 --limit 200
167+
simdeck processes
168+
simdeck stats --watch
169+
simdeck sample --seconds 3
193170
```
194171

195172
`simdeck list` defaults to compact JSON for agent-friendly device selection.
196173
Use `simdeck list --format json` for the full inventory with paths and display
197174
metadata.
198175

199-
`boot` uses SimDeck's private CoreSimulator boot path so it can start devices
200-
without launching Simulator.app. If that private path is unavailable, the
201-
command returns the CoreSimulator error instead of falling back to
202-
`xcrun simctl boot`.
203-
204-
Android emulators appear in `simdeck list` with IDs like
205-
`android:SimDeck_Pixel_8_API_36`. For Android IDs, lifecycle, install, launch,
206-
URL, screenshot, logs, UIAutomator `describe`, tap, swipe, text, key, home, app
207-
switcher, rotation, pasteboard, and browser live view route through the Android
208-
SDK tools (`emulator` and `adb`) plus the emulator gRPC screenshot stream for
209-
live video. `simdeck stream` remains iOS-only because it writes the iOS H.264
210-
transport stream.
211-
212-
`stream` writes an Annex B H.264 elementary stream to stdout for diagnostics or
213-
external tools such as `ffplay`.
214-
215-
`describe` uses the project daemon to prefer React Native, NativeScript,
216-
Flutter, or UIKit in-app inspectors, then falls back to the built-in private
217-
CoreSimulator accessibility bridge. Use `--format agent` or
218-
`--format compact-json` for
219-
lower-token hierarchy dumps. Coordinate commands accept screen coordinates from
220-
the accessibility tree by default; pass `--normalized` to send `0.0..1.0`
221-
coordinates directly.
176+
`simdeck use <udid>` stores a default simulator for the current project
177+
directory. Most device commands accept `[<udid>]`; when it is omitted, SimDeck
178+
uses `--device`, `SIMDECK_DEVICE`, `SIMDECK_UDID`, the saved project default,
179+
or the only booted simulator, in that order.
222180

223181
## JS/TS Tests
224182

@@ -240,7 +198,11 @@ try {
240198
`connect()` starts the project daemon when needed, reuses it when it is already
241199
healthy, and only stops daemons it started itself. Pass `udid` to `connect()`
242200
to make it the default for session methods; each method still accepts an
243-
explicit UDID as the first argument when needed.
201+
explicit UDID as the first argument when needed. Query helpers such as
202+
`tree()`, `query()`, `waitFor()`, `assert()`, and selector `tapElement()`
203+
default to `source: "native-ax"` for fast agent control; pass
204+
`source: "auto"` when a test intentionally wants richer framework inspector
205+
trees first.
244206

245207
## NativeScript Inspector
246208

@@ -297,13 +259,6 @@ void main() {
297259
}
298260
```
299261

300-
## VS Code
301-
302-
Install the `nativescript.simdeck-vscode` extension from the VS Code Marketplace, then
303-
run `SimDeck: Open Simulator View` from the Command Palette. The extension
304-
opens the simulator inside a VS Code panel and auto-starts the local daemon
305-
when it is not already reachable.
306-
307262
## Contributing
308263

309264
Contributors should read [CONTRIBUTING.md](CONTRIBUTING.md) for local build

cli/XCWAccessibilityBridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
77
+ (nullable NSDictionary *)accessibilitySnapshotForSimulatorUDID:(NSString *)udid
88
atPoint:(nullable NSValue *)pointValue
99
maxDepth:(NSUInteger)maxDepth
10+
interactiveOnly:(BOOL)interactiveOnly
1011
error:(NSError * _Nullable * _Nullable)error;
1112

1213
@end

0 commit comments

Comments
 (0)