@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.
@@ -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, or after typing into an iOS secure field even when its keyboard is hidden. Do not call it for a slim hardware-keyboard toolbar or suggestions alone. On cloud iOS, optionally provide x and y for a dismissal control or clearly noninteractive background that you trust; never target Return, Go, Search, Next, Continue, Send, or an app action.",
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 cloudIosKeyboardGuidance = isCloudIosTarget(deviceInfo) ? `
160
- - On this cloud iOS device, \`close_keyboard\` can optionally tap a point you choose from the current screenshot.
161
- - If you can identify a dedicated keyboard dismissal control or clearly noninteractive background that you trust, pass its \`x\` and \`y\` coordinates to \`close_keyboard\`.
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 it returns.
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
- ${cloudIosKeyboardGuidance}
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
- return isBrowserTarget(deviceInfo)
254
- ? buildBrowserBaseSystemPrompt(deviceInfo, options)
255
- : buildMobileBaseSystemPrompt(deviceInfo, options);
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 matrix-like CSV rows, prefer splitting repeated dimensions into separate entities when the structure is clear.",
21
- "- If rows are indivisible scenarios or examples, keep them grouped as Test Data Entries or Use Case Data.",
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.33.0",
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": ">=18.17.0"
83
+ "node": ">=20.0.0"
82
84
  }
83
85
  }