@loadmill/droid-cua 1.1.2 → 2.0.0
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 +71 -197
- package/build/index.js +2 -0
- package/build/src/cli/app.js +60 -3
- package/build/src/cli/components/CommandSuggestions.js +46 -6
- package/build/src/cli/components/OutputPanel.js +16 -0
- package/build/src/cli/device-selector.js +55 -28
- package/build/src/commands/help.js +4 -3
- package/build/src/core/execution-engine.js +127 -25
- package/build/src/core/prompts.js +71 -10
- package/build/src/device/actions.js +1 -1
- package/build/src/device/android/actions.js +97 -20
- package/build/src/device/android/connection.js +176 -73
- package/build/src/device/android/tools.js +21 -0
- package/build/src/device/assertions.js +28 -6
- package/build/src/device/connection.js +2 -2
- package/build/src/device/factory.js +1 -1
- package/build/src/device/interface.js +6 -2
- package/build/src/device/ios/actions.js +87 -26
- package/build/src/device/ios/appium-server.js +62 -8
- package/build/src/device/ios/connection.js +41 -3
- package/build/src/device/loadmill.js +66 -17
- package/build/src/device/openai.js +84 -73
- package/build/src/integrations/loadmill/client.js +24 -3
- package/build/src/integrations/loadmill/executor.js +2 -2
- package/build/src/integrations/loadmill/interpreter.js +11 -7
- package/build/src/modes/design-mode-ink.js +13 -0
- package/build/src/modes/design-mode.js +9 -0
- package/build/src/modes/execution-mode.js +225 -29
- package/build/src/utils/cua-debug-tracer.js +362 -0
- package/build/src/utils/desktop-debug.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,267 +7,141 @@
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="#what-is-droid-cua">What is droid-cua?</a> •
|
|
9
9
|
<a href="#quick-start">Quick Start</a> •
|
|
10
|
+
<a href="#platforms">Platforms</a> •
|
|
10
11
|
<a href="#features">Features</a> •
|
|
11
|
-
<a href="#usage">Usage</a> •
|
|
12
|
-
<a href="#assertions">Assertions</a> •
|
|
13
|
-
<a href="#command-line-options">Command Line Options</a> •
|
|
14
12
|
<a href="#how-it-works">How It Works</a> •
|
|
13
|
+
<a href="#cli-and-automation">CLI & Automation</a> •
|
|
15
14
|
<a href="#license">License</a>
|
|
16
15
|
</p>
|
|
17
16
|
|
|
18
17
|
---
|
|
19
18
|
|
|
20
|
-
**AI-powered mobile testing
|
|
19
|
+
**AI-powered mobile testing desktop app for Android and iOS**
|
|
21
20
|
|
|
22
|
-
Create
|
|
21
|
+
Create, run, and manage mobile tests with natural language. The desktop app guides setup, connects to your target device or simulator, and turns AI exploration into reusable test scripts.
|
|
23
22
|
|
|
24
|
-
https://github.com/user-attachments/assets/
|
|
23
|
+
https://github.com/user-attachments/assets/b9e15a1d-8072-4a2f-a4c5-db180ae38620
|
|
25
24
|
|
|
26
25
|
---
|
|
27
26
|
|
|
28
27
|
<h2 id="what-is-droid-cua">💡 What is droid-cua?</h2>
|
|
29
28
|
|
|
30
|
-
`droid-cua`
|
|
29
|
+
`droid-cua` is a desktop app for AI-powered mobile testing.
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
* **Test Scripts** – Simple text files with natural language instructions and assertions
|
|
34
|
-
* **AI Agent** – Autonomous exploration powered by OpenAI's computer-use model
|
|
31
|
+
It helps teams create, edit, and run tests for **Android devices and emulators** and **iOS simulators on macOS** using natural language instead of traditional test code.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Under the hood, `droid-cua` uses an AI agent to explore your app, execute actions, and save reusable test scripts that can also be run later in headless workflows.
|
|
37
34
|
|
|
38
35
|
---
|
|
39
36
|
|
|
40
37
|
<h2 id="quick-start">🚀 Quick Start</h2>
|
|
41
38
|
|
|
42
|
-
**1.
|
|
39
|
+
**1. Download the desktop app**
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
```sh
|
|
46
|
-
npm install -g @loadmill/droid-cua
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Or from source:
|
|
50
|
-
```sh
|
|
51
|
-
git clone https://github.com/loadmill/droid-cua
|
|
52
|
-
cd droid-cua
|
|
53
|
-
npm install
|
|
54
|
-
npm run build
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**2. Set your OpenAI API key**
|
|
58
|
-
|
|
59
|
-
Using environment variable:
|
|
60
|
-
```sh
|
|
61
|
-
export OPENAI_API_KEY=your-api-key
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Or create a `.env` file:
|
|
65
|
-
```sh
|
|
66
|
-
echo "OPENAI_API_KEY=your-api-key" > .env
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**3. Setup for your platform**
|
|
70
|
-
|
|
71
|
-
For Android:
|
|
72
|
-
```sh
|
|
73
|
-
adb version # Ensure ADB is available
|
|
74
|
-
```
|
|
41
|
+
Get the latest stable desktop build from [GitHub Releases](https://github.com/loadmill/droid-cua/releases).
|
|
75
42
|
|
|
76
|
-
|
|
77
|
-
```sh
|
|
78
|
-
# Install Appium and XCUITest driver
|
|
79
|
-
npm install -g appium
|
|
80
|
-
appium driver install xcuitest
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**4. Run**
|
|
84
|
-
|
|
85
|
-
```sh
|
|
86
|
-
droid-cua
|
|
87
|
-
```
|
|
43
|
+
**2. Launch the app**
|
|
88
44
|
|
|
89
|
-
|
|
45
|
+
Open the desktop app and choose the platform you want to test.
|
|
90
46
|
|
|
91
|
-
|
|
92
|
-
┌──────────────────────────────────────┐
|
|
93
|
-
│ Select Platform │
|
|
94
|
-
└──────────────────────────────────────┘
|
|
47
|
+
**3. Add your credentials**
|
|
95
48
|
|
|
96
|
-
|
|
97
|
-
iOS - 5 simulator(s)
|
|
49
|
+
Set your OpenAI API key in the app Settings screen, or log in with your Loadmill account.
|
|
98
50
|
|
|
99
|
-
|
|
100
|
-
```
|
|
51
|
+
**4. Connect a target device**
|
|
101
52
|
|
|
102
|
-
|
|
53
|
+
- Android: connect a device or select an emulator
|
|
54
|
+
- iOS: choose a simulator on macOS
|
|
103
55
|
|
|
104
|
-
|
|
56
|
+
**5. Create or run a test**
|
|
105
57
|
|
|
106
|
-
|
|
58
|
+
Use the desktop app to create a new test, edit an existing one, or run a saved script with live execution logs.
|
|
107
59
|
|
|
108
|
-
|
|
109
|
-
- **Execution Mode** - Run tests with real-time feedback and assertion handling
|
|
110
|
-
- **Headless Mode** - Run tests in CI/CD pipelines
|
|
111
|
-
- **Test Management** - Create, edit, view, and run test scripts
|
|
112
|
-
- **Smart Actions** - Automatic wait detection and coordinate mapping
|
|
60
|
+
You can also keep project run history in a results folder and review past runs from desktop app reports.
|
|
113
61
|
|
|
114
62
|
---
|
|
115
63
|
|
|
116
|
-
<h2 id="
|
|
117
|
-
|
|
118
|
-
### Interactive Commands
|
|
119
|
-
|
|
120
|
-
| Command | Description |
|
|
121
|
-
|---------|-------------|
|
|
122
|
-
| `/create <name>` | Create a new test |
|
|
123
|
-
| `/run <name>` | Execute a test |
|
|
124
|
-
| `/list` | List all tests |
|
|
125
|
-
| `/view <name>` | View test contents |
|
|
126
|
-
| `/edit <name>` | Edit a test |
|
|
127
|
-
| `/help` | Show help |
|
|
128
|
-
| `/exit` | Exit shell |
|
|
129
|
-
|
|
130
|
-
### Creating Tests
|
|
131
|
-
|
|
132
|
-
```sh
|
|
133
|
-
droid-cua
|
|
134
|
-
> /create login-test
|
|
135
|
-
> Test the login flow with valid credentials
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
The AI will explore your app and generate a test script. Review and save it.
|
|
139
|
-
|
|
140
|
-
### Running Tests
|
|
141
|
-
|
|
142
|
-
Interactive:
|
|
143
|
-
```sh
|
|
144
|
-
droid-cua
|
|
145
|
-
> /run login-test
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Headless (CI/CD):
|
|
149
|
-
```sh
|
|
150
|
-
droid-cua --instructions tests/login-test.dcua
|
|
151
|
-
```
|
|
64
|
+
<h2 id="platforms">📱 Platforms</h2>
|
|
152
65
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
One instruction per line:
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
Open the Calculator app
|
|
159
|
-
assert: Calculator app is visible
|
|
160
|
-
Type "2"
|
|
161
|
-
Click the plus button
|
|
162
|
-
Type "3"
|
|
163
|
-
Click the equals button
|
|
164
|
-
assert: result shows 5
|
|
165
|
-
exit
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
<h3 id="assertions">Assertions</h3>
|
|
169
|
-
|
|
170
|
-
Assertions validate the app state during test execution. Add them anywhere in your test script.
|
|
171
|
-
|
|
172
|
-
**Syntax** (all valid):
|
|
173
|
-
```
|
|
174
|
-
assert: the login button is visible
|
|
175
|
-
Assert: error message appears
|
|
176
|
-
ASSERT the result shows 5
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Interactive Mode** - When an assertion fails:
|
|
180
|
-
- `retry` - Retry the same assertion
|
|
181
|
-
- `skip` - Continue to next instruction
|
|
182
|
-
- `stop` - Stop test execution
|
|
183
|
-
|
|
184
|
-
**Headless Mode** - Assertions fail immediately and exit with code 1.
|
|
185
|
-
|
|
186
|
-
**Examples**:
|
|
187
|
-
```
|
|
188
|
-
assert: Calculator app is open
|
|
189
|
-
assert: the result shows 8
|
|
190
|
-
assert: error message is displayed in red
|
|
191
|
-
assert: login button is enabled
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
<h2 id="command-line-options">💻 Command Line Options</h2>
|
|
197
|
-
|
|
198
|
-
| Option | Description |
|
|
199
|
-
|--------|-------------|
|
|
200
|
-
| `--avd=NAME` | Specify emulator/simulator name |
|
|
201
|
-
| `--platform=PLATFORM` | Force platform: `android` or `ios` |
|
|
202
|
-
| `--instructions=FILE` | Run test headless |
|
|
203
|
-
| `--record` | Save screenshots |
|
|
204
|
-
| `--debug` | Enable debug logs |
|
|
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
|
-
|
|
224
|
-
---
|
|
66
|
+
- **Android** - Physical devices and emulators
|
|
67
|
+
- **iOS** - Simulators on macOS
|
|
225
68
|
|
|
226
69
|
## Requirements
|
|
227
70
|
|
|
228
71
|
**All platforms:**
|
|
229
|
-
-
|
|
230
|
-
- OpenAI API Key (Tier 3 for computer-use-preview model)
|
|
72
|
+
- OpenAI API key
|
|
231
73
|
|
|
232
74
|
**Android:**
|
|
233
75
|
- Android Debug Bridge (ADB)
|
|
234
|
-
- Android Emulator
|
|
76
|
+
- Android Emulator CLI for launchable emulators
|
|
235
77
|
|
|
236
78
|
**iOS (macOS only):**
|
|
237
79
|
- Xcode with iOS Simulator
|
|
238
|
-
- Appium
|
|
239
|
-
- XCUITest driver
|
|
80
|
+
- Appium
|
|
81
|
+
- XCUITest driver
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<h2 id="features">✨ Features</h2>
|
|
86
|
+
|
|
87
|
+
- **Desktop-first workflow** - Create, run, and manage tests from one app
|
|
88
|
+
- **Setup guidance** - Configure API access and platform prerequisites in the app
|
|
89
|
+
- **Device and simulator connection** - Connect Android targets and iOS simulators
|
|
90
|
+
- **Natural-language test creation** - Describe flows in plain English
|
|
91
|
+
- **Test management** - Create, edit, save, and rerun reusable scripts
|
|
92
|
+
- **Live execution logs** - Watch actions and progress as tests run
|
|
93
|
+
- **Reports and history** - Review past runs from a project results folder inside the desktop app
|
|
94
|
+
- **JUnit XML output** - Write standard test reports for CI systems and external tooling
|
|
95
|
+
- **Headless support** - Reuse scripts in CLI and automation workflows
|
|
240
96
|
|
|
241
97
|
---
|
|
242
98
|
|
|
243
99
|
<h2 id="how-it-works">🔧 How It Works</h2>
|
|
244
100
|
|
|
245
|
-
1.
|
|
101
|
+
1. The desktop app connects to an Android device or emulator through ADB, or to an iOS simulator through Appium + XCUITest
|
|
246
102
|
2. Captures full-screen device screenshots
|
|
247
103
|
3. Scales down the screenshots for OpenAI model compatibility
|
|
248
|
-
4. Sends screenshots and user instructions to OpenAI's computer-use
|
|
249
|
-
5. Receives structured actions
|
|
104
|
+
4. Sends screenshots and user instructions to OpenAI's computer-use model
|
|
105
|
+
5. Receives structured actions such as click, scroll, type, keypress, wait, and drag
|
|
250
106
|
6. Rescales model outputs back to real device coordinates
|
|
251
|
-
7. Executes the actions on the device
|
|
107
|
+
7. Executes the actions on the device or simulator
|
|
252
108
|
8. Validates assertions and handles failures
|
|
253
109
|
9. Repeats until task completion
|
|
254
110
|
|
|
255
111
|
---
|
|
256
112
|
|
|
257
|
-
|
|
113
|
+
<h2 id="cli-and-automation">💻 CLI & Automation</h2>
|
|
258
114
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
droid-cua
|
|
115
|
+
The desktop app is the primary way to use `droid-cua`.
|
|
116
|
+
|
|
117
|
+
For CI, scripting, or advanced workflows, `droid-cua` also includes a CLI for running saved instructions headlessly.
|
|
118
|
+
|
|
119
|
+
Desktop projects can also keep run reports in a results folder, including JUnit XML output that the app can read back as project history.
|
|
120
|
+
|
|
121
|
+
Install:
|
|
122
|
+
```sh
|
|
123
|
+
npm install -g @loadmill/droid-cua
|
|
262
124
|
```
|
|
263
125
|
|
|
264
|
-
|
|
126
|
+
Examples:
|
|
265
127
|
```sh
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
128
|
+
# Interactive CLI
|
|
129
|
+
droid-cua
|
|
130
|
+
|
|
131
|
+
# Headless Android run
|
|
132
|
+
droid-cua --avd adb:emulator-5554 --instructions tests/login.dcua
|
|
133
|
+
|
|
134
|
+
# Headless iOS simulator run
|
|
135
|
+
droid-cua --platform ios --avd "iPhone 16" --instructions tests/login.dcua
|
|
269
136
|
```
|
|
270
137
|
|
|
138
|
+
Supported CLI options include:
|
|
139
|
+
- `--avd`
|
|
140
|
+
- `--platform`
|
|
141
|
+
- `--instructions`
|
|
142
|
+
- `--record`
|
|
143
|
+
- `--debug`
|
|
144
|
+
|
|
271
145
|
---
|
|
272
146
|
|
|
273
147
|
<h2 id="license">📄 License</h2>
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import minimist from "minimist";
|
|
2
|
+
import dotenv from "dotenv";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { mkdir, readFile } from "fs/promises";
|
|
4
5
|
import { connectToDevice, getDeviceInfo } from "./src/device/connection.js";
|
|
@@ -9,6 +10,7 @@ import { startInkShell } from "./src/cli/ink-shell.js";
|
|
|
9
10
|
import { ExecutionMode } from "./src/modes/execution-mode.js";
|
|
10
11
|
import { logger } from "./src/utils/logger.js";
|
|
11
12
|
import { selectDevice } from "./src/cli/device-selector.js";
|
|
13
|
+
dotenv.config();
|
|
12
14
|
const args = minimist(process.argv.slice(2));
|
|
13
15
|
let avdName = args["avd"];
|
|
14
16
|
let platform = args["platform"] || null; // 'ios' or 'android'
|
package/build/src/cli/app.js
CHANGED
|
@@ -5,13 +5,27 @@ import { OutputPanel } from './components/OutputPanel.js';
|
|
|
5
5
|
import { InputPanel } from './components/InputPanel.js';
|
|
6
6
|
import { AgentStatus } from './components/AgentStatus.js';
|
|
7
7
|
import { CommandSuggestions } from './components/CommandSuggestions.js';
|
|
8
|
-
import { COMMANDS } from './command-parser.js';
|
|
8
|
+
import { COMMANDS, getCommandSuggestions } from './command-parser.js';
|
|
9
|
+
import { listTests } from '../test-store/test-manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} CliExecutionOutputItem
|
|
12
|
+
* @property {string} [type]
|
|
13
|
+
* @property {string} [text]
|
|
14
|
+
* @property {string} [eventType]
|
|
15
|
+
* @property {string} [actionType]
|
|
16
|
+
* @property {string} [runId]
|
|
17
|
+
* @property {string} [stepId]
|
|
18
|
+
* @property {number} [instructionIndex]
|
|
19
|
+
* @property {Record<string, unknown>} [payload]
|
|
20
|
+
* @property {unknown} [metadata]
|
|
21
|
+
*/
|
|
9
22
|
/**
|
|
10
23
|
* Main Ink App component - conversational split-pane UI
|
|
11
24
|
*/
|
|
12
25
|
export function App({ session, initialMode = 'command', onInput, onExit }) {
|
|
13
26
|
const [mode, setMode] = useState(initialMode);
|
|
14
27
|
const [testName, setTestName] = useState(null);
|
|
28
|
+
/** @type {[CliExecutionOutputItem[], React.Dispatch<React.SetStateAction<CliExecutionOutputItem[]>>]} */
|
|
15
29
|
const [output, setOutput] = useState([]);
|
|
16
30
|
const [agentWorking, setAgentWorking] = useState(false);
|
|
17
31
|
const [agentMessage, setAgentMessage] = useState('');
|
|
@@ -25,9 +39,21 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
|
|
|
25
39
|
const [commandHistory, setCommandHistory] = useState([]);
|
|
26
40
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
27
41
|
const [tempInput, setTempInput] = useState(''); // Store current typing when navigating history
|
|
42
|
+
const [availableTests, setAvailableTests] = useState([]); // For tab completion
|
|
43
|
+
// Load available tests for tab completion
|
|
44
|
+
const refreshTests = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const tests = await listTests();
|
|
47
|
+
setAvailableTests(tests.map(t => t.name));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
setAvailableTests([]);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
28
53
|
// Context object passed to modes and commands
|
|
29
54
|
const context = {
|
|
30
55
|
// Output methods
|
|
56
|
+
/** @param {CliExecutionOutputItem} item */
|
|
31
57
|
addOutput: (item) => {
|
|
32
58
|
setOutput((prev) => [...prev, item]);
|
|
33
59
|
},
|
|
@@ -69,11 +95,39 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
|
|
|
69
95
|
setInputResolver(() => resolve);
|
|
70
96
|
});
|
|
71
97
|
},
|
|
98
|
+
// Refresh tests list (for autocomplete)
|
|
99
|
+
refreshTests,
|
|
72
100
|
};
|
|
73
|
-
// Handle up/down arrow keys for command history
|
|
101
|
+
// Handle up/down arrow keys for command history and Tab for autocomplete
|
|
74
102
|
useInput((input, key) => {
|
|
75
103
|
if (inputDisabled)
|
|
76
104
|
return;
|
|
105
|
+
// Tab key for autocomplete
|
|
106
|
+
if (key.tab) {
|
|
107
|
+
if (inputValue.startsWith('/')) {
|
|
108
|
+
const parts = inputValue.slice(1).split(' ');
|
|
109
|
+
const commandPart = parts[0].toLowerCase();
|
|
110
|
+
if (parts.length === 1 && !inputValue.includes(' ')) {
|
|
111
|
+
// Autocomplete command name (e.g., /he -> /help)
|
|
112
|
+
const matches = getCommandSuggestions(commandPart);
|
|
113
|
+
if (matches.length === 1) {
|
|
114
|
+
setInputValue(`/${matches[0]} `);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (parts.length >= 1) {
|
|
118
|
+
// Autocomplete test name for /run, /view, /edit
|
|
119
|
+
const testCommands = ['run', 'view', 'edit'];
|
|
120
|
+
if (testCommands.includes(commandPart)) {
|
|
121
|
+
const testPart = parts.slice(1).join(' ').toLowerCase();
|
|
122
|
+
const matchingTests = availableTests.filter(t => t.toLowerCase().startsWith(testPart));
|
|
123
|
+
if (matchingTests.length === 1) {
|
|
124
|
+
setInputValue(`/${commandPart} ${matchingTests[0]}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
77
131
|
if (key.upArrow && commandHistory.length > 0) {
|
|
78
132
|
const newIndex = historyIndex === -1
|
|
79
133
|
? commandHistory.length - 1
|
|
@@ -104,6 +158,9 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
|
|
|
104
158
|
global.inkContext = context;
|
|
105
159
|
}
|
|
106
160
|
}, [context]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
refreshTests();
|
|
163
|
+
}, []);
|
|
107
164
|
// Show welcome banner on mount
|
|
108
165
|
useEffect(() => {
|
|
109
166
|
const banner = `
|
|
@@ -148,5 +205,5 @@ export function App({ session, initialMode = 'command', onInput, onExit }) {
|
|
|
148
205
|
React.createElement(AgentStatus, { isWorking: agentWorking, message: agentMessage })),
|
|
149
206
|
React.createElement(Box, { flexDirection: "column" },
|
|
150
207
|
React.createElement(InputPanel, { value: inputValue, onChange: setInputValue, onSubmit: handleInput, placeholder: inputPlaceholder, disabled: inputDisabled }),
|
|
151
|
-
React.createElement(CommandSuggestions, { input: inputValue, commands: COMMANDS }))));
|
|
208
|
+
React.createElement(CommandSuggestions, { input: inputValue, commands: COMMANDS, tests: availableTests }))));
|
|
152
209
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
/**
|
|
4
|
-
* Command suggestions shown when user types "/"
|
|
4
|
+
* Command and test suggestions shown when user types "/" or "/run "
|
|
5
5
|
*/
|
|
6
|
-
export function CommandSuggestions({ input, commands }) {
|
|
6
|
+
export function CommandSuggestions({ input, commands, tests = [] }) {
|
|
7
7
|
const MAX_VISIBLE = 6;
|
|
8
8
|
const HEADER_LINES = 1;
|
|
9
9
|
const PANEL_HEIGHT = HEADER_LINES + MAX_VISIBLE;
|
|
@@ -11,9 +11,47 @@ export function CommandSuggestions({ input, commands }) {
|
|
|
11
11
|
if (!input.startsWith('/')) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
const commandPart =
|
|
16
|
-
|
|
14
|
+
const parts = input.slice(1).split(' ');
|
|
15
|
+
const commandPart = parts[0].toLowerCase();
|
|
16
|
+
const hasSpace = input.includes(' ');
|
|
17
|
+
// Check if we should show test suggestions
|
|
18
|
+
const testCommands = ['run', 'view', 'edit'];
|
|
19
|
+
if (hasSpace && testCommands.includes(commandPart)) {
|
|
20
|
+
const testPart = parts.slice(1).join(' ').toLowerCase();
|
|
21
|
+
// Filter tests that match
|
|
22
|
+
const suggestions = tests
|
|
23
|
+
.filter(t => t.toLowerCase().startsWith(testPart))
|
|
24
|
+
.sort()
|
|
25
|
+
.slice(0, MAX_VISIBLE);
|
|
26
|
+
if (suggestions.length === 0 && testPart.length === 0) {
|
|
27
|
+
// Show all tests if nothing typed yet
|
|
28
|
+
const allTests = tests.slice(0, MAX_VISIBLE);
|
|
29
|
+
const usedLines = HEADER_LINES + allTests.length;
|
|
30
|
+
const padLines = Math.max(0, PANEL_HEIGHT - usedLines);
|
|
31
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, height: PANEL_HEIGHT },
|
|
32
|
+
React.createElement(Text, { dimColor: true },
|
|
33
|
+
"Available tests: ",
|
|
34
|
+
React.createElement(Text, { color: "gray" }, "(Tab to complete)")),
|
|
35
|
+
allTests.map((test) => (React.createElement(Box, { key: test },
|
|
36
|
+
React.createElement(Text, { color: "green" },
|
|
37
|
+
" ",
|
|
38
|
+
test)))),
|
|
39
|
+
allTests.length === 0 && (React.createElement(Text, { dimColor: true }, " No tests found. Use /create to make one.")),
|
|
40
|
+
Array.from({ length: padLines }).map((_, i) => (React.createElement(Text, { key: `pad-${i}` }, " ")))));
|
|
41
|
+
}
|
|
42
|
+
const usedLines = HEADER_LINES + suggestions.length;
|
|
43
|
+
const padLines = Math.max(0, PANEL_HEIGHT - usedLines);
|
|
44
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, height: PANEL_HEIGHT },
|
|
45
|
+
React.createElement(Text, { dimColor: true },
|
|
46
|
+
"Matching tests: ",
|
|
47
|
+
React.createElement(Text, { color: "gray" }, "(Tab to complete)")),
|
|
48
|
+
suggestions.map((test) => (React.createElement(Box, { key: test },
|
|
49
|
+
React.createElement(Text, { color: "green" },
|
|
50
|
+
" ",
|
|
51
|
+
test)))),
|
|
52
|
+
Array.from({ length: padLines }).map((_, i) => (React.createElement(Text, { key: `pad-${i}` }, " ")))));
|
|
53
|
+
}
|
|
54
|
+
// Show command suggestions
|
|
17
55
|
const suggestions = Object.entries(commands)
|
|
18
56
|
.filter(([cmd]) => cmd.toLowerCase().startsWith(commandPart))
|
|
19
57
|
.sort()
|
|
@@ -21,7 +59,9 @@ export function CommandSuggestions({ input, commands }) {
|
|
|
21
59
|
const usedLines = HEADER_LINES + suggestions.length;
|
|
22
60
|
const padLines = Math.max(0, PANEL_HEIGHT - usedLines);
|
|
23
61
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, height: PANEL_HEIGHT },
|
|
24
|
-
React.createElement(Text, { dimColor: true },
|
|
62
|
+
React.createElement(Text, { dimColor: true },
|
|
63
|
+
"Available commands: ",
|
|
64
|
+
React.createElement(Text, { color: "gray" }, "(Tab to complete)")),
|
|
25
65
|
suggestions.map(([cmd, description]) => (React.createElement(Box, { key: cmd },
|
|
26
66
|
React.createElement(Text, { color: "cyan" },
|
|
27
67
|
" /",
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} CliExecutionOutputItem
|
|
5
|
+
* @property {string} [type]
|
|
6
|
+
* @property {string} [text]
|
|
7
|
+
* @property {string} [eventType]
|
|
8
|
+
* @property {string} [actionType]
|
|
9
|
+
* @property {string} [runId]
|
|
10
|
+
* @property {string} [stepId]
|
|
11
|
+
* @property {number} [instructionIndex]
|
|
12
|
+
* @property {Record<string, unknown>} [payload]
|
|
13
|
+
* @property {unknown} [metadata]
|
|
14
|
+
*/
|
|
3
15
|
/**
|
|
4
16
|
* Scrollable output panel for agent actions and reasoning
|
|
17
|
+
* @param {{ items: CliExecutionOutputItem[] }} props
|
|
5
18
|
*/
|
|
6
19
|
export function OutputPanel({ items }) {
|
|
7
20
|
if (items.length === 0) {
|
|
@@ -10,6 +23,9 @@ export function OutputPanel({ items }) {
|
|
|
10
23
|
}
|
|
11
24
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, paddingY: 1 }, items.map((item, index) => (React.createElement(OutputItem, { key: index, item: item })))));
|
|
12
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* @param {{ item: CliExecutionOutputItem }} props
|
|
28
|
+
*/
|
|
13
29
|
function OutputItem({ item }) {
|
|
14
30
|
switch (item.type) {
|
|
15
31
|
case 'reasoning':
|