@loadmill/droid-cua 1.1.1 → 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.
Files changed (32) hide show
  1. package/README.md +71 -197
  2. package/build/index.js +2 -0
  3. package/build/src/cli/app.js +60 -3
  4. package/build/src/cli/components/CommandSuggestions.js +46 -6
  5. package/build/src/cli/components/OutputPanel.js +16 -0
  6. package/build/src/cli/device-selector.js +55 -28
  7. package/build/src/commands/help.js +4 -3
  8. package/build/src/core/execution-engine.js +127 -25
  9. package/build/src/core/prompts.js +71 -10
  10. package/build/src/device/actions.js +1 -1
  11. package/build/src/device/android/actions.js +97 -20
  12. package/build/src/device/android/connection.js +176 -73
  13. package/build/src/device/android/tools.js +21 -0
  14. package/build/src/device/assertions.js +28 -6
  15. package/build/src/device/connection.js +2 -2
  16. package/build/src/device/factory.js +1 -1
  17. package/build/src/device/interface.js +6 -2
  18. package/build/src/device/ios/actions.js +87 -26
  19. package/build/src/device/ios/appium-server.js +62 -8
  20. package/build/src/device/ios/connection.js +41 -3
  21. package/build/src/device/loadmill.js +66 -17
  22. package/build/src/device/openai.js +84 -73
  23. package/build/src/integrations/loadmill/client.js +24 -3
  24. package/build/src/integrations/loadmill/executor.js +2 -2
  25. package/build/src/integrations/loadmill/interpreter.js +11 -7
  26. package/build/src/modes/design-mode-ink.js +13 -0
  27. package/build/src/modes/design-mode.js +9 -0
  28. package/build/src/modes/execution-mode.js +225 -29
  29. package/build/src/test-store/test-manager.js +12 -4
  30. package/build/src/utils/cua-debug-tracer.js +362 -0
  31. package/build/src/utils/desktop-debug.js +36 -0
  32. 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 using OpenAI's computer-use model**
19
+ **AI-powered mobile testing desktop app for Android and iOS**
21
20
 
22
- Create and run automated Android and iOS tests using natural language. The AI explores your app and generates executable test scripts.
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/e6450f45-3700-4cb6-aad5-33ba5f0437c3
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` gives you three core components for mobile testing:
29
+ `droid-cua` is a desktop app for AI-powered mobile testing.
31
30
 
32
- * **Interactive Shell** Design and run tests with real-time feedback and visual status indicators
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
- Together, these let you create and execute Android and iOS tests without writing traditional test code.
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. Install**
39
+ **1. Download the desktop app**
43
40
 
44
- Globally (recommended):
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
- For iOS (macOS only):
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
- An interactive menu will let you select your platform and device:
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
- Android (1 running) - 2 emulator(s)
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
- ↑/↓ Navigate Enter Select q Quit
100
- ```
51
+ **4. Connect a target device**
101
52
 
102
- The emulator/simulator will auto-launch if not already running.
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
- <h2 id="features">✨ Features</h2>
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
- - **Design Mode** - Describe what to test, AI explores and creates test scripts
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="usage">📚 Usage</h2>
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
- ### Test Script Format
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
- - Node.js 18.17.0+
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 (AVD)
76
+ - Android Emulator CLI for launchable emulators
235
77
 
236
78
  **iOS (macOS only):**
237
79
  - Xcode with iOS Simulator
238
- - Appium (`npm install -g appium`)
239
- - XCUITest driver (`appium driver install xcuitest`)
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. Connects to Android emulator (via ADB) or iOS Simulator (via Appium)
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-preview model
249
- 5. Receives structured actions (click, scroll, type, keypress, wait, drag)
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
- ## 🎞️ Convert Screenshots to Video
113
+ <h2 id="cli-and-automation">💻 CLI & Automation</h2>
258
114
 
259
- If you run with `--record`, screenshots are saved to:
260
- ```
261
- droid-cua-recording-<timestamp>/
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
- Convert to video with ffmpeg:
126
+ Examples:
265
127
  ```sh
266
- ffmpeg -framerate 1 -pattern_type glob -i 'droid-cua-recording-*/frame_*.png' \
267
- -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \
268
- -c:v libx264 -pix_fmt yuv420p session.mp4
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'
@@ -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
- // Extract the command part (without the /)
15
- const commandPart = input.slice(1).toLowerCase();
16
- // Filter commands that match
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 }, "Available commands:"),
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':