@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.
- package/README.md +7 -4
- package/build/index.js +131 -74
- package/build/src/browser/actions.js +9 -0
- package/build/src/browser/connection.js +37 -0
- package/build/src/browser/factory.js +22 -0
- package/build/src/browser/interface.js +14 -0
- package/build/src/browser/navigation.js +11 -0
- package/build/src/browser/normalize.js +36 -0
- package/build/src/browser/playwright/actions.js +199 -0
- package/build/src/browser/playwright/connection.js +207 -0
- package/build/src/browser/playwright/installed-browser.js +131 -0
- package/build/src/browser/playwright/session-state.js +19 -0
- package/build/src/browser/playwright/url.js +79 -0
- package/build/src/cli/headless-execution-config.js +4 -36
- package/build/src/cli/headless-target.js +64 -0
- package/build/src/cli/ink-shell.js +5 -2
- package/build/src/commands/help.js +9 -4
- package/build/src/commands/loadmill.js +10 -1
- package/build/src/commands/run.js +17 -28
- package/build/src/core/app-context.js +57 -33
- package/build/src/core/capture-sources.js +76 -0
- package/build/src/core/context-optimization.js +1 -1
- package/build/src/core/cua-models.js +9 -0
- package/build/src/core/execution-engine.js +43 -19
- package/build/src/core/prompts.js +1 -1
- package/build/src/core/runtime-config.js +96 -0
- package/build/src/core/session.js +18 -4
- package/build/src/device/action-event-meta.js +18 -0
- package/build/src/device/android/actions.js +6 -13
- package/build/src/device/cloud/actions.js +9 -14
- package/build/src/device/cloud/connection.js +8 -0
- package/build/src/device/ios/actions.js +6 -13
- package/build/src/device/ios/connection.js +8 -0
- package/build/src/device/loadmill.js +15 -9
- package/build/src/device/openai.js +67 -139
- package/build/src/integrations/loadmill/client.js +149 -131
- package/build/src/integrations/loadmill/executor.js +17 -11
- package/build/src/integrations/loadmill/index.js +1 -1
- package/build/src/integrations/loadmill/interpreter.js +4 -14
- package/build/src/modes/design-mode-ink.js +4 -3
- package/build/src/modes/design-mode.js +4 -3
- package/build/src/modes/execution-mode.js +139 -21
- package/build/src/prompts/base.js +92 -20
- package/build/src/prompts/design.js +6 -11
- package/build/src/prompts/execution.js +33 -109
- package/build/src/utils/console-output.js +8 -0
- package/build/src/utils/cua-debug-tracer.js +0 -1
- package/build/src/utils/run-screenshot-recorder.js +1 -1
- package/build/src/utils/structured-debug-log-manager.js +4 -1
- 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":
|
|
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 `
|
|
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 {
|
|
14
|
-
import {
|
|
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
|
-
|
|
23
|
-
let
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
contextPath: appContext.contextPath,
|
|
154
|
+
status: appContext.status,
|
|
155
|
+
text: appContextText
|
|
93
156
|
});
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
171
|
+
emitDesktopDebug("app_context.loaded", "execution", { runId }, {
|
|
135
172
|
source: "cli_context_flag",
|
|
136
|
-
contextPath:
|
|
137
|
-
|
|
138
|
-
status: "missing"
|
|
173
|
+
contextPath: appContext.contextPath,
|
|
174
|
+
status: appContext.status
|
|
139
175
|
});
|
|
140
176
|
}
|
|
141
|
-
const executionPrompt = buildExecutionModePrompt(deviceInfo, headlessConfig.promptCustomizations,
|
|
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
|
|
149
|
-
?
|
|
184
|
+
const engineOptions = screenshotRecorder
|
|
185
|
+
? {
|
|
150
186
|
recordScreenshots: true,
|
|
151
187
|
screenshotRecorder,
|
|
152
|
-
strictMode:
|
|
153
|
-
|
|
154
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,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
|
+
}
|