@loadmill/droid-cua 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,23 +17,23 @@
17
17
 
18
18
  ---
19
19
 
20
- **AI-powered Android testing using OpenAI's computer-use model**
20
+ **AI-powered mobile testing using OpenAI's computer-use model**
21
21
 
22
- Create and run automated Android tests using natural language. The AI explores your app and generates executable test scripts.
22
+ Create and run automated Android and iOS tests using natural language. The AI explores your app and generates executable test scripts.
23
23
 
24
- https://github.com/user-attachments/assets/36b2ea7e-820a-432d-9294-8aa61dceb4b0
24
+ https://github.com/user-attachments/assets/e6450f45-3700-4cb6-aad5-33ba5f0437c3
25
25
 
26
26
  ---
27
27
 
28
28
  <h2 id="what-is-droid-cua">💡 What is droid-cua?</h2>
29
29
 
30
- `droid-cua` gives you three core components for Android testing:
30
+ `droid-cua` gives you three core components for mobile testing:
31
31
 
32
32
  * **Interactive Shell** – Design and run tests with real-time feedback and visual status indicators
33
33
  * **Test Scripts** – Simple text files with natural language instructions and assertions
34
34
  * **AI Agent** – Autonomous exploration powered by OpenAI's computer-use model
35
35
 
36
- Together, these let you create and execute Android tests without writing traditional test code.
36
+ Together, these let you create and execute Android and iOS tests without writing traditional test code.
37
37
 
38
38
  ---
39
39
 
@@ -66,10 +66,18 @@ Or create a `.env` file:
66
66
  echo "OPENAI_API_KEY=your-api-key" > .env
67
67
  ```
68
68
 
69
- **3. Ensure ADB is available**
69
+ **3. Setup for your platform**
70
70
 
71
+ For Android:
71
72
  ```sh
72
- adb version
73
+ adb version # Ensure ADB is available
74
+ ```
75
+
76
+ For iOS (macOS only):
77
+ ```sh
78
+ # Install Appium and XCUITest driver
79
+ npm install -g appium
80
+ appium driver install xcuitest
73
81
  ```
74
82
 
75
83
  **4. Run**
@@ -78,7 +86,20 @@ adb version
78
86
  droid-cua
79
87
  ```
80
88
 
81
- The emulator will auto-launch if not already running.
89
+ An interactive menu will let you select your platform and device:
90
+
91
+ ```
92
+ ┌──────────────────────────────────────┐
93
+ │ Select Platform │
94
+ └──────────────────────────────────────┘
95
+
96
+ ❯ Android (1 running) - 2 emulator(s)
97
+ iOS - 5 simulator(s)
98
+
99
+ ↑/↓ Navigate Enter Select q Quit
100
+ ```
101
+
102
+ The emulator/simulator will auto-launch if not already running.
82
103
 
83
104
  ---
84
105
 
@@ -176,31 +197,58 @@ assert: login button is enabled
176
197
 
177
198
  | Option | Description |
178
199
  |--------|-------------|
179
- | `--avd=NAME` | Specify emulator |
200
+ | `--avd=NAME` | Specify emulator/simulator name |
201
+ | `--platform=PLATFORM` | Force platform: `android` or `ios` |
180
202
  | `--instructions=FILE` | Run test headless |
181
203
  | `--record` | Save screenshots |
182
204
  | `--debug` | Enable debug logs |
183
205
 
206
+ **Examples:**
207
+ ```sh
208
+ # Interactive device selection
209
+ droid-cua
210
+
211
+ # Android emulator
212
+ droid-cua --avd Pixel_8_API_35
213
+
214
+ # iOS Simulator (auto-detected from name)
215
+ droid-cua --avd "iPhone 16"
216
+
217
+ # iOS Simulator (explicit platform)
218
+ droid-cua --platform ios --avd "iPhone 16"
219
+
220
+ # Headless CI mode
221
+ droid-cua --avd "iPhone 16" --instructions tests/login.dcua
222
+ ```
223
+
184
224
  ---
185
225
 
186
226
  ## Requirements
187
227
 
228
+ **All platforms:**
188
229
  - Node.js 18.17.0+
230
+ - OpenAI API Key (Tier 3 for computer-use-preview model)
231
+
232
+ **Android:**
189
233
  - Android Debug Bridge (ADB)
190
234
  - Android Emulator (AVD)
191
- - OpenAI API Key (Tier 3 for computer-use-preview model)
235
+
236
+ **iOS (macOS only):**
237
+ - Xcode with iOS Simulator
238
+ - Appium (`npm install -g appium`)
239
+ - XCUITest driver (`appium driver install xcuitest`)
192
240
 
193
241
  ---
194
242
 
195
243
  <h2 id="how-it-works">🔧 How It Works</h2>
196
244
 
197
- 1. Connects to a running Android emulator
245
+ 1. Connects to Android emulator (via ADB) or iOS Simulator (via Appium)
198
246
  2. Captures full-screen device screenshots
199
247
  3. Scales down the screenshots for OpenAI model compatibility
200
248
  4. Sends screenshots and user instructions to OpenAI's computer-use-preview model
201
249
  5. Receives structured actions (click, scroll, type, keypress, wait, drag)
202
250
  6. Rescales model outputs back to real device coordinates
203
- 7. Executes the actions on the device via ADB
251
+ 7. Executes the actions on the device
204
252
  8. Validates assertions and handles failures
205
253
  9. Repeats until task completion
206
254
 
package/build/index.js CHANGED
@@ -8,8 +8,10 @@ import { buildBaseSystemPrompt } from "./src/core/prompts.js";
8
8
  import { startInkShell } from "./src/cli/ink-shell.js";
9
9
  import { ExecutionMode } from "./src/modes/execution-mode.js";
10
10
  import { logger } from "./src/utils/logger.js";
11
+ import { selectDevice } from "./src/cli/device-selector.js";
11
12
  const args = minimist(process.argv.slice(2));
12
- const avdName = args["avd"];
13
+ let avdName = args["avd"];
14
+ let platform = args["platform"] || null; // 'ios' or 'android'
13
15
  const recordScreenshots = args["record"] || false;
14
16
  const instructionsFile = args.instructions || args.i || null;
15
17
  const debugMode = args["debug"] || false;
@@ -19,8 +21,14 @@ const screenshotDir = path.join("droid-cua-recording-" + Date.now());
19
21
  if (recordScreenshots)
20
22
  await mkdir(screenshotDir, { recursive: true });
21
23
  async function main() {
24
+ // If no device specified, show interactive selection menu
25
+ if (!avdName && !platform) {
26
+ const selection = await selectDevice();
27
+ platform = selection.platform;
28
+ avdName = selection.deviceName;
29
+ }
22
30
  // Connect to device
23
- const deviceId = await connectToDevice(avdName);
31
+ const deviceId = await connectToDevice(avdName, platform);
24
32
  const deviceInfo = await getDeviceInfo(deviceId);
25
33
  console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
26
34
  console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Box } from 'ink';
2
+ import { Box, useInput } from 'ink';
3
3
  import { StatusBar } from './components/StatusBar.js';
4
4
  import { OutputPanel } from './components/OutputPanel.js';
5
5
  import { InputPanel } from './components/InputPanel.js';
@@ -22,6 +22,9 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
22
22
  const [isExecutionMode, setIsExecutionMode] = useState(false);
23
23
  const [inputValue, setInputValue] = useState('');
24
24
  const [inputResolver, setInputResolver] = useState(null); // For waiting on user input
25
+ const [commandHistory, setCommandHistory] = useState([]);
26
+ const [historyIndex, setHistoryIndex] = useState(-1);
27
+ const [tempInput, setTempInput] = useState(''); // Store current typing when navigating history
25
28
  // Context object passed to modes and commands
26
29
  const context = {
27
30
  // Output methods
@@ -67,6 +70,34 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
67
70
  });
68
71
  },
69
72
  };
73
+ // Handle up/down arrow keys for command history
74
+ useInput((input, key) => {
75
+ if (inputDisabled)
76
+ return;
77
+ if (key.upArrow && commandHistory.length > 0) {
78
+ const newIndex = historyIndex === -1
79
+ ? commandHistory.length - 1
80
+ : Math.max(0, historyIndex - 1);
81
+ if (historyIndex === -1) {
82
+ setTempInput(inputValue); // Save current input
83
+ }
84
+ setHistoryIndex(newIndex);
85
+ setInputValue(commandHistory[newIndex]);
86
+ }
87
+ if (key.downArrow) {
88
+ if (historyIndex === -1)
89
+ return;
90
+ const newIndex = historyIndex + 1;
91
+ if (newIndex >= commandHistory.length) {
92
+ setHistoryIndex(-1);
93
+ setInputValue(tempInput); // Restore saved input
94
+ }
95
+ else {
96
+ setHistoryIndex(newIndex);
97
+ setInputValue(commandHistory[newIndex]);
98
+ }
99
+ }
100
+ });
70
101
  // Make context available globally for modes
71
102
  useEffect(() => {
72
103
  if (typeof global !== 'undefined') {
@@ -95,6 +126,12 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
95
126
  setInputValue('');
96
127
  return;
97
128
  }
129
+ // Add to command history (avoid duplicates of last command)
130
+ if (input && input !== commandHistory[commandHistory.length - 1]) {
131
+ setCommandHistory(prev => [...prev, input]);
132
+ }
133
+ setHistoryIndex(-1);
134
+ setTempInput('');
98
135
  // Add user input to output
99
136
  context.addOutput({ type: 'user', text: input });
100
137
  // Clear input value
@@ -45,6 +45,7 @@ export const COMMANDS = {
45
45
  view: 'View test contents with line numbers',
46
46
  edit: 'Edit a test in your default editor',
47
47
  stop: 'Stop current test creation or execution',
48
+ loadmill: 'Run Loadmill test flows using natural language',
48
49
  };
49
50
  /**
50
51
  * Get command suggestions for autocomplete
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Interactive device selection menu using Ink
3
+ */
4
+ import React, { useState } from 'react';
5
+ import { render, Box, Text, useInput, useApp } from 'ink';
6
+ import { exec } from "child_process";
7
+ import { promisify } from "util";
8
+ const execAsync = promisify(exec);
9
+ /**
10
+ * Interactive selection component with arrow key navigation
11
+ */
12
+ function SelectList({ title, items, onSelect }) {
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const { exit } = useApp();
15
+ useInput((input, key) => {
16
+ if (key.upArrow) {
17
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
18
+ }
19
+ if (key.downArrow) {
20
+ setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
21
+ }
22
+ if (key.return) {
23
+ onSelect(items[selectedIndex]);
24
+ }
25
+ if (input === 'q' || key.escape) {
26
+ exit();
27
+ process.exit(0);
28
+ }
29
+ });
30
+ return (React.createElement(Box, { flexDirection: "column" },
31
+ React.createElement(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1 },
32
+ React.createElement(Text, { bold: true, color: "cyan" }, title)),
33
+ React.createElement(Box, { flexDirection: "column", paddingX: 1 }, items.map((item, index) => {
34
+ const isSelected = index === selectedIndex;
35
+ return (React.createElement(Box, { key: item.value + index },
36
+ React.createElement(Text, { color: isSelected ? 'green' : undefined }, isSelected ? '❯ ' : ' '),
37
+ React.createElement(Text, { bold: isSelected, color: isSelected ? 'green' : undefined }, item.label)));
38
+ })),
39
+ React.createElement(Box, { marginTop: 1, paddingX: 1 },
40
+ React.createElement(Text, { dimColor: true }, "\u2191/\u2193 Navigate Enter Select q Quit"))));
41
+ }
42
+ /**
43
+ * Render a selection and wait for result
44
+ */
45
+ function renderSelection(title, items) {
46
+ return new Promise((resolve) => {
47
+ const { unmount } = render(React.createElement(SelectList, { title: title, items: items, onSelect: (item) => {
48
+ unmount();
49
+ resolve(item);
50
+ } }));
51
+ });
52
+ }
53
+ /**
54
+ * Get list of available Android AVDs
55
+ */
56
+ async function getAndroidDevices() {
57
+ const devices = [];
58
+ // Get running emulators
59
+ try {
60
+ const { stdout: adbOutput } = await execAsync("adb devices");
61
+ const runningIds = adbOutput
62
+ .trim()
63
+ .split("\n")
64
+ .slice(1)
65
+ .map((line) => line.split("\t")[0])
66
+ .filter((id) => id.startsWith("emulator-"));
67
+ for (const id of runningIds) {
68
+ try {
69
+ const { stdout } = await execAsync(`adb -s ${id} emu avd name`);
70
+ const name = stdout.trim();
71
+ devices.push({
72
+ label: `${name} (running)`,
73
+ value: name,
74
+ running: true,
75
+ });
76
+ }
77
+ catch { }
78
+ }
79
+ }
80
+ catch { }
81
+ // Get available AVDs
82
+ try {
83
+ const { stdout } = await execAsync("emulator -list-avds");
84
+ const avds = stdout.trim().split("\n").filter((name) => name.length > 0);
85
+ for (const avd of avds) {
86
+ if (!devices.some((d) => d.value === avd)) {
87
+ devices.push({
88
+ label: avd,
89
+ value: avd,
90
+ running: false,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ catch { }
96
+ return devices;
97
+ }
98
+ /**
99
+ * Get list of available iOS Simulators (iPhones only)
100
+ */
101
+ async function getIOSDevices() {
102
+ const devices = [];
103
+ try {
104
+ const { stdout } = await execAsync("xcrun simctl list devices --json");
105
+ const data = JSON.parse(stdout);
106
+ const seen = new Set();
107
+ for (const [runtime, deviceList] of Object.entries(data.devices)) {
108
+ const versionMatch = runtime.match(/iOS[- ](\d+[-\.]\d+)/i);
109
+ const version = versionMatch ? versionMatch[1].replace("-", ".") : "";
110
+ for (const device of deviceList) {
111
+ if (device.isAvailable && !seen.has(device.name) && device.name.startsWith("iPhone")) {
112
+ seen.add(device.name);
113
+ const isBooted = device.state === "Booted";
114
+ devices.push({
115
+ label: isBooted
116
+ ? `${device.name} (running)${version ? ` - iOS ${version}` : ""}`
117
+ : `${device.name}${version ? ` - iOS ${version}` : ""}`,
118
+ value: device.name,
119
+ running: isBooted,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ // Sort: running first, then alphabetically
125
+ devices.sort((a, b) => {
126
+ if (a.running && !b.running)
127
+ return -1;
128
+ if (!a.running && b.running)
129
+ return 1;
130
+ return a.value.localeCompare(b.value);
131
+ });
132
+ }
133
+ catch { }
134
+ return devices;
135
+ }
136
+ /**
137
+ * Interactive device selection
138
+ * @returns {Promise<{platform: string, deviceName: string}>}
139
+ */
140
+ export async function selectDevice() {
141
+ // Check what's available
142
+ const [androidDevices, iosDevices] = await Promise.all([
143
+ getAndroidDevices(),
144
+ getIOSDevices(),
145
+ ]);
146
+ const hasAndroid = androidDevices.length > 0;
147
+ const hasIOS = iosDevices.length > 0;
148
+ if (!hasAndroid && !hasIOS) {
149
+ console.error("\nNo devices found!");
150
+ console.error(" Android: Create an AVD with Android Studio");
151
+ console.error(" iOS: Xcode Simulator must be available");
152
+ process.exit(1);
153
+ }
154
+ // Build platform options
155
+ const platformOptions = [];
156
+ if (hasAndroid) {
157
+ const runningCount = androidDevices.filter((d) => d.running).length;
158
+ platformOptions.push({
159
+ label: `Android${runningCount > 0 ? ` (${runningCount} running)` : ""} - ${androidDevices.length} emulator(s)`,
160
+ value: "android",
161
+ });
162
+ }
163
+ if (hasIOS) {
164
+ const runningCount = iosDevices.filter((d) => d.running).length;
165
+ platformOptions.push({
166
+ label: `iOS${runningCount > 0 ? ` (${runningCount} running)` : ""} - ${iosDevices.length} simulator(s)`,
167
+ value: "ios",
168
+ });
169
+ }
170
+ // Select platform
171
+ let platform;
172
+ if (platformOptions.length === 1) {
173
+ platform = platformOptions[0].value;
174
+ console.log(`\nUsing ${platform} (only available platform)\n`);
175
+ }
176
+ else {
177
+ const selected = await renderSelection("Select Platform", platformOptions);
178
+ platform = selected.value;
179
+ }
180
+ // Select device
181
+ const deviceList = platform === "ios" ? iosDevices : androidDevices;
182
+ const deviceType = platform === "ios" ? "Simulator" : "Emulator";
183
+ let deviceName;
184
+ if (deviceList.length === 1) {
185
+ deviceName = deviceList[0].value;
186
+ console.log(`Using ${deviceName} (only available ${deviceType.toLowerCase()})\n`);
187
+ }
188
+ else {
189
+ const selected = await renderSelection(`Select ${deviceType}`, deviceList);
190
+ deviceName = selected.value;
191
+ }
192
+ // Clear some space after selection
193
+ console.log();
194
+ return { platform, deviceName };
195
+ }
@@ -10,9 +10,19 @@
10
10
  */
11
11
  export async function handleHelp(args, session, context) {
12
12
  const addOutput = context?.addOutput || ((item) => console.log(item.text || item));
13
- addOutput({ type: 'system', text: 'droid-cua - AI-powered Android testing CLI' });
13
+ addOutput({ type: 'system', text: 'droid-cua - AI-powered mobile device testing CLI' });
14
14
  addOutput({ type: 'info', text: '' });
15
- addOutput({ type: 'info', text: 'Available commands:' });
15
+ addOutput({ type: 'info', text: 'Usage:' });
16
+ addOutput({ type: 'info', text: ' droid-cua --avd <device-name> [options]' });
17
+ addOutput({ type: 'info', text: '' });
18
+ addOutput({ type: 'info', text: 'Options:' });
19
+ addOutput({ type: 'info', text: ' --avd <name> Device name (Android AVD or iOS Simulator)' });
20
+ addOutput({ type: 'info', text: ' --platform <platform> Force platform: android or ios' });
21
+ addOutput({ type: 'info', text: ' --instructions <file> Run test file in headless mode' });
22
+ addOutput({ type: 'info', text: ' --record Record screenshots during execution' });
23
+ addOutput({ type: 'info', text: ' --debug Enable debug logging' });
24
+ addOutput({ type: 'info', text: '' });
25
+ addOutput({ type: 'info', text: 'Interactive commands:' });
16
26
  addOutput({ type: 'info', text: ' /help Show this help message' });
17
27
  addOutput({ type: 'info', text: ' /exit Exit the CLI' });
18
28
  addOutput({ type: 'info', text: '' });
@@ -23,11 +33,33 @@ export async function handleHelp(args, session, context) {
23
33
  addOutput({ type: 'info', text: ' /view <test-name> View test contents with line numbers' });
24
34
  addOutput({ type: 'info', text: ' /edit <test-name> Edit a test in your default editor' });
25
35
  addOutput({ type: 'info', text: '' });
36
+ addOutput({ type: 'info', text: 'Integrations:' });
37
+ addOutput({ type: 'info', text: ' /loadmill <command> Run Loadmill test flows using natural language' });
38
+ addOutput({ type: 'info', text: '' });
39
+ addOutput({ type: 'info', text: 'Platform Support:' });
40
+ addOutput({ type: 'info', text: ' Android: Uses ADB to communicate with Android emulators' });
41
+ addOutput({ type: 'info', text: ' iOS: Uses Appium + XCUITest for iOS Simulator automation' });
42
+ addOutput({ type: 'info', text: '' });
43
+ addOutput({ type: 'info', text: 'Platform Detection:' });
44
+ addOutput({ type: 'info', text: ' - Use --platform flag to force a specific platform' });
45
+ addOutput({ type: 'info', text: ' - Auto-detects iOS from device names containing "iPhone" or "iPad"' });
46
+ addOutput({ type: 'info', text: ' - Set DROID_CUA_PLATFORM env var to "ios" or "android"' });
47
+ addOutput({ type: 'info', text: ' - Defaults to Android if not detected' });
48
+ addOutput({ type: 'info', text: '' });
49
+ addOutput({ type: 'info', text: 'iOS Prerequisites:' });
50
+ addOutput({ type: 'info', text: ' 1. Xcode with iOS Simulator installed' });
51
+ addOutput({ type: 'info', text: ' 2. Appium: npm install -g appium' });
52
+ addOutput({ type: 'info', text: ' 3. XCUITest driver: appium driver install xcuitest' });
53
+ addOutput({ type: 'info', text: ' Note: Appium server is auto-started when iOS platform is detected' });
54
+ addOutput({ type: 'info', text: '' });
26
55
  addOutput({ type: 'info', text: 'Examples:' });
27
- addOutput({ type: 'info', text: ' /create login-test (design a new test)' });
28
- addOutput({ type: 'info', text: ' /list (see all tests)' });
29
- addOutput({ type: 'info', text: ' /view login-test (view test contents)' });
30
- addOutput({ type: 'info', text: ' /run login-test (execute test)' });
56
+ addOutput({ type: 'info', text: ' droid-cua --avd Pixel_8_API_35 (Android emulator)' });
57
+ addOutput({ type: 'info', text: ' droid-cua --avd "iPhone 16" (iOS Simulator, auto-detected)' });
58
+ addOutput({ type: 'info', text: ' droid-cua --platform ios --avd MySim (Force iOS platform)' });
59
+ addOutput({ type: 'info', text: ' /create login-test (design a new test)' });
60
+ addOutput({ type: 'info', text: ' /list (see all tests)' });
61
+ addOutput({ type: 'info', text: ' /view login-test (view test contents)' });
62
+ addOutput({ type: 'info', text: ' /run login-test (execute test)' });
31
63
  addOutput({ type: 'info', text: '' });
32
64
  addOutput({ type: 'info', text: 'For more info, see README.md' });
33
65
  return true; // Continue loop
@@ -9,6 +9,7 @@ import { handleList } from './list.js';
9
9
  import { handleView } from './view.js';
10
10
  import { handleEdit } from './edit.js';
11
11
  import { handleStop } from './stop.js';
12
+ import { handleLoadmill } from './loadmill.js';
12
13
  /**
13
14
  * Map of command names to their handlers
14
15
  * Each handler receives (args, session, context)
@@ -22,6 +23,7 @@ const COMMAND_HANDLERS = {
22
23
  view: handleView,
23
24
  edit: handleEdit,
24
25
  stop: handleStop,
26
+ loadmill: handleLoadmill,
25
27
  };
26
28
  /**
27
29
  * Route a command to its handler
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Loadmill command handler
3
+ */
4
+ import { executeLoadmillCommand, getApiToken } from "../integrations/loadmill/index.js";
5
+ /**
6
+ * Handle /loadmill command
7
+ * @param {string} args - Command arguments (natural language command)
8
+ * @param {Object} session - Current session
9
+ * @param {Object} context - Additional context
10
+ * @returns {Promise<boolean>} - true to continue loop
11
+ */
12
+ export async function handleLoadmill(args, session, context) {
13
+ const addOutput = context?.addOutput || ((item) => console.log(item.text || item));
14
+ const command = args.trim();
15
+ // Show help if no arguments
16
+ if (!command) {
17
+ addOutput({ type: 'system', text: 'Loadmill - Run test flows using natural language' });
18
+ addOutput({ type: 'info', text: '' });
19
+ addOutput({ type: 'info', text: 'Usage:' });
20
+ addOutput({ type: 'info', text: ' /loadmill <command> Execute a Loadmill flow' });
21
+ addOutput({ type: 'info', text: '' });
22
+ addOutput({ type: 'info', text: 'Examples:' });
23
+ addOutput({ type: 'info', text: ' /loadmill search for login flow' });
24
+ addOutput({ type: 'info', text: ' /loadmill run checkout flow with user=test123' });
25
+ addOutput({ type: 'info', text: ' /loadmill run payment test with amount=100' });
26
+ addOutput({ type: 'info', text: '' });
27
+ addOutput({ type: 'info', text: 'In test scripts (.dcua files):' });
28
+ addOutput({ type: 'info', text: ' loadmill: run user authentication flow' });
29
+ addOutput({ type: 'info', text: '' });
30
+ addOutput({ type: 'info', text: 'Configuration:' });
31
+ addOutput({ type: 'info', text: ' Set LOADMILL_API_TOKEN in your .env file' });
32
+ // Check if token is configured
33
+ if (!getApiToken()) {
34
+ addOutput({ type: 'warning', text: '' });
35
+ addOutput({ type: 'warning', text: 'Warning: LOADMILL_API_TOKEN is not set.' });
36
+ }
37
+ return true;
38
+ }
39
+ // Check for API token
40
+ if (!getApiToken()) {
41
+ addOutput({ type: 'error', text: 'LOADMILL_API_TOKEN environment variable is not set.' });
42
+ addOutput({ type: 'info', text: 'Add it to your .env file:' });
43
+ addOutput({ type: 'info', text: ' LOADMILL_API_TOKEN=your-token-here' });
44
+ return true;
45
+ }
46
+ // Set agent working status
47
+ if (context.setAgentWorking) {
48
+ context.setAgentWorking(true, 'Running Loadmill flow...');
49
+ }
50
+ // Execute the command
51
+ const result = await executeLoadmillCommand(command, {
52
+ onProgress: ({ message }) => {
53
+ addOutput({ type: 'info', text: `[Loadmill] ${message}` });
54
+ }
55
+ });
56
+ // Clear agent working status
57
+ if (context.setAgentWorking) {
58
+ context.setAgentWorking(false);
59
+ }
60
+ // Display results
61
+ if (result.success) {
62
+ if (result.action === "search") {
63
+ addOutput({ type: 'success', text: `Found ${result.result.flows.length} flow(s):` });
64
+ result.result.flows.forEach((flow, i) => {
65
+ const name = flow.description || flow.name || "Unknown";
66
+ const suite = flow.testSuiteDescription ? ` (Suite: ${flow.testSuiteDescription})` : '';
67
+ addOutput({ type: 'info', text: ` ${i + 1}. ${name}${suite}` });
68
+ addOutput({ type: 'info', text: ` ID: ${flow.id}` });
69
+ });
70
+ if (result.result.selectedFlow) {
71
+ const selectedName = result.result.selectedFlow.description || result.result.selectedFlow.name || "Unknown";
72
+ addOutput({ type: 'info', text: '' });
73
+ addOutput({ type: 'info', text: `Best match: "${selectedName}" (confidence: ${(result.result.confidence * 100).toFixed(0)}%)` });
74
+ }
75
+ }
76
+ else {
77
+ addOutput({ type: 'success', text: `Flow "${result.flowName}" passed` });
78
+ if (result.runId) {
79
+ addOutput({ type: 'info', text: `Run ID: ${result.runId}` });
80
+ }
81
+ }
82
+ }
83
+ else {
84
+ addOutput({ type: 'error', text: `Loadmill failed: ${result.error}` });
85
+ }
86
+ return true; // Continue loop
87
+ }
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import { writeFile } from "fs/promises";
3
- import { getScreenshotAsBase64 } from "../device/connection.js";
3
+ import { getScreenshotAsBase64, getCurrentPlatform } from "../device/connection.js";
4
4
  import { handleModelAction } from "../device/actions.js";
5
5
  import { sendCUARequest } from "../device/openai.js";
6
6
  export class ExecutionEngine {
@@ -93,7 +93,7 @@ export class ExecutionEngine {
93
93
  type: "computer_screenshot",
94
94
  image_url: `data:image/png;base64,${screenshotBase64}`,
95
95
  },
96
- current_url: "android://emulator", // Android emulator doesn't have URLs like a browser
96
+ current_url: getCurrentPlatform() === "ios" ? "ios://simulator" : "android://emulator",
97
97
  ...(pendingSafetyChecks.length > 0 ? { acknowledged_safety_checks: pendingSafetyChecks } : {})
98
98
  }];
99
99
  response = await sendCUARequest({