@yielded/agent-platform-cloudflare 0.1.0-beta.173 → 0.1.0-beta.175
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/dist/{Alarm-BMEAZo3p.mjs → Alarm-B44efvB1.mjs} +162 -103
- package/dist/Alarm-B44efvB1.mjs.map +1 -0
- package/dist/Alarm.d.mts +2 -0
- package/dist/Alarm.mjs +1 -1
- package/dist/BrowserRestCapture.mjs.map +1 -1
- package/dist/BrowserRestCrawl.mjs.map +1 -1
- package/dist/{BrowserSession-Bx0V1-6_.d.mts → BrowserSession-Cojp7nth.d.mts} +3 -2
- package/dist/{BrowserSession-BfNmyayH.mjs → BrowserSession-aovMjfL4.mjs} +14 -14
- package/dist/BrowserSession-aovMjfL4.mjs.map +1 -0
- package/dist/BrowserSession.d.mts +1 -1
- package/dist/BrowserSession.mjs +2 -2
- package/dist/BrowserUse.d.mts +40 -64
- package/dist/BrowserUse.mjs +223 -2
- package/dist/BrowserUse.mjs.map +1 -0
- package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -1
- package/dist/CloudflareCodeMode.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +9 -9
- package/dist/CloudflareScheduling.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.mjs +15 -15
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/{CloudflareThreadClient-DgZVrZzF.mjs → CloudflareThreadClient-Ai3MTs3X.mjs} +43 -40
- package/dist/CloudflareThreadClient-Ai3MTs3X.mjs.map +1 -0
- package/dist/CloudflareThreadClient.mjs +1 -1
- package/dist/{InteractiveBrowser-LdKzQeLR.mjs → InteractiveBrowser-BHNlwjZL.mjs} +40 -31
- package/dist/InteractiveBrowser-BHNlwjZL.mjs.map +1 -0
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-CRbLbdXM.d.mts → ThreadObject-CxNlhoPL.d.mts} +3 -3
- package/dist/{ThreadObject-CGIaz6Jv.mjs → ThreadObject-DGvlHWYi.mjs} +50 -52
- package/dist/ThreadObject-DGvlHWYi.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/ThreadObject.mjs +1 -1
- package/dist/WakeScheduler.d.mts +1 -1
- package/dist/WakeScheduler.mjs +8 -3
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/{prepared-admission-CSdzFM_v.mjs → prepared-admission-DYdffQ8K.mjs} +2 -2
- package/dist/{prepared-admission-CSdzFM_v.mjs.map → prepared-admission-DYdffQ8K.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +293 -155
- package/src/BrowserRestCapture.ts +1 -0
- package/src/BrowserRestCrawl.ts +1 -0
- package/src/BrowserSession.ts +3 -2
- package/src/BrowserUse.ts +287 -1504
- package/src/CloudflareCodeMode.ts +6 -0
- package/src/CloudflareScheduling.ts +103 -103
- package/src/CloudflareSubscriptions.ts +192 -199
- package/src/CloudflareThreadClient.ts +258 -269
- package/src/InteractiveBrowser.ts +43 -17
- package/src/ThreadObject.ts +51 -45
- package/src/WakeScheduler.ts +12 -9
- package/src/internal/browser-binding.ts +8 -0
- package/src/internal/browser-credentials.ts +18 -13
- package/src/internal/browser-file-selection.ts +13 -2
- package/src/internal/browser-quick-action.ts +1 -0
- package/src/internal/browser-session-lifecycle.ts +32 -36
- package/src/internal/due-queue.ts +38 -19
- package/src/internal/layers.ts +108 -87
- package/src/internal/message-delivery.ts +61 -37
- package/dist/Alarm-BMEAZo3p.mjs.map +0 -1
- package/dist/BrowserSession-BfNmyayH.mjs.map +0 -1
- package/dist/BrowserUse-Bc2JdLW2.mjs +0 -1528
- package/dist/BrowserUse-Bc2JdLW2.mjs.map +0 -1
- package/dist/CloudflareThreadClient-DgZVrZzF.mjs.map +0 -1
- package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +0 -1
- package/dist/ThreadObject-CGIaz6Jv.mjs.map +0 -1
- package/dist/page-function-CmUCcyNP.mjs +0 -11
- package/dist/page-function-CmUCcyNP.mjs.map +0 -1
- package/src/internal/browser-dom.ts +0 -886
- package/src/internal/browser-jev-dom.ts +0 -362
|
@@ -1,1528 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { t as pageFunction } from "./page-function-CmUCcyNP.mjs";
|
|
3
|
-
import { Deferred, Effect, Fiber, Layer, Schema, Semaphore } from "effect";
|
|
4
|
-
import { InteractiveBrowserTargetUrl } from "@yielded/agent/interactive-browser";
|
|
5
|
-
import { ElementHandle, TimeoutError } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
6
|
-
import { Action, BrowserActions, BrowserControl, BrowserUseError, DialogRequest, InspectRequest, Observation, PressRequest, ScrollRequest, TabRequest, WaitRequest } from "@yielded/agent/browser-use";
|
|
7
|
-
//#region src/internal/browser-dom.ts
|
|
8
|
-
/** Executor-owned input settling, matching jev-ultrafast's frame/option readiness predicate. */
|
|
9
|
-
const settleInputDom = pageFunction((ref) => new Promise((resolve) => {
|
|
10
|
-
const field = ref === void 0 ? void 0 : Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
11
|
-
const autocomplete = field?.getAttribute("role") === "combobox";
|
|
12
|
-
let frames = 0;
|
|
13
|
-
let stopped = false;
|
|
14
|
-
let animation = 0;
|
|
15
|
-
const finish = () => {
|
|
16
|
-
if (stopped) return;
|
|
17
|
-
stopped = true;
|
|
18
|
-
clearTimeout(deadline);
|
|
19
|
-
cancelAnimationFrame(animation);
|
|
20
|
-
resolve();
|
|
21
|
-
};
|
|
22
|
-
const deadline = setTimeout(finish, autocomplete ? 200 : 50);
|
|
23
|
-
const ready = () => {
|
|
24
|
-
if (stopped) return;
|
|
25
|
-
const ids = (field?.getAttribute("aria-controls") || field?.getAttribute("aria-owns") || "").split(/\s+/).filter(Boolean);
|
|
26
|
-
const options = (ids.length ? ids.map((id) => document.getElementById(id)) : [document]).flatMap((root) => [...root?.querySelectorAll("[role=\"option\"]") ?? []]);
|
|
27
|
-
if (++frames >= 2 && (!autocomplete || options.some((element) => {
|
|
28
|
-
const rect = element.getBoundingClientRect();
|
|
29
|
-
return rect.width && rect.height && rect.bottom > 0 && rect.top < innerHeight && element.checkVisibility({
|
|
30
|
-
checkOpacity: true,
|
|
31
|
-
checkVisibilityCSS: true
|
|
32
|
-
});
|
|
33
|
-
}))) finish();
|
|
34
|
-
else animation = requestAnimationFrame(ready);
|
|
35
|
-
};
|
|
36
|
-
animation = requestAnimationFrame(ready);
|
|
37
|
-
}));
|
|
38
|
-
/** Read-only executor settling: 100 ms without a DOM mutation, bounded by one second. */
|
|
39
|
-
const settleDom = pageFunction(() => new Promise((resolve) => {
|
|
40
|
-
let quiet;
|
|
41
|
-
const observer = new MutationObserver(() => {
|
|
42
|
-
clearTimeout(quiet);
|
|
43
|
-
quiet = setTimeout(() => finish(true), 100);
|
|
44
|
-
});
|
|
45
|
-
const finish = (settled) => {
|
|
46
|
-
observer.disconnect();
|
|
47
|
-
clearTimeout(quiet);
|
|
48
|
-
clearTimeout(deadline);
|
|
49
|
-
resolve(settled);
|
|
50
|
-
};
|
|
51
|
-
const deadline = setTimeout(() => finish(false), 1e3);
|
|
52
|
-
observer.observe(document, {
|
|
53
|
-
subtree: true,
|
|
54
|
-
childList: true,
|
|
55
|
-
attributes: true,
|
|
56
|
-
characterData: true
|
|
57
|
-
});
|
|
58
|
-
quiet = setTimeout(() => finish(true), 100);
|
|
59
|
-
}));
|
|
60
|
-
/** Executed in the isolated page realm; the page cannot forge the reference registry. */
|
|
61
|
-
const inspectDom = pageFunction((selector, prefix, maximum, optionFilter, viewportOnly = false) => {
|
|
62
|
-
const registry = /* @__PURE__ */ new Map();
|
|
63
|
-
const nameChecks = /* @__PURE__ */ new Map();
|
|
64
|
-
Reflect.set(globalThis, "@effect-agent/native-browser", registry);
|
|
65
|
-
Reflect.set(globalThis, "@effect-agent/native-browser-name-checks", nameChecks);
|
|
66
|
-
const roots = [document];
|
|
67
|
-
const closedRoots = Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots");
|
|
68
|
-
const visited = /* @__PURE__ */ new Set();
|
|
69
|
-
let discoveredElements = 0;
|
|
70
|
-
const candidates = [];
|
|
71
|
-
let scanned = 0;
|
|
72
|
-
let truncated = false;
|
|
73
|
-
let text = "";
|
|
74
|
-
const controlSelector = "button,a[href],input,textarea,select,label,[contenteditable=\"true\"],[role=\"button\"],[role=\"link\"],[role=\"checkbox\"],[role=\"radio\"],[role=\"switch\"],[role=\"tab\"],[role=\"option\"],[role=\"combobox\"],[role=\"textbox\"],[role=\"menuitem\"],[role=\"scrollbar\"]";
|
|
75
|
-
const filter = { acceptNode(node) {
|
|
76
|
-
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
77
|
-
const style = getComputedStyle(node);
|
|
78
|
-
return node.matches("script,style,template,option,optgroup,[inert],[aria-hidden=\"true\"]") || style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
79
|
-
} };
|
|
80
|
-
for (let index = 0; index < roots.length && !truncated; index++) {
|
|
81
|
-
const root = roots[index];
|
|
82
|
-
if (root === void 0) break;
|
|
83
|
-
if (selector !== void 0) {
|
|
84
|
-
const discovery = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter);
|
|
85
|
-
let node = discovery.nextNode();
|
|
86
|
-
while (node !== null) {
|
|
87
|
-
if (++discoveredElements > 1e4) {
|
|
88
|
-
truncated = true;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
if (node instanceof Element) {
|
|
92
|
-
const shadow = node.shadowRoot ?? closedRoots?.get(node);
|
|
93
|
-
if (shadow !== void 0 && shadow !== null) roots.push(shadow);
|
|
94
|
-
}
|
|
95
|
-
node = discovery.nextNode();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const matches = selector === void 0 ? [root] : Array.from(root.querySelectorAll(selector));
|
|
99
|
-
const scopes = matches.slice(0, 256);
|
|
100
|
-
const nodes = [];
|
|
101
|
-
truncated ||= matches.length > scopes.length;
|
|
102
|
-
for (const scope of scopes) {
|
|
103
|
-
if (scope instanceof Element && filter.acceptNode(scope) === NodeFilter.FILTER_REJECT) continue;
|
|
104
|
-
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter);
|
|
105
|
-
let node = scope instanceof Element ? scope : walker.nextNode();
|
|
106
|
-
while (node !== null) {
|
|
107
|
-
if (visited.has(node)) {
|
|
108
|
-
node = walker.nextNode();
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
visited.add(node);
|
|
112
|
-
if (++scanned > 1e4) {
|
|
113
|
-
truncated = true;
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
if (node instanceof Element) {
|
|
117
|
-
nodes.push(node);
|
|
118
|
-
const shadow = node.shadowRoot ?? closedRoots?.get(node);
|
|
119
|
-
if (selector === void 0 && shadow !== void 0 && shadow !== null) roots.push(shadow);
|
|
120
|
-
} else if (node instanceof Text) {
|
|
121
|
-
const parent = node.parentElement;
|
|
122
|
-
if (parent instanceof HTMLElement && parent.closest("select,script,style,[inert],[aria-hidden=\"true\"]") === null && parent.checkVisibility({
|
|
123
|
-
checkOpacity: true,
|
|
124
|
-
checkVisibilityCSS: true
|
|
125
|
-
})) {
|
|
126
|
-
const value = node.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
127
|
-
if (value.length > 0) {
|
|
128
|
-
const room = Math.max(0, 12e3 - text.length);
|
|
129
|
-
text += `${value.slice(0, room)}\n`;
|
|
130
|
-
truncated ||= value.length >= room;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
node = walker.nextNode();
|
|
135
|
-
}
|
|
136
|
-
if (scanned > 1e4) break;
|
|
137
|
-
}
|
|
138
|
-
for (const node of nodes) {
|
|
139
|
-
if (selector !== void 0 && !node.matches(selector) && node.closest(selector) === null) continue;
|
|
140
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
141
|
-
const scrollable = node.scrollHeight > node.clientHeight && ["auto", "scroll"].includes(getComputedStyle(node).overflowY);
|
|
142
|
-
if (!node.matches(controlSelector) && !scrollable) continue;
|
|
143
|
-
if (!node.checkVisibility({
|
|
144
|
-
checkOpacity: true,
|
|
145
|
-
checkVisibilityCSS: true
|
|
146
|
-
}) || node.closest("[inert],[aria-hidden=\"true\"]") !== null) continue;
|
|
147
|
-
const modal = document.querySelector("dialog:modal");
|
|
148
|
-
if (modal !== null && !modal.contains(node) && node.getRootNode() === document) continue;
|
|
149
|
-
candidates.push(node);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
let retained = candidates;
|
|
153
|
-
if (viewportOnly || candidates.length > maximum) {
|
|
154
|
-
const visible = [];
|
|
155
|
-
const popupControls = /* @__PURE__ */ new Map();
|
|
156
|
-
const visiblePopups = /* @__PURE__ */ new Set();
|
|
157
|
-
const offscreen = [];
|
|
158
|
-
const popupSelector = "dialog[open],[role=\"dialog\"],[role=\"listbox\"],[role=\"menu\"],:popover-open";
|
|
159
|
-
for (const node of candidates) {
|
|
160
|
-
const rect = node.getBoundingClientRect();
|
|
161
|
-
const x = rect.x + rect.width / 2;
|
|
162
|
-
const y = rect.y + rect.height / 2;
|
|
163
|
-
const inView = x >= 0 && x < innerWidth && y >= 0 && y < innerHeight;
|
|
164
|
-
if (viewportOnly) {
|
|
165
|
-
let popup = node.closest(popupSelector);
|
|
166
|
-
if (popup?.checkVisibility({
|
|
167
|
-
checkOpacity: true,
|
|
168
|
-
checkVisibilityCSS: true
|
|
169
|
-
})) {
|
|
170
|
-
if (!inView) {
|
|
171
|
-
const group = popupControls.get(popup) ?? [];
|
|
172
|
-
group.push(node);
|
|
173
|
-
popupControls.set(popup, group);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
while (popup !== null) {
|
|
177
|
-
visiblePopups.add(popup);
|
|
178
|
-
popup = popup.parentElement?.closest(popupSelector) ?? null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
(inView ? visible : offscreen).push(node);
|
|
183
|
-
}
|
|
184
|
-
const preferred = [...visible, ...[...popupControls].flatMap(([popup, nodes]) => visiblePopups.has(popup) ? [] : nodes)];
|
|
185
|
-
retained = (viewportOnly ? preferred.length > 0 ? preferred : offscreen : [...visible, ...offscreen]).slice(0, maximum);
|
|
186
|
-
truncated ||= candidates.length > retained.length;
|
|
187
|
-
}
|
|
188
|
-
const readName = (node) => {
|
|
189
|
-
const associated = node instanceof HTMLLabelElement ? node.control ?? node : node;
|
|
190
|
-
const nativeField = associated instanceof HTMLInputElement || associated instanceof HTMLTextAreaElement || associated instanceof HTMLSelectElement;
|
|
191
|
-
const labels = nativeField ? associated.labels : null;
|
|
192
|
-
const root = node.getRootNode();
|
|
193
|
-
const labelledBy = (node.getAttribute("aria-labelledby") ?? "").split(/\s+/).map((id) => root instanceof Document || root instanceof ShadowRoot ? root.getElementById(id)?.textContent ?? "" : "").join(" ");
|
|
194
|
-
return (node.getAttribute("aria-label") || labelledBy.trim() || (labels === null ? "" : Array.from(labels).map((label) => label.textContent).join(" ")) || (nativeField ? node.getAttribute("placeholder") : node instanceof HTMLElement ? node.innerText : "") || "").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
195
|
-
};
|
|
196
|
-
const controls = retained.map((node, index) => {
|
|
197
|
-
const ref = `${prefix}-${index}`;
|
|
198
|
-
const tag = node.tagName.toLowerCase();
|
|
199
|
-
const associated = node instanceof HTMLLabelElement ? node.control ?? node : node;
|
|
200
|
-
const type = associated instanceof HTMLInputElement ? associated.type : "";
|
|
201
|
-
const nativeField = associated instanceof HTMLInputElement || associated instanceof HTMLTextAreaElement || associated instanceof HTMLSelectElement;
|
|
202
|
-
const role = node.getAttribute("role");
|
|
203
|
-
const kind = tag === "label" && associated instanceof HTMLInputElement && ["checkbox", "radio"].includes(type) ? type : role ?? (type === "checkbox" || type === "radio" ? type : tag === "a" ? "link" : tag);
|
|
204
|
-
const name = readName(node);
|
|
205
|
-
const allOptions = node instanceof HTMLSelectElement ? Array.from(node.options) : [];
|
|
206
|
-
const matchingOptions = allOptions.flatMap((option, index) => option.selected || optionFilter === void 0 || `${option.label}\n${option.value}`.toLowerCase().includes(optionFilter.toLowerCase()) ? [{
|
|
207
|
-
option,
|
|
208
|
-
index
|
|
209
|
-
}] : []);
|
|
210
|
-
const selectedOptions = matchingOptions.filter(({ option }) => option.selected);
|
|
211
|
-
const visibleOptions = [...selectedOptions.slice(0, 256), ...matchingOptions.filter(({ option }) => !option.selected).slice(0, Math.max(0, 256 - selectedOptions.length))];
|
|
212
|
-
if (matchingOptions.length > visibleOptions.length) truncated = true;
|
|
213
|
-
const attributes = Object.fromEntries([
|
|
214
|
-
"id",
|
|
215
|
-
"name",
|
|
216
|
-
"type",
|
|
217
|
-
"role",
|
|
218
|
-
"tabindex",
|
|
219
|
-
"placeholder",
|
|
220
|
-
"aria-label",
|
|
221
|
-
"aria-labelledby",
|
|
222
|
-
"aria-expanded",
|
|
223
|
-
"aria-selected",
|
|
224
|
-
"aria-checked",
|
|
225
|
-
"aria-controls",
|
|
226
|
-
"href"
|
|
227
|
-
].flatMap((key) => {
|
|
228
|
-
const value = node.getAttribute(key);
|
|
229
|
-
return value === null ? [] : [[key, value.slice(0, 1024)]];
|
|
230
|
-
}));
|
|
231
|
-
registry.set(ref, node);
|
|
232
|
-
nameChecks.set(ref, {
|
|
233
|
-
name,
|
|
234
|
-
originalName: name,
|
|
235
|
-
verify: () => readName(node) === name
|
|
236
|
-
});
|
|
237
|
-
const pointerEvents = getComputedStyle(node).pointerEvents?.slice(0, 64);
|
|
238
|
-
return {
|
|
239
|
-
ref,
|
|
240
|
-
kind,
|
|
241
|
-
name,
|
|
242
|
-
attributes,
|
|
243
|
-
...pointerEvents ? { pointerEvents } : {},
|
|
244
|
-
value: nativeField && type !== "password" ? associated.value.slice(0, 4096) : node.isContentEditable ? node.innerText.slice(0, 4096) : "",
|
|
245
|
-
options: visibleOptions.map(({ option }) => option.value.slice(0, 4096)),
|
|
246
|
-
...node instanceof HTMLSelectElement ? {
|
|
247
|
-
optionCount: allOptions.length,
|
|
248
|
-
optionDetails: visibleOptions.map(({ option, index }) => ({
|
|
249
|
-
index,
|
|
250
|
-
value: option.value.slice(0, 4096),
|
|
251
|
-
label: option.label.slice(0, 300),
|
|
252
|
-
disabled: option.disabled || option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled,
|
|
253
|
-
selected: option.selected
|
|
254
|
-
}))
|
|
255
|
-
} : {},
|
|
256
|
-
disabled: associated.matches(":disabled") || associated.getAttribute("aria-disabled") === "true",
|
|
257
|
-
editable: node instanceof HTMLInputElement && !node.readOnly && ![
|
|
258
|
-
"password",
|
|
259
|
-
"file",
|
|
260
|
-
"checkbox",
|
|
261
|
-
"radio",
|
|
262
|
-
"hidden",
|
|
263
|
-
"submit",
|
|
264
|
-
"button"
|
|
265
|
-
].includes(node.type) || node instanceof HTMLTextAreaElement && !node.readOnly || node.isContentEditable && node.getAttribute("aria-readonly") !== "true",
|
|
266
|
-
...associated instanceof HTMLInputElement && (type === "checkbox" || type === "radio") ? { checked: associated.checked } : {}
|
|
267
|
-
};
|
|
268
|
-
});
|
|
269
|
-
const names = /* @__PURE__ */ new Map();
|
|
270
|
-
for (const control of controls) names.set(control.name, (names.get(control.name) ?? 0) + 1);
|
|
271
|
-
const explicitLabel = (node) => {
|
|
272
|
-
const root = node.getRootNode();
|
|
273
|
-
return (node.getAttribute("aria-label") || (node.getAttribute("aria-labelledby") ?? "").split(/\s+/).map((id) => root instanceof Document || root instanceof ShadowRoot ? root.getElementById(id)?.textContent ?? "" : "").join(" ")).replace(/\s+/g, " ").trim();
|
|
274
|
-
};
|
|
275
|
-
for (const [index, control] of controls.entries()) {
|
|
276
|
-
const node = retained[index];
|
|
277
|
-
if (node === void 0 || control.name.length === 0 || (names.get(control.name) ?? 0) < 2) continue;
|
|
278
|
-
let branch = node;
|
|
279
|
-
for (let scope = node.parentElement; scope !== null; scope = scope.parentElement) {
|
|
280
|
-
let source = explicitLabel(scope) ? scope : void 0;
|
|
281
|
-
let read = () => source === void 0 ? "" : explicitLabel(source);
|
|
282
|
-
if (source === void 0) for (let previous = branch.previousElementSibling; previous !== null; previous = previous.previousElementSibling) {
|
|
283
|
-
if (!(previous instanceof HTMLElement) || !previous.checkVisibility({
|
|
284
|
-
checkOpacity: true,
|
|
285
|
-
checkVisibilityCSS: true
|
|
286
|
-
}) || previous.closest("[inert],[aria-hidden=\"true\"]") !== null || previous.matches(controlSelector) || previous.querySelector(controlSelector) !== null || previous.childElementCount > 0 && !previous.matches("h1,h2,h3,h4,h5,h6,caption,figcaption,legend,[role=\"heading\"]")) continue;
|
|
287
|
-
const caption = previous.innerText.replace(/\s+/g, " ").trim();
|
|
288
|
-
if (caption.length === 0 || caption.length > 120) continue;
|
|
289
|
-
source = previous;
|
|
290
|
-
const captionElement = previous;
|
|
291
|
-
read = () => captionElement.innerText.replace(/\s+/g, " ").trim();
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
const context = read();
|
|
295
|
-
if (source !== void 0 && context.length > 0) {
|
|
296
|
-
const originalName = control.name;
|
|
297
|
-
const container = scope;
|
|
298
|
-
const caption = source;
|
|
299
|
-
control.name = `${originalName} (${context})`.slice(0, 300);
|
|
300
|
-
nameChecks.set(control.ref, {
|
|
301
|
-
name: control.name,
|
|
302
|
-
originalName,
|
|
303
|
-
verify: () => readName(node) === originalName && container.isConnected && container.contains(node) && container.contains(caption) && (caption === container || (caption.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) && read() === context
|
|
304
|
-
});
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
branch = scope;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return {
|
|
311
|
-
text: text.slice(0, 12e3),
|
|
312
|
-
controls,
|
|
313
|
-
readyState: document.readyState,
|
|
314
|
-
truncated: truncated || text.length > 12e3
|
|
315
|
-
};
|
|
316
|
-
});
|
|
317
|
-
/** Current state after standard DOM scrolling. Pointer actions also require a hit test;
|
|
318
|
-
* keyboard actions verify native focus at dispatch instead. Never dispatches input.
|
|
319
|
-
*/
|
|
320
|
-
const checkDom = pageFunction((target, expected, scroll, pointer) => {
|
|
321
|
-
const node = typeof target === "string" ? Reflect.get(globalThis, "@effect-agent/native-browser")?.get(target) : target;
|
|
322
|
-
if (!(node instanceof HTMLElement) || !node.isConnected) return false;
|
|
323
|
-
const nameCheck = Reflect.get(globalThis, "@effect-agent/native-browser-name-checks")?.get(expected.ref);
|
|
324
|
-
if (nameCheck !== void 0 && (nameCheck.name !== expected.name || !nameCheck.verify())) return false;
|
|
325
|
-
if (scroll && !nameCheck?.viewport) node.scrollIntoView({
|
|
326
|
-
block: "center",
|
|
327
|
-
inline: "center",
|
|
328
|
-
behavior: "instant"
|
|
329
|
-
});
|
|
330
|
-
const associated = node instanceof HTMLLabelElement ? node.control ?? node : node;
|
|
331
|
-
const type = associated instanceof HTMLInputElement ? associated.type : "";
|
|
332
|
-
if (associated instanceof HTMLSelectElement && expected.optionDetails !== void 0 && ((expected.optionCount === void 0 ? expected.optionDetails.length : expected.optionCount) !== associated.options.length || expected.optionDetails.some((option, index) => {
|
|
333
|
-
const current = associated.options.item(option.index ?? index);
|
|
334
|
-
return current === null || current.value !== option.value || current.label.slice(0, 300) !== option.label || current.selected !== option.selected || (current.disabled || current.parentElement instanceof HTMLOptGroupElement && current.parentElement.disabled) !== option.disabled;
|
|
335
|
-
}))) return false;
|
|
336
|
-
if (node.isContentEditable && node.innerText.slice(0, 4096) !== expected.value) return false;
|
|
337
|
-
if ((nameCheck?.kind ?? (node instanceof HTMLLabelElement && associated instanceof HTMLInputElement && ["checkbox", "radio"].includes(type) ? type : node.getAttribute("role") ?? (type === "checkbox" || type === "radio" ? type : node.tagName === "A" ? "link" : node.tagName.toLowerCase()))) !== expected.kind || associated.matches(":disabled") || associated.getAttribute("aria-disabled") === "true" || !node.checkVisibility({
|
|
338
|
-
checkOpacity: true,
|
|
339
|
-
checkVisibilityCSS: true
|
|
340
|
-
})) return false;
|
|
341
|
-
if ((associated instanceof HTMLInputElement || associated instanceof HTMLTextAreaElement || associated instanceof HTMLSelectElement) && type !== "password" && associated.value.slice(0, 4096) !== expected.value) return false;
|
|
342
|
-
if (!(associated instanceof HTMLInputElement || associated instanceof HTMLTextAreaElement || associated instanceof HTMLSelectElement) && !node.isContentEditable && !node.hasAttribute("aria-label") && !node.hasAttribute("aria-labelledby") && node.innerText.replace(/\s+/g, " ").trim().slice(0, 300) !== (nameCheck?.originalName ?? expected.name)) return false;
|
|
343
|
-
if (expected.checked !== void 0 && (!(associated instanceof HTMLInputElement) || associated.checked !== expected.checked)) return false;
|
|
344
|
-
if (Object.entries(expected.attributes ?? {}).some(([key, value]) => (node.getAttribute(key) ?? "").slice(0, 1024) !== value)) return false;
|
|
345
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
346
|
-
let ancestor = node;
|
|
347
|
-
while (ancestor !== null) {
|
|
348
|
-
if (ancestor instanceof Element && ancestor.matches("[inert],[aria-hidden=\"true\"]")) return false;
|
|
349
|
-
ancestors.add(ancestor);
|
|
350
|
-
ancestor = ancestor instanceof ShadowRoot ? ancestor.host : ancestor.parentNode;
|
|
351
|
-
}
|
|
352
|
-
const modalSelector = "dialog:modal,[role=\"dialog\"][aria-modal=\"true\"],[role=\"alertdialog\"][aria-modal=\"true\"]";
|
|
353
|
-
const roots = /* @__PURE__ */ new Set([document, node.getRootNode()]);
|
|
354
|
-
for (const root of roots) {
|
|
355
|
-
if (!(root instanceof Document || root instanceof ShadowRoot)) continue;
|
|
356
|
-
for (const modal of root.querySelectorAll(modalSelector)) {
|
|
357
|
-
if (!(modal instanceof HTMLElement) || ancestors.has(modal) || !modal.checkVisibility({
|
|
358
|
-
checkOpacity: true,
|
|
359
|
-
checkVisibilityCSS: true
|
|
360
|
-
})) continue;
|
|
361
|
-
const box = modal.getBoundingClientRect();
|
|
362
|
-
if (modal.matches("dialog:modal") || box.width > 0 && box.height > 0 && box.right > 0 && box.bottom > 0 && box.left < innerWidth && box.top < innerHeight) return false;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
const rect = node.getBoundingClientRect();
|
|
366
|
-
const x = Math.max(0, rect.left) + (Math.min(innerWidth, rect.right) - Math.max(0, rect.left)) / 2;
|
|
367
|
-
const y = Math.max(0, rect.top) + (Math.min(innerHeight, rect.bottom) - Math.max(0, rect.top)) / 2;
|
|
368
|
-
let hit = document.elementFromPoint(x, y);
|
|
369
|
-
while (hit !== null && hit !== node && !node.contains(hit)) {
|
|
370
|
-
const shadow = hit.shadowRoot ?? Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots")?.get(hit);
|
|
371
|
-
if (shadow === void 0 || shadow === null) break;
|
|
372
|
-
const next = shadow.elementFromPoint(x, y);
|
|
373
|
-
if (next === null || next === hit) break;
|
|
374
|
-
hit = next;
|
|
375
|
-
}
|
|
376
|
-
return rect.width > 0 && rect.height > 0 && node.isConnected && (hit === node || hit !== null && (node.contains(hit) || !pointer && node.tabIndex >= 0 && getComputedStyle(node).pointerEvents === "none" && node.parentElement?.contains(hit))) ? {
|
|
377
|
-
x,
|
|
378
|
-
y
|
|
379
|
-
} : false;
|
|
380
|
-
});
|
|
381
|
-
/** Translate a checked child point and refuse input through hidden/covered frame owners. */
|
|
382
|
-
const checkFrameDom = pageFunction(async (node, point, scroll) => {
|
|
383
|
-
if (!(node instanceof HTMLElement) || !node.isConnected || !node.checkVisibility({
|
|
384
|
-
checkOpacity: true,
|
|
385
|
-
checkVisibilityCSS: true
|
|
386
|
-
})) return false;
|
|
387
|
-
if (scroll) {
|
|
388
|
-
node.scrollIntoView({
|
|
389
|
-
block: "center",
|
|
390
|
-
inline: "center",
|
|
391
|
-
behavior: "instant"
|
|
392
|
-
});
|
|
393
|
-
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
394
|
-
}
|
|
395
|
-
const style = getComputedStyle(node);
|
|
396
|
-
let ancestor = node;
|
|
397
|
-
while (ancestor !== null) {
|
|
398
|
-
const current = getComputedStyle(ancestor);
|
|
399
|
-
const matrix = new DOMMatrixReadOnly(current.transform === "none" ? void 0 : current.transform);
|
|
400
|
-
if (ancestor.matches("[inert],[aria-hidden=\"true\"]") || !(matrix.m11 === 1 && matrix.m12 === 0 && matrix.m13 === 0 && matrix.m14 === 0 && matrix.m21 === 0 && matrix.m22 === 1 && matrix.m23 === 0 && matrix.m24 === 0 && matrix.m31 === 0 && matrix.m32 === 0 && matrix.m33 === 1 && matrix.m34 === 0 && matrix.m44 === 1) || current.perspective !== "none" || current.scale !== "none" || current.rotate !== "none" || current.offsetPath !== "none" || current.zoom !== "1" && current.zoom !== "normal") return false;
|
|
401
|
-
const root = ancestor.getRootNode();
|
|
402
|
-
ancestor = ancestor.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
|
|
403
|
-
}
|
|
404
|
-
const rect = node.getBoundingClientRect();
|
|
405
|
-
const x = rect.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft) + point.x;
|
|
406
|
-
const y = rect.top + parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop) + point.y;
|
|
407
|
-
let hit = document.elementFromPoint(x, y);
|
|
408
|
-
while (hit !== null && hit !== node && !node.contains(hit)) {
|
|
409
|
-
const shadow = hit.shadowRoot ?? Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots")?.get(hit);
|
|
410
|
-
if (shadow === void 0 || shadow === null) break;
|
|
411
|
-
const next = shadow.elementFromPoint(x, y);
|
|
412
|
-
if (next === null || next === hit) break;
|
|
413
|
-
hit = next;
|
|
414
|
-
}
|
|
415
|
-
return hit === node ? {
|
|
416
|
-
x,
|
|
417
|
-
y
|
|
418
|
-
} : false;
|
|
419
|
-
});
|
|
420
|
-
const waitDom = pageFunction((selector, state, text) => {
|
|
421
|
-
const roots = [document];
|
|
422
|
-
const closedRoots = Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots");
|
|
423
|
-
const expectedText = text?.replace(/\s+/g, " ").trim();
|
|
424
|
-
let scanned = 0;
|
|
425
|
-
for (let index = 0; index < roots.length; index++) {
|
|
426
|
-
const root = roots[index];
|
|
427
|
-
if (root === void 0) break;
|
|
428
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { acceptNode(node) {
|
|
429
|
-
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
430
|
-
const style = getComputedStyle(node);
|
|
431
|
-
return node.matches("script,style,template,option,optgroup,[inert],[aria-hidden=\"true\"]") || style.display === "none" || style.opacity === "0" || style.contentVisibility === "hidden" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
432
|
-
} });
|
|
433
|
-
let node = walker.nextNode();
|
|
434
|
-
while (node !== null) {
|
|
435
|
-
if (++scanned > 1e4) return false;
|
|
436
|
-
if (node instanceof Element) {
|
|
437
|
-
const shadow = node.shadowRoot ?? closedRoots?.get(node);
|
|
438
|
-
if (shadow !== void 0 && shadow !== null) roots.push(shadow);
|
|
439
|
-
if (node instanceof HTMLElement && node.matches(selector) && node.checkVisibility({
|
|
440
|
-
checkOpacity: true,
|
|
441
|
-
checkVisibilityCSS: true
|
|
442
|
-
}) && (expectedText === void 0 || node.innerText.replace(/\s+/g, " ").trim().includes(expectedText))) {
|
|
443
|
-
if (state === "hidden") return false;
|
|
444
|
-
if (state === "visible" || state === "enabled" && !node.matches(":disabled") && node.getAttribute("aria-disabled") !== "true" || state === "text" && text !== void 0) return true;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
node = walker.nextNode();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return state === "hidden";
|
|
451
|
-
});
|
|
452
|
-
//#endregion
|
|
453
|
-
//#region src/internal/browser-jev-dom.ts
|
|
454
|
-
/* @license
|
|
455
|
-
* Selection and accessible naming adapted from jev-ultrafast snapshot.js at 1231850.
|
|
456
|
-
* Copyright (c) 2026 Browser Use
|
|
457
|
-
*
|
|
458
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
459
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
460
|
-
* in the Software without restriction, including without limitation the rights
|
|
461
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
462
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
463
|
-
* furnished to do so, subject to the following conditions:
|
|
464
|
-
*
|
|
465
|
-
* The above copyright notice and this permission notice shall be included in all
|
|
466
|
-
* copies or substantial portions of the Software.
|
|
467
|
-
*
|
|
468
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
469
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
470
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
471
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
472
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
473
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
474
|
-
* SOFTWARE.
|
|
475
|
-
*/
|
|
476
|
-
/** Isolated-realm Jev observation. The same private refs and native guards own input. */
|
|
477
|
-
const inspectJevDom = pageFunction((selector, prefix, maximum, optionFilter) => {
|
|
478
|
-
const registry = /* @__PURE__ */ new Map();
|
|
479
|
-
const checks = /* @__PURE__ */ new Map();
|
|
480
|
-
Reflect.set(globalThis, "@effect-agent/native-browser", registry);
|
|
481
|
-
Reflect.set(globalThis, "@effect-agent/native-browser-name-checks", checks);
|
|
482
|
-
const visible = (node) => {
|
|
483
|
-
let ancestor = node;
|
|
484
|
-
while (ancestor !== null) {
|
|
485
|
-
if (ancestor.matches("[aria-hidden=\"true\"],[inert]")) return false;
|
|
486
|
-
const root = ancestor.getRootNode();
|
|
487
|
-
ancestor = ancestor.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
|
|
488
|
-
}
|
|
489
|
-
return node.checkVisibility({
|
|
490
|
-
checkOpacity: true,
|
|
491
|
-
checkVisibilityCSS: true
|
|
492
|
-
});
|
|
493
|
-
};
|
|
494
|
-
const inView = (node) => {
|
|
495
|
-
const rect = node.getBoundingClientRect();
|
|
496
|
-
const x = rect.x + rect.width / 2;
|
|
497
|
-
const y = rect.y + rect.height / 2;
|
|
498
|
-
return rect.width > 0 && rect.height > 0 && x >= 0 && y >= 0 && x < innerWidth && y < innerHeight;
|
|
499
|
-
};
|
|
500
|
-
const name = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
501
|
-
if (node === null || seen.has(node) || seen.size >= 1e4) return "";
|
|
502
|
-
seen.add(node);
|
|
503
|
-
const referenced = (node.getAttribute("aria-labelledby") ?? "").split(/\s+/).map((id) => name(document.getElementById(id), seen)).filter(Boolean).join(" ");
|
|
504
|
-
const labels = node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement ? node.labels : null;
|
|
505
|
-
return (referenced || node.getAttribute("aria-label") || Array.from(labels ?? []).map((label) => name(label, seen)).filter(Boolean).join(" ") || (node instanceof HTMLInputElement && [
|
|
506
|
-
"button",
|
|
507
|
-
"submit",
|
|
508
|
-
"reset"
|
|
509
|
-
].includes(node.type) ? node.value : "") || node.getAttribute("alt") || (node.tagName === "INPUT" ? "" : Array.from(node.childNodes).map((child) => child instanceof Text ? child.textContent : child instanceof Element && child.getAttribute("aria-hidden") !== "true" ? name(child, seen) : "").join(" ").trim()) || node.getAttribute("title") || node.getAttribute("placeholder") || "").slice(0, 4096);
|
|
510
|
-
};
|
|
511
|
-
const roles = [
|
|
512
|
-
"button",
|
|
513
|
-
"link",
|
|
514
|
-
"checkbox",
|
|
515
|
-
"radio",
|
|
516
|
-
"switch",
|
|
517
|
-
"tab",
|
|
518
|
-
"menuitem",
|
|
519
|
-
"menuitemradio",
|
|
520
|
-
"option",
|
|
521
|
-
"gridcell",
|
|
522
|
-
"combobox",
|
|
523
|
-
"textbox",
|
|
524
|
-
"searchbox",
|
|
525
|
-
"spinbutton"
|
|
526
|
-
];
|
|
527
|
-
const role = (node) => {
|
|
528
|
-
const explicit = node.getAttribute("role");
|
|
529
|
-
if (explicit !== null && roles.includes(explicit)) return explicit;
|
|
530
|
-
if (node.tagName === "BUTTON" || node.tagName === "SUMMARY") return "button";
|
|
531
|
-
if (node.tagName === "A") return "link";
|
|
532
|
-
if (node instanceof HTMLSelectElement) return "combobox";
|
|
533
|
-
if (node instanceof HTMLTextAreaElement || node.isContentEditable) return "textbox";
|
|
534
|
-
if (node instanceof HTMLInputElement) {
|
|
535
|
-
if (["checkbox", "radio"].includes(node.type)) return node.type;
|
|
536
|
-
if ([
|
|
537
|
-
"button",
|
|
538
|
-
"submit",
|
|
539
|
-
"reset",
|
|
540
|
-
"image"
|
|
541
|
-
].includes(node.type)) return "button";
|
|
542
|
-
if (node.type === "search") return "searchbox";
|
|
543
|
-
if (node.type === "number") return "spinbutton";
|
|
544
|
-
if ([
|
|
545
|
-
"text",
|
|
546
|
-
"email",
|
|
547
|
-
"url",
|
|
548
|
-
"tel"
|
|
549
|
-
].includes(node.type)) return "textbox";
|
|
550
|
-
}
|
|
551
|
-
return null;
|
|
552
|
-
};
|
|
553
|
-
const value = (node) => ("value" in node ? String(node.value) : node.isContentEditable || role(node) === "combobox" ? node.innerText.trim() : "").slice(0, 4096);
|
|
554
|
-
const readOnly = (node) => (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) && node.readOnly || node.getAttribute("aria-readonly") === "true";
|
|
555
|
-
const closedRoots = Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots");
|
|
556
|
-
const candidates = [document, ...closedRoots?.values() ?? []].flatMap((root) => Array.from(root.querySelectorAll("a[href],button,input,textarea,select,summary,[contenteditable=\"true\"]," + roles.map((role) => `[role="${role}"]`).join(","))));
|
|
557
|
-
const controls = [];
|
|
558
|
-
let truncated = candidates.length > 1e4;
|
|
559
|
-
const modal = document.querySelector("dialog:modal");
|
|
560
|
-
for (const node of Array.from(candidates).slice(0, 1e4)) {
|
|
561
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
562
|
-
if (selector !== void 0 && !node.matches(selector) && node.closest(selector) === null) continue;
|
|
563
|
-
if (node instanceof HTMLInputElement && [
|
|
564
|
-
"password",
|
|
565
|
-
"file",
|
|
566
|
-
"hidden"
|
|
567
|
-
].includes(node.type)) continue;
|
|
568
|
-
if (!visible(node) || !inView(node) || modal !== null && !modal.contains(node) || node.matches(":disabled") || node.closest("[aria-disabled=\"true\"]")) continue;
|
|
569
|
-
const kind = role(node);
|
|
570
|
-
if (kind === null || kind === "gridcell" && node.querySelector("button,[role=\"button\"]")) continue;
|
|
571
|
-
if (controls.length >= maximum) {
|
|
572
|
-
truncated = true;
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
const ref = `${prefix}-${controls.length}`;
|
|
576
|
-
const label = name(node) || kind;
|
|
577
|
-
const currentValue = value(node);
|
|
578
|
-
const readonly = readOnly(node);
|
|
579
|
-
const allOptions = node instanceof HTMLSelectElement ? Array.from(node.options) : [];
|
|
580
|
-
const matching = allOptions.flatMap((option, index) => option.selected || optionFilter === void 0 || `${option.label}\n${option.value}`.toLowerCase().includes(optionFilter.toLowerCase()) ? [{
|
|
581
|
-
option,
|
|
582
|
-
index
|
|
583
|
-
}] : []);
|
|
584
|
-
const selected = matching.filter(({ option }) => option.selected);
|
|
585
|
-
const options = [...selected.slice(0, 256), ...matching.filter(({ option }) => !option.selected).slice(0, Math.max(0, 256 - selected.length))];
|
|
586
|
-
truncated ||= matching.length > options.length;
|
|
587
|
-
registry.set(ref, node);
|
|
588
|
-
checks.set(ref, {
|
|
589
|
-
name: label,
|
|
590
|
-
originalName: node.innerText.replace(/\s+/g, " ").trim().slice(0, 300),
|
|
591
|
-
kind,
|
|
592
|
-
viewport: true,
|
|
593
|
-
verify: () => role(node) === kind && (name(node) || kind) === label && value(node) === currentValue && readOnly(node) === readonly && node.closest("[aria-disabled=\"true\"]") === null && inView(node)
|
|
594
|
-
});
|
|
595
|
-
controls.push({
|
|
596
|
-
ref,
|
|
597
|
-
kind,
|
|
598
|
-
name: label,
|
|
599
|
-
value: currentValue,
|
|
600
|
-
disabled: false,
|
|
601
|
-
editable: !readonly && ([
|
|
602
|
-
"textbox",
|
|
603
|
-
"searchbox",
|
|
604
|
-
"spinbutton"
|
|
605
|
-
].includes(kind) || kind === "combobox" && (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement)),
|
|
606
|
-
pointerEvents: getComputedStyle(node).pointerEvents.slice(0, 64),
|
|
607
|
-
options: options.map(({ option }) => option.value.slice(0, 4096)),
|
|
608
|
-
...node instanceof HTMLSelectElement ? {
|
|
609
|
-
optionCount: allOptions.length,
|
|
610
|
-
optionDetails: options.map(({ option, index }) => ({
|
|
611
|
-
index,
|
|
612
|
-
value: option.value.slice(0, 4096),
|
|
613
|
-
label: option.label.slice(0, 300),
|
|
614
|
-
selected: option.selected,
|
|
615
|
-
disabled: option.disabled || option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled
|
|
616
|
-
}))
|
|
617
|
-
} : {},
|
|
618
|
-
...node instanceof HTMLInputElement && ["checkbox", "radio"].includes(node.type) ? { checked: node.checked } : {},
|
|
619
|
-
attributes: Object.fromEntries([
|
|
620
|
-
"id",
|
|
621
|
-
"name",
|
|
622
|
-
"type",
|
|
623
|
-
"role",
|
|
624
|
-
"tabindex",
|
|
625
|
-
"placeholder",
|
|
626
|
-
"aria-label",
|
|
627
|
-
"aria-labelledby",
|
|
628
|
-
"aria-expanded",
|
|
629
|
-
"aria-selected",
|
|
630
|
-
"aria-checked",
|
|
631
|
-
"aria-readonly",
|
|
632
|
-
"aria-disabled",
|
|
633
|
-
"aria-controls",
|
|
634
|
-
"href"
|
|
635
|
-
].flatMap((key) => {
|
|
636
|
-
const attribute = node.getAttribute(key);
|
|
637
|
-
return attribute === null ? [] : [[key, attribute.slice(0, 1024)]];
|
|
638
|
-
}))
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
const scopes = selector === void 0 ? [document.body] : Array.from(document.querySelectorAll(selector));
|
|
642
|
-
const visited = /* @__PURE__ */ new Set();
|
|
643
|
-
const words = [];
|
|
644
|
-
const range = document.createRange();
|
|
645
|
-
let length = 0;
|
|
646
|
-
let scanned = 0;
|
|
647
|
-
truncated ||= scopes.length > 256;
|
|
648
|
-
for (const scope of scopes.slice(0, 256)) {
|
|
649
|
-
if (scope === null) continue;
|
|
650
|
-
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
|
|
651
|
-
let node = walker.nextNode();
|
|
652
|
-
while (node !== null && length < 6e3 && scanned++ < 1e4) {
|
|
653
|
-
const text = node.textContent?.trim() ?? "";
|
|
654
|
-
const parent = node.parentElement;
|
|
655
|
-
if (!visited.has(node) && text && parent && !parent.closest("script,style,noscript,template") && visible(parent)) {
|
|
656
|
-
range.selectNodeContents(node);
|
|
657
|
-
const rect = range.getBoundingClientRect();
|
|
658
|
-
if (rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < innerHeight && rect.right > 0 && rect.left < innerWidth) {
|
|
659
|
-
words.push(text);
|
|
660
|
-
length += text.length;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
visited.add(node);
|
|
664
|
-
node = walker.nextNode();
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
const text = words.join("\n");
|
|
668
|
-
return {
|
|
669
|
-
text: text.slice(0, 6e3),
|
|
670
|
-
controls,
|
|
671
|
-
readyState: document.readyState,
|
|
672
|
-
truncated: truncated || text.length > 6e3 || scanned >= 1e4,
|
|
673
|
-
page: {
|
|
674
|
-
title: document.title,
|
|
675
|
-
scrollY,
|
|
676
|
-
viewportHeight: innerHeight,
|
|
677
|
-
documentHeight: document.documentElement.scrollHeight
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
});
|
|
681
|
-
//#endregion
|
|
682
|
-
//#region src/BrowserUse.ts
|
|
683
|
-
var BrowserUse_exports = /* @__PURE__ */ __exportAll({ make: () => make });
|
|
684
|
-
const Limits = Schema.Struct({
|
|
685
|
-
maxActions: Schema.Int.check(Schema.isBetween({
|
|
686
|
-
minimum: 1,
|
|
687
|
-
maximum: 1e3
|
|
688
|
-
})),
|
|
689
|
-
maxReturnedBytes: Schema.Int.check(Schema.isBetween({
|
|
690
|
-
minimum: 1,
|
|
691
|
-
maximum: 8388608
|
|
692
|
-
})),
|
|
693
|
-
viewportOnly: Schema.Boolean,
|
|
694
|
-
observationMode: Schema.Literals(["default", "jev"]),
|
|
695
|
-
settleAfterAction: Schema.Union([Schema.Boolean, Schema.Literal("input")]),
|
|
696
|
-
maxWaitMillis: Schema.NullOr(Schema.Int.check(Schema.isBetween({
|
|
697
|
-
minimum: 1,
|
|
698
|
-
maximum: 15e3
|
|
699
|
-
})))
|
|
700
|
-
});
|
|
701
|
-
const invalid = (message) => new BrowserUseError({
|
|
702
|
-
code: "invalid",
|
|
703
|
-
message,
|
|
704
|
-
dispatch: "not-dispatched"
|
|
705
|
-
});
|
|
706
|
-
const observedUrl = (url) => {
|
|
707
|
-
const payload = url.startsWith("data:") ? url.indexOf(",") : -1;
|
|
708
|
-
return payload < 0 ? url.slice(0, 8192) : `${url.slice(0, Math.min(payload, 256))},[payload omitted]`;
|
|
709
|
-
};
|
|
710
|
-
const discoverClosedRoots = async (frame) => {
|
|
711
|
-
const realm = frame.isolatedRealm();
|
|
712
|
-
await using document = await realm.evaluateHandle(() => globalThis.document);
|
|
713
|
-
const { node } = await frame.client.send("DOM.describeNode", {
|
|
714
|
-
objectId: document.remoteObject().objectId,
|
|
715
|
-
depth: -1,
|
|
716
|
-
pierce: true
|
|
717
|
-
});
|
|
718
|
-
const pending = [node];
|
|
719
|
-
const roots = [];
|
|
720
|
-
let scanned = 0;
|
|
721
|
-
while (pending.length > 0 && scanned++ < 1e4 && roots.length < 256) {
|
|
722
|
-
const current = pending.pop();
|
|
723
|
-
if (current === void 0) break;
|
|
724
|
-
if (current.shadowRootType === "closed") roots.push(current.backendNodeId);
|
|
725
|
-
pending.push(...current.children ?? []);
|
|
726
|
-
pending.push(...(current.shadowRoots ?? []).filter((root) => root.shadowRootType !== "user-agent"));
|
|
727
|
-
}
|
|
728
|
-
await realm.evaluate(() => {
|
|
729
|
-
Reflect.set(globalThis, "@effect-agent/native-browser-closed-roots", /* @__PURE__ */ new Map());
|
|
730
|
-
});
|
|
731
|
-
for (const id of roots) {
|
|
732
|
-
await using root = await realm.adoptBackendNode(id);
|
|
733
|
-
await realm.evaluate((node) => {
|
|
734
|
-
if (!(node instanceof ShadowRoot) || node.mode !== "closed") throw new Error("Closed shadow root is unavailable");
|
|
735
|
-
Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots").set(node.host, node);
|
|
736
|
-
}, root);
|
|
737
|
-
}
|
|
738
|
-
return pending.length > 0;
|
|
739
|
-
};
|
|
740
|
-
/** One application-neutral controller over an existing scoped native attachment.
|
|
741
|
-
* No allocation, model, provider choice, input retry, or arbitrary page JS is exposed.
|
|
742
|
-
* Reads re-authorize at most five times when page/frame URLs change during authorization.
|
|
743
|
-
* References expire on inspection; outstanding native work retains the session's fencing.
|
|
744
|
-
*/
|
|
745
|
-
const make = Effect.fnUntraced(function* (session, options) {
|
|
746
|
-
const authorityServices = yield* Effect.context();
|
|
747
|
-
const authorize = (command, context) => options.authorize(command, context).pipe(Effect.provideContext(authorityServices));
|
|
748
|
-
const limits = yield* Schema.decodeEffect(Limits)({
|
|
749
|
-
maxActions: options.maxActions,
|
|
750
|
-
maxReturnedBytes: options.maxReturnedBytes,
|
|
751
|
-
viewportOnly: options.viewportOnly ?? false,
|
|
752
|
-
observationMode: options.observationMode ?? "default",
|
|
753
|
-
settleAfterAction: options.settleAfterAction ?? false,
|
|
754
|
-
maxWaitMillis: options.maxWaitMillis ?? null
|
|
755
|
-
}).pipe(Effect.mapError(() => invalid("Invalid browser controller limits.")));
|
|
756
|
-
const lock = yield* Semaphore.make(1);
|
|
757
|
-
const ownerScope = yield* Effect.scope;
|
|
758
|
-
let pendingInput;
|
|
759
|
-
let inputPage;
|
|
760
|
-
let dialogSignal;
|
|
761
|
-
let nextInput = 0;
|
|
762
|
-
let lastTabs = [];
|
|
763
|
-
const clearInput = () => {
|
|
764
|
-
pendingInput = void 0;
|
|
765
|
-
dialogSignal = void 0;
|
|
766
|
-
inputPage = void 0;
|
|
767
|
-
};
|
|
768
|
-
const targets = /* @__PURE__ */ new Map();
|
|
769
|
-
const frames = /* @__PURE__ */ new Set();
|
|
770
|
-
const frameIds = /* @__PURE__ */ new WeakMap();
|
|
771
|
-
const tabIds = /* @__PURE__ */ new WeakMap();
|
|
772
|
-
const tabs = /* @__PURE__ */ new Map();
|
|
773
|
-
const pendingDialogs = /* @__PURE__ */ new Map();
|
|
774
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
775
|
-
let activePage;
|
|
776
|
-
let lastNativePage;
|
|
777
|
-
let nextTab = 0;
|
|
778
|
-
let nextDialog = 0;
|
|
779
|
-
const tabId = (page) => {
|
|
780
|
-
let id = tabIds.get(page);
|
|
781
|
-
if (id === void 0) {
|
|
782
|
-
id = `t${scopeId}-${nextTab++}`;
|
|
783
|
-
tabIds.set(page, id);
|
|
784
|
-
}
|
|
785
|
-
return id;
|
|
786
|
-
};
|
|
787
|
-
const attach = (page) => {
|
|
788
|
-
if (listeners.has(page) || listeners.size >= 32) return;
|
|
789
|
-
const dialog = (dialog) => {
|
|
790
|
-
pendingDialogs.set(page, {
|
|
791
|
-
ref: `dialog-${scopeId}-${nextDialog++}`,
|
|
792
|
-
dialog
|
|
793
|
-
});
|
|
794
|
-
if (page === inputPage && dialogSignal !== void 0) Deferred.doneUnsafe(dialogSignal, Effect.void);
|
|
795
|
-
};
|
|
796
|
-
const popup = (page) => {
|
|
797
|
-
if (page !== null) attach(page);
|
|
798
|
-
};
|
|
799
|
-
listeners.set(page, {
|
|
800
|
-
dialog,
|
|
801
|
-
popup
|
|
802
|
-
});
|
|
803
|
-
page.on("dialog", dialog);
|
|
804
|
-
page.on("popup", popup);
|
|
805
|
-
};
|
|
806
|
-
yield* Effect.addFinalizer(() => session.run(Effect.void, async () => {
|
|
807
|
-
for (const { dialog } of pendingDialogs.values()) try {
|
|
808
|
-
await dialog.dismiss();
|
|
809
|
-
} catch {}
|
|
810
|
-
}).pipe(Effect.ignore, Effect.andThen(Effect.sync(() => {
|
|
811
|
-
for (const [page, listener] of listeners) {
|
|
812
|
-
page.off("dialog", listener.dialog);
|
|
813
|
-
page.off("popup", listener.popup);
|
|
814
|
-
}
|
|
815
|
-
listeners.clear();
|
|
816
|
-
pendingDialogs.clear();
|
|
817
|
-
tabs.clear();
|
|
818
|
-
activePage = void 0;
|
|
819
|
-
targets.clear();
|
|
820
|
-
frames.clear();
|
|
821
|
-
}))));
|
|
822
|
-
const scopeId = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)).join("-");
|
|
823
|
-
let nextFrame = 0;
|
|
824
|
-
const frameId = (frame) => {
|
|
825
|
-
let id = frameIds.get(frame);
|
|
826
|
-
if (id === void 0) {
|
|
827
|
-
id = `f${scopeId}-${nextFrame++}`;
|
|
828
|
-
frameIds.set(frame, id);
|
|
829
|
-
}
|
|
830
|
-
return id;
|
|
831
|
-
};
|
|
832
|
-
const resolveFrame = (page, ref) => page.frames().find((frame) => frameIds.get(frame) === ref);
|
|
833
|
-
let generation = 0;
|
|
834
|
-
let consumed = 0;
|
|
835
|
-
const native = (command, action, timeoutMillis, phase) => Effect.gen(function* () {
|
|
836
|
-
if (pendingInput !== void 0 && phase !== "input" && command.kind !== "respond-dialog") return yield* invalid("Native input is pending. Inspect and respond to its dialog before another browser operation.");
|
|
837
|
-
const span = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
838
|
-
let authorizedUrl;
|
|
839
|
-
let authorizedTabUrl;
|
|
840
|
-
let authorizedFrame;
|
|
841
|
-
const read = [
|
|
842
|
-
"observe",
|
|
843
|
-
"inspect",
|
|
844
|
-
"wait",
|
|
845
|
-
"screenshot"
|
|
846
|
-
].includes(command.kind);
|
|
847
|
-
for (let attempt = 0;; attempt++) {
|
|
848
|
-
let authorizationChanged = false;
|
|
849
|
-
let lostDocument = false;
|
|
850
|
-
const result = yield* session.run(Effect.suspend(() => {
|
|
851
|
-
const page = activePage ?? lastNativePage;
|
|
852
|
-
const target = command.kind === "act" ? targets.get(command.action.ref) : command.kind === "scroll" && command.request.ref !== void 0 ? targets.get(command.request.ref) : void 0;
|
|
853
|
-
authorizedUrl = page?.url();
|
|
854
|
-
authorizedTabUrl = command.kind === "select-tab" ? tabs.get(command.request.ref)?.url() : void 0;
|
|
855
|
-
const frameRef = target?.frame ?? (command.kind === "inspect" || command.kind === "wait" ? command.request.frame : void 0);
|
|
856
|
-
authorizedFrame = frameRef === void 0 ? void 0 : {
|
|
857
|
-
ref: frameRef,
|
|
858
|
-
url: page === void 0 ? void 0 : resolveFrame(page, frameRef)?.url()
|
|
859
|
-
};
|
|
860
|
-
return authorize(command, {
|
|
861
|
-
...authorizedUrl === void 0 ? {} : { pageUrl: authorizedUrl },
|
|
862
|
-
...authorizedTabUrl === void 0 ? {} : { tabUrl: authorizedTabUrl },
|
|
863
|
-
...authorizedFrame === void 0 ? {} : { frameUrl: authorizedFrame.url },
|
|
864
|
-
...target === void 0 ? {} : { target: target.control }
|
|
865
|
-
});
|
|
866
|
-
}).pipe(Effect.mapError((error) => new BrowserUseError({
|
|
867
|
-
code: error.code,
|
|
868
|
-
message: error.message,
|
|
869
|
-
dispatch: "not-dispatched"
|
|
870
|
-
}))), async (page) => {
|
|
871
|
-
attach(page);
|
|
872
|
-
const selected = activePage ?? page;
|
|
873
|
-
lastNativePage = page;
|
|
874
|
-
if (selected.isClosed() || selected.browserContext() !== page.browserContext()) throw new Error("Observed tab is unavailable");
|
|
875
|
-
if (authorizedUrl !== void 0 && selected.url() !== authorizedUrl) {
|
|
876
|
-
authorizationChanged = true;
|
|
877
|
-
throw new Error("Page changed during authorization");
|
|
878
|
-
}
|
|
879
|
-
if (authorizedFrame !== void 0 && resolveFrame(selected, authorizedFrame.ref)?.url() !== authorizedFrame.url) {
|
|
880
|
-
authorizationChanged = true;
|
|
881
|
-
throw new Error("Frame changed during authorization");
|
|
882
|
-
}
|
|
883
|
-
if (command.kind === "select-tab" && tabs.get(command.request.ref)?.url() !== authorizedTabUrl) throw new Error("Tab changed during authorization");
|
|
884
|
-
attach(selected);
|
|
885
|
-
try {
|
|
886
|
-
return await action(selected, (name) => span.attribute("browser.native.stage", name));
|
|
887
|
-
} catch (error) {
|
|
888
|
-
lostDocument = error instanceof Error && /Execution context was destroyed|Cannot find context with specified id/.test(error.message);
|
|
889
|
-
if (read && (lostDocument || authorizedUrl !== void 0 && selected.url() !== authorizedUrl)) authorizationChanged = true;
|
|
890
|
-
throw error;
|
|
891
|
-
}
|
|
892
|
-
}, timeoutMillis === void 0 ? void 0 : { timeoutMillis }).pipe(Effect.result);
|
|
893
|
-
if (result._tag === "Success") return result.success;
|
|
894
|
-
if (!read || !authorizationChanged || attempt >= 4 || result.failure._tag !== "BrowserSessionError" || result.failure.reason !== "provider" || result.failure.cleanup !== "not-requested") return yield* result.failure;
|
|
895
|
-
span.attribute("browser.read.authorization_retries", attempt + 1);
|
|
896
|
-
yield* Effect.sleep(lostDocument ? "250 millis" : "20 millis");
|
|
897
|
-
}
|
|
898
|
-
}).pipe(Effect.mapError((error) => {
|
|
899
|
-
if (error._tag === "BrowserUseError") return error;
|
|
900
|
-
const read = [
|
|
901
|
-
"observe",
|
|
902
|
-
"inspect",
|
|
903
|
-
"wait",
|
|
904
|
-
"screenshot"
|
|
905
|
-
].includes(command.kind);
|
|
906
|
-
return new BrowserUseError({
|
|
907
|
-
code: "browser",
|
|
908
|
-
message: read ? `Browser ${command.kind} failed (${error.reason}); cleanup ${error.cleanup}. No new input was dispatched.` : `Browser ${error.reason}; native outcome ${error.dispatch}; cleanup ${error.cleanup}. Input is never replayed.`,
|
|
909
|
-
dispatch: read || error.dispatch === "not-dispatched" ? "not-dispatched" : "unknown"
|
|
910
|
-
});
|
|
911
|
-
}), Effect.withSpan(`BrowserUse.${command.kind}${phase === void 0 ? "" : `.${phase}`}`));
|
|
912
|
-
const bounded = (value) => Effect.gen(function* () {
|
|
913
|
-
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > limits.maxReturnedBytes) return yield* invalid("Browser result exceeds the host byte budget. Narrow inspection before acting.");
|
|
914
|
-
return value;
|
|
915
|
-
});
|
|
916
|
-
const inspect = Effect.fnUntraced(function* (request, command = {
|
|
917
|
-
kind: "inspect",
|
|
918
|
-
request
|
|
919
|
-
}) {
|
|
920
|
-
yield* Schema.decodeEffect(InspectRequest)(request).pipe(Effect.mapError(() => invalid("Invalid inspection request.")));
|
|
921
|
-
const selectedFrame = request.frame;
|
|
922
|
-
if (request.frame !== void 0 && !frames.has(request.frame)) return yield* invalid("Frame was not observed. Inspect the page first.");
|
|
923
|
-
targets.clear();
|
|
924
|
-
generation++;
|
|
925
|
-
if (pendingInput?.fiber.pollUnsafe()?._tag === "Failure") {
|
|
926
|
-
const failed = pendingInput.fiber;
|
|
927
|
-
clearInput();
|
|
928
|
-
yield* Fiber.join(failed);
|
|
929
|
-
}
|
|
930
|
-
const blocked = inputPage === void 0 ? void 0 : pendingDialogs.get(inputPage);
|
|
931
|
-
if (pendingInput !== void 0 && blocked !== void 0) {
|
|
932
|
-
yield* authorize(command, { pageUrl: inputPage?.url() });
|
|
933
|
-
return yield* bounded({
|
|
934
|
-
text: "Native input is awaiting this JavaScript dialog. Respond before further page operations; never replay the input.",
|
|
935
|
-
controls: [],
|
|
936
|
-
tabs: lastTabs,
|
|
937
|
-
dialogs: [{
|
|
938
|
-
ref: blocked.ref,
|
|
939
|
-
type: blocked.dialog.type(),
|
|
940
|
-
message: blocked.dialog.message().slice(0, 4096),
|
|
941
|
-
defaultValue: blocked.dialog.defaultValue().slice(0, 4096)
|
|
942
|
-
}]
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
const value = yield* native(command, async (page) => {
|
|
946
|
-
const currentTabs = await page.browserContext().pages();
|
|
947
|
-
tabs.clear();
|
|
948
|
-
currentTabs.slice(0, 32).forEach((tab) => {
|
|
949
|
-
tabs.set(tabId(tab), tab);
|
|
950
|
-
attach(tab);
|
|
951
|
-
});
|
|
952
|
-
const tabObservation = currentTabs.slice(0, 32).map((tab) => ({
|
|
953
|
-
ref: tabId(tab),
|
|
954
|
-
url: observedUrl(tab.url()),
|
|
955
|
-
active: tab === page
|
|
956
|
-
}));
|
|
957
|
-
const pending = pendingDialogs.get(page);
|
|
958
|
-
lastTabs = tabObservation;
|
|
959
|
-
if (pending !== void 0) return {
|
|
960
|
-
text: "A native JavaScript dialog blocks page interaction. Respond to its observed reference before continuing.",
|
|
961
|
-
controls: [],
|
|
962
|
-
tabs: tabObservation,
|
|
963
|
-
truncated: currentTabs.length > 32,
|
|
964
|
-
dialogs: [{
|
|
965
|
-
ref: pending.ref,
|
|
966
|
-
type: pending.dialog.type(),
|
|
967
|
-
message: pending.dialog.message().slice(0, 4096),
|
|
968
|
-
defaultValue: pending.dialog.defaultValue().slice(0, 4096)
|
|
969
|
-
}]
|
|
970
|
-
};
|
|
971
|
-
const currentFrames = page.frames();
|
|
972
|
-
if (selectedFrame !== void 0 && command.kind !== "observe" && !currentFrames.some((frame) => frameId(frame) === selectedFrame)) throw new Error("Observed frame is detached");
|
|
973
|
-
frames.clear();
|
|
974
|
-
currentFrames.slice(0, 32).forEach((frame) => frames.add(frameId(frame)));
|
|
975
|
-
const inspected = selectedFrame === void 0 ? [page.mainFrame()] : currentFrames.filter((frame) => frameId(frame) === selectedFrame);
|
|
976
|
-
if (inspected.length === 0) inspected.push(page.mainFrame());
|
|
977
|
-
let text = "";
|
|
978
|
-
let readyState;
|
|
979
|
-
let truncated = currentFrames.length > 32 || currentTabs.length > 32;
|
|
980
|
-
let metrics;
|
|
981
|
-
const controls = [];
|
|
982
|
-
for (const frame of inspected) {
|
|
983
|
-
if (controls.length >= 256) {
|
|
984
|
-
truncated = true;
|
|
985
|
-
break;
|
|
986
|
-
}
|
|
987
|
-
const ref = frameId(frame);
|
|
988
|
-
if (ref === void 0) continue;
|
|
989
|
-
const read = async () => {
|
|
990
|
-
const rootsTruncated = await discoverClosedRoots(frame);
|
|
991
|
-
truncated ||= rootsTruncated;
|
|
992
|
-
return limits.observationMode === "jev" ? frame.isolatedRealm().evaluate(inspectJevDom, request.selector, `r${scopeId}-${generation}-${controls.length}`, 256 - controls.length, request.optionFilter) : frame.isolatedRealm().evaluate(inspectDom, request.selector, `r${scopeId}-${generation}-${controls.length}`, 256 - controls.length, request.optionFilter, limits.viewportOnly);
|
|
993
|
-
};
|
|
994
|
-
let result = Schema.decodeUnknownSync(Observation)(await read());
|
|
995
|
-
if (result.readyState === "loading") {
|
|
996
|
-
await (await frame.isolatedRealm().waitForFunction(() => document.readyState !== "loading", {
|
|
997
|
-
timeout: 2e3,
|
|
998
|
-
polling: "raf"
|
|
999
|
-
}).catch((error) => {
|
|
1000
|
-
if (error instanceof TimeoutError) return void 0;
|
|
1001
|
-
throw error;
|
|
1002
|
-
}))?.dispose();
|
|
1003
|
-
result = Schema.decodeUnknownSync(Observation)(await read());
|
|
1004
|
-
}
|
|
1005
|
-
readyState = result.readyState;
|
|
1006
|
-
if (frame === page.mainFrame()) metrics = result.page;
|
|
1007
|
-
text += limits.observationMode === "jev" ? result.text : `\n[${ref}]${result.text}`;
|
|
1008
|
-
truncated ||= result.truncated ?? false;
|
|
1009
|
-
for (const control of result.controls) {
|
|
1010
|
-
const current = {
|
|
1011
|
-
...control,
|
|
1012
|
-
frame: ref
|
|
1013
|
-
};
|
|
1014
|
-
targets.set(control.ref, {
|
|
1015
|
-
frame: ref,
|
|
1016
|
-
control: current
|
|
1017
|
-
});
|
|
1018
|
-
controls.push(current);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
const textLimit = limits.observationMode === "jev" ? 6e3 : 24e3;
|
|
1022
|
-
return {
|
|
1023
|
-
text: text.slice(0, textLimit),
|
|
1024
|
-
controls,
|
|
1025
|
-
...readyState === void 0 ? {} : { readyState },
|
|
1026
|
-
...metrics === void 0 ? {} : { page: metrics },
|
|
1027
|
-
truncated: truncated || text.length > textLimit,
|
|
1028
|
-
frames: currentFrames.slice(0, 32).map((frame) => ({
|
|
1029
|
-
ref: frameId(frame),
|
|
1030
|
-
name: frame.name().slice(0, 300),
|
|
1031
|
-
url: observedUrl(frame.url().startsWith("data:") ? frame.url() : frame.url().split("#", 1)[0]),
|
|
1032
|
-
inspected: inspected.includes(frame)
|
|
1033
|
-
})),
|
|
1034
|
-
tabs: tabObservation,
|
|
1035
|
-
dialogs: []
|
|
1036
|
-
};
|
|
1037
|
-
}).pipe(Effect.tapError(() => Effect.sync(() => targets.clear())));
|
|
1038
|
-
return yield* bounded(value).pipe(Effect.tapError(() => Effect.sync(() => targets.clear())));
|
|
1039
|
-
});
|
|
1040
|
-
const charge = Effect.fnUntraced(function* () {
|
|
1041
|
-
if (consumed >= limits.maxActions) return yield* invalid("Browser action budget exhausted.");
|
|
1042
|
-
consumed++;
|
|
1043
|
-
});
|
|
1044
|
-
const checkedPoint = async (element, frame, expected, scroll, pointer = true) => {
|
|
1045
|
-
let point = await element.evaluate(checkDom, expected, scroll, pointer);
|
|
1046
|
-
if (point === false) return false;
|
|
1047
|
-
let current = frame;
|
|
1048
|
-
let parent = current.parentFrame();
|
|
1049
|
-
while (parent !== null) {
|
|
1050
|
-
await discoverClosedRoots(parent);
|
|
1051
|
-
await using owner = await current.frameElement();
|
|
1052
|
-
if (owner === null) return false;
|
|
1053
|
-
await using isolatedOwner = await parent.isolatedRealm().adoptHandle(owner);
|
|
1054
|
-
point = await isolatedOwner.evaluate(checkFrameDom, point, scroll);
|
|
1055
|
-
if (point === false) return false;
|
|
1056
|
-
current = parent;
|
|
1057
|
-
parent = current.parentFrame();
|
|
1058
|
-
}
|
|
1059
|
-
return point;
|
|
1060
|
-
};
|
|
1061
|
-
const settle = (input) => Effect.suspend(() => limits.settleAfterAction ? native({ kind: "observe" }, async (page) => {
|
|
1062
|
-
const frame = limits.settleAfterAction === "input" && input !== void 0 ? resolveFrame(page, input.frame) : page.mainFrame();
|
|
1063
|
-
if (frame === void 0) return;
|
|
1064
|
-
if (limits.settleAfterAction === "input") await frame.isolatedRealm().evaluate(settleInputDom, input?.ref);
|
|
1065
|
-
else await frame.isolatedRealm().evaluate(settleDom);
|
|
1066
|
-
}, void 0, "settle").pipe(Effect.asVoid) : Effect.void);
|
|
1067
|
-
const after = Effect.fnUntraced(function* (completed, error, dispatch, frame, alreadySettled = false, observe = true) {
|
|
1068
|
-
if (dispatch === "acknowledged" && !alreadySettled) yield* settle().pipe(Effect.catch((failure) => Effect.sync(() => {
|
|
1069
|
-
error ??= failure.message;
|
|
1070
|
-
})));
|
|
1071
|
-
if (!observe) targets.clear();
|
|
1072
|
-
const observation = !observe ? null : yield* inspect(frame === void 0 ? {} : { frame }, { kind: "observe" }).pipe(Effect.catch((failure) => {
|
|
1073
|
-
error ??= `Input receipt retained; observation failed. Inspect before continuing. ${failure.message}`;
|
|
1074
|
-
return Effect.succeed(null);
|
|
1075
|
-
}));
|
|
1076
|
-
return {
|
|
1077
|
-
completed,
|
|
1078
|
-
error,
|
|
1079
|
-
observation,
|
|
1080
|
-
dispatch,
|
|
1081
|
-
...pendingInput === void 0 ? {} : { pendingInput: {
|
|
1082
|
-
ref: pendingInput.ref,
|
|
1083
|
-
reason: "dialog"
|
|
1084
|
-
} }
|
|
1085
|
-
};
|
|
1086
|
-
});
|
|
1087
|
-
const action = Effect.fnUntraced(function* (value) {
|
|
1088
|
-
if (pendingInput !== void 0) return yield* invalid("Native input is pending; respond to its dialog without replaying it.");
|
|
1089
|
-
const target = targets.get(value.ref);
|
|
1090
|
-
if (target === void 0) return yield* invalid("Target was not observed. Inspect before acting.");
|
|
1091
|
-
if (value.kind === "select" && !target.control.options.includes(value.value)) return yield* invalid("Select option was not observed. Inspect with optionFilter first.");
|
|
1092
|
-
yield* charge();
|
|
1093
|
-
const command = {
|
|
1094
|
-
kind: "act",
|
|
1095
|
-
action: value
|
|
1096
|
-
};
|
|
1097
|
-
const inFrame = ![...tabs.values()].some((tab) => frameIds.get(tab.mainFrame()) === target.frame);
|
|
1098
|
-
let dispatch = "not-dispatched";
|
|
1099
|
-
let refusal;
|
|
1100
|
-
if (!(yield* native(command, async (page, stage) => {
|
|
1101
|
-
const frame = resolveFrame(page, target.frame);
|
|
1102
|
-
if (frame === void 0) return false;
|
|
1103
|
-
if (frame.parentFrame() === null) {
|
|
1104
|
-
stage("validate-and-scroll");
|
|
1105
|
-
return await frame.isolatedRealm().evaluate(checkDom, value.ref, target.control, true, value.kind !== "press") !== false;
|
|
1106
|
-
}
|
|
1107
|
-
stage("resolve-reference");
|
|
1108
|
-
const handle = await frame.isolatedRealm().evaluateHandle((ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null, value.ref);
|
|
1109
|
-
try {
|
|
1110
|
-
const element = handle.asElement();
|
|
1111
|
-
stage("validate-and-scroll");
|
|
1112
|
-
return element !== null && await checkedPoint(element, frame, target.control, true, value.kind !== "press") !== false;
|
|
1113
|
-
} finally {
|
|
1114
|
-
stage("release-reference");
|
|
1115
|
-
await handle.dispose();
|
|
1116
|
-
}
|
|
1117
|
-
}, inFrame ? 8e3 : 2e3, "prepare").pipe(Effect.mapError((error) => new BrowserUseError({
|
|
1118
|
-
code: error.code,
|
|
1119
|
-
message: error.message,
|
|
1120
|
-
dispatch: "not-dispatched"
|
|
1121
|
-
}))))) return yield* invalid("Observed target is stale, disabled or obstructed. No input dispatched. Inspect for a current target.");
|
|
1122
|
-
const input = native(command, async (page, stage) => {
|
|
1123
|
-
inputPage = page;
|
|
1124
|
-
const frame = resolveFrame(page, target.frame);
|
|
1125
|
-
if (frame === void 0) return "not-dispatched";
|
|
1126
|
-
if (frame.parentFrame() === null) {
|
|
1127
|
-
const realm = frame.isolatedRealm();
|
|
1128
|
-
const point = await realm.evaluate(checkDom, value.ref, target.control, false, value.kind !== "press");
|
|
1129
|
-
if (point === false) return "not-dispatched";
|
|
1130
|
-
stage("dispatch-input");
|
|
1131
|
-
if (value.kind === "click") {
|
|
1132
|
-
dispatch = "unknown";
|
|
1133
|
-
await page.mouse.click(point.x, point.y);
|
|
1134
|
-
} else if (value.kind === "select") {
|
|
1135
|
-
dispatch = "unknown";
|
|
1136
|
-
if (!await realm.evaluate((ref, value) => {
|
|
1137
|
-
const node = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
1138
|
-
if (!(node instanceof HTMLSelectElement) || !Array.from(node.options).some((option) => !option.disabled && !(option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled) && option.value === value)) return false;
|
|
1139
|
-
if (node.multiple) for (const option of Array.from(node.options)) option.selected = option.value === value;
|
|
1140
|
-
else {
|
|
1141
|
-
for (const option of Array.from(node.options)) option.selected = false;
|
|
1142
|
-
const match = Array.from(node.options).find((option) => option.value === value);
|
|
1143
|
-
if (match !== void 0) match.selected = true;
|
|
1144
|
-
}
|
|
1145
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1146
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1147
|
-
return true;
|
|
1148
|
-
}, value.ref, value.value)) {
|
|
1149
|
-
dispatch = "not-dispatched";
|
|
1150
|
-
return "not-dispatched";
|
|
1151
|
-
}
|
|
1152
|
-
} else if (value.kind === "press") {
|
|
1153
|
-
if (!(await realm.evaluate((ref) => {
|
|
1154
|
-
const node = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
1155
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
1156
|
-
node.focus();
|
|
1157
|
-
return true;
|
|
1158
|
-
}, value.ref) && await realm.evaluate((ref) => {
|
|
1159
|
-
const node = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
1160
|
-
return node instanceof Node && node.isConnected && Reflect.get(node.getRootNode(), "activeElement") === node;
|
|
1161
|
-
}, value.ref))) return "not-dispatched";
|
|
1162
|
-
dispatch = "unknown";
|
|
1163
|
-
await page.keyboard.press(value.key);
|
|
1164
|
-
} else {
|
|
1165
|
-
if (!await realm.evaluate((ref) => {
|
|
1166
|
-
const node = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
1167
|
-
if (!(node instanceof HTMLInputElement && ![
|
|
1168
|
-
"password",
|
|
1169
|
-
"file",
|
|
1170
|
-
"checkbox",
|
|
1171
|
-
"radio",
|
|
1172
|
-
"hidden",
|
|
1173
|
-
"submit",
|
|
1174
|
-
"button"
|
|
1175
|
-
].includes(node.type) && !node.readOnly || node instanceof HTMLTextAreaElement && !node.readOnly || node instanceof HTMLElement && node.isContentEditable && node.getAttribute("aria-readonly") !== "true")) return false;
|
|
1176
|
-
node.focus();
|
|
1177
|
-
return true;
|
|
1178
|
-
}, value.ref)) return "not-dispatched";
|
|
1179
|
-
if (!await realm.evaluate((ref) => {
|
|
1180
|
-
const node = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref);
|
|
1181
|
-
if (!(node instanceof HTMLElement) || !node.isConnected || !(node.getRootNode() instanceof Document || node.getRootNode() instanceof ShadowRoot) || Reflect.get(node.getRootNode(), "activeElement") !== node) return false;
|
|
1182
|
-
let selected = false;
|
|
1183
|
-
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
|
1184
|
-
node.select();
|
|
1185
|
-
selected = node.value.length === 0 || node.selectionStart === 0 && node.selectionEnd === node.value.length || globalThis.getSelection()?.toString() === node.value;
|
|
1186
|
-
} else {
|
|
1187
|
-
const selection = globalThis.getSelection();
|
|
1188
|
-
if (selection === null) return false;
|
|
1189
|
-
const range = document.createRange();
|
|
1190
|
-
range.selectNodeContents(node);
|
|
1191
|
-
selection.removeAllRanges();
|
|
1192
|
-
selection.addRange(range);
|
|
1193
|
-
selected = selection.rangeCount === 1 && selection.getRangeAt(0).compareBoundaryPoints(Range.START_TO_START, range) === 0 && selection.getRangeAt(0).compareBoundaryPoints(Range.END_TO_END, range) === 0;
|
|
1194
|
-
}
|
|
1195
|
-
return selected && node.isConnected && Reflect.get(node.getRootNode(), "activeElement") === node;
|
|
1196
|
-
}, value.ref)) {
|
|
1197
|
-
refusal = "Native selection could not prepare this field for replacement. No input dispatched.";
|
|
1198
|
-
return "not-dispatched";
|
|
1199
|
-
}
|
|
1200
|
-
dispatch = "unknown";
|
|
1201
|
-
await page.keyboard.sendCharacter(value.value);
|
|
1202
|
-
}
|
|
1203
|
-
dispatch = "acknowledged";
|
|
1204
|
-
return "acknowledged";
|
|
1205
|
-
}
|
|
1206
|
-
const handle = await frame.isolatedRealm().evaluateHandle((ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null, value.ref);
|
|
1207
|
-
try {
|
|
1208
|
-
const element = handle.asElement();
|
|
1209
|
-
if (element === null || !(element instanceof ElementHandle)) return "not-dispatched";
|
|
1210
|
-
const point = await checkedPoint(element, frame, target.control, false, value.kind !== "press");
|
|
1211
|
-
if (point === false) return "not-dispatched";
|
|
1212
|
-
stage("dispatch-input");
|
|
1213
|
-
if (value.kind === "click") {
|
|
1214
|
-
dispatch = "unknown";
|
|
1215
|
-
await page.mouse.click(point.x, point.y);
|
|
1216
|
-
} else if (value.kind === "select") {
|
|
1217
|
-
if (!await element.evaluate((node, value) => node instanceof HTMLSelectElement && Array.from(node.options).some((option) => !option.disabled && !(option.parentElement instanceof HTMLOptGroupElement && option.parentElement.disabled) && option.value === value), value.value)) return "not-dispatched";
|
|
1218
|
-
dispatch = "unknown";
|
|
1219
|
-
await element.select(value.value);
|
|
1220
|
-
} else if (value.kind === "press") {
|
|
1221
|
-
await element.focus();
|
|
1222
|
-
if (!await element.evaluate((node) => node.isConnected && Reflect.get(node.getRootNode(), "activeElement") === node)) return "not-dispatched";
|
|
1223
|
-
dispatch = "unknown";
|
|
1224
|
-
await page.keyboard.press(value.key);
|
|
1225
|
-
} else {
|
|
1226
|
-
if (!await element.evaluate((node) => node instanceof HTMLInputElement && ![
|
|
1227
|
-
"password",
|
|
1228
|
-
"file",
|
|
1229
|
-
"checkbox",
|
|
1230
|
-
"radio",
|
|
1231
|
-
"hidden",
|
|
1232
|
-
"submit",
|
|
1233
|
-
"button"
|
|
1234
|
-
].includes(node.type) && !node.readOnly || node instanceof HTMLTextAreaElement && !node.readOnly || node instanceof HTMLElement && node.isContentEditable && node.getAttribute("aria-readonly") !== "true")) return "not-dispatched";
|
|
1235
|
-
await element.focus();
|
|
1236
|
-
if (!await element.evaluate((node) => node.isConnected && (node.getRootNode() instanceof Document || node.getRootNode() instanceof ShadowRoot) && Reflect.get(node.getRootNode(), "activeElement") === node)) return "not-dispatched";
|
|
1237
|
-
if (!await element.evaluate((node) => {
|
|
1238
|
-
let selected = false;
|
|
1239
|
-
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
|
1240
|
-
node.select();
|
|
1241
|
-
selected = node.value.length === 0 || node.selectionStart === 0 && node.selectionEnd === node.value.length || globalThis.getSelection()?.toString() === node.value;
|
|
1242
|
-
} else {
|
|
1243
|
-
const selection = globalThis.getSelection();
|
|
1244
|
-
if (selection === null) return false;
|
|
1245
|
-
const range = document.createRange();
|
|
1246
|
-
range.selectNodeContents(node);
|
|
1247
|
-
selection.removeAllRanges();
|
|
1248
|
-
selection.addRange(range);
|
|
1249
|
-
selected = selection.rangeCount === 1 && selection.getRangeAt(0).compareBoundaryPoints(Range.START_TO_START, range) === 0 && selection.getRangeAt(0).compareBoundaryPoints(Range.END_TO_END, range) === 0;
|
|
1250
|
-
}
|
|
1251
|
-
return selected && node.isConnected && Reflect.get(node.getRootNode(), "activeElement") === node;
|
|
1252
|
-
})) {
|
|
1253
|
-
refusal = "Native selection could not prepare this field for replacement. No input dispatched.";
|
|
1254
|
-
return "not-dispatched";
|
|
1255
|
-
}
|
|
1256
|
-
dispatch = "unknown";
|
|
1257
|
-
await page.keyboard.sendCharacter(value.value);
|
|
1258
|
-
}
|
|
1259
|
-
dispatch = "acknowledged";
|
|
1260
|
-
return "acknowledged";
|
|
1261
|
-
} finally {
|
|
1262
|
-
await handle.dispose();
|
|
1263
|
-
}
|
|
1264
|
-
}, void 0, "input").pipe(Effect.mapError((error) => new BrowserUseError({
|
|
1265
|
-
code: error.code,
|
|
1266
|
-
message: error.message,
|
|
1267
|
-
dispatch
|
|
1268
|
-
})));
|
|
1269
|
-
const start = yield* Deferred.make();
|
|
1270
|
-
const dialogReady = yield* Deferred.make();
|
|
1271
|
-
const ref = `op${scopeId}-${nextInput++}`;
|
|
1272
|
-
const fiber = yield* Deferred.await(start).pipe(Effect.andThen(input), Effect.forkIn(ownerScope));
|
|
1273
|
-
pendingInput = {
|
|
1274
|
-
ref,
|
|
1275
|
-
fiber
|
|
1276
|
-
};
|
|
1277
|
-
dialogSignal = dialogReady;
|
|
1278
|
-
yield* Deferred.succeed(start, void 0);
|
|
1279
|
-
const result = yield* Effect.raceFirst(Fiber.join(fiber), Deferred.await(dialogReady).pipe(Effect.as("unknown"))).pipe(Effect.onInterrupt(() => Fiber.interrupt(fiber)), Effect.result);
|
|
1280
|
-
if (result._tag === "Failure") {
|
|
1281
|
-
clearInput();
|
|
1282
|
-
return yield* result.failure;
|
|
1283
|
-
}
|
|
1284
|
-
if (result.success !== "unknown") clearInput();
|
|
1285
|
-
if (result.success === "not-dispatched" && refusal !== void 0) return yield* invalid(refusal);
|
|
1286
|
-
if (result.success === "acknowledged") yield* settle(value.kind === "fill" ? {
|
|
1287
|
-
frame: target.frame,
|
|
1288
|
-
ref: value.ref
|
|
1289
|
-
} : void 0).pipe(Effect.mapError((failure) => new BrowserUseError({
|
|
1290
|
-
code: failure.code,
|
|
1291
|
-
message: failure.message,
|
|
1292
|
-
dispatch: "acknowledged"
|
|
1293
|
-
})));
|
|
1294
|
-
return result.success;
|
|
1295
|
-
});
|
|
1296
|
-
const act = Effect.fnUntraced(function* (values, options = {}) {
|
|
1297
|
-
yield* Schema.decodeEffect(Schema.Array(Action).check(Schema.isMinLength(1), Schema.isMaxLength(8)))(values).pipe(Effect.mapError(() => invalid("Expected 1–8 valid actions.")));
|
|
1298
|
-
let completed = 0;
|
|
1299
|
-
let error = null;
|
|
1300
|
-
let dispatch = "not-dispatched";
|
|
1301
|
-
let frame;
|
|
1302
|
-
for (const value of values) {
|
|
1303
|
-
frame = targets.get(value.ref)?.frame ?? frame;
|
|
1304
|
-
const outcome = yield* action(value).pipe(Effect.result);
|
|
1305
|
-
if (outcome._tag === "Failure") {
|
|
1306
|
-
error = outcome.failure.message;
|
|
1307
|
-
if (outcome.failure.dispatch === "acknowledged") completed++;
|
|
1308
|
-
if (outcome.failure.dispatch === "unknown") dispatch = "unknown";
|
|
1309
|
-
else if (completed > 0) dispatch = "acknowledged";
|
|
1310
|
-
break;
|
|
1311
|
-
}
|
|
1312
|
-
if (outcome.success === "not-dispatched") {
|
|
1313
|
-
error = "Observed target changed before input. Inspect before continuing.";
|
|
1314
|
-
break;
|
|
1315
|
-
}
|
|
1316
|
-
if (outcome.success === "unknown") {
|
|
1317
|
-
dispatch = "unknown";
|
|
1318
|
-
error = "Native input is awaiting a JavaScript dialog. Respond to it; do not replay this input.";
|
|
1319
|
-
break;
|
|
1320
|
-
}
|
|
1321
|
-
dispatch = "acknowledged";
|
|
1322
|
-
completed++;
|
|
1323
|
-
}
|
|
1324
|
-
yield* Effect.annotateCurrentSpan({
|
|
1325
|
-
"browser.completed": completed,
|
|
1326
|
-
"browser.dispatch": dispatch
|
|
1327
|
-
});
|
|
1328
|
-
return yield* after(completed, error, dispatch, frame, true, options.observe !== false);
|
|
1329
|
-
});
|
|
1330
|
-
const navigate = Effect.fnUntraced(function* (request) {
|
|
1331
|
-
yield* Schema.decodeEffect(InteractiveBrowserTargetUrl)(request.url).pipe(Effect.mapError(() => invalid("Navigation requires a credential-free HTTP(S) URL.")));
|
|
1332
|
-
yield* charge();
|
|
1333
|
-
targets.clear();
|
|
1334
|
-
const outcome = yield* native({
|
|
1335
|
-
kind: "navigate",
|
|
1336
|
-
request
|
|
1337
|
-
}, (page) => page.goto(request.url, { waitUntil: "domcontentloaded" })).pipe(Effect.result);
|
|
1338
|
-
return yield* after(outcome._tag === "Success" ? 1 : 0, outcome._tag === "Failure" ? outcome.failure.message : null, outcome._tag === "Success" ? "acknowledged" : outcome.failure.dispatch ?? "unknown");
|
|
1339
|
-
});
|
|
1340
|
-
const scroll = Effect.fnUntraced(function* (request) {
|
|
1341
|
-
yield* Schema.decodeEffect(ScrollRequest)(request).pipe(Effect.mapError(() => invalid("Invalid scroll request.")));
|
|
1342
|
-
yield* charge();
|
|
1343
|
-
const target = request.ref === void 0 ? void 0 : targets.get(request.ref);
|
|
1344
|
-
if (request.ref !== void 0 && target === void 0) return yield* invalid("Scroll target was not observed.");
|
|
1345
|
-
const outcome = yield* native({
|
|
1346
|
-
kind: "scroll",
|
|
1347
|
-
request
|
|
1348
|
-
}, async (page) => {
|
|
1349
|
-
if (target === void 0) {
|
|
1350
|
-
const viewport = await page.evaluate(() => ({
|
|
1351
|
-
width: innerWidth,
|
|
1352
|
-
height: innerHeight
|
|
1353
|
-
}));
|
|
1354
|
-
await page.mouse.move(viewport.width / 2, viewport.height / 2);
|
|
1355
|
-
await page.mouse.wheel({
|
|
1356
|
-
deltaX: request.deltaX,
|
|
1357
|
-
deltaY: request.deltaY
|
|
1358
|
-
});
|
|
1359
|
-
return true;
|
|
1360
|
-
}
|
|
1361
|
-
const frame = resolveFrame(page, target.frame);
|
|
1362
|
-
if (frame === void 0) return false;
|
|
1363
|
-
const handle = await frame.isolatedRealm().evaluateHandle((ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null, request.ref ?? "");
|
|
1364
|
-
try {
|
|
1365
|
-
return await handle.evaluate((node, x, y) => {
|
|
1366
|
-
if (!(node instanceof HTMLElement) || !node.isConnected) return false;
|
|
1367
|
-
node.scrollBy({
|
|
1368
|
-
left: x,
|
|
1369
|
-
top: y,
|
|
1370
|
-
behavior: "instant"
|
|
1371
|
-
});
|
|
1372
|
-
return true;
|
|
1373
|
-
}, request.deltaX, request.deltaY);
|
|
1374
|
-
} finally {
|
|
1375
|
-
await handle.dispose();
|
|
1376
|
-
}
|
|
1377
|
-
}, 2e3).pipe(Effect.result);
|
|
1378
|
-
return yield* after(outcome._tag === "Success" && outcome.success ? 1 : 0, outcome._tag === "Failure" ? outcome.failure.message : outcome.success ? null : "Scroll target is stale.", outcome._tag === "Failure" ? outcome.failure.dispatch ?? "unknown" : outcome.success ? "acknowledged" : "not-dispatched");
|
|
1379
|
-
});
|
|
1380
|
-
const wait = Effect.fnUntraced(function* (request) {
|
|
1381
|
-
yield* Schema.decodeEffect(WaitRequest)(request).pipe(Effect.mapError(() => invalid("Invalid condition wait.")));
|
|
1382
|
-
const frameRef = request.frame;
|
|
1383
|
-
if (request.state === "text" && request.text === void 0) return yield* invalid("A text wait requires expected text.");
|
|
1384
|
-
if (frameRef !== void 0 && !frames.has(frameRef)) return yield* invalid("Frame was not observed.");
|
|
1385
|
-
yield* native({
|
|
1386
|
-
kind: "wait",
|
|
1387
|
-
request
|
|
1388
|
-
}, async (page) => {
|
|
1389
|
-
const frame = frameRef === void 0 ? page.mainFrame() : resolveFrame(page, frameRef);
|
|
1390
|
-
if (frame === void 0) throw new Error("Frame detached");
|
|
1391
|
-
await discoverClosedRoots(frame);
|
|
1392
|
-
try {
|
|
1393
|
-
await (await frame.isolatedRealm().waitForFunction(waitDom, {
|
|
1394
|
-
timeout: Math.min(request.timeoutMillis, limits.maxWaitMillis ?? request.timeoutMillis),
|
|
1395
|
-
polling: "raf"
|
|
1396
|
-
}, request.selector, request.state, request.text)).dispose();
|
|
1397
|
-
} catch (error) {
|
|
1398
|
-
if (limits.maxWaitMillis === null || !(error instanceof Error) || error.name !== "TimeoutError") throw error;
|
|
1399
|
-
}
|
|
1400
|
-
});
|
|
1401
|
-
return yield* inspect(frameRef === void 0 ? {} : { frame: frameRef });
|
|
1402
|
-
});
|
|
1403
|
-
const screenshot = native({ kind: "screenshot" }, async (page) => {
|
|
1404
|
-
return {
|
|
1405
|
-
mediaType: "image/png",
|
|
1406
|
-
base64: await page.screenshot({
|
|
1407
|
-
type: "png",
|
|
1408
|
-
encoding: "base64"
|
|
1409
|
-
})
|
|
1410
|
-
};
|
|
1411
|
-
}).pipe(Effect.flatMap(bounded));
|
|
1412
|
-
const press = Effect.fnUntraced(function* (request) {
|
|
1413
|
-
yield* Schema.decodeEffect(PressRequest)(request).pipe(Effect.mapError(() => invalid("Invalid native key request.")));
|
|
1414
|
-
const frame = targets.get(request.ref)?.frame;
|
|
1415
|
-
const result = yield* action({
|
|
1416
|
-
kind: "press",
|
|
1417
|
-
...request
|
|
1418
|
-
}).pipe(Effect.result);
|
|
1419
|
-
const dispatch = result._tag === "Success" ? result.success : result.failure.dispatch ?? "not-dispatched";
|
|
1420
|
-
return yield* after(dispatch === "acknowledged" ? 1 : 0, result._tag === "Failure" ? result.failure.message : result.success === "not-dispatched" ? "Target changed before keyboard input." : null, dispatch, frame, true);
|
|
1421
|
-
});
|
|
1422
|
-
const selectTab = Effect.fnUntraced(function* (request) {
|
|
1423
|
-
yield* Schema.decodeEffect(TabRequest)(request).pipe(Effect.mapError(() => invalid("Invalid tab request.")));
|
|
1424
|
-
const tab = tabs.get(request.ref);
|
|
1425
|
-
if (tab === void 0) return yield* invalid("Tab was not observed. Inspect before selecting it.");
|
|
1426
|
-
yield* native({
|
|
1427
|
-
kind: "select-tab",
|
|
1428
|
-
request
|
|
1429
|
-
}, async (page) => {
|
|
1430
|
-
if (tab.isClosed() || tab.browserContext() !== page.browserContext()) throw new Error("Observed tab is unavailable");
|
|
1431
|
-
await tab.bringToFront();
|
|
1432
|
-
activePage = tab;
|
|
1433
|
-
targets.clear();
|
|
1434
|
-
frames.clear();
|
|
1435
|
-
});
|
|
1436
|
-
return yield* inspect({});
|
|
1437
|
-
});
|
|
1438
|
-
const respondDialog = Effect.fnUntraced(function* (request) {
|
|
1439
|
-
yield* Schema.decodeEffect(DialogRequest)(request).pipe(Effect.mapError(() => invalid("Invalid dialog response.")));
|
|
1440
|
-
yield* charge();
|
|
1441
|
-
const outstanding = pendingInput;
|
|
1442
|
-
if (outstanding !== void 0 && inputPage !== void 0) {
|
|
1443
|
-
const page = inputPage;
|
|
1444
|
-
const pending = pendingDialogs.get(page);
|
|
1445
|
-
if (pending === void 0 || pending.ref !== request.ref || request.text !== void 0 && pending.dialog.type() !== "prompt") return yield* invalid("Observed dialog is no longer current.");
|
|
1446
|
-
if (outstanding.fiber.pollUnsafe()?._tag === "Failure") {
|
|
1447
|
-
clearInput();
|
|
1448
|
-
const settled = yield* Fiber.join(outstanding.fiber).pipe(Effect.result);
|
|
1449
|
-
return {
|
|
1450
|
-
...yield* after(0, "Pending native input failed. No dialog response dispatched.", "not-dispatched"),
|
|
1451
|
-
settledInput: {
|
|
1452
|
-
ref: outstanding.ref,
|
|
1453
|
-
dispatch: settled._tag === "Success" ? settled.success : settled.failure.dispatch ?? "unknown"
|
|
1454
|
-
}
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
const dialogReady = yield* Deferred.make();
|
|
1458
|
-
dialogSignal = dialogReady;
|
|
1459
|
-
const result = yield* (outstanding.fiber.pollUnsafe() === void 0 ? authorize({
|
|
1460
|
-
kind: "respond-dialog",
|
|
1461
|
-
request
|
|
1462
|
-
}, { pageUrl: page.url() }).pipe(Effect.andThen(Effect.tryPromise({
|
|
1463
|
-
try: () => request.accept ? pending.dialog.accept(request.text) : pending.dialog.dismiss(),
|
|
1464
|
-
catch: () => new BrowserUseError({
|
|
1465
|
-
code: "browser",
|
|
1466
|
-
message: "Native dialog response failed. Do not replay it.",
|
|
1467
|
-
dispatch: "unknown"
|
|
1468
|
-
})
|
|
1469
|
-
}))) : native({
|
|
1470
|
-
kind: "respond-dialog",
|
|
1471
|
-
request
|
|
1472
|
-
}, async () => {
|
|
1473
|
-
if (request.accept) await pending.dialog.accept(request.text);
|
|
1474
|
-
else await pending.dialog.dismiss();
|
|
1475
|
-
})).pipe(Effect.withSpan("BrowserUse.respond-dialog.input"), Effect.result);
|
|
1476
|
-
if (result._tag === "Failure") return yield* after(0, result.failure.message, result.failure.dispatch ?? "not-dispatched");
|
|
1477
|
-
if (pendingDialogs.get(page) === pending) pendingDialogs.delete(page);
|
|
1478
|
-
const settled = yield* Effect.raceFirst(Fiber.join(outstanding.fiber), Deferred.await(dialogReady).pipe(Effect.as("unknown"))).pipe(Effect.onInterrupt(() => Fiber.interrupt(outstanding.fiber)), Effect.result);
|
|
1479
|
-
if (settled._tag === "Success" && settled.success === "unknown") return yield* after(1, null, "acknowledged", void 0, true);
|
|
1480
|
-
clearInput();
|
|
1481
|
-
return {
|
|
1482
|
-
...yield* after(1, settled._tag === "Failure" ? settled.failure.message : null, "acknowledged"),
|
|
1483
|
-
settledInput: {
|
|
1484
|
-
ref: outstanding.ref,
|
|
1485
|
-
dispatch: settled._tag === "Success" ? settled.success : settled.failure.dispatch ?? "unknown"
|
|
1486
|
-
}
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
const receipt = { dispatch: "not-dispatched" };
|
|
1490
|
-
const result = yield* native({
|
|
1491
|
-
kind: "respond-dialog",
|
|
1492
|
-
request
|
|
1493
|
-
}, async (page) => {
|
|
1494
|
-
const pending = pendingDialogs.get(page);
|
|
1495
|
-
if (pending === void 0 || pending.ref !== request.ref || request.text !== void 0 && pending.dialog.type() !== "prompt") return false;
|
|
1496
|
-
receipt.dispatch = "unknown";
|
|
1497
|
-
if (request.accept) await pending.dialog.accept(request.text);
|
|
1498
|
-
else await pending.dialog.dismiss();
|
|
1499
|
-
receipt.dispatch = "acknowledged";
|
|
1500
|
-
if (pendingDialogs.get(page) === pending) pendingDialogs.delete(page);
|
|
1501
|
-
return true;
|
|
1502
|
-
}).pipe(Effect.result);
|
|
1503
|
-
return yield* after(receipt.dispatch === "acknowledged" ? 1 : 0, result._tag === "Failure" ? result.failure.message : result.success ? null : "Observed dialog is no longer current.", receipt.dispatch);
|
|
1504
|
-
});
|
|
1505
|
-
const actions = BrowserActions.of({
|
|
1506
|
-
observe: lock.withPermit(inspect({}, { kind: "observe" })),
|
|
1507
|
-
act: (values, options) => lock.withPermit(act(values, options)).pipe(Effect.withSpan("BrowserUse.act"))
|
|
1508
|
-
});
|
|
1509
|
-
const control = BrowserControl.of({
|
|
1510
|
-
inspect: (request) => lock.withPermit(inspect(request)),
|
|
1511
|
-
navigate: (request) => lock.withPermit(navigate(request)),
|
|
1512
|
-
scroll: (request) => lock.withPermit(scroll(request)),
|
|
1513
|
-
wait: (request) => lock.withPermit(wait(request)),
|
|
1514
|
-
screenshot: lock.withPermit(screenshot),
|
|
1515
|
-
press: (request) => lock.withPermit(press(request)),
|
|
1516
|
-
selectTab: (request) => lock.withPermit(selectTab(request)),
|
|
1517
|
-
respondDialog: (request) => lock.withPermit(respondDialog(request)).pipe(Effect.withSpan("BrowserUse.respond-dialog"))
|
|
1518
|
-
});
|
|
1519
|
-
return {
|
|
1520
|
-
actions,
|
|
1521
|
-
control,
|
|
1522
|
-
layer: Layer.merge(Layer.succeed(BrowserActions, actions), Layer.succeed(BrowserControl, control))
|
|
1523
|
-
};
|
|
1524
|
-
});
|
|
1525
|
-
//#endregion
|
|
1526
|
-
export { make as n, BrowserUse_exports as t };
|
|
1527
|
-
|
|
1528
|
-
//# sourceMappingURL=BrowserUse-Bc2JdLW2.mjs.map
|