@loadmill/droid-cua 2.6.0 → 2.8.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.
Files changed (38) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -2
  3. package/build/index.js +55 -20
  4. package/build/src/browser/actions.js +9 -0
  5. package/build/src/browser/connection.js +37 -0
  6. package/build/src/browser/factory.js +22 -0
  7. package/build/src/browser/interface.js +14 -0
  8. package/build/src/browser/navigation.js +11 -0
  9. package/build/src/browser/normalize.js +36 -0
  10. package/build/src/browser/playwright/actions.js +199 -0
  11. package/build/src/browser/playwright/connection.js +196 -0
  12. package/build/src/browser/playwright/installed-browser.js +131 -0
  13. package/build/src/browser/playwright/session-state.js +19 -0
  14. package/build/src/browser/playwright/url.js +79 -0
  15. package/build/src/cli/headless-execution-config.js +59 -3
  16. package/build/src/cli/headless-instructions.js +12 -0
  17. package/build/src/cli/headless-target.js +64 -0
  18. package/build/src/commands/help.js +10 -2
  19. package/build/src/core/capture-sources.js +76 -0
  20. package/build/src/core/context-optimization.js +36 -0
  21. package/build/src/core/execution-engine.js +45 -12
  22. package/build/src/core/prompts.js +1 -1
  23. package/build/src/core/session.js +18 -4
  24. package/build/src/device/action-event-meta.js +18 -0
  25. package/build/src/device/android/actions.js +6 -13
  26. package/build/src/device/cloud/actions.js +9 -14
  27. package/build/src/device/cloud/connection.js +8 -0
  28. package/build/src/device/ios/actions.js +6 -13
  29. package/build/src/device/ios/appium-server.js +4 -3
  30. package/build/src/device/ios/connection.js +8 -0
  31. package/build/src/device/ios/node-cli-env.js +28 -0
  32. package/build/src/device/openai.js +47 -2
  33. package/build/src/modes/execution-mode.js +113 -21
  34. package/build/src/prompts/base.js +95 -21
  35. package/build/src/prompts/design.js +6 -11
  36. package/build/src/prompts/execution.js +26 -4
  37. package/build/src/utils/run-screenshot-recorder.js +1 -1
  38. package/package.json +7 -1
package/LICENSE CHANGED
@@ -1 +1 @@
1
- © 2025 Loadmill. All rights reserved.
1
+ © 2026 Loadmill. All rights reserved.
package/README.md CHANGED
@@ -155,7 +155,9 @@ Example headless config:
155
155
  },
156
156
  "appContextEnabled": true,
157
157
  "appContextBudget": 300,
158
- "appContextPath": "../tests/context.md"
158
+ "appContextPath": "../tests/context.md",
159
+ "contextOptimizationEnabled": true,
160
+ "contextOptimizationThreshold": 30000
159
161
  }
160
162
  ```
161
163
 
@@ -177,10 +179,14 @@ Supported CLI options include:
177
179
  - `--context`
178
180
  - `--app-context-budget`
179
181
  - `--no-context`
182
+ - `--context-optimization-threshold`
183
+ - `--no-context-optimization`
180
184
  - `--base-prompt`
181
185
  - `--execution-prompt`
182
186
  - `--base-prompt-file`
183
187
  - `--execution-prompt-file`
188
+ - `--strict`
189
+ - `--no-strict`
184
190
  - `--record`
185
191
  - `--debug`
186
192
 
@@ -189,6 +195,9 @@ Config and precedence rules:
189
195
  - CLI flags override config file values.
190
196
  - `--context` overrides the config app-context path.
191
197
  - `--no-context` disables app context entirely.
198
+ - Context Optimization is enabled by default with a `30000` input-token threshold.
199
+ - `--context-optimization-threshold <tokens>` sets the threshold and enables Context Optimization.
200
+ - `--no-context-optimization` disables Context Optimization.
192
201
  - `--base-prompt` and `--execution-prompt` let you pass prompt customizations inline on the command line.
193
202
  - `--base-prompt-file` and `--execution-prompt-file` override the corresponding prompt customizations from config.
194
203
  - If both inline and file-based prompt overrides are provided, the inline prompt flags win.
@@ -216,4 +225,4 @@ Current headless behavior is documented in [docs/headless-cli-spec.md](docs/head
216
225
 
217
226
  <h2 id="license">📄 License</h2>
218
227
 
219
- © 2025 Loadmill. All rights reserved.
228
+ © 2026 Loadmill. All rights reserved.
package/build/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import minimist from "minimist";
2
2
  import dotenv from "dotenv";
3
3
  import path from "path";
4
- import { mkdir, readFile } from "fs/promises";
4
+ import { mkdir } from "fs/promises";
5
5
  import { connectToDevice, getDeviceInfo } from "./src/device/connection.js";
6
+ import { connectToBrowser, getBrowserInfo, getCurrentPlatform as getBrowserCurrentPlatform, getScreenshotAsBase64 as getBrowserScreenshotAsBase64, } from "./src/browser/connection.js";
7
+ import { handleModelAction as handleBrowserModelAction } from "./src/browser/actions.js";
8
+ import { normalizeBrowserInfoForSession } from "./src/browser/normalize.js";
6
9
  import { Session } from "./src/core/session.js";
7
10
  import { ExecutionEngine } from "./src/core/execution-engine.js";
8
11
  import { buildBaseSystemPrompt, buildExecutionModePrompt } from "./src/core/prompts.js";
@@ -11,14 +14,18 @@ import { ExecutionMode } from "./src/modes/execution-mode.js";
11
14
  import { logger } from "./src/utils/logger.js";
12
15
  import { selectDevice } from "./src/cli/device-selector.js";
13
16
  import { buildAppContextBriefing, DEFAULT_APP_CONTEXT_BUDGET } from "./src/core/app-context.js";
17
+ import { CONTEXT_OPTIMIZATION_ENABLED_ENV, CONTEXT_OPTIMIZATION_THRESHOLD_ENV, } from "./src/core/context-optimization.js";
14
18
  import { resolveHeadlessExecutionConfig } from "./src/cli/headless-execution-config.js";
19
+ import { resolveHeadlessTarget } from "./src/cli/headless-target.js";
15
20
  import { printCliOutput } from "./src/utils/console-output.js";
16
21
  import { emitDesktopDebug } from "./src/utils/desktop-debug.js";
17
22
  import { createHeadlessDebugArtifacts } from "./src/cli/headless-debug.js";
23
+ import { loadHeadlessInstructions } from "./src/cli/headless-instructions.js";
18
24
  dotenv.config();
19
25
  const args = minimist(process.argv.slice(2));
20
- let avdName = args["avd"];
21
- let platform = args["platform"] || null; // 'ios' or 'android'
26
+ const targetConfig = resolveHeadlessTarget(args);
27
+ let avdName = targetConfig.avdName;
28
+ let platform = targetConfig.platform || null; // 'ios' or 'android'
22
29
  const recordScreenshots = args["record"] || false;
23
30
  const instructionsFile = args.instructions || args.i || null;
24
31
  const appContextPath = typeof args.context === "string" ? args.context : null;
@@ -33,6 +40,7 @@ if (recordScreenshots)
33
40
  await mkdir(screenshotDir, { recursive: true });
34
41
  async function main() {
35
42
  const isHeadlessInstructionsRun = Boolean(instructionsFile);
43
+ const isBrowserTarget = targetConfig.target === "web";
36
44
  const headlessDebug = createHeadlessDebugArtifacts({
37
45
  cwd: process.cwd(),
38
46
  enabled: isHeadlessInstructionsRun && debugMode
@@ -42,19 +50,38 @@ async function main() {
42
50
  }
43
51
  // Initialize legacy plain-text debug logging only for non-headless flows.
44
52
  await logger.init(debugMode && !isHeadlessInstructionsRun);
53
+ if (isBrowserTarget && !isHeadlessInstructionsRun) {
54
+ throw new Error("--target web is only supported with --instructions headless execution.");
55
+ }
45
56
  // If no device specified, show interactive selection menu
46
- if (!avdName && !platform) {
57
+ if (!isBrowserTarget && !avdName && !platform) {
47
58
  const selection = await selectDevice();
48
59
  platform = selection.platform;
49
60
  avdName = selection.deviceName;
50
61
  }
51
- // Connect to device
52
- const deviceId = await connectToDevice(avdName, platform);
53
- const deviceInfo = await getDeviceInfo(deviceId);
54
- console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
55
- console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
62
+ let targetId;
63
+ let deviceInfo;
64
+ if (isBrowserTarget) {
65
+ targetId = await connectToBrowser({
66
+ url: targetConfig.url,
67
+ headless: true,
68
+ sessionMode: targetConfig.sessionMode
69
+ });
70
+ deviceInfo = normalizeBrowserInfoForSession(await getBrowserInfo(targetId));
71
+ console.log(`Using browser viewport: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
72
+ console.log(`Model sees viewport: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
73
+ if (deviceInfo.currentUrl) {
74
+ console.log(`Starting URL: ${deviceInfo.currentUrl}`);
75
+ }
76
+ }
77
+ else {
78
+ targetId = await connectToDevice(avdName, platform);
79
+ deviceInfo = await getDeviceInfo(targetId);
80
+ console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
81
+ console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
82
+ }
56
83
  // Create session to manage state
57
- const session = new Session(deviceId, deviceInfo);
84
+ const session = new Session(targetId, deviceInfo);
58
85
  const initialSystemText = buildBaseSystemPrompt(deviceInfo);
59
86
  session.setSystemPrompt(initialSystemText);
60
87
  // If --instructions provided, run in headless mode
@@ -74,14 +101,13 @@ async function main() {
74
101
  console.log(`Debug logging enabled: ${currentLogFilePath}`);
75
102
  }
76
103
  console.log(`\nRunning test from: ${instructionsFile}\n`);
77
- const content = await readFile(instructionsFile, "utf-8");
78
- instructions = content
79
- .split("\n")
80
- .map(line => line.trim())
81
- .filter(line => line.length > 0);
104
+ instructions = await loadHeadlessInstructions(instructionsFile);
82
105
  const taskText = instructions.join("\n");
83
106
  const headlessConfig = await resolveHeadlessExecutionConfig(args);
84
107
  process.env.OPENAI_CUA_MODEL = headlessConfig.cuaModel;
108
+ process.env[CONTEXT_OPTIMIZATION_ENABLED_ENV] = String(headlessConfig.contextOptimizationEnabled);
109
+ process.env[CONTEXT_OPTIMIZATION_THRESHOLD_ENV] = String(headlessConfig.contextOptimizationThreshold);
110
+ console.log(`Using CUA model: ${headlessConfig.cuaModel}`);
85
111
  let appContextBriefing = "";
86
112
  if (!headlessConfig.appContextEnabled) {
87
113
  emitDesktopDebug("app_context.status", "execution", { runId }, {
@@ -136,6 +162,9 @@ async function main() {
136
162
  budget: headlessConfig.appContextBudget,
137
163
  status: "missing"
138
164
  });
165
+ if (isBrowserTarget && deviceInfo.currentUrl) {
166
+ appContextBriefing = `Starting URL: ${deviceInfo.currentUrl}`;
167
+ }
139
168
  }
140
169
  const executionPrompt = buildExecutionModePrompt(deviceInfo, headlessConfig.promptCustomizations, appContextBriefing, { strictMode: headlessConfig.strictMode });
141
170
  session.setSystemPrompt(executionPrompt);
@@ -144,17 +173,23 @@ async function main() {
144
173
  recordScreenshots,
145
174
  screenshotDir
146
175
  });
147
- const engine = screenshotRecorder
148
- ? new ExecutionEngine(session, {
176
+ const engineOptions = screenshotRecorder
177
+ ? {
149
178
  recordScreenshots: true,
150
179
  screenshotRecorder,
151
180
  strictMode: headlessConfig.strictMode
152
- })
153
- : new ExecutionEngine(session, {
181
+ }
182
+ : {
154
183
  recordScreenshots,
155
184
  screenshotDir,
156
185
  strictMode: headlessConfig.strictMode
157
- });
186
+ };
187
+ if (isBrowserTarget) {
188
+ engineOptions.getScreenshotAsBase64 = (browserSessionId, browserInfo) => getBrowserScreenshotAsBase64(browserSessionId, browserInfo);
189
+ engineOptions.handleModelAction = (browserSessionId, action, scale, actionContext) => handleBrowserModelAction(browserSessionId, action, scale, actionContext);
190
+ engineOptions.getCurrentPlatform = getBrowserCurrentPlatform;
191
+ }
192
+ const engine = new ExecutionEngine(session, engineOptions);
158
193
  executionMode = new ExecutionMode(session, engine, instructions, true);
159
194
  const result = await executionMode.execute({
160
195
  runId,
@@ -0,0 +1,9 @@
1
+ import { getBrowserBackend, getCurrentPlatform } from "./factory.js";
2
+ export async function handleModelAction(browserSessionId, action, scale = 1.0, context = null) {
3
+ const platform = getCurrentPlatform();
4
+ if (!platform) {
5
+ throw new Error("No platform set. Call connectToBrowser first.");
6
+ }
7
+ const backend = getBrowserBackend(platform);
8
+ return backend.handleModelAction(browserSessionId, action, scale, context);
9
+ }
@@ -0,0 +1,37 @@
1
+ import { getBrowserBackend, detectPlatform, setCurrentPlatform, getCurrentPlatform } from "./factory.js";
2
+ let currentBackend = null;
3
+ export async function connectToBrowser(targetOrOptions, platform = null) {
4
+ if (platform && platform !== "browser") {
5
+ throw new Error(`Unsupported platform: ${platform}. Only browser is supported.`);
6
+ }
7
+ const detectedPlatform = platform || detectPlatform(targetOrOptions);
8
+ setCurrentPlatform(detectedPlatform);
9
+ currentBackend = getBrowserBackend(detectedPlatform);
10
+ console.log(`Platform: ${detectedPlatform}`);
11
+ return currentBackend.connectToBrowser(targetOrOptions);
12
+ }
13
+ export async function getBrowserInfo(browserSessionId) {
14
+ if (!currentBackend) {
15
+ throw new Error("Not connected to a browser session. Call connectToBrowser first.");
16
+ }
17
+ return currentBackend.getBrowserInfo(browserSessionId);
18
+ }
19
+ export async function getScreenshotAsBase64(browserSessionId, browserInfo) {
20
+ if (!currentBackend) {
21
+ throw new Error("Not connected to a browser session. Call connectToBrowser first.");
22
+ }
23
+ return currentBackend.getScreenshotAsBase64(browserSessionId, browserInfo);
24
+ }
25
+ export async function navigateToUrl(browserSessionId, url) {
26
+ if (!currentBackend?.navigateToUrl) {
27
+ throw new Error("Active browser backend does not support direct navigation.");
28
+ }
29
+ return currentBackend.navigateToUrl(browserSessionId, url);
30
+ }
31
+ export { getCurrentPlatform };
32
+ export async function disconnect() {
33
+ if (currentBackend?.disconnect) {
34
+ await currentBackend.disconnect();
35
+ }
36
+ currentBackend = null;
37
+ }
@@ -0,0 +1,22 @@
1
+ import * as browserConnection from "./playwright/connection.js";
2
+ import * as browserActions from "./playwright/actions.js";
3
+ let currentPlatform = null;
4
+ export function detectPlatform() {
5
+ return "browser";
6
+ }
7
+ export function getBrowserBackend(_platform = "browser") {
8
+ return {
9
+ connectToBrowser: browserConnection.connectToBrowser,
10
+ getBrowserInfo: browserConnection.getBrowserInfo,
11
+ getScreenshotAsBase64: browserConnection.getScreenshotAsBase64,
12
+ navigateToUrl: browserConnection.navigateToUrl,
13
+ handleModelAction: browserActions.handleModelAction,
14
+ disconnect: browserConnection.disconnect,
15
+ };
16
+ }
17
+ export function setCurrentPlatform(platform) {
18
+ currentPlatform = platform;
19
+ }
20
+ export function getCurrentPlatform() {
21
+ return currentPlatform;
22
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Browser interface - contract the browser backend must implement.
3
+ */
4
+ export const SUPPORTED_ACTIONS = [
5
+ "click",
6
+ "double_click",
7
+ "move",
8
+ "type",
9
+ "scroll",
10
+ "drag",
11
+ "keypress",
12
+ "wait",
13
+ "screenshot",
14
+ ];
@@ -0,0 +1,11 @@
1
+ export function isNavigateInstruction(userInput) {
2
+ const trimmed = typeof userInput === "string" ? userInput.trim() : "";
3
+ return trimmed.toLowerCase().startsWith("navigate:");
4
+ }
5
+ export function extractNavigateTarget(userInput) {
6
+ const trimmed = typeof userInput === "string" ? userInput.trim() : "";
7
+ if (!trimmed.toLowerCase().startsWith("navigate:")) {
8
+ return trimmed;
9
+ }
10
+ return trimmed.slice("navigate:".length).trim();
11
+ }
@@ -0,0 +1,36 @@
1
+ const DEFAULT_BROWSER_VIEWPORT = {
2
+ width: 1440,
3
+ height: 900,
4
+ };
5
+ function finiteNumber(value, fallback) {
6
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7
+ }
8
+ export function normalizeBrowserInfoForSession(browserInfo = {}) {
9
+ const viewportWidth = finiteNumber(browserInfo.viewportWidth, DEFAULT_BROWSER_VIEWPORT.width);
10
+ const viewportHeight = finiteNumber(browserInfo.viewportHeight, DEFAULT_BROWSER_VIEWPORT.height);
11
+ const scaledWidth = finiteNumber(browserInfo.scaledWidth, viewportWidth);
12
+ const scaledHeight = finiteNumber(browserInfo.scaledHeight, viewportHeight);
13
+ const scale = finiteNumber(browserInfo.scale, 1);
14
+ const browserName = typeof browserInfo.browserName === "string" && browserInfo.browserName.trim()
15
+ ? browserInfo.browserName.trim()
16
+ : "Browser";
17
+ const browserFamily = browserInfo.browserFamily === "chrome" || browserInfo.browserFamily === "edge"
18
+ ? browserInfo.browserFamily
19
+ : null;
20
+ const currentUrl = typeof browserInfo.currentUrl === "string" && browserInfo.currentUrl.trim()
21
+ ? browserInfo.currentUrl.trim()
22
+ : null;
23
+ return {
24
+ ...browserInfo,
25
+ platform: "browser",
26
+ device_width: viewportWidth,
27
+ device_height: viewportHeight,
28
+ scaled_width: scaledWidth,
29
+ scaled_height: scaledHeight,
30
+ scale,
31
+ device_name: browserName,
32
+ browserName,
33
+ browserFamily,
34
+ currentUrl,
35
+ };
36
+ }
@@ -0,0 +1,199 @@
1
+ import { logger } from "../../utils/logger.js";
2
+ import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
3
+ import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
4
+ import { buildActionEventMeta } from "../../device/action-event-meta.js";
5
+ import { getActiveBrowserPage } from "./session-state.js";
6
+ const KEY_ALIASES = {
7
+ CTRL: "Control",
8
+ CONTROL: "Control",
9
+ CMD: "Meta",
10
+ COMMAND: "Meta",
11
+ META: "Meta",
12
+ ALT: "Alt",
13
+ OPTION: "Alt",
14
+ SHIFT: "Shift",
15
+ ENTER: "Enter",
16
+ RETURN: "Enter",
17
+ ESC: "Escape",
18
+ ESCAPE: "Escape",
19
+ BACKSPACE: "Backspace",
20
+ DELETE: "Delete",
21
+ TAB: "Tab",
22
+ SPACE: "Space",
23
+ HOME: "Home",
24
+ END: "End",
25
+ PAGEUP: "PageUp",
26
+ PAGEDOWN: "PageDown",
27
+ ARROWUP: "ArrowUp",
28
+ ARROWDOWN: "ArrowDown",
29
+ ARROWLEFT: "ArrowLeft",
30
+ ARROWRIGHT: "ArrowRight",
31
+ };
32
+ function normalizeKeyEntry(key) {
33
+ const raw = String(key ?? "").trim();
34
+ if (!raw) {
35
+ throw new Error("Keypress action is missing keys.");
36
+ }
37
+ const normalized = KEY_ALIASES[raw.toUpperCase()];
38
+ if (normalized) {
39
+ return normalized;
40
+ }
41
+ if (raw.length === 1) {
42
+ return raw;
43
+ }
44
+ return raw[0].toUpperCase() + raw.slice(1);
45
+ }
46
+ function normalizeShortcut(keys = []) {
47
+ if (!Array.isArray(keys) || keys.length === 0) {
48
+ throw new Error("Keypress action is missing keys.");
49
+ }
50
+ return keys.map(normalizeKeyEntry).join("+");
51
+ }
52
+ function buildMeta(context, action, payload = {}) {
53
+ return buildActionEventMeta({
54
+ context,
55
+ action,
56
+ platform: "browser",
57
+ payload,
58
+ });
59
+ }
60
+ export async function handleModelAction(browserSessionId, action, scale = 1, context = null) {
61
+ const addOutput = context?.addOutput || ((item) => console.log(item.text || item));
62
+ const page = getActiveBrowserPage(browserSessionId);
63
+ try {
64
+ emitDesktopDebug("browser.action.execute", "device", {
65
+ runId: context?.runId,
66
+ sessionId: context?.sessionId,
67
+ stepId: context?.stepId,
68
+ instructionIndex: context?.instructionIndex,
69
+ }, {
70
+ platform: "browser",
71
+ browserSessionId,
72
+ actionType: action?.type,
73
+ scale,
74
+ x: action?.x,
75
+ y: action?.y,
76
+ x1: action?.x1,
77
+ y1: action?.y1,
78
+ x2: action?.x2,
79
+ y2: action?.y2,
80
+ text: typeof action?.text === "string" ? truncateForDebug(action.text, 300) : undefined,
81
+ keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
82
+ pathPoints: Array.isArray(action?.path) ? action.path.length : 0,
83
+ });
84
+ switch (action.type) {
85
+ case "click": {
86
+ const x = Math.round(action.x / scale);
87
+ const y = Math.round(action.y / scale);
88
+ addOutput({ type: "action", text: "Click", ...buildMeta(context, action, { x, y, unit: "px" }) });
89
+ await page.mouse.click(x, y);
90
+ break;
91
+ }
92
+ case "double_click": {
93
+ const x = Math.round(action.x / scale);
94
+ const y = Math.round(action.y / scale);
95
+ addOutput({ type: "action", text: `Double-clicking at (${x}, ${y})`, ...buildMeta(context, action, { x, y, unit: "px" }) });
96
+ await page.mouse.dblclick(x, y);
97
+ break;
98
+ }
99
+ case "move": {
100
+ const x = Math.round(action.x / scale);
101
+ const y = Math.round(action.y / scale);
102
+ addOutput({ type: "action", text: `Moving mouse to (${x}, ${y})`, ...buildMeta(context, action, { x, y, unit: "px" }) });
103
+ await page.mouse.move(x, y);
104
+ break;
105
+ }
106
+ case "scroll": {
107
+ const scrollX = Math.round((action.scroll_x ?? 0) / scale);
108
+ const scrollY = Math.round((action.scroll_y ?? 0) / scale);
109
+ addOutput({
110
+ type: "action",
111
+ text: `Scrolling by (${scrollX}, ${scrollY})`,
112
+ ...buildMeta(context, action, { scrollX, scrollY, unit: "px" }),
113
+ });
114
+ await page.mouse.wheel(scrollX, scrollY);
115
+ break;
116
+ }
117
+ case "drag": {
118
+ const dragPath = Array.isArray(action.path) ? action.path : [];
119
+ if (dragPath.length < 2) {
120
+ throw new Error("Drag action requires a path with at least two points.");
121
+ }
122
+ const [start] = dragPath;
123
+ const end = dragPath[dragPath.length - 1];
124
+ const scaledStart = { x: Math.round(start.x / scale), y: Math.round(start.y / scale) };
125
+ const scaledEnd = { x: Math.round(end.x / scale), y: Math.round(end.y / scale) };
126
+ addOutput({
127
+ type: "action",
128
+ text: `Dragging from (${scaledStart.x}, ${scaledStart.y}) to (${scaledEnd.x}, ${scaledEnd.y})`,
129
+ ...buildMeta(context, action, { pathStart: scaledStart, pathEnd: scaledEnd, unit: "px" }),
130
+ });
131
+ await page.mouse.move(scaledStart.x, scaledStart.y);
132
+ await page.mouse.down();
133
+ for (const point of dragPath.slice(1)) {
134
+ await page.mouse.move(Math.round(point.x / scale), Math.round(point.y / scale));
135
+ }
136
+ await page.mouse.up();
137
+ break;
138
+ }
139
+ case "type": {
140
+ const text = typeof action.text === "string" ? action.text : "";
141
+ addOutput({ type: "action", text: `Typing text: ${text}`, ...buildMeta(context, action, { text }) });
142
+ await page.keyboard.type(text);
143
+ break;
144
+ }
145
+ case "keypress": {
146
+ const shortcut = normalizeShortcut(action.keys);
147
+ addOutput({
148
+ type: "action",
149
+ text: `Pressing key: ${shortcut}`,
150
+ ...buildMeta(context, action, { keys: action.keys, shortcut }),
151
+ });
152
+ await page.keyboard.press(shortcut);
153
+ break;
154
+ }
155
+ case "wait": {
156
+ addOutput({ type: "action", text: "Waiting...", ...buildMeta(context, action) });
157
+ const timeoutMs = typeof action.ms === "number" && Number.isFinite(action.ms) && action.ms >= 0
158
+ ? Math.round(action.ms)
159
+ : getConfiguredStepDelayMs();
160
+ await page.waitForTimeout(timeoutMs);
161
+ break;
162
+ }
163
+ default:
164
+ addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
165
+ }
166
+ }
167
+ catch (error) {
168
+ logger.error("Action execution error", {
169
+ action,
170
+ message: error.message,
171
+ stack: error.stack,
172
+ });
173
+ emitDesktopDebug("browser.error", "device", {
174
+ runId: context?.runId,
175
+ sessionId: context?.sessionId,
176
+ stepId: context?.stepId,
177
+ instructionIndex: context?.instructionIndex,
178
+ }, {
179
+ platform: "browser",
180
+ operation: "action.execute",
181
+ actionType: action?.type,
182
+ message: error.message,
183
+ });
184
+ addOutput({
185
+ type: "error",
186
+ text: `Error executing action: ${error.message}`,
187
+ eventType: "error",
188
+ actionType: action?.type,
189
+ runId: context?.runId,
190
+ stepId: context?.stepId,
191
+ instructionIndex: context?.instructionIndex,
192
+ payload: {
193
+ message: error.message,
194
+ platform: "browser",
195
+ },
196
+ });
197
+ addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
198
+ }
199
+ }