@loadmill/droid-cua 2.32.0 → 2.34.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/core/execution-engine.js +5 -0
- 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/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/mobile-keypress.js +3 -0
- 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 +6 -6
- package/build/src/prompts/execution.js +2 -1
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ import { handleAssertionToolCall } from "./tool-handlers/assert.js";
|
|
|
19
19
|
import { handleLoadmillToolCall } from "./tool-handlers/loadmill.js";
|
|
20
20
|
import { handleTestDesignCompleteToolCall } from "./tool-handlers/test-design-complete.js";
|
|
21
21
|
import { handleTypeSensitiveTextToolCall } from "./tool-handlers/type-sensitive-text.js";
|
|
22
|
+
import { handleTypeSecretSlowlyToolCall, handleTypeTextSlowlyToolCall, } from "./tool-handlers/type-text-slowly.js";
|
|
22
23
|
import { buildDebugContext, buildTextToolOutputItem, } from "./tool-handlers/utils.js";
|
|
23
24
|
const HOLO_RESPONSE_ID_PREFIX = "resp_holo_";
|
|
24
25
|
function isHoloResponseId(responseId) {
|
|
@@ -334,6 +335,10 @@ export class ExecutionEngine {
|
|
|
334
335
|
return await handleLoadmillToolCall(handlerArgs);
|
|
335
336
|
case "type_sensitive_text":
|
|
336
337
|
return await handleTypeSensitiveTextToolCall(handlerArgs);
|
|
338
|
+
case "type_text_slowly":
|
|
339
|
+
return await handleTypeTextSlowlyToolCall(handlerArgs);
|
|
340
|
+
case "type_secret_slowly":
|
|
341
|
+
return await handleTypeSecretSlowlyToolCall(handlerArgs);
|
|
337
342
|
case "test_design_complete":
|
|
338
343
|
return await handleTestDesignCompleteToolCall(handlerArgs);
|
|
339
344
|
default:
|
|
@@ -11,6 +11,9 @@ function parseDismissalPoint(argumentsText, deviceInfo = {}) {
|
|
|
11
11
|
const hasX = Object.prototype.hasOwnProperty.call(parsed.value, "x");
|
|
12
12
|
const hasY = Object.prototype.hasOwnProperty.call(parsed.value, "y");
|
|
13
13
|
if (!hasX && !hasY) {
|
|
14
|
+
if (deviceInfo.platform?.trim().toLowerCase() === "ios") {
|
|
15
|
+
return { ok: false, message: "Invalid close_keyboard arguments: x and y are required on iOS." };
|
|
16
|
+
}
|
|
14
17
|
return { ok: true, value: null };
|
|
15
18
|
}
|
|
16
19
|
if (!hasX || !hasY || !Number.isFinite(parsed.value.x) || !Number.isFinite(parsed.value.y)) {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CAPTURE_SOURCES } from "../capture-sources.js";
|
|
3
|
+
import { emitDesktopDebug } from "../../utils/desktop-debug.js";
|
|
4
|
+
import { buildFunctionCallOutputItem, buildToolOutputContent, buildTextToolOutputItem, parseJsonArguments, parseStringToolArgument, } from "./utils.js";
|
|
5
|
+
const SUPPORTED_TEXT_PATTERN = /^[\x20-\x7e]+$/;
|
|
6
|
+
export async function handleTypeTextSlowlyToolCall({ engine, callId, argumentsText, context = null, stepContext = null, }) {
|
|
7
|
+
if (engine.session?.deviceInfo?.platform !== "ios") {
|
|
8
|
+
return continueWithMessage(callId, "type_text_slowly is only supported for iOS runs.");
|
|
9
|
+
}
|
|
10
|
+
const parsed = parseLiteralText(argumentsText);
|
|
11
|
+
if (!parsed.ok) {
|
|
12
|
+
return continueWithMessage(callId, parsed.message);
|
|
13
|
+
}
|
|
14
|
+
return runSlowTyping({
|
|
15
|
+
engine,
|
|
16
|
+
callId,
|
|
17
|
+
context,
|
|
18
|
+
stepContext,
|
|
19
|
+
action: { type: "type", text: parsed.text, typingMode: "slow" },
|
|
20
|
+
resultText: "Typed text slowly.",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function handleTypeSecretSlowlyToolCall({ engine, callId, argumentsText, context = null, stepContext = null, }) {
|
|
24
|
+
if (engine.session?.deviceInfo?.platform !== "ios") {
|
|
25
|
+
return continueWithMessage(callId, "type_secret_slowly is only supported for iOS runs.");
|
|
26
|
+
}
|
|
27
|
+
const parsed = parseStringToolArgument(argumentsText, "secretKey", "type_secret_slowly");
|
|
28
|
+
if (!parsed.ok) {
|
|
29
|
+
return continueWithMessage(callId, parsed.message);
|
|
30
|
+
}
|
|
31
|
+
const resolved = resolveSecretAction(engine, parsed.value);
|
|
32
|
+
if (!resolved.ok) {
|
|
33
|
+
return continueWithMessage(callId, resolved.message);
|
|
34
|
+
}
|
|
35
|
+
return runSlowTyping({
|
|
36
|
+
engine,
|
|
37
|
+
callId,
|
|
38
|
+
context,
|
|
39
|
+
stepContext,
|
|
40
|
+
action: resolved.action,
|
|
41
|
+
resultText: `Typed secret slowly: ${parsed.value}.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function runSlowTyping({ engine, callId, context, stepContext, action, resultText }) {
|
|
45
|
+
const typed = await executeSlowTyping(engine, action, context, stepContext);
|
|
46
|
+
if (!typed) {
|
|
47
|
+
return { kind: "fail", error: "Slow iOS typing failed." };
|
|
48
|
+
}
|
|
49
|
+
const { screenshotBase64, currentScreenshot } = await capturePostTypingScreenshot(engine, callId, context, stepContext);
|
|
50
|
+
return {
|
|
51
|
+
kind: "continue",
|
|
52
|
+
outputItem: buildFunctionCallOutputItem(callId, buildToolOutputContent({
|
|
53
|
+
text: resultText,
|
|
54
|
+
imageBase64: screenshotBase64,
|
|
55
|
+
})),
|
|
56
|
+
currentScreenshot,
|
|
57
|
+
executedActions: [action],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function continueWithMessage(callId, message) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "continue",
|
|
63
|
+
outputItem: buildTextToolOutputItem(callId, message),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function parseLiteralText(argumentsText) {
|
|
67
|
+
const parsed = parseJsonArguments(argumentsText);
|
|
68
|
+
const text = parsed.ok ? parsed.value?.text : null;
|
|
69
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
message: 'Invalid type_text_slowly arguments: "text" must be a non-empty string.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!SUPPORTED_TEXT_PATTERN.test(text)) {
|
|
76
|
+
return { ok: false, message: "type_text_slowly currently supports printable ASCII text only." };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, text };
|
|
79
|
+
}
|
|
80
|
+
function resolveSecretAction(engine, secretKey) {
|
|
81
|
+
const secretsByKey = engine.secretsByKey && typeof engine.secretsByKey === "object" ? engine.secretsByKey : {};
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(secretsByKey, secretKey)) {
|
|
83
|
+
return { ok: false, message: `Secret key not found: ${secretKey}` };
|
|
84
|
+
}
|
|
85
|
+
const text = String(secretsByKey[secretKey] ?? "");
|
|
86
|
+
if (!SUPPORTED_TEXT_PATTERN.test(text)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
message: `Secret ${secretKey} cannot be typed slowly because its value is empty or contains unsupported characters.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
action: {
|
|
95
|
+
type: "type",
|
|
96
|
+
text,
|
|
97
|
+
sensitive: true,
|
|
98
|
+
sensitiveKey: secretKey,
|
|
99
|
+
typingMode: "slow",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function executeSlowTyping(engine, action, context, stepContext) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await engine.handleModelAction(engine.session.deviceId, action, engine.session.deviceInfo.scale, {
|
|
106
|
+
...context,
|
|
107
|
+
stepId: stepContext?.stepId,
|
|
108
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
109
|
+
eventId: randomUUID(),
|
|
110
|
+
});
|
|
111
|
+
return result?.success !== false;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function capturePostTypingScreenshot(engine, callId, context, stepContext) {
|
|
118
|
+
const screenshotBase64 = await engine.getScreenshotAsBase64(engine.session.deviceId, engine.session.deviceInfo);
|
|
119
|
+
const screenshotEventId = randomUUID();
|
|
120
|
+
emitDesktopDebug("device.screenshot", "device", {
|
|
121
|
+
runId: context?.runId,
|
|
122
|
+
stepId: stepContext?.stepId,
|
|
123
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
124
|
+
}, {
|
|
125
|
+
source: "post_action",
|
|
126
|
+
callId,
|
|
127
|
+
width: engine.session.deviceInfo?.scaled_width,
|
|
128
|
+
height: engine.session.deviceInfo?.scaled_height,
|
|
129
|
+
base64Length: screenshotBase64.length,
|
|
130
|
+
});
|
|
131
|
+
const currentScreenshot = await engine.recordScreenshot(screenshotBase64, {
|
|
132
|
+
runId: context?.runId,
|
|
133
|
+
sessionId: context?.sessionId,
|
|
134
|
+
stepId: stepContext?.stepId,
|
|
135
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
136
|
+
eventId: screenshotEventId,
|
|
137
|
+
captureSource: CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION,
|
|
138
|
+
});
|
|
139
|
+
context?.addOutput?.({
|
|
140
|
+
type: "info",
|
|
141
|
+
text: "Capturing screen",
|
|
142
|
+
eventType: "screenshot_captured",
|
|
143
|
+
actionType: "screenshot",
|
|
144
|
+
runId: context?.runId,
|
|
145
|
+
stepId: stepContext?.stepId,
|
|
146
|
+
instructionIndex: stepContext?.instructionIndex,
|
|
147
|
+
payload: {
|
|
148
|
+
eventId: screenshotEventId,
|
|
149
|
+
captureSource: CAPTURE_SOURCES.CALL_OUTPUT_POST_ACTION,
|
|
150
|
+
source: "post_action",
|
|
151
|
+
...(currentScreenshot?.screenshotPath ? { screenshotPath: currentScreenshot.screenshotPath } : {}),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
return { screenshotBase64, currentScreenshot };
|
|
155
|
+
}
|
|
@@ -77,12 +77,22 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
|
|
|
77
77
|
break;
|
|
78
78
|
}
|
|
79
79
|
case "type": {
|
|
80
|
+
const typingSlowly = platform === "ios" && action.typingMode === "slow";
|
|
81
|
+
const actionText = buildTypeActionText(action, action.text);
|
|
80
82
|
addOutput({
|
|
81
83
|
type: "action",
|
|
82
|
-
text:
|
|
83
|
-
...meta(
|
|
84
|
+
text: typingSlowly ? actionText.replace(":", " slowly:") : actionText,
|
|
85
|
+
...meta({
|
|
86
|
+
...buildTypeActionPayload(action, action.text),
|
|
87
|
+
...(typingSlowly ? { typingMode: "slow" } : {})
|
|
88
|
+
})
|
|
84
89
|
});
|
|
85
|
-
|
|
90
|
+
if (typingSlowly) {
|
|
91
|
+
await session.client.typeSlowly(session.sessionId, action.text);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await session.client.type(session.sessionId, action.text);
|
|
95
|
+
}
|
|
86
96
|
break;
|
|
87
97
|
}
|
|
88
98
|
case "scroll": {
|
|
@@ -119,6 +119,12 @@ export class CloudAppiumClient {
|
|
|
119
119
|
]
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
async typeSlowly(sessionId, text) {
|
|
123
|
+
await this.request("POST", `/session/${sessionId}/execute/sync`, {
|
|
124
|
+
script: "mobile: keys",
|
|
125
|
+
args: [{ keys: [...text] }]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
122
128
|
async isKeyboardShown(sessionId) {
|
|
123
129
|
const response = await this.request("POST", `/session/${sessionId}/execute/sync`, {
|
|
124
130
|
script: "mobile: isKeyboardShown",
|
|
@@ -12,17 +12,19 @@ import { logger } from "../../utils/logger.js";
|
|
|
12
12
|
import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
|
|
13
13
|
import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
|
|
14
14
|
import { resolveScrollGesture } from "../scroll-gesture.js";
|
|
15
|
-
import {
|
|
16
|
-
export async function closeKeyboard() {
|
|
15
|
+
import { closeAppiumKeyboardWithTap } from "../keyboard.js";
|
|
16
|
+
export async function closeKeyboard(_deviceId, { dismissalPoint = null, scale = 1 } = {}) {
|
|
17
17
|
const session = getActiveSession();
|
|
18
18
|
if (!session) {
|
|
19
19
|
throw new Error("No active iOS session");
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const dpr = getDevicePixelRatio();
|
|
22
|
+
const tap = dismissalPoint
|
|
23
|
+
? () => appium.tap(session.sessionId, Math.round((dismissalPoint.x / scale) / dpr), Math.round((dismissalPoint.y / scale) / dpr))
|
|
24
|
+
: null;
|
|
25
|
+
return closeAppiumKeyboardWithTap({
|
|
24
26
|
isKeyboardShown: () => appium.isKeyboardShown(session.sessionId),
|
|
25
|
-
|
|
27
|
+
tap,
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
@@ -74,12 +76,22 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
|
|
|
74
76
|
break;
|
|
75
77
|
}
|
|
76
78
|
case "type": {
|
|
79
|
+
const typingSlowly = action.typingMode === "slow";
|
|
80
|
+
const actionText = buildTypeActionText(action, action.text);
|
|
77
81
|
addOutput({
|
|
78
82
|
type: "action",
|
|
79
|
-
text:
|
|
80
|
-
...meta(
|
|
83
|
+
text: typingSlowly ? actionText.replace(":", " slowly:") : actionText,
|
|
84
|
+
...meta({
|
|
85
|
+
...buildTypeActionPayload(action, action.text),
|
|
86
|
+
...(typingSlowly ? { typingMode: "slow" } : {})
|
|
87
|
+
})
|
|
81
88
|
});
|
|
82
|
-
|
|
89
|
+
if (typingSlowly) {
|
|
90
|
+
await appium.typeSlowly(session.sessionId, action.text);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await appium.type(session.sessionId, action.text);
|
|
94
|
+
}
|
|
83
95
|
break;
|
|
84
96
|
}
|
|
85
97
|
case "scroll": {
|
|
@@ -123,6 +123,18 @@ export async function type(sessionId, text) {
|
|
|
123
123
|
],
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Type text one key at a time through XCUITest's keyboard input API.
|
|
128
|
+
* @param {string} sessionId
|
|
129
|
+
* @param {string} text
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
export async function typeSlowly(sessionId, text) {
|
|
133
|
+
await appiumRequest("POST", `/session/${sessionId}/execute/sync`, {
|
|
134
|
+
script: "mobile: keys",
|
|
135
|
+
args: [{ keys: [...text] }],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
126
138
|
export async function isKeyboardShown(sessionId) {
|
|
127
139
|
const response = await appiumRequest("POST", `/session/${sessionId}/execute/sync`, {
|
|
128
140
|
script: "mobile: isKeyboardShown",
|
|
@@ -131,9 +143,8 @@ export async function isKeyboardShown(sessionId) {
|
|
|
131
143
|
return response.value === true;
|
|
132
144
|
}
|
|
133
145
|
export async function hideKeyboard(sessionId) {
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// unreliable there and sending Return directly can submit the current form.
|
|
146
|
+
// The CUA close_keyboard path uses a model-selected tap because this command
|
|
147
|
+
// may send Return. Keep the low-level command available for direct callers.
|
|
137
148
|
await appiumRequest("POST", `/session/${sessionId}/execute/sync`, {
|
|
138
149
|
script: "mobile: hideKeyboard",
|
|
139
150
|
args: [{ keys: ["return"] }],
|
|
@@ -6,10 +6,9 @@ import { getConfiguredStepDelayMs } from "../utils/step-delay.js";
|
|
|
6
6
|
* navigate away. Android's mInputShown flag stays true when a hardware
|
|
7
7
|
* keyboard suppresses the software keyboard, so that configuration is
|
|
8
8
|
* treated as already closed before Back is considered.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* unreliable on the tested real devices, while Return/newline could submit a
|
|
9
|
+
* - Cloud Android uses Appium's native keyboard command.
|
|
10
|
+
* - Local and cloud iOS use a model-selected tap. XCUITest's hideKeyboard
|
|
11
|
+
* command was unreliable in testing, while Return/newline could submit a
|
|
13
12
|
* form. The caller therefore supplies a trusted dismissal point instead.
|
|
14
13
|
*
|
|
15
14
|
* Every strategy follows the same query -> dismiss -> wait -> verify shape and
|
|
@@ -37,6 +37,7 @@ export function normalizeMobileKeypress(platform, keys = []) {
|
|
|
37
37
|
ESCAPE: { semanticKey: "BACK", logText: "Pressing Back" },
|
|
38
38
|
BACK: { semanticKey: "BACK", logText: "Pressing Back" },
|
|
39
39
|
HOME: { semanticKey: "HOME", logText: "Pressing Home" },
|
|
40
|
+
APP_SWITCH: { semanticKey: "APP_SWITCH", logText: "Pressing Recent Apps" },
|
|
40
41
|
ENTER: { semanticKey: "ENTER", logText: "Pressing Enter" },
|
|
41
42
|
RETURN: { semanticKey: "ENTER", logText: "Pressing Enter" },
|
|
42
43
|
BACKSPACE: { semanticKey: "BACKSPACE", logText: "Pressing Backspace" },
|
|
@@ -58,6 +59,7 @@ export function getAndroidAdbTransport(normalized) {
|
|
|
58
59
|
const transportMap = {
|
|
59
60
|
BACK: "KEYCODE_BACK",
|
|
60
61
|
HOME: "KEYCODE_HOME",
|
|
62
|
+
APP_SWITCH: "KEYCODE_APP_SWITCH",
|
|
61
63
|
ENTER: "KEYCODE_ENTER",
|
|
62
64
|
BACKSPACE: "KEYCODE_DEL",
|
|
63
65
|
SPACE: "KEYCODE_SPACE"
|
|
@@ -75,6 +77,7 @@ export function getCloudAndroidTransport(normalized) {
|
|
|
75
77
|
const transportMap = {
|
|
76
78
|
BACK: 4,
|
|
77
79
|
HOME: 3,
|
|
80
|
+
APP_SWITCH: 187,
|
|
78
81
|
ENTER: 66,
|
|
79
82
|
BACKSPACE: 67,
|
|
80
83
|
SPACE: 62
|
|
@@ -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
|
};
|
|
@@ -156,11 +156,10 @@ function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
156
156
|
}
|
|
157
157
|
function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
158
158
|
const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
|
|
159
|
-
const
|
|
160
|
-
- On
|
|
161
|
-
-
|
|
159
|
+
const iosKeyboardGuidance = deviceInfo.platform?.trim().toLowerCase() === "ios" ? `
|
|
160
|
+
- On iOS, \`close_keyboard\` requires \`x\` and \`y\` coordinates from the current screenshot and taps that point.
|
|
161
|
+
- Prefer a dedicated keyboard dismissal control. Otherwise choose a clearly noninteractive background that you trust.
|
|
162
162
|
- 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
163
|
- Choose coordinates directly from the post-type screenshot; do not call \`preview_click\` between \`type\` and \`close_keyboard\`.` : "";
|
|
165
164
|
return `
|
|
166
165
|
You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
|
|
@@ -188,14 +187,14 @@ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
188
187
|
- If a full software keyboard is visibly covering part of the screen, your next action must be \`close_keyboard\`.
|
|
189
188
|
- Do not finish the current instruction while a required \`close_keyboard\` action is still pending.
|
|
190
189
|
- 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
|
|
190
|
+
- 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
191
|
- Whenever dismissal is required, do not click, scroll, inspect, submit, type again, issue a keypress, or call another tool before \`close_keyboard\`.
|
|
193
192
|
- Consecutive DELETE or BACKSPACE keypresses may be used for correction without closing the keyboard between them. After typing the replacement, call \`close_keyboard\`.
|
|
194
193
|
- Password and other secure fields may appear empty in screenshots even after text was entered successfully.
|
|
195
194
|
- 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
195
|
- Do not type into a secure field again solely because its characters or bullets are not visible.
|
|
197
196
|
- Retry secure-field input only when there is separate evidence of failure, such as another field changing or the app rejecting the value.
|
|
198
|
-
${
|
|
197
|
+
${iosKeyboardGuidance}
|
|
199
198
|
- Prefer tapping visible controls over hardware key events.
|
|
200
199
|
- Prefer on-screen navigation controls such as menus, tabs, drawer items, back arrows, close buttons, and explicit logout buttons over keypress actions.
|
|
201
200
|
- Do NOT use Back or ESC for normal app navigation when a reliable on-screen control is visible.
|
|
@@ -243,6 +242,7 @@ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
|
|
|
243
242
|
|
|
244
243
|
Mobile-Specific Notes:
|
|
245
244
|
- HOME key returns to the home screen
|
|
245
|
+
- On Android, APP_SWITCH opens Recent Apps
|
|
246
246
|
- On Android, ESC key maps to Back
|
|
247
247
|
- On iOS, ESC has no effect; use visible on-screen controls instead
|
|
248
248
|
- Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
|
|
@@ -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
|