@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.
- package/build/src/device/cloud/actions.js +10 -1
- package/build/src/device/cloud/cloud-appium-client.js +6 -0
- package/build/src/device/ios/actions.js +10 -1
- package/build/src/device/ios/appium-client.js +6 -0
- package/build/src/device/mobile-keypress.js +13 -0
- package/build/src/device/openai.js +2 -2
- package/build/src/prompts/base.js +1 -1
- package/build/src/prompts/execution.js +1 -1
- package/package.json +1 -1
|
@@ -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: "
|
|
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
|
-
-
|
|
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
|
|
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
|