@loadmill/droid-cua 2.37.0 → 2.38.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.
@@ -3,7 +3,7 @@ import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js
3
3
  import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
4
4
  import { buildActionEventMeta } from "../action-event-meta.js";
5
5
  import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
6
- import { buildMobileKeypressPayload, getCloudAndroidTransport, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
6
+ import { buildIosAppSwitcherGesture, buildMobileKeypressPayload, getCloudAndroidTransport, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
7
7
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
8
8
  import { resolveScrollGesture } from "../scroll-gesture.js";
9
9
  import { closeAppiumKeyboard, closeAppiumKeyboardWithTap } from "../keyboard.js";
@@ -149,6 +149,15 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
149
149
  });
150
150
  await session.client.pressIosButton(session.sessionId, transport.transportValue);
151
151
  }
152
+ else if (transport.transportType === "ios-gesture") {
153
+ const gesture = buildIosAppSwitcherGesture(await session.client.getWindowRect(session.sessionId));
154
+ addOutput({
155
+ type: "action",
156
+ text: normalized.logText,
157
+ ...meta(buildMobileKeypressPayload(normalized, transport, { gesture }))
158
+ });
159
+ await session.client.dragFromToForDuration(session.sessionId, gesture);
160
+ }
152
161
  else {
153
162
  addOutput({
154
163
  type: "action",
@@ -175,6 +175,12 @@ export class CloudAppiumClient {
175
175
  ]
176
176
  });
177
177
  }
178
+ async dragFromToForDuration(sessionId, gesture) {
179
+ await this.request("POST", `/session/${sessionId}/execute/sync`, {
180
+ script: "mobile: dragFromToForDuration",
181
+ args: [gesture]
182
+ });
183
+ }
178
184
  async pressIosButton(sessionId, buttonName) {
179
185
  await this.request("POST", `/session/${sessionId}/execute/sync`, {
180
186
  script: "mobile: pressButton",
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as appium from "./appium-client.js";
7
7
  import { buildActionEventMeta } from "../action-event-meta.js";
8
- import { buildMobileKeypressPayload, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
8
+ import { buildIosAppSwitcherGesture, buildMobileKeypressPayload, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
9
9
  import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
10
10
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
11
11
  import { logger } from "../../utils/logger.js";
@@ -151,6 +151,15 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
151
151
  });
152
152
  await appium.pressButton(session.sessionId, transport.transportValue);
153
153
  }
154
+ else if (transport.transportType === "ios-gesture") {
155
+ const gesture = buildIosAppSwitcherGesture(await appium.getWindowSize(session.sessionId));
156
+ addOutput({
157
+ type: "action",
158
+ text: normalized.logText,
159
+ ...meta(buildMobileKeypressPayload(normalized, transport, { gesture }))
160
+ });
161
+ await appium.dragFromToForDuration(session.sessionId, gesture);
162
+ }
154
163
  else {
155
164
  addOutput({
156
165
  type: "action",
@@ -206,6 +206,12 @@ export async function drag(sessionId, startX, startY, endX, endY, duration = 500
206
206
  ],
207
207
  });
208
208
  }
209
+ export async function dragFromToForDuration(sessionId, gesture) {
210
+ await appiumRequest("POST", `/session/${sessionId}/execute/sync`, {
211
+ script: "mobile: dragFromToForDuration",
212
+ args: [gesture],
213
+ });
214
+ }
209
215
  /**
210
216
  * Press a device button (home, volumeUp, volumeDown)
211
217
  * @param {string} sessionId
@@ -15,6 +15,7 @@ export function normalizeMobileKeypress(platform, keys = []) {
15
15
  ESC: { semanticKey: "IGNORE_ESC", logText: "Ignoring Escape on iOS" },
16
16
  ESCAPE: { semanticKey: "IGNORE_ESC", logText: "Ignoring Escape on iOS" },
17
17
  HOME: { semanticKey: "HOME", logText: "Pressing Home" },
18
+ APP_SWITCH: { semanticKey: "APP_SWITCH", logText: "Opening App Switcher" },
18
19
  ENTER: { semanticKey: "ENTER", logText: "Pressing Enter" },
19
20
  RETURN: { semanticKey: "ENTER", logText: "Pressing Enter" },
20
21
  BACKSPACE: { semanticKey: "BACKSPACE", logText: "Pressing Backspace" },
@@ -98,6 +99,8 @@ export function getIosTransport(normalized) {
98
99
  return { transportType: "noop", transportValue: null, ignored: true };
99
100
  case "HOME":
100
101
  return { transportType: "ios-button", transportValue: "home" };
102
+ case "APP_SWITCH":
103
+ return { transportType: "ios-gesture", transportValue: "mobile: dragFromToForDuration" };
101
104
  case "ENTER":
102
105
  return { transportType: "text", transportValue: "\n" };
103
106
  case "BACKSPACE":
@@ -108,6 +111,16 @@ export function getIosTransport(normalized) {
108
111
  throw new Error(`Unsupported iOS semantic key: ${normalized.semanticKey}`);
109
112
  }
110
113
  }
114
+ export function buildIosAppSwitcherGesture({ width, height }) {
115
+ const centerX = Math.round(width / 2);
116
+ return {
117
+ duration: 1,
118
+ fromX: centerX,
119
+ fromY: Math.round(height) - 5,
120
+ toX: centerX,
121
+ toY: Math.round(height / 2)
122
+ };
123
+ }
111
124
  export function buildMobileKeypressPayload(normalized, transport, extras = {}) {
112
125
  return {
113
126
  keys: [normalized.requestedKey],
@@ -130,12 +130,12 @@ const iosCloseKeyboardTool = {
130
130
  const loadmillTool = {
131
131
  type: "function",
132
132
  name: "loadmill",
133
- description: "Search Loadmill for the requested flow, run the best match, and wait for completion. Use this when the instruction explicitly mentions running or using a Loadmill flow.",
133
+ description: "Run a saved Loadmill API flow. Use this tool only when the current instruction contains the word 'Loadmill' (case-insensitive) or starts with the explicit 'loadmill:' directive. Never call it based only on generic words such as 'run', 'execute', 'flow', 'test', or 'script'. Without an explicit Loadmill marker, treat the instruction as a normal UI or browser test step.",
134
134
  strict: true,
135
135
  parameters: {
136
136
  type: "object",
137
137
  properties: {
138
- command: { type: "string" },
138
+ command: { type: "string", description: "The explicitly requested Loadmill flow name or intent and any parameters." },
139
139
  },
140
140
  required: ["command"],
141
141
  additionalProperties: false,
@@ -248,7 +248,7 @@ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
248
248
 
249
249
  Mobile-Specific Notes:
250
250
  - HOME key returns to the home screen
251
- - On Android, APP_SWITCH opens Recent Apps
251
+ - APP_SWITCH opens Recent Apps on Android and the App Switcher on iOS
252
252
  - On Android, ESC key maps to Back
253
253
  - On iOS, ESC has no effect; use visible on-screen controls instead
254
254
  - Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
@@ -83,7 +83,7 @@ ${browserTarget ? "" : ` - Every \`type\` action must end its computer-action
83
83
  - Otherwise, including when only a slim hardware-keyboard toolbar or suggestions are visible, continue without \`close_keyboard\`
84
84
  - Apply the same dismissal decision after \`type_sensitive_text\`, \`type_text_slowly\`, and \`type_secret_slowly\`
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
+ - Use the \`loadmill\` tool only when the current instruction contains the word "Loadmill" (case-insensitive) or starts with the explicit \`loadmill:\` directive. 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. Never infer Loadmill intent from generic words such as "run", "execute", "flow", "test", or "script". Without an explicit Loadmill marker, handle the instruction as a normal UI or browser test step. Do not handle an explicitly requested Loadmill flow by typing in the app UI.
87
87
  3. Execute the required actions
88
88
  4. ${browserTarget ? "Before tapping a mutating action, dismiss the keyboard if it is open and not needed" : "Use `close_keyboard` before any later interaction when the post-type dismissal rules require it"}
89
89
  5. After a mutating action, inspect the resulting screen for success cues such as cleared fields, reset buttons, changed status, refreshed content, or navigation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadmill/droid-cua",
3
- "version": "2.37.0",
3
+ "version": "2.38.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",