@loadmill/droid-cua 2.7.0 → 2.9.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 (50) hide show
  1. package/README.md +7 -4
  2. package/build/index.js +131 -74
  3. package/build/src/browser/actions.js +9 -0
  4. package/build/src/browser/connection.js +37 -0
  5. package/build/src/browser/factory.js +22 -0
  6. package/build/src/browser/interface.js +14 -0
  7. package/build/src/browser/navigation.js +11 -0
  8. package/build/src/browser/normalize.js +36 -0
  9. package/build/src/browser/playwright/actions.js +199 -0
  10. package/build/src/browser/playwright/connection.js +207 -0
  11. package/build/src/browser/playwright/installed-browser.js +131 -0
  12. package/build/src/browser/playwright/session-state.js +19 -0
  13. package/build/src/browser/playwright/url.js +79 -0
  14. package/build/src/cli/headless-execution-config.js +4 -36
  15. package/build/src/cli/headless-target.js +64 -0
  16. package/build/src/cli/ink-shell.js +5 -2
  17. package/build/src/commands/help.js +9 -4
  18. package/build/src/commands/loadmill.js +10 -1
  19. package/build/src/commands/run.js +17 -28
  20. package/build/src/core/app-context.js +57 -33
  21. package/build/src/core/capture-sources.js +76 -0
  22. package/build/src/core/context-optimization.js +1 -1
  23. package/build/src/core/cua-models.js +9 -0
  24. package/build/src/core/execution-engine.js +43 -19
  25. package/build/src/core/prompts.js +1 -1
  26. package/build/src/core/runtime-config.js +96 -0
  27. package/build/src/core/session.js +18 -4
  28. package/build/src/device/action-event-meta.js +18 -0
  29. package/build/src/device/android/actions.js +6 -13
  30. package/build/src/device/cloud/actions.js +9 -14
  31. package/build/src/device/cloud/connection.js +8 -0
  32. package/build/src/device/ios/actions.js +6 -13
  33. package/build/src/device/ios/connection.js +8 -0
  34. package/build/src/device/loadmill.js +15 -9
  35. package/build/src/device/openai.js +67 -139
  36. package/build/src/integrations/loadmill/client.js +149 -131
  37. package/build/src/integrations/loadmill/executor.js +17 -11
  38. package/build/src/integrations/loadmill/index.js +1 -1
  39. package/build/src/integrations/loadmill/interpreter.js +4 -14
  40. package/build/src/modes/design-mode-ink.js +4 -3
  41. package/build/src/modes/design-mode.js +4 -3
  42. package/build/src/modes/execution-mode.js +139 -21
  43. package/build/src/prompts/base.js +92 -20
  44. package/build/src/prompts/design.js +6 -11
  45. package/build/src/prompts/execution.js +33 -109
  46. package/build/src/utils/console-output.js +8 -0
  47. package/build/src/utils/cua-debug-tracer.js +0 -1
  48. package/build/src/utils/run-screenshot-recorder.js +1 -1
  49. package/build/src/utils/structured-debug-log-manager.js +4 -1
  50. package/package.json +2 -1
package/README.md CHANGED
@@ -40,6 +40,7 @@ Under the hood, `droid-cua` uses an AI agent to explore your app, execute action
40
40
 
41
41
  - Mac: [Download](https://github.com/loadmill/droid-cua-release/releases/latest/download/Loadmill-Droid-CUA.dmg)
42
42
  - Mac (Intel): [Download](https://github.com/loadmill/droid-cua-release/releases/latest/download/Loadmill-Droid-CUA-intel.dmg)
43
+ - Windows: [Download](https://github.com/loadmill/droid-cua-release/releases/latest/download/Loadmill-Droid-CUA-Setup.exe)
43
44
 
44
45
  **2. Launch the app**
45
46
 
@@ -67,6 +68,10 @@ You can also keep project run history in a results folder and review past runs f
67
68
  - **Android** - Physical devices and emulators
68
69
  - **iOS** - Simulators on macOS
69
70
 
71
+ Desktop app support:
72
+ - macOS: Android and iOS simulator workflows
73
+ - Windows: Android workflows only
74
+
70
75
  ## Requirements
71
76
 
72
77
  **All platforms:**
@@ -154,10 +159,9 @@ Example headless config:
154
159
  "executionModeInstructions": ""
155
160
  },
156
161
  "appContextEnabled": true,
157
- "appContextBudget": 300,
158
162
  "appContextPath": "../tests/context.md",
159
163
  "contextOptimizationEnabled": true,
160
- "contextOptimizationThreshold": 30000
164
+ "contextOptimizationThreshold": 20000
161
165
  }
162
166
  ```
163
167
 
@@ -177,7 +181,6 @@ Supported CLI options include:
177
181
  - `--config`
178
182
  - `--cua-model`
179
183
  - `--context`
180
- - `--app-context-budget`
181
184
  - `--no-context`
182
185
  - `--context-optimization-threshold`
183
186
  - `--no-context-optimization`
@@ -195,7 +198,7 @@ Config and precedence rules:
195
198
  - CLI flags override config file values.
196
199
  - `--context` overrides the config app-context path.
197
200
  - `--no-context` disables app context entirely.
198
- - Context Optimization is enabled by default with a `30000` input-token threshold.
201
+ - Context Optimization is enabled by default with a `20000` input-token threshold.
199
202
  - `--context-optimization-threshold <tokens>` sets the threshold and enables Context Optimization.
200
203
  - `--no-context-optimization` disables Context Optimization.
201
204
  - `--base-prompt` and `--execution-prompt` let you pass prompt customizations inline on the command line.
package/build/index.js CHANGED
@@ -3,24 +3,31 @@ import dotenv from "dotenv";
3
3
  import path from "path";
4
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";
9
12
  import { startInkShell } from "./src/cli/ink-shell.js";
10
13
  import { ExecutionMode } from "./src/modes/execution-mode.js";
14
+ import { createOpenAiExecutor } from "./src/device/openai.js";
11
15
  import { logger } from "./src/utils/logger.js";
12
16
  import { selectDevice } from "./src/cli/device-selector.js";
13
- import { buildAppContextBriefing, DEFAULT_APP_CONTEXT_BUDGET } from "./src/core/app-context.js";
14
- import { CONTEXT_OPTIMIZATION_ENABLED_ENV, CONTEXT_OPTIMIZATION_THRESHOLD_ENV, } from "./src/core/context-optimization.js";
17
+ import { resolveExecutionAppContext } from "./src/core/app-context.js";
18
+ import { createRunContext, resolveRuntimeConfigFromEnv, resolveRuntimeCredentialsFromEnv, } from "./src/core/runtime-config.js";
15
19
  import { resolveHeadlessExecutionConfig } from "./src/cli/headless-execution-config.js";
20
+ import { resolveHeadlessTarget } from "./src/cli/headless-target.js";
16
21
  import { printCliOutput } from "./src/utils/console-output.js";
17
22
  import { emitDesktopDebug } from "./src/utils/desktop-debug.js";
18
23
  import { createHeadlessDebugArtifacts } from "./src/cli/headless-debug.js";
19
24
  import { loadHeadlessInstructions } from "./src/cli/headless-instructions.js";
25
+ import { createLoadmillApiClient } from "./src/integrations/loadmill/client.js";
20
26
  dotenv.config();
21
27
  const args = minimist(process.argv.slice(2));
22
- let avdName = args["avd"];
23
- let platform = args["platform"] || null; // 'ios' or 'android'
28
+ const targetConfig = resolveHeadlessTarget(args);
29
+ let avdName = targetConfig.avdName;
30
+ let platform = targetConfig.platform || null; // 'ios' or 'android'
24
31
  const recordScreenshots = args["record"] || false;
25
32
  const instructionsFile = args.instructions || args.i || null;
26
33
  const appContextPath = typeof args.context === "string" ? args.context : null;
@@ -33,8 +40,31 @@ if (strictFlag && noStrictFlag) {
33
40
  const screenshotDir = path.join("droid-cua-recording-" + Date.now());
34
41
  if (recordScreenshots)
35
42
  await mkdir(screenshotDir, { recursive: true });
43
+ function createCliRuntimeContext(configOverrides = {}) {
44
+ // CLI env/config is bootstrap input only. After this boundary the shared
45
+ // runtime uses explicit run-scoped config and credentials objects.
46
+ const context = createRunContext({
47
+ config: resolveRuntimeConfigFromEnv(process.env, configOverrides),
48
+ credentials: resolveRuntimeCredentialsFromEnv(process.env),
49
+ });
50
+ const openAi = createOpenAiExecutor({
51
+ client: context.clients.openai,
52
+ config: context.config,
53
+ });
54
+ const loadmillClient = createLoadmillApiClient({
55
+ token: context.credentials.loadmillApiToken,
56
+ baseUrl: context.credentials.loadmillBaseUrl,
57
+ });
58
+ return {
59
+ ...context,
60
+ openAi,
61
+ openAiClient: context.clients.openai,
62
+ loadmillClient,
63
+ };
64
+ }
36
65
  async function main() {
37
66
  const isHeadlessInstructionsRun = Boolean(instructionsFile);
67
+ const isBrowserTarget = targetConfig.target === "web";
38
68
  const headlessDebug = createHeadlessDebugArtifacts({
39
69
  cwd: process.cwd(),
40
70
  enabled: isHeadlessInstructionsRun && debugMode
@@ -44,19 +74,38 @@ async function main() {
44
74
  }
45
75
  // Initialize legacy plain-text debug logging only for non-headless flows.
46
76
  await logger.init(debugMode && !isHeadlessInstructionsRun);
77
+ if (isBrowserTarget && !isHeadlessInstructionsRun) {
78
+ throw new Error("--target web is only supported with --instructions headless execution.");
79
+ }
47
80
  // If no device specified, show interactive selection menu
48
- if (!avdName && !platform) {
81
+ if (!isBrowserTarget && !avdName && !platform) {
49
82
  const selection = await selectDevice();
50
83
  platform = selection.platform;
51
84
  avdName = selection.deviceName;
52
85
  }
53
- // Connect to device
54
- const deviceId = await connectToDevice(avdName, platform);
55
- const deviceInfo = await getDeviceInfo(deviceId);
56
- console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
57
- console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
86
+ let targetId;
87
+ let deviceInfo;
88
+ if (isBrowserTarget) {
89
+ targetId = await connectToBrowser({
90
+ url: targetConfig.url,
91
+ headless: true,
92
+ sessionMode: targetConfig.sessionMode
93
+ });
94
+ deviceInfo = normalizeBrowserInfoForSession(await getBrowserInfo(targetId));
95
+ console.log(`Using browser viewport: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
96
+ console.log(`Model sees viewport: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
97
+ if (deviceInfo.currentUrl) {
98
+ console.log(`Starting URL: ${deviceInfo.currentUrl}`);
99
+ }
100
+ }
101
+ else {
102
+ targetId = await connectToDevice(avdName, platform);
103
+ deviceInfo = await getDeviceInfo(targetId);
104
+ console.log(`Using real resolution: ${deviceInfo.device_width}x${deviceInfo.device_height}`);
105
+ console.log(`Model sees resolution: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
106
+ }
58
107
  // Create session to manage state
59
- const session = new Session(deviceId, deviceInfo);
108
+ const session = new Session(targetId, deviceInfo);
60
109
  const initialSystemText = buildBaseSystemPrompt(deviceInfo);
61
110
  session.setSystemPrompt(initialSystemText);
62
111
  // If --instructions provided, run in headless mode
@@ -77,89 +126,91 @@ async function main() {
77
126
  }
78
127
  console.log(`\nRunning test from: ${instructionsFile}\n`);
79
128
  instructions = await loadHeadlessInstructions(instructionsFile);
80
- const taskText = instructions.join("\n");
81
129
  const headlessConfig = await resolveHeadlessExecutionConfig(args);
82
- process.env.OPENAI_CUA_MODEL = headlessConfig.cuaModel;
83
- process.env[CONTEXT_OPTIMIZATION_ENABLED_ENV] = String(headlessConfig.contextOptimizationEnabled);
84
- process.env[CONTEXT_OPTIMIZATION_THRESHOLD_ENV] = String(headlessConfig.contextOptimizationThreshold);
85
- console.log(`Using CUA model: ${headlessConfig.cuaModel}`);
86
- let appContextBriefing = "";
87
- if (!headlessConfig.appContextEnabled) {
88
- emitDesktopDebug("app_context.status", "execution", { runId }, {
130
+ const appContext = await resolveExecutionAppContext({
131
+ enabled: headlessConfig.appContextEnabled,
132
+ contextPath: headlessConfig.appContextPath,
133
+ });
134
+ const appContextText = appContext.text;
135
+ const runtimeContext = createCliRuntimeContext({
136
+ cuaModel: headlessConfig.cuaModel,
137
+ strictMode: headlessConfig.strictMode,
138
+ promptCustomizations: headlessConfig.promptCustomizations,
139
+ contextOptimizationEnabled: headlessConfig.contextOptimizationEnabled,
140
+ contextOptimizationThreshold: headlessConfig.contextOptimizationThreshold,
141
+ appContext: {
142
+ enabled: headlessConfig.appContextEnabled,
143
+ path: appContext.contextPath,
144
+ status: appContext.status,
145
+ text: appContextText,
146
+ message: appContext.message,
147
+ },
148
+ });
149
+ console.log(`Using CUA model: ${runtimeContext.config.cuaModel}`);
150
+ if (appContext.status === "loaded" || appContext.status === "empty") {
151
+ emitDesktopDebug("app_context.loaded", "execution", { runId }, {
89
152
  source: "cli_context_flag",
90
- contextPath: null,
91
- budget: headlessConfig.appContextBudget,
92
- status: "disabled"
153
+ contextPath: appContext.contextPath,
154
+ status: appContext.status,
155
+ text: appContextText
93
156
  });
94
- }
95
- else if (headlessConfig.appContextPath) {
96
- try {
97
- const result = await buildAppContextBriefing({
98
- contextPath: headlessConfig.appContextPath,
99
- taskText,
100
- budget: headlessConfig.appContextBudget,
101
- });
102
- appContextBriefing = result.briefing;
103
- emitDesktopDebug("app_context.status", "execution", { runId }, {
104
- source: "cli_context_flag",
105
- contextPath: result.contextPath,
106
- budget: headlessConfig.appContextBudget,
107
- outputTokens: result.outputTokens,
108
- status: appContextBriefing.trim().length > 0 ? "loaded" : "empty"
109
- });
110
- if (appContextBriefing) {
111
- emitDesktopDebug("app_context.briefing.full", "execution", { runId }, {
112
- source: "cli_context_flag",
113
- contextPath: result.contextPath,
114
- budget: headlessConfig.appContextBudget,
115
- outputTokens: result.outputTokens,
116
- briefing: appContextBriefing
117
- });
118
- console.log(`Using app context briefing from: ${result.contextPath}`);
119
- }
120
- }
121
- catch (error) {
122
- const message = error instanceof Error ? error.message : "Unknown app context error";
123
- emitDesktopDebug("app_context.status", "execution", { runId }, {
124
- source: "cli_context_flag",
125
- contextPath: headlessConfig.appContextPath,
126
- budget: headlessConfig.appContextBudget,
127
- status: "failed",
128
- message
129
- });
130
- console.warn(`Warning: could not load app context from ${headlessConfig.appContextPath}. Running without briefing.`);
157
+ if (appContextText) {
158
+ console.log(`Using app context from: ${appContext.contextPath}`);
131
159
  }
132
160
  }
161
+ else if (appContext.status === "failed") {
162
+ emitDesktopDebug("app_context.loaded", "execution", { runId }, {
163
+ source: "cli_context_flag",
164
+ contextPath: appContext.contextPath,
165
+ status: "failed",
166
+ message: appContext.message
167
+ });
168
+ console.warn(`Warning: could not load app context from ${appContext.contextPath}. Running without it.`);
169
+ }
133
170
  else {
134
- emitDesktopDebug("app_context.status", "execution", { runId }, {
171
+ emitDesktopDebug("app_context.loaded", "execution", { runId }, {
135
172
  source: "cli_context_flag",
136
- contextPath: null,
137
- budget: headlessConfig.appContextBudget,
138
- status: "missing"
173
+ contextPath: appContext.contextPath,
174
+ status: appContext.status
139
175
  });
140
176
  }
141
- const executionPrompt = buildExecutionModePrompt(deviceInfo, headlessConfig.promptCustomizations, appContextBriefing, { strictMode: headlessConfig.strictMode });
177
+ const executionPrompt = buildExecutionModePrompt(deviceInfo, headlessConfig.promptCustomizations, appContextText, { strictMode: headlessConfig.strictMode });
142
178
  session.setSystemPrompt(executionPrompt);
143
179
  const screenshotRecorder = headlessDebug.createExecutionScreenshotRecorder({
144
180
  runId,
145
181
  recordScreenshots,
146
182
  screenshotDir
147
183
  });
148
- const engine = screenshotRecorder
149
- ? new ExecutionEngine(session, {
184
+ const engineOptions = screenshotRecorder
185
+ ? {
150
186
  recordScreenshots: true,
151
187
  screenshotRecorder,
152
- strictMode: headlessConfig.strictMode
153
- })
154
- : new ExecutionEngine(session, {
188
+ strictMode: runtimeContext.config.strictMode,
189
+ stepDelayMs: runtimeContext.config.stepDelayMs,
190
+ sendCUARequest: runtimeContext.openAi.sendCUARequest,
191
+ reviseTestScript: runtimeContext.openAi.reviseTestScript,
192
+ }
193
+ : {
155
194
  recordScreenshots,
156
195
  screenshotDir,
157
- strictMode: headlessConfig.strictMode
158
- });
196
+ strictMode: runtimeContext.config.strictMode,
197
+ stepDelayMs: runtimeContext.config.stepDelayMs,
198
+ sendCUARequest: runtimeContext.openAi.sendCUARequest,
199
+ reviseTestScript: runtimeContext.openAi.reviseTestScript,
200
+ };
201
+ if (isBrowserTarget) {
202
+ engineOptions.getScreenshotAsBase64 = (browserSessionId, browserInfo) => getBrowserScreenshotAsBase64(browserSessionId, browserInfo);
203
+ engineOptions.handleModelAction = (browserSessionId, action, scale, actionContext) => handleBrowserModelAction(browserSessionId, action, scale, actionContext);
204
+ engineOptions.getCurrentPlatform = getBrowserCurrentPlatform;
205
+ }
206
+ const engine = new ExecutionEngine(session, engineOptions);
159
207
  executionMode = new ExecutionMode(session, engine, instructions, true);
160
208
  const result = await executionMode.execute({
161
209
  runId,
162
- addOutput: printCliOutput
210
+ addOutput: printCliOutput,
211
+ openAiClient: runtimeContext.openAiClient,
212
+ loadmillClient: runtimeContext.loadmillClient,
213
+ runtimeCredentials: runtimeContext.credentials,
163
214
  });
164
215
  const stats = executionMode.stats || {};
165
216
  const durationMs = stats.startTime ? Math.max(0, Date.now() - stats.startTime) : 0;
@@ -204,15 +255,21 @@ async function main() {
204
255
  throw error;
205
256
  }
206
257
  }
258
+ const runtimeContext = createCliRuntimeContext({
259
+ strictMode: strictFlag,
260
+ });
207
261
  const engine = new ExecutionEngine(session, {
208
262
  recordScreenshots,
209
263
  screenshotDir,
210
- strictMode: strictFlag,
264
+ strictMode: runtimeContext.config.strictMode,
265
+ stepDelayMs: runtimeContext.config.stepDelayMs,
266
+ sendCUARequest: runtimeContext.openAi.sendCUARequest,
267
+ reviseTestScript: runtimeContext.openAi.reviseTestScript,
211
268
  });
212
269
  // Otherwise, start interactive Ink shell
213
270
  await startInkShell(session, engine, {
214
271
  appContextPath: appContextPath ? path.resolve(appContextPath) : null,
215
- appContextBudget: DEFAULT_APP_CONTEXT_BUDGET,
272
+ runtimeContext,
216
273
  });
217
274
  }
218
275
  main().catch((error) => {
@@ -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
+ }