@loadmill/droid-cua 2.33.0 → 2.35.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 +18 -1
- package/THIRD_PARTY_NOTICES.md +5 -0
- package/build/index.js +43 -2
- package/build/src/cli/headless-runner.js +16 -6
- package/build/src/cli/headless-target.js +50 -3
- package/build/src/commands/help.js +5 -1
- package/build/src/core/execution-engine.js +5 -0
- package/build/src/core/session.js +5 -1
- package/build/src/core/test-data-import.js +2 -2
- package/build/src/core/tool-handlers/close-keyboard.js +3 -0
- package/build/src/core/tool-handlers/type-text-slowly.js +155 -0
- package/build/src/desktop/computer-use.js +432 -0
- package/build/src/device/cloud/actions.js +13 -3
- package/build/src/device/cloud/cloud-appium-client.js +6 -0
- package/build/src/device/ios/actions.js +21 -9
- package/build/src/device/ios/appium-client.js +14 -3
- package/build/src/device/keyboard.js +3 -4
- package/build/src/device/openai.js +50 -4
- package/build/src/modes/design-mode-ink.js +1 -0
- package/build/src/modes/design-mode.js +1 -0
- package/build/src/modes/execution-mode.js +1 -0
- package/build/src/prompts/base.js +43 -10
- package/build/src/prompts/execution.js +2 -1
- package/build/src/prompts/test-data-import.js +2 -2
- package/package.json +4 -2
|
@@ -103,7 +103,7 @@ const closeCurrentTabTool = {
|
|
|
103
103
|
const closeKeyboardTool = {
|
|
104
104
|
type: "function",
|
|
105
105
|
name: "close_keyboard",
|
|
106
|
-
description: "Close the mobile software keyboard after text entry. Call this when a full software keyboard is visible
|
|
106
|
+
description: "Close the mobile software keyboard after text entry. Call this when a full software keyboard is visible. Do not call it for a slim hardware-keyboard toolbar or suggestions alone.",
|
|
107
107
|
strict: false,
|
|
108
108
|
parameters: {
|
|
109
109
|
type: "object",
|
|
@@ -115,6 +115,18 @@ const closeKeyboardTool = {
|
|
|
115
115
|
additionalProperties: false,
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
|
+
const iosCloseKeyboardTool = {
|
|
119
|
+
...closeKeyboardTool,
|
|
120
|
+
description: "Close the iOS software keyboard after text entry by tapping a safe dismissal point from the current screenshot. Call this when a full software keyboard is visible, or after typing into a secure field even when its keyboard is hidden. Prefer a dedicated keyboard dismissal control or clearly noninteractive background. Never target Return, Go, Search, Next, Continue, Send, or an app action.",
|
|
121
|
+
parameters: {
|
|
122
|
+
...closeKeyboardTool.parameters,
|
|
123
|
+
properties: {
|
|
124
|
+
x: { type: "number", description: "Horizontal coordinate of the dismissal point in the current screenshot." },
|
|
125
|
+
y: { type: "number", description: "Vertical coordinate of the dismissal point in the current screenshot." },
|
|
126
|
+
},
|
|
127
|
+
required: ["x", "y"],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
118
130
|
const loadmillTool = {
|
|
119
131
|
type: "function",
|
|
120
132
|
name: "loadmill",
|
|
@@ -143,6 +155,34 @@ const typeSensitiveTextTool = {
|
|
|
143
155
|
additionalProperties: false,
|
|
144
156
|
},
|
|
145
157
|
};
|
|
158
|
+
const typeTextSlowlyTool = {
|
|
159
|
+
type: "function",
|
|
160
|
+
name: "type_text_slowly",
|
|
161
|
+
description: "Type literal text into the currently focused iOS field using the slower XCTest compatibility input method. Use this when app context says normal typing may drop characters, or after a screenshot shows incomplete input. This method can still drop characters, so inspect the returned screenshot. It does not clear existing text; clear the field first when retrying.",
|
|
162
|
+
strict: true,
|
|
163
|
+
parameters: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
text: { type: "string" },
|
|
167
|
+
},
|
|
168
|
+
required: ["text"],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const typeSecretSlowlyTool = {
|
|
173
|
+
type: "function",
|
|
174
|
+
name: "type_secret_slowly",
|
|
175
|
+
description: "Type a listed secret into the currently focused iOS field using the slower XCTest compatibility input method. Use this when app context says normal typing may drop characters, or when separate evidence shows normal secret entry failed. This method can still drop characters and does not clear existing text; clear the field first when retrying.",
|
|
176
|
+
strict: true,
|
|
177
|
+
parameters: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
secretKey: { type: "string" },
|
|
181
|
+
},
|
|
182
|
+
required: ["secretKey"],
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
146
186
|
// Design completion is the handoff into script generation; it is separate from
|
|
147
187
|
// execution step-completion markers used by model adapters.
|
|
148
188
|
const testDesignCompleteTool = {
|
|
@@ -177,7 +217,7 @@ const testDesignCompleteTool = {
|
|
|
177
217
|
additionalProperties: false,
|
|
178
218
|
},
|
|
179
219
|
};
|
|
180
|
-
export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, toolCapabilities = {}, } = {}) {
|
|
220
|
+
export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, toolCapabilities = {}, deviceInfo = null, } = {}) {
|
|
181
221
|
const tools = [];
|
|
182
222
|
if (aimAssistMode !== "off" && toolCapabilities.previewClick !== false) {
|
|
183
223
|
tools.push(previewClickTool);
|
|
@@ -192,7 +232,7 @@ export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, to
|
|
|
192
232
|
tools.push(closeCurrentTabTool);
|
|
193
233
|
}
|
|
194
234
|
if (toolCapabilities.closeKeyboard) {
|
|
195
|
-
tools.push(closeKeyboardTool);
|
|
235
|
+
tools.push(deviceInfo?.platform?.toLowerCase() === "ios" ? iosCloseKeyboardTool : closeKeyboardTool);
|
|
196
236
|
}
|
|
197
237
|
if (toolCapabilities.loadmill) {
|
|
198
238
|
tools.push(loadmillTool);
|
|
@@ -200,6 +240,12 @@ export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, to
|
|
|
200
240
|
if (toolCapabilities.typeSensitiveText) {
|
|
201
241
|
tools.push(typeSensitiveTextTool);
|
|
202
242
|
}
|
|
243
|
+
if (toolCapabilities.typeTextSlowly) {
|
|
244
|
+
tools.push(typeTextSlowlyTool);
|
|
245
|
+
}
|
|
246
|
+
if (toolCapabilities.typeTextSlowly && toolCapabilities.typeSensitiveText) {
|
|
247
|
+
tools.push(typeSecretSlowlyTool);
|
|
248
|
+
}
|
|
203
249
|
if (toolCapabilities.testDesignComplete) {
|
|
204
250
|
tools.push(testDesignCompleteTool);
|
|
205
251
|
}
|
|
@@ -333,7 +379,7 @@ export function buildCuaRequestParams({ cuaModel, aimAssistMode = DEFAULT_AIM_AS
|
|
|
333
379
|
truncation: "auto",
|
|
334
380
|
tools: [
|
|
335
381
|
...(includeComputerTool ? [buildComputerTool({ cuaModel, deviceInfo })] : []),
|
|
336
|
-
...buildFunctionTools({ aimAssistMode, toolCapabilities }),
|
|
382
|
+
...buildFunctionTools({ aimAssistMode, toolCapabilities, deviceInfo }),
|
|
337
383
|
],
|
|
338
384
|
};
|
|
339
385
|
}
|
|
@@ -42,6 +42,7 @@ export class DesignModeInk {
|
|
|
42
42
|
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
43
43
|
closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
|
|
44
44
|
closeKeyboard: this.session?.deviceInfo?.platform !== "browser",
|
|
45
|
+
typeTextSlowly: this.session?.deviceInfo?.platform === "ios",
|
|
45
46
|
loadmill: false,
|
|
46
47
|
typeSensitiveText: this.engine.hasSecrets(),
|
|
47
48
|
testDesignComplete: true,
|
|
@@ -39,6 +39,7 @@ export class DesignMode {
|
|
|
39
39
|
navigate: this.session?.deviceInfo?.platform === "browser",
|
|
40
40
|
closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
|
|
41
41
|
closeKeyboard: this.session?.deviceInfo?.platform !== "browser",
|
|
42
|
+
typeTextSlowly: this.session?.deviceInfo?.platform === "ios",
|
|
42
43
|
loadmill: false,
|
|
43
44
|
typeSensitiveText: this.engine.hasSecrets(),
|
|
44
45
|
testDesignComplete: true,
|
|
@@ -96,6 +96,7 @@ export class ExecutionMode {
|
|
|
96
96
|
navigate: this.isBrowserSession,
|
|
97
97
|
closeCurrentTab: this.isBrowserSession,
|
|
98
98
|
closeKeyboard: !this.isBrowserSession,
|
|
99
|
+
typeTextSlowly: this.session?.deviceInfo?.platform === "ios",
|
|
99
100
|
loadmill: true,
|
|
100
101
|
typeSensitiveText: this.engine.hasSecrets(),
|
|
101
102
|
};
|
|
@@ -30,6 +30,9 @@ ${customSection}
|
|
|
30
30
|
function isBrowserTarget(deviceInfo = {}) {
|
|
31
31
|
return typeof deviceInfo.platform === "string" && deviceInfo.platform.trim().toLowerCase() === "browser";
|
|
32
32
|
}
|
|
33
|
+
function isDesktopTarget(deviceInfo = {}) {
|
|
34
|
+
return typeof deviceInfo.platform === "string" && deviceInfo.platform.trim().toLowerCase() === "desktop";
|
|
35
|
+
}
|
|
33
36
|
function isCloudIosTarget(deviceInfo = {}) {
|
|
34
37
|
const platform = deviceInfo.platform?.trim().toLowerCase();
|
|
35
38
|
const source = deviceInfo.source?.trim().toLowerCase() || "local";
|
|
@@ -42,6 +45,9 @@ function describeControlledTarget(deviceInfo = {}) {
|
|
|
42
45
|
if (platform === "browser") {
|
|
43
46
|
return browserName ? `a browser session (${browserName})` : "a browser session";
|
|
44
47
|
}
|
|
48
|
+
if (platform === "desktop") {
|
|
49
|
+
return deviceName ? `a desktop application (${deviceName})` : "a desktop application";
|
|
50
|
+
}
|
|
45
51
|
if (platform === "ios") {
|
|
46
52
|
const target = isCloudIosTarget(deviceInfo) ? "an iOS device" : "an iOS simulator";
|
|
47
53
|
return deviceName ? `${target} (${deviceName})` : target;
|
|
@@ -156,11 +162,10 @@ function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
156
162
|
}
|
|
157
163
|
function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
158
164
|
const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
|
|
159
|
-
const
|
|
160
|
-
- On
|
|
161
|
-
-
|
|
165
|
+
const iosKeyboardGuidance = deviceInfo.platform?.trim().toLowerCase() === "ios" ? `
|
|
166
|
+
- On iOS, \`close_keyboard\` requires \`x\` and \`y\` coordinates from the current screenshot and taps that point.
|
|
167
|
+
- Prefer a dedicated keyboard dismissal control. Otherwise choose a clearly noninteractive background that you trust.
|
|
162
168
|
- Never use Return, Go, Search, Next, Continue, Send, or an app action as the dismissal point because it may submit or mutate the app.
|
|
163
|
-
- If dismissal is required but no safe point is identifiable, call \`close_keyboard\` without coordinates. Do not guess.
|
|
164
169
|
- Choose coordinates directly from the post-type screenshot; do not call \`preview_click\` between \`type\` and \`close_keyboard\`.` : "";
|
|
165
170
|
return `
|
|
166
171
|
You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
|
|
@@ -188,14 +193,14 @@ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
188
193
|
- If a full software keyboard is visibly covering part of the screen, your next action must be \`close_keyboard\`.
|
|
189
194
|
- Do not finish the current instruction while a required \`close_keyboard\` action is still pending.
|
|
190
195
|
- A slim hardware-keyboard toolbar, autocomplete suggestions, or a focused field without a full software keyboard does not require \`close_keyboard\`.
|
|
191
|
-
- Treat \`type_sensitive_text\` as text entry too and apply the same dismissal decision after
|
|
196
|
+
- Treat \`type_sensitive_text\`, \`type_text_slowly\`, and \`type_secret_slowly\` as text entry too and apply the same dismissal decision after any of them returns.
|
|
192
197
|
- Whenever dismissal is required, do not click, scroll, inspect, submit, type again, issue a keypress, or call another tool before \`close_keyboard\`.
|
|
193
198
|
- Consecutive DELETE or BACKSPACE keypresses may be used for correction without closing the keyboard between them. After typing the replacement, call \`close_keyboard\`.
|
|
194
199
|
- Password and other secure fields may appear empty in screenshots even after text was entered successfully.
|
|
195
200
|
- On iOS, a secure field may also hide an open keyboard from Appium screenshots. After typing into an iOS secure field, call \`close_keyboard\` even when the keyboard is not visible.
|
|
196
201
|
- Do not type into a secure field again solely because its characters or bullets are not visible.
|
|
197
202
|
- Retry secure-field input only when there is separate evidence of failure, such as another field changing or the app rejecting the value.
|
|
198
|
-
${
|
|
203
|
+
${iosKeyboardGuidance}
|
|
199
204
|
- Prefer tapping visible controls over hardware key events.
|
|
200
205
|
- Prefer on-screen navigation controls such as menus, tabs, drawer items, back arrows, close buttons, and explicit logout buttons over keypress actions.
|
|
201
206
|
- Do NOT use Back or ESC for normal app navigation when a reliable on-screen control is visible.
|
|
@@ -249,9 +254,37 @@ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
249
254
|
- Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
|
|
250
255
|
`;
|
|
251
256
|
}
|
|
257
|
+
function buildDesktopBaseSystemPrompt(deviceInfo, options = {}) {
|
|
258
|
+
let browserPrompt = buildBrowserBaseSystemPrompt(deviceInfo, options)
|
|
259
|
+
.replace("Follow the user's instructions to interact with the browser.", "Follow the user's instructions to interact with the selected application.")
|
|
260
|
+
.replace("visible browser window", "selected application window")
|
|
261
|
+
.replace("CRITICAL - Desktop Browser Input Constraints:", "CRITICAL - Desktop Input Constraints:")
|
|
262
|
+
.replace("This is a desktop browser environment.", "This is a desktop application environment.");
|
|
263
|
+
if (deviceInfo.desktopInputMode !== "physical") {
|
|
264
|
+
browserPrompt = browserPrompt
|
|
265
|
+
.replace("Available actions: click, scroll, type, keypress, wait, screenshot.", "Available actions: click, scroll, wait, screenshot.")
|
|
266
|
+
.replace("Standard desktop keys and modifier chords are allowed when they are clearly useful.", "Keyboard input is unavailable because it would focus the application.")
|
|
267
|
+
.replace("Prefer clicking visible controls over keyboard shortcuts when both are equally reliable.", "Use visible application controls for every interaction.")
|
|
268
|
+
.replace("Use keypress chords only when they directly help the task, such as Enter to submit, Escape to dismiss a modal, or Control/Meta shortcuts for focused text fields.", "Use visible application controls for submit, dismiss, and correction actions.");
|
|
269
|
+
browserPrompt = browserPrompt.replace("After EVERY action (click, type, keypress, scroll)", "After EVERY action (click, scroll)");
|
|
270
|
+
}
|
|
271
|
+
const notesIndex = browserPrompt.indexOf(" Browser-Specific Notes:");
|
|
272
|
+
const withoutBrowserNotes = notesIndex === -1 ? browserPrompt : browserPrompt.slice(0, notesIndex);
|
|
273
|
+
const inputNote = deviceInfo.desktopInputMode === "physical"
|
|
274
|
+
? "Droid may use the physical OS pointer for this run"
|
|
275
|
+
: "Cursor-free mode never focuses the application: use only clicks and scrolling on controls with matching accessibility actions; never emit type, keypress, move, hover, or drag";
|
|
276
|
+
return `${withoutBrowserNotes} Desktop-Specific Notes:
|
|
277
|
+
- Droid is controlling only the selected application window, not the whole display
|
|
278
|
+
- ${inputNote}
|
|
279
|
+
- Do not use shortcuts that switch applications, virtual desktops, or operating-system context
|
|
280
|
+
- Dialogs owned by the selected application may be controlled when they become its active window
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
252
283
|
function buildBaseSystemPrompt(deviceInfo, options = {}) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
if (isBrowserTarget(deviceInfo))
|
|
285
|
+
return buildBrowserBaseSystemPrompt(deviceInfo, options);
|
|
286
|
+
if (isDesktopTarget(deviceInfo))
|
|
287
|
+
return buildDesktopBaseSystemPrompt(deviceInfo, options);
|
|
288
|
+
return buildMobileBaseSystemPrompt(deviceInfo, options);
|
|
256
289
|
}
|
|
257
|
-
export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, };
|
|
290
|
+
export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, isDesktopTarget, };
|
|
@@ -32,6 +32,7 @@ export function buildSecretKeysContextSection(keys = []) {
|
|
|
32
32
|
return `SECRET KEYS FOR THIS RUN:
|
|
33
33
|
The following secret keys are available. You can see only the keys, not the values.
|
|
34
34
|
Use the \`type_sensitive_text\` tool when you need to enter passwords, OTPs, tokens, API keys, or other hidden credentials.
|
|
35
|
+
When the iOS \`type_secret_slowly\` tool is available and slow typing is needed, pass it the matching \`secretKey\`.
|
|
35
36
|
Never ask the user to reveal these values and never type placeholder text when a matching secret key is available.
|
|
36
37
|
|
|
37
38
|
${normalized.map((key) => `- ${key}`).join("\n")}`;
|
|
@@ -80,7 +81,7 @@ ${browserTarget ? "" : ` - Every \`type\` action must end its computer-action
|
|
|
80
81
|
- Call \`close_keyboard\` next when a full software keyboard is visible, or after typing into an iOS secure field even when its keyboard is hidden
|
|
81
82
|
- Do not complete the current instruction while that required \`close_keyboard\` action is pending
|
|
82
83
|
- Otherwise, including when only a slim hardware-keyboard toolbar or suggestions are visible, continue without \`close_keyboard\`
|
|
83
|
-
- Apply the same dismissal decision after \`type_sensitive_text\`
|
|
84
|
+
- Apply the same dismissal decision after \`type_sensitive_text\`, \`type_text_slowly\`, and \`type_secret_slowly\`
|
|
84
85
|
`}
|
|
85
86
|
- Use the \`loadmill\` tool whenever the instruction explicitly references running or using a Loadmill flow. Pass the requested flow name/intent and parameters as the command; the tool will search Loadmill, select the best matching flow, run it, and wait for completion. Do not handle Loadmill by typing in the app UI.
|
|
86
87
|
3. Execute the required actions
|
|
@@ -17,8 +17,8 @@ export function buildTestDataImportMessages({ currentContent = "", importedFileN
|
|
|
17
17
|
"- Do not invent values that are not present in the current file, imported file, or user instructions.",
|
|
18
18
|
"- If currentContent is template-only, replace it with real imported data and do not keep commented template examples.",
|
|
19
19
|
"- If currentContent is not template-only, preserve its useful structure and add or update entities within it.",
|
|
20
|
-
"- For
|
|
21
|
-
"- If
|
|
20
|
+
"- For tabular or nested data, preserve meaningful groups and relationships. Split repeated dimensions into separate entities when the structure is clear.",
|
|
21
|
+
"- If records are indivisible scenarios or examples, keep them grouped as Test Data Entries or Use Case Data.",
|
|
22
22
|
"- User instructions can guide structure and naming, but content must remain concrete data, not test instructions.",
|
|
23
23
|
"- Output markdown only inside the JSON content string; do not use code fences.",
|
|
24
24
|
].join("\n")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmill/droid-cua",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
4
4
|
"description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"bin/**/*",
|
|
13
13
|
"skill/**/*",
|
|
14
14
|
"README.md",
|
|
15
|
+
"THIRD_PARTY_NOTICES.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"license": "UNLICENSED",
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"@aws-sdk/client-device-farm": "^3.1065.0",
|
|
67
|
+
"@zavora-ai/computer-use-mcp": "7.1.0",
|
|
66
68
|
"dotenv": "^16.4.7",
|
|
67
69
|
"ink": "^4.4.1",
|
|
68
70
|
"ink-spinner": "^5.0.0",
|
|
@@ -78,6 +80,6 @@
|
|
|
78
80
|
"typescript": "^5.7.3"
|
|
79
81
|
},
|
|
80
82
|
"engines": {
|
|
81
|
-
"node": ">=
|
|
83
|
+
"node": ">=20.0.0"
|
|
82
84
|
}
|
|
83
85
|
}
|