@yielded/agent-platform-cloudflare 0.1.0-beta.172 → 0.1.0-beta.174
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-4J9B8Fg8.mjs} +54 -32
- package/dist/Alarm-4J9B8Fg8.mjs.map +1 -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-DU548EEj.mjs} +42 -39
- package/dist/CloudflareThreadClient-DU548EEj.mjs.map +1 -0
- package/dist/{CloudflareThreadClient-DuRG0MLd.d.mts → CloudflareThreadClient-lTm6baDP.d.mts} +353 -353
- package/dist/CloudflareThreadClient.d.mts +1 -1
- 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-CugPnWsJ.d.mts → ThreadObject-BLYgzcdT.d.mts} +6 -6
- package/dist/{ThreadObject-CGIaz6Jv.mjs → ThreadObject-C2LF3btz.mjs} +13 -14
- package/dist/ThreadObject-C2LF3btz.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 +3 -3
- package/dist/index.mjs +5 -5
- package/dist/{prepared-admission-CSdzFM_v.mjs → prepared-admission-BoUPHSNQ.mjs} +2 -2
- package/dist/{prepared-admission-CSdzFM_v.mjs.map → prepared-admission-BoUPHSNQ.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +82 -39
- 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 +256 -268
- package/src/InteractiveBrowser.ts +43 -17
- package/src/ThreadObject.ts +35 -24
- 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 +53 -53
- 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
package/src/BrowserUse.ts
CHANGED
|
@@ -1,1591 +1,374 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
type ActionResult,
|
|
4
|
-
type NavigateRequest,
|
|
5
|
-
Action,
|
|
6
|
-
BrowserActions,
|
|
7
|
-
BrowserControl,
|
|
2
|
+
BrowserCdp,
|
|
8
3
|
BrowserUseError,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
DialogRequest,
|
|
15
|
-
PressRequest,
|
|
16
|
-
TabRequest,
|
|
4
|
+
CloseTabRequest,
|
|
5
|
+
FrameInfo,
|
|
6
|
+
FramesRequest,
|
|
7
|
+
SendRequest,
|
|
8
|
+
Tab,
|
|
17
9
|
} from "@yielded/agent/browser-use";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type Frame,
|
|
24
|
-
type Page,
|
|
25
|
-
type Dialog,
|
|
10
|
+
import { Effect, Layer, Schema, Semaphore } from "effect";
|
|
11
|
+
import type {
|
|
12
|
+
CDPSession,
|
|
13
|
+
Frame,
|
|
14
|
+
Page,
|
|
26
15
|
} from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
27
16
|
|
|
28
|
-
import type { BrowserSession } from "./BrowserSession.ts";
|
|
29
|
-
import {
|
|
30
|
-
checkDom,
|
|
31
|
-
checkFrameDom,
|
|
32
|
-
inspectDom,
|
|
33
|
-
settleDom,
|
|
34
|
-
settleInputDom,
|
|
35
|
-
waitDom,
|
|
36
|
-
} from "./internal/browser-dom.ts";
|
|
37
|
-
import { inspectJevDom } from "./internal/browser-jev-dom.ts";
|
|
17
|
+
import type { BrowserSession, BrowserSessionError } from "./BrowserSession.ts";
|
|
38
18
|
|
|
39
19
|
export type Command =
|
|
40
|
-
| { readonly kind: "
|
|
41
|
-
| { readonly kind: "
|
|
42
|
-
| { readonly kind: "
|
|
43
|
-
| { readonly kind: "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
20
|
+
| { readonly kind: "connect" | "tabs" | "new-tab" }
|
|
21
|
+
| { readonly kind: "send"; readonly request: typeof SendRequest.Type }
|
|
22
|
+
| { readonly kind: "frames"; readonly request: typeof FramesRequest.Type }
|
|
23
|
+
| { readonly kind: "close-tab"; readonly request: typeof CloseTabRequest.Type };
|
|
24
|
+
|
|
25
|
+
/** Current SDK metadata, never a grant supplied by page content or generated code. */
|
|
26
|
+
export interface AuthorizationContext {
|
|
27
|
+
/** Absent during initial attachment. */
|
|
28
|
+
readonly pageUrl?: string;
|
|
29
|
+
readonly targetId?: string;
|
|
30
|
+
readonly targetUrl?: string;
|
|
31
|
+
readonly frameUrl?: string;
|
|
32
|
+
}
|
|
51
33
|
|
|
52
34
|
export interface Options<R = never> {
|
|
53
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Runs under the BrowserSession lock before every command. Arbitrary CDP/page JavaScript
|
|
37
|
+
* is a broad capability: URL checks cannot prove that code is read-only or constrain egress.
|
|
38
|
+
* The host must authorize that capability and enforce network policy outside page code.
|
|
39
|
+
*/
|
|
54
40
|
readonly authorize: (
|
|
55
41
|
command: Command,
|
|
56
42
|
context: AuthorizationContext,
|
|
57
43
|
) => Effect.Effect<void, BrowserUseError, R>;
|
|
58
|
-
|
|
44
|
+
/** Total native operations for this attachment, including authorization refusals. */
|
|
45
|
+
readonly maxCommands: number;
|
|
46
|
+
/** Per-result UTF-8 JSON limit; does not bound the upstream CDP transfer. */
|
|
59
47
|
readonly maxReturnedBytes: number;
|
|
60
|
-
/**
|
|
61
|
-
readonly
|
|
62
|
-
/** Jev reads enabled controls centered in the viewport with accessible names and at most 6,000 visible text characters. Defaults to "default". */
|
|
63
|
-
readonly observationMode?: "default" | "jev";
|
|
64
|
-
/** True waits for 100 ms of DOM quiet (at most 1 s). "input" waits two frames/50 ms, or visible combobox options/200 ms. Defaults to false. */
|
|
65
|
-
readonly settleAfterAction?: boolean | "input";
|
|
66
|
-
/** Cap condition waits and return a fresh observation on a condition timeout. Omit to retain timeout errors. */
|
|
67
|
-
readonly maxWaitMillis?: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Host-only current SDK metadata. Opaque refs never replace origin or target authority. */
|
|
71
|
-
export interface AuthorizationContext {
|
|
72
|
-
/** Absent only before the initial attachment read; the host authorizes that attachment. */
|
|
73
|
-
readonly pageUrl?: string;
|
|
74
|
-
readonly frameUrl?: string;
|
|
75
|
-
/** Destination of an observed select-tab command, checked again before selection. */
|
|
76
|
-
readonly tabUrl?: string;
|
|
77
|
-
readonly target?: typeof Control.Type;
|
|
48
|
+
/** Maximum pages discoverable through this attachment. Defaults to 8. */
|
|
49
|
+
readonly maxTabs?: number;
|
|
78
50
|
}
|
|
79
51
|
|
|
80
52
|
const Limits = Schema.Struct({
|
|
81
|
-
|
|
53
|
+
maxCommands: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10_000 })),
|
|
82
54
|
maxReturnedBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 * 1024 * 1024 })),
|
|
83
|
-
|
|
84
|
-
observationMode: Schema.Literals(["default", "jev"]),
|
|
85
|
-
settleAfterAction: Schema.Union([Schema.Boolean, Schema.Literal("input")]),
|
|
86
|
-
maxWaitMillis: Schema.NullOr(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 15_000 }))),
|
|
55
|
+
maxTabs: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 64 })),
|
|
87
56
|
});
|
|
88
57
|
|
|
89
58
|
const invalid = (message: string) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// URL evidence identifies pages; opaque document payloads are not page observations.
|
|
93
|
-
// Authorization and navigation fencing always use the complete native SDK URL.
|
|
94
|
-
const observedUrl = (url: string): string => {
|
|
95
|
-
const payload = url.startsWith("data:") ? url.indexOf(",") : -1;
|
|
96
|
-
|
|
97
|
-
return payload < 0
|
|
98
|
-
? url.slice(0, 8_192)
|
|
99
|
-
: `${url.slice(0, Math.min(payload, 256))},[payload omitted]`;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// CDP can resolve closed roots without modifying the page's realm or attachShadow.
|
|
103
|
-
// Keep those nodes in the same isolated realm as native refs and discard old roots per read.
|
|
104
|
-
// The command deadline bounds this full-frame read; the node/root caps below only bound
|
|
105
|
-
// local adoption, not the CDP response size.
|
|
106
|
-
const discoverClosedRoots = async (frame: Frame): Promise<boolean> => {
|
|
107
|
-
const realm = frame.isolatedRealm();
|
|
108
|
-
|
|
109
|
-
await using document = await realm.evaluateHandle(() => globalThis.document);
|
|
110
|
-
|
|
111
|
-
const { node } = await frame.client.send("DOM.describeNode", {
|
|
112
|
-
objectId: document.remoteObject().objectId,
|
|
113
|
-
depth: -1,
|
|
114
|
-
pierce: true,
|
|
115
|
-
});
|
|
59
|
+
BrowserUseError.make({ code: "invalid", message, dispatch: "not-dispatched" });
|
|
116
60
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
while (pending.length > 0 && scanned++ < 10_000 && roots.length < 256) {
|
|
122
|
-
const current = pending.pop();
|
|
123
|
-
|
|
124
|
-
if (current === undefined) break;
|
|
125
|
-
if (current.shadowRootType === "closed") roots.push(current.backendNodeId);
|
|
126
|
-
pending.push(...(current.children ?? []));
|
|
127
|
-
pending.push(
|
|
128
|
-
...(current.shadowRoots ?? []).filter((root) => root.shadowRootType !== "user-agent"),
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
await realm.evaluate(() => {
|
|
132
|
-
Reflect.set(globalThis, "@effect-agent/native-browser-closed-roots", new Map());
|
|
133
|
-
});
|
|
134
|
-
for (const id of roots) {
|
|
135
|
-
await using root = await realm.adoptBackendNode(id);
|
|
136
|
-
|
|
137
|
-
await realm.evaluate((node) => {
|
|
138
|
-
if (!(node instanceof ShadowRoot) || node.mode !== "closed")
|
|
139
|
-
throw new Error("Closed shadow root is unavailable");
|
|
140
|
-
Reflect.get(globalThis, "@effect-agent/native-browser-closed-roots").set(node.host, node);
|
|
141
|
-
}, root);
|
|
142
|
-
}
|
|
61
|
+
const decode = <A>(schema: Schema.Codec<A>, input: unknown) =>
|
|
62
|
+
Schema.decodeUnknownEffect(schema)(input).pipe(
|
|
63
|
+
Effect.mapError(() => invalid("Invalid browser command or limits")),
|
|
64
|
+
);
|
|
143
65
|
|
|
144
|
-
|
|
145
|
-
|
|
66
|
+
interface AttachedPage {
|
|
67
|
+
readonly page: Page;
|
|
68
|
+
readonly cdp: CDPSession;
|
|
69
|
+
readonly targetId: string;
|
|
70
|
+
}
|
|
146
71
|
|
|
147
|
-
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
72
|
+
/**
|
|
73
|
+
* A scoped CDP bridge over an existing BrowserSession. No DOM classifier, selectors,
|
|
74
|
+
* implicit waits, input retries, or generated-code execution live in this adapter.
|
|
75
|
+
* CDP connections persist within the attachment (including remote object IDs); pages persist
|
|
76
|
+
* for the host-owned session. Scope release detaches connections without closing pages.
|
|
151
77
|
*/
|
|
152
78
|
export const make = Effect.fnUntraced(function* <R>(
|
|
153
79
|
session: Pick<BrowserSession, "run">,
|
|
154
80
|
options: Options<R>,
|
|
155
81
|
) {
|
|
156
|
-
const
|
|
82
|
+
const limits = yield* decode(Limits, { ...options, maxTabs: options.maxTabs ?? 8 });
|
|
83
|
+
const authority = yield* Effect.context<R>();
|
|
84
|
+
// Code Mode permits concurrent host calls; this native attachment admits one at a time.
|
|
85
|
+
const serial = yield* Semaphore.make(1);
|
|
157
86
|
|
|
158
87
|
const authorize = (command: Command, context: AuthorizationContext) =>
|
|
159
|
-
options.authorize(command, context)
|
|
160
|
-
|
|
161
|
-
const limits = yield* Schema.decodeEffect(Limits)({
|
|
162
|
-
maxActions: options.maxActions,
|
|
163
|
-
maxReturnedBytes: options.maxReturnedBytes,
|
|
164
|
-
viewportOnly: options.viewportOnly ?? false,
|
|
165
|
-
observationMode: options.observationMode ?? "default",
|
|
166
|
-
settleAfterAction: options.settleAfterAction ?? false,
|
|
167
|
-
maxWaitMillis: options.maxWaitMillis ?? null,
|
|
168
|
-
}).pipe(Effect.mapError(() => invalid("Invalid browser controller limits.")));
|
|
169
|
-
|
|
170
|
-
const lock = yield* Semaphore.make(1);
|
|
171
|
-
const ownerScope = yield* Effect.scope;
|
|
172
|
-
|
|
173
|
-
type Dispatch = NonNullable<(typeof ActionResult.Type)["dispatch"]>;
|
|
174
|
-
let pendingInput: { ref: string; fiber: Fiber.Fiber<Dispatch, BrowserUseError> } | undefined;
|
|
175
|
-
let inputPage: Page | undefined;
|
|
176
|
-
let dialogSignal: Deferred.Deferred<void> | undefined;
|
|
177
|
-
let nextInput = 0;
|
|
178
|
-
let lastTabs: NonNullable<(typeof Observation.Type)["tabs"]> = [];
|
|
88
|
+
Effect.provideContext(options.authorize(command, context), authority);
|
|
179
89
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const frameIds = new WeakMap<Frame, string>();
|
|
189
|
-
const tabIds = new WeakMap<Page, string>();
|
|
190
|
-
const tabs = new Map<string, Page>();
|
|
191
|
-
const pendingDialogs = new Map<Page, { ref: string; dialog: Dialog }>();
|
|
192
|
-
|
|
193
|
-
const listeners = new Map<
|
|
194
|
-
Page,
|
|
195
|
-
{ dialog: (dialog: Dialog) => void; popup: (page: Page | null) => void }
|
|
196
|
-
>();
|
|
197
|
-
|
|
198
|
-
let activePage: Page | undefined;
|
|
199
|
-
let lastNativePage: Page | undefined;
|
|
200
|
-
let nextTab = 0;
|
|
201
|
-
let nextDialog = 0;
|
|
202
|
-
|
|
203
|
-
const tabId = (page: Page) => {
|
|
204
|
-
let id = tabIds.get(page);
|
|
205
|
-
|
|
206
|
-
if (id === undefined) {
|
|
207
|
-
id = `t${scopeId}-${nextTab++}`;
|
|
208
|
-
tabIds.set(page, id);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return id;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const attach = (page: Page) => {
|
|
215
|
-
if (listeners.has(page) || listeners.size >= 32) return;
|
|
216
|
-
|
|
217
|
-
const dialog = (dialog: Dialog) => {
|
|
218
|
-
pendingDialogs.set(page, { ref: `dialog-${scopeId}-${nextDialog++}`, dialog });
|
|
219
|
-
if (page === inputPage && dialogSignal !== undefined)
|
|
220
|
-
Deferred.doneUnsafe(dialogSignal, Effect.void);
|
|
221
|
-
};
|
|
90
|
+
const mapSessionError = (error: BrowserSessionError | BrowserUseError) =>
|
|
91
|
+
error._tag === "BrowserUseError"
|
|
92
|
+
? error
|
|
93
|
+
: BrowserUseError.make({
|
|
94
|
+
code: "browser",
|
|
95
|
+
message: "Browser command failed; inspect the current state before further input",
|
|
96
|
+
dispatch: error.dispatch,
|
|
97
|
+
});
|
|
222
98
|
|
|
223
|
-
|
|
224
|
-
if (page !== null) attach(page);
|
|
225
|
-
};
|
|
99
|
+
let released = false;
|
|
226
100
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const { dialog } of pendingDialogs.values()) {
|
|
101
|
+
// Register cleanup before creating any native connection, including a partially failed attachment.
|
|
102
|
+
const connections = yield* Effect.acquireRelease(
|
|
103
|
+
Effect.sync(() => new Map<Page | CDPSession, CDPSession>()),
|
|
104
|
+
(connections) =>
|
|
105
|
+
Effect.promise(async (signal) => {
|
|
106
|
+
released = true;
|
|
107
|
+
for (const cdp of connections.values()) {
|
|
108
|
+
if (signal.aborted) break;
|
|
236
109
|
try {
|
|
237
|
-
await
|
|
110
|
+
await cdp.detach();
|
|
238
111
|
} catch {
|
|
239
|
-
|
|
112
|
+
// The session may already have terminated on timeout or interruption.
|
|
240
113
|
}
|
|
241
114
|
}
|
|
242
|
-
|
|
243
|
-
.pipe(
|
|
244
|
-
Effect.ignore,
|
|
245
|
-
Effect.andThen(
|
|
246
|
-
Effect.sync(() => {
|
|
247
|
-
for (const [page, listener] of listeners) {
|
|
248
|
-
page.off("dialog", listener.dialog);
|
|
249
|
-
page.off("popup", listener.popup);
|
|
250
|
-
}
|
|
251
|
-
listeners.clear();
|
|
252
|
-
pendingDialogs.clear();
|
|
253
|
-
tabs.clear();
|
|
254
|
-
activePage = undefined;
|
|
255
|
-
targets.clear();
|
|
256
|
-
frames.clear();
|
|
257
|
-
}),
|
|
258
|
-
),
|
|
259
|
-
),
|
|
115
|
+
connections.clear();
|
|
116
|
+
}).pipe(Effect.interruptible, Effect.timeoutOption("1 second"), Effect.asVoid),
|
|
260
117
|
);
|
|
261
|
-
const scopeId = crypto.getRandomValues(new Uint32Array(2)).join("-");
|
|
262
|
-
let nextFrame = 0;
|
|
263
118
|
|
|
264
|
-
const
|
|
265
|
-
let id = frameIds.get(frame);
|
|
266
|
-
|
|
267
|
-
if (id === undefined) {
|
|
268
|
-
id = `f${scopeId}-${nextFrame++}`;
|
|
269
|
-
frameIds.set(frame, id);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return id;
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const resolveFrame = (page: Page, ref: string) =>
|
|
276
|
-
page.frames().find((frame) => frameIds.get(frame) === ref);
|
|
277
|
-
|
|
278
|
-
let generation = 0;
|
|
279
|
-
let consumed = 0;
|
|
280
|
-
|
|
281
|
-
const native = <A>(
|
|
282
|
-
command: Command,
|
|
283
|
-
action: (page: Page, stage: (name: string) => void) => Promise<A>,
|
|
284
|
-
timeoutMillis?: number,
|
|
285
|
-
phase?: "prepare" | "input" | "settle",
|
|
286
|
-
) =>
|
|
287
|
-
Effect.gen(function* () {
|
|
288
|
-
if (pendingInput !== undefined && phase !== "input" && command.kind !== "respond-dialog")
|
|
289
|
-
return yield* invalid(
|
|
290
|
-
"Native input is pending. Inspect and respond to its dialog before another browser operation.",
|
|
291
|
-
);
|
|
292
|
-
const span = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
293
|
-
let authorizedUrl: string | undefined;
|
|
294
|
-
let authorizedTabUrl: string | undefined;
|
|
295
|
-
let authorizedFrame: { ref: string; url: string | undefined } | undefined;
|
|
296
|
-
const read = ["observe", "inspect", "wait", "screenshot"].includes(command.kind);
|
|
119
|
+
const pages = new Map<string, AttachedPage>();
|
|
297
120
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let lostDocument = false;
|
|
121
|
+
const attach = async (page: Page): Promise<AttachedPage> => {
|
|
122
|
+
let cdp = connections.get(page);
|
|
301
123
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
command.kind === "act"
|
|
309
|
-
? targets.get(command.action.ref)
|
|
310
|
-
: command.kind === "scroll" && command.request.ref !== undefined
|
|
311
|
-
? targets.get(command.request.ref)
|
|
312
|
-
: undefined;
|
|
124
|
+
if (cdp === undefined) {
|
|
125
|
+
cdp = await page.createCDPSession();
|
|
126
|
+
connections.set(page, cdp);
|
|
127
|
+
}
|
|
128
|
+
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
|
129
|
+
const attached = { page, cdp, targetId: targetInfo.targetId };
|
|
313
130
|
|
|
314
|
-
|
|
315
|
-
authorizedTabUrl =
|
|
316
|
-
command.kind === "select-tab" ? tabs.get(command.request.ref)?.url() : undefined;
|
|
131
|
+
pages.set(attached.targetId, attached);
|
|
317
132
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
(command.kind === "inspect" || command.kind === "wait"
|
|
321
|
-
? command.request.frame
|
|
322
|
-
: undefined);
|
|
133
|
+
return attached;
|
|
134
|
+
};
|
|
323
135
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
: {
|
|
328
|
-
ref: frameRef,
|
|
329
|
-
url: page === undefined ? undefined : resolveFrame(page, frameRef)?.url(),
|
|
330
|
-
};
|
|
136
|
+
const primary = yield* session
|
|
137
|
+
.run(authorize({ kind: "connect" }, {}), attach)
|
|
138
|
+
.pipe(Effect.mapError(mapSessionError));
|
|
331
139
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
...(authorizedTabUrl === undefined ? {} : { tabUrl: authorizedTabUrl }),
|
|
335
|
-
...(authorizedFrame === undefined ? {} : { frameUrl: authorizedFrame.url }),
|
|
336
|
-
...(target === undefined
|
|
337
|
-
? {}
|
|
338
|
-
: {
|
|
339
|
-
target: target.control,
|
|
340
|
-
}),
|
|
341
|
-
});
|
|
342
|
-
}).pipe(
|
|
343
|
-
Effect.mapError(
|
|
344
|
-
(error) =>
|
|
345
|
-
new BrowserUseError({
|
|
346
|
-
code: error.code,
|
|
347
|
-
message: error.message,
|
|
348
|
-
dispatch: "not-dispatched",
|
|
349
|
-
}),
|
|
350
|
-
),
|
|
351
|
-
),
|
|
352
|
-
async (page) => {
|
|
353
|
-
attach(page);
|
|
354
|
-
const selected = activePage ?? page;
|
|
140
|
+
const context = primary.page.browserContext();
|
|
141
|
+
let calls = 0;
|
|
355
142
|
|
|
356
|
-
|
|
143
|
+
const tab = async (attached: AttachedPage): Promise<typeof Tab.Type> => {
|
|
144
|
+
const { targetInfo } = await attached.cdp.send("Target.getTargetInfo");
|
|
357
145
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
authorizedFrame !== undefined &&
|
|
366
|
-
resolveFrame(selected, authorizedFrame.ref)?.url() !== authorizedFrame.url
|
|
367
|
-
) {
|
|
368
|
-
authorizationChanged = true;
|
|
369
|
-
throw new Error("Frame changed during authorization");
|
|
370
|
-
}
|
|
371
|
-
if (
|
|
372
|
-
command.kind === "select-tab" &&
|
|
373
|
-
tabs.get(command.request.ref)?.url() !== authorizedTabUrl
|
|
374
|
-
)
|
|
375
|
-
throw new Error("Tab changed during authorization");
|
|
376
|
-
attach(selected);
|
|
146
|
+
return Schema.decodeSync(Tab)({
|
|
147
|
+
targetId: targetInfo.targetId,
|
|
148
|
+
url: targetInfo.url,
|
|
149
|
+
title: targetInfo.title,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
377
152
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
153
|
+
const run = Effect.fnUntraced(function* <A>(
|
|
154
|
+
command: Command,
|
|
155
|
+
action: (selected: AttachedPage, frame?: Frame) => Promise<A | BrowserUseError>,
|
|
156
|
+
targetId?: string,
|
|
157
|
+
frameId?: string,
|
|
158
|
+
) {
|
|
159
|
+
let selected = primary;
|
|
160
|
+
let pageUrl = "";
|
|
161
|
+
let targetUrl = "";
|
|
162
|
+
let frame: Frame | undefined;
|
|
163
|
+
let frameUrl: string | undefined;
|
|
164
|
+
|
|
165
|
+
const result = yield* session
|
|
166
|
+
.run(
|
|
167
|
+
Effect.gen(function* () {
|
|
168
|
+
if (released) return yield* Effect.fail(invalid("Browser attachment scope is closed"));
|
|
169
|
+
if (calls++ >= limits.maxCommands)
|
|
170
|
+
return yield* Effect.fail(invalid("Browser command limit reached"));
|
|
171
|
+
selected = targetId === undefined ? primary : (pages.get(targetId) ?? primary);
|
|
172
|
+
if (targetId !== undefined && !pages.has(targetId))
|
|
173
|
+
return yield* Effect.fail(
|
|
174
|
+
invalid("Unknown page; call cdp.tabs({}) to discover current targets"),
|
|
175
|
+
);
|
|
176
|
+
if (selected.page.isClosed())
|
|
177
|
+
return yield* Effect.fail(invalid("The selected page is closed"));
|
|
178
|
+
pageUrl = primary.page.url();
|
|
179
|
+
targetUrl = selected.page.url();
|
|
180
|
+
frame =
|
|
181
|
+
frameId === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: selected.page.frames().find((candidate) => candidate._id === frameId);
|
|
184
|
+
if (frameId !== undefined && frame === undefined)
|
|
185
|
+
return yield* Effect.fail(invalid("Unknown frame; call cdp.frames({}) again"));
|
|
186
|
+
frameUrl = frame?.url();
|
|
187
|
+
yield* authorize(command, {
|
|
188
|
+
pageUrl,
|
|
189
|
+
targetId: selected.targetId,
|
|
190
|
+
targetUrl,
|
|
191
|
+
...(frameUrl === undefined ? {} : { frameUrl }),
|
|
192
|
+
});
|
|
193
|
+
}),
|
|
194
|
+
async () => {
|
|
195
|
+
if (
|
|
196
|
+
primary.page.url() !== pageUrl ||
|
|
197
|
+
selected.page.url() !== targetUrl ||
|
|
198
|
+
selected.page.isClosed() ||
|
|
199
|
+
(frame !== undefined && (frame.detached || frame.url() !== frameUrl))
|
|
402
200
|
)
|
|
403
|
-
|
|
201
|
+
return {
|
|
202
|
+
ok: false as const,
|
|
203
|
+
error: invalid("Page changed during authorization; inspect it again"),
|
|
204
|
+
};
|
|
404
205
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
attempt >= 4 ||
|
|
410
|
-
result.failure._tag !== "BrowserSessionError" ||
|
|
411
|
-
result.failure.reason !== "provider" ||
|
|
412
|
-
result.failure.cleanup !== "not-requested"
|
|
413
|
-
)
|
|
414
|
-
return yield* result.failure;
|
|
206
|
+
return { ok: true as const, value: await action(selected, frame) };
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
.pipe(serial.withPermits(1), Effect.mapError(mapSessionError));
|
|
415
210
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
}).pipe(
|
|
421
|
-
Effect.mapError((error) => {
|
|
422
|
-
if (error._tag === "BrowserUseError") return error;
|
|
423
|
-
const read = ["observe", "inspect", "wait", "screenshot"].includes(command.kind);
|
|
211
|
+
if (!result.ok) return yield* Effect.fail(result.error);
|
|
212
|
+
// Only host-created failures belong in E; a protocol response with similar fields is data.
|
|
213
|
+
if (result.value instanceof BrowserUseError) return yield* Effect.fail(result.value);
|
|
424
214
|
|
|
425
|
-
|
|
215
|
+
const encoded = yield* Effect.try({
|
|
216
|
+
try: () => JSON.stringify(result.value),
|
|
217
|
+
catch: () =>
|
|
218
|
+
BrowserUseError.make({
|
|
426
219
|
code: "browser",
|
|
427
|
-
message:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
dispatch: read || error.dispatch === "not-dispatched" ? "not-dispatched" : "unknown",
|
|
431
|
-
});
|
|
432
|
-
}),
|
|
433
|
-
Effect.withSpan(`BrowserUse.${command.kind}${phase === undefined ? "" : `.${phase}`}`),
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
const bounded = <A>(value: A) =>
|
|
437
|
-
Effect.gen(function* () {
|
|
438
|
-
const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
439
|
-
|
|
440
|
-
if (bytes > limits.maxReturnedBytes)
|
|
441
|
-
return yield* invalid(
|
|
442
|
-
"Browser result exceeds the host byte budget. Narrow inspection before acting.",
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
return value;
|
|
220
|
+
message: "Browser returned a non-JSON result",
|
|
221
|
+
dispatch: "possibly-dispatched",
|
|
222
|
+
}),
|
|
446
223
|
});
|
|
447
224
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
generation++;
|
|
461
|
-
|
|
462
|
-
if (pendingInput?.fiber.pollUnsafe()?._tag === "Failure") {
|
|
463
|
-
const failed = pendingInput.fiber;
|
|
464
|
-
|
|
465
|
-
clearInput();
|
|
466
|
-
yield* Fiber.join(failed);
|
|
467
|
-
}
|
|
468
|
-
const blocked = inputPage === undefined ? undefined : pendingDialogs.get(inputPage);
|
|
469
|
-
|
|
470
|
-
if (pendingInput !== undefined && blocked !== undefined) {
|
|
471
|
-
yield* authorize(command, { pageUrl: inputPage?.url() });
|
|
472
|
-
|
|
473
|
-
const observation = yield* bounded({
|
|
474
|
-
text: "Native input is awaiting this JavaScript dialog. Respond before further page operations; never replay the input.",
|
|
475
|
-
controls: [],
|
|
476
|
-
tabs: lastTabs,
|
|
477
|
-
dialogs: [
|
|
478
|
-
{
|
|
479
|
-
ref: blocked.ref,
|
|
480
|
-
type: blocked.dialog.type(),
|
|
481
|
-
message: blocked.dialog.message().slice(0, 4096),
|
|
482
|
-
defaultValue: blocked.dialog.defaultValue().slice(0, 4096),
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
return observation;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const value = yield* native(command, async (page) => {
|
|
491
|
-
const currentTabs = await page.browserContext().pages();
|
|
492
|
-
|
|
493
|
-
tabs.clear();
|
|
494
|
-
currentTabs.slice(0, 32).forEach((tab) => {
|
|
495
|
-
tabs.set(tabId(tab), tab);
|
|
496
|
-
attach(tab);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const tabObservation = currentTabs
|
|
500
|
-
.slice(0, 32)
|
|
501
|
-
.map((tab) => ({ ref: tabId(tab), url: observedUrl(tab.url()), active: tab === page }));
|
|
502
|
-
|
|
503
|
-
const pending = pendingDialogs.get(page);
|
|
225
|
+
if (
|
|
226
|
+
encoded === undefined ||
|
|
227
|
+
new TextEncoder().encode(encoded).byteLength > limits.maxReturnedBytes
|
|
228
|
+
)
|
|
229
|
+
return yield* Effect.fail(
|
|
230
|
+
BrowserUseError.make({
|
|
231
|
+
code: "browser",
|
|
232
|
+
message:
|
|
233
|
+
"Browser result exceeded its byte limit; request a smaller result without repeating mutations",
|
|
234
|
+
dispatch: "possibly-dispatched",
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
504
237
|
|
|
505
|
-
|
|
238
|
+
return result.value;
|
|
239
|
+
});
|
|
506
240
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
controls: [],
|
|
511
|
-
tabs: tabObservation,
|
|
512
|
-
truncated: currentTabs.length > 32,
|
|
513
|
-
dialogs: [
|
|
514
|
-
{
|
|
515
|
-
ref: pending.ref,
|
|
516
|
-
type: pending.dialog.type(),
|
|
517
|
-
message: pending.dialog.message().slice(0, 4096),
|
|
518
|
-
defaultValue: pending.dialog.defaultValue().slice(0, 4096),
|
|
519
|
-
},
|
|
520
|
-
],
|
|
521
|
-
};
|
|
522
|
-
const currentFrames = page.frames();
|
|
241
|
+
const client: BrowserCdp["Service"] = {
|
|
242
|
+
send: Effect.fnUntraced(function* (request) {
|
|
243
|
+
const value = yield* decode(SendRequest, request);
|
|
523
244
|
|
|
524
245
|
if (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
246
|
+
value.method.startsWith("Browser.") ||
|
|
247
|
+
value.method.startsWith("Target.") ||
|
|
248
|
+
value.method === "Page.close" ||
|
|
249
|
+
value.method === "Page.crash"
|
|
528
250
|
)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
currentFrames.slice(0, 32).forEach((frame) => frames.add(frameId(frame)));
|
|
532
|
-
|
|
533
|
-
const inspected =
|
|
534
|
-
selectedFrame === undefined
|
|
535
|
-
? [page.mainFrame()]
|
|
536
|
-
: currentFrames.filter((frame) => frameId(frame) === selectedFrame);
|
|
537
|
-
|
|
538
|
-
if (inspected.length === 0) inspected.push(page.mainFrame());
|
|
539
|
-
|
|
540
|
-
let text = "";
|
|
541
|
-
let readyState: (typeof Observation.Type)["readyState"];
|
|
542
|
-
let truncated = currentFrames.length > 32 || currentTabs.length > 32;
|
|
543
|
-
let metrics: (typeof Observation.Type)["page"];
|
|
544
|
-
const controls: Array<typeof Control.Type> = [];
|
|
545
|
-
|
|
546
|
-
for (const frame of inspected) {
|
|
547
|
-
if (controls.length >= 256) {
|
|
548
|
-
truncated = true;
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
const ref = frameId(frame);
|
|
552
|
-
|
|
553
|
-
if (ref === undefined) continue;
|
|
554
|
-
|
|
555
|
-
const read = async (): Promise<unknown> => {
|
|
556
|
-
const rootsTruncated = await discoverClosedRoots(frame);
|
|
557
|
-
|
|
558
|
-
truncated ||= rootsTruncated;
|
|
559
|
-
|
|
560
|
-
return limits.observationMode === "jev"
|
|
561
|
-
? frame
|
|
562
|
-
.isolatedRealm()
|
|
563
|
-
.evaluate(
|
|
564
|
-
inspectJevDom,
|
|
565
|
-
request.selector,
|
|
566
|
-
`r${scopeId}-${generation}-${controls.length}`,
|
|
567
|
-
256 - controls.length,
|
|
568
|
-
request.optionFilter,
|
|
569
|
-
)
|
|
570
|
-
: frame
|
|
571
|
-
.isolatedRealm()
|
|
572
|
-
.evaluate(
|
|
573
|
-
inspectDom,
|
|
574
|
-
request.selector,
|
|
575
|
-
`r${scopeId}-${generation}-${controls.length}`,
|
|
576
|
-
256 - controls.length,
|
|
577
|
-
request.optionFilter,
|
|
578
|
-
limits.viewportOnly,
|
|
579
|
-
);
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
let result = Schema.decodeUnknownSync(Observation)(await read());
|
|
583
|
-
|
|
584
|
-
// Native click acknowledgement can precede document parsing. Settle this
|
|
585
|
-
// explicit readiness condition before returning an empty loading document. A document
|
|
586
|
-
// still parsing after 2 s is observed as it stands, reporting readyState "loading".
|
|
587
|
-
if (result.readyState === "loading") {
|
|
588
|
-
const ready = await frame
|
|
589
|
-
.isolatedRealm()
|
|
590
|
-
.waitForFunction(() => document.readyState !== "loading", {
|
|
591
|
-
timeout: 2_000,
|
|
592
|
-
polling: "raf",
|
|
593
|
-
})
|
|
594
|
-
.catch((error: unknown) => {
|
|
595
|
-
if (error instanceof TimeoutError) return undefined;
|
|
596
|
-
throw error;
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
await ready?.dispose();
|
|
600
|
-
result = Schema.decodeUnknownSync(Observation)(await read());
|
|
601
|
-
}
|
|
602
|
-
readyState = result.readyState;
|
|
603
|
-
if (frame === page.mainFrame()) metrics = result.page;
|
|
604
|
-
|
|
605
|
-
text += limits.observationMode === "jev" ? result.text : `\n[${ref}]${result.text}`;
|
|
606
|
-
truncated ||= result.truncated ?? false;
|
|
607
|
-
for (const control of result.controls) {
|
|
608
|
-
const current = { ...control, frame: ref };
|
|
609
|
-
|
|
610
|
-
targets.set(control.ref, { frame: ref, control: current });
|
|
611
|
-
controls.push(current);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const textLimit = limits.observationMode === "jev" ? 6_000 : 24_000;
|
|
616
|
-
|
|
617
|
-
return {
|
|
618
|
-
text: text.slice(0, textLimit),
|
|
619
|
-
controls,
|
|
620
|
-
...(readyState === undefined ? {} : { readyState }),
|
|
621
|
-
...(metrics === undefined ? {} : { page: metrics }),
|
|
622
|
-
truncated: truncated || text.length > textLimit,
|
|
623
|
-
frames: currentFrames.slice(0, 32).map((frame) => ({
|
|
624
|
-
ref: frameId(frame),
|
|
625
|
-
name: frame.name().slice(0, 300),
|
|
626
|
-
// Fragments are client-side state; payment frames put kilobytes of parameters there.
|
|
627
|
-
url: observedUrl(
|
|
628
|
-
frame.url().startsWith("data:") ? frame.url() : frame.url().split("#", 1)[0],
|
|
629
|
-
),
|
|
630
|
-
inspected: inspected.includes(frame),
|
|
631
|
-
})),
|
|
632
|
-
tabs: tabObservation,
|
|
633
|
-
dialogs: [],
|
|
634
|
-
};
|
|
635
|
-
}).pipe(Effect.tapError(() => Effect.sync(() => targets.clear())));
|
|
636
|
-
|
|
637
|
-
const observation = yield* bounded(value).pipe(
|
|
638
|
-
Effect.tapError(() => Effect.sync(() => targets.clear())),
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
return observation;
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
const charge = Effect.fnUntraced(function* () {
|
|
645
|
-
if (consumed >= limits.maxActions) return yield* invalid("Browser action budget exhausted.");
|
|
646
|
-
consumed++;
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
const checkedPoint = async (
|
|
650
|
-
element: ElementHandle<Element>,
|
|
651
|
-
frame: Frame,
|
|
652
|
-
expected: typeof Control.Type,
|
|
653
|
-
scroll: boolean,
|
|
654
|
-
pointer = true,
|
|
655
|
-
) => {
|
|
656
|
-
let point = await element.evaluate(checkDom, expected, scroll, pointer);
|
|
657
|
-
|
|
658
|
-
if (point === false) return false;
|
|
659
|
-
let current = frame;
|
|
660
|
-
let parent = current.parentFrame();
|
|
661
|
-
|
|
662
|
-
while (parent !== null) {
|
|
663
|
-
await discoverClosedRoots(parent);
|
|
664
|
-
await using owner = await current.frameElement();
|
|
665
|
-
|
|
666
|
-
if (owner === null) return false;
|
|
667
|
-
await using isolatedOwner = await parent.isolatedRealm().adoptHandle(owner);
|
|
668
|
-
|
|
669
|
-
point = await isolatedOwner.evaluate(checkFrameDom, point, scroll);
|
|
670
|
-
if (point === false) return false;
|
|
671
|
-
current = parent;
|
|
672
|
-
parent = current.parentFrame();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return point;
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
// Separate settling from the following read's authorization snapshot.
|
|
679
|
-
const settle = (input?: { readonly frame: string; readonly ref: string }) =>
|
|
680
|
-
Effect.suspend(() =>
|
|
681
|
-
limits.settleAfterAction
|
|
682
|
-
? native(
|
|
683
|
-
{ kind: "observe" },
|
|
684
|
-
async (page) => {
|
|
685
|
-
const frame =
|
|
686
|
-
limits.settleAfterAction === "input" && input !== undefined
|
|
687
|
-
? resolveFrame(page, input.frame)
|
|
688
|
-
: page.mainFrame();
|
|
689
|
-
|
|
690
|
-
if (frame === undefined) return;
|
|
691
|
-
|
|
692
|
-
if (limits.settleAfterAction === "input")
|
|
693
|
-
await frame.isolatedRealm().evaluate(settleInputDom, input?.ref);
|
|
694
|
-
else await frame.isolatedRealm().evaluate(settleDom);
|
|
695
|
-
},
|
|
696
|
-
undefined,
|
|
697
|
-
"settle",
|
|
698
|
-
).pipe(Effect.asVoid)
|
|
699
|
-
: Effect.void,
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
const after = Effect.fnUntraced(function* (
|
|
703
|
-
completed: number,
|
|
704
|
-
error: string | null,
|
|
705
|
-
dispatch: NonNullable<(typeof ActionResult.Type)["dispatch"]>,
|
|
706
|
-
frame?: string,
|
|
707
|
-
alreadySettled = false,
|
|
708
|
-
observe = true,
|
|
709
|
-
) {
|
|
710
|
-
if (dispatch === "acknowledged" && !alreadySettled)
|
|
711
|
-
yield* settle().pipe(
|
|
712
|
-
Effect.catch((failure) =>
|
|
713
|
-
Effect.sync(() => {
|
|
714
|
-
error ??= failure.message;
|
|
715
|
-
}),
|
|
716
|
-
),
|
|
717
|
-
);
|
|
718
|
-
// Without an observation, refs from before the input can no longer be authorized.
|
|
719
|
-
if (!observe) targets.clear();
|
|
720
|
-
|
|
721
|
-
const observation = !observe
|
|
722
|
-
? null
|
|
723
|
-
: yield* inspect(frame === undefined ? {} : { frame }, {
|
|
724
|
-
kind: "observe",
|
|
725
|
-
}).pipe(
|
|
726
|
-
Effect.catch((failure) => {
|
|
727
|
-
error ??= `Input receipt retained; observation failed. Inspect before continuing. ${failure.message}`;
|
|
728
|
-
|
|
729
|
-
return Effect.succeed(null);
|
|
730
|
-
}),
|
|
251
|
+
return yield* Effect.fail(
|
|
252
|
+
invalid("Browser lifecycle commands belong to the host; use the tab helpers"),
|
|
731
253
|
);
|
|
732
254
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
dispatch,
|
|
738
|
-
...(pendingInput === undefined
|
|
739
|
-
? {}
|
|
740
|
-
: { pendingInput: { ref: pendingInput.ref, reason: "dialog" as const } }),
|
|
741
|
-
};
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
const action = Effect.fnUntraced(function* (
|
|
745
|
-
value: Action | ({ readonly kind: "press" } & typeof PressRequest.Type),
|
|
746
|
-
) {
|
|
747
|
-
if (pendingInput !== undefined)
|
|
748
|
-
return yield* invalid("Native input is pending; respond to its dialog without replaying it.");
|
|
749
|
-
const target = targets.get(value.ref);
|
|
750
|
-
|
|
751
|
-
if (target === undefined)
|
|
752
|
-
return yield* invalid("Target was not observed. Inspect before acting.");
|
|
753
|
-
if (value.kind === "select" && !target.control.options.includes(value.value))
|
|
754
|
-
return yield* invalid("Select option was not observed. Inspect with optionFilter first.");
|
|
755
|
-
yield* charge();
|
|
756
|
-
const command: Command = { kind: "act", action: value };
|
|
757
|
-
|
|
758
|
-
// Preparation has its own budget: 2 s, or 8 s inside a frame, where each frame level adds
|
|
759
|
-
// owner checks and a newly mounted frame first creates its isolated world. It never waits
|
|
760
|
-
// for a missing selector; an actually pending CDP request is terminated by BrowserSession,
|
|
761
|
-
// not abandoned.
|
|
762
|
-
const inFrame = ![...tabs.values()].some(
|
|
763
|
-
(tab) => frameIds.get(tab.mainFrame()) === target.frame,
|
|
764
|
-
);
|
|
765
|
-
|
|
766
|
-
let dispatch: NonNullable<(typeof ActionResult.Type)["dispatch"]> = "not-dispatched";
|
|
767
|
-
let refusal: string | undefined;
|
|
768
|
-
|
|
769
|
-
const ready = yield* native(
|
|
770
|
-
command,
|
|
771
|
-
async (page, stage) => {
|
|
772
|
-
const frame = resolveFrame(page, target.frame);
|
|
773
|
-
|
|
774
|
-
if (frame === undefined) return false;
|
|
775
|
-
// Main-frame refs are checked in place, without resolving and releasing a handle.
|
|
776
|
-
if (frame.parentFrame() === null) {
|
|
777
|
-
stage("validate-and-scroll");
|
|
778
|
-
|
|
779
|
-
const checked = await frame
|
|
780
|
-
.isolatedRealm()
|
|
781
|
-
.evaluate(checkDom, value.ref, target.control, true, value.kind !== "press");
|
|
782
|
-
|
|
783
|
-
return checked !== false;
|
|
784
|
-
}
|
|
785
|
-
stage("resolve-reference");
|
|
786
|
-
|
|
787
|
-
const handle = await frame
|
|
788
|
-
.isolatedRealm()
|
|
789
|
-
.evaluateHandle(
|
|
790
|
-
(ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null,
|
|
791
|
-
value.ref,
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
try {
|
|
795
|
-
const element = handle.asElement();
|
|
255
|
+
const result: unknown = yield* run(
|
|
256
|
+
{ kind: "send", request: value },
|
|
257
|
+
async ({ cdp, page }, frame): Promise<unknown> => {
|
|
258
|
+
let target = cdp;
|
|
796
259
|
|
|
797
|
-
|
|
260
|
+
if (frame !== undefined) {
|
|
261
|
+
const source = frame.client;
|
|
262
|
+
const originalUrl = primary.page.url();
|
|
263
|
+
const pageUrl = page.url();
|
|
264
|
+
const frameUrl = frame.url();
|
|
265
|
+
const existing = connections.get(source);
|
|
798
266
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
false
|
|
803
|
-
);
|
|
804
|
-
} finally {
|
|
805
|
-
stage("release-reference");
|
|
806
|
-
await handle.dispose();
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
inFrame ? 8_000 : 2_000,
|
|
810
|
-
"prepare",
|
|
811
|
-
).pipe(
|
|
812
|
-
Effect.mapError(
|
|
813
|
-
(error) =>
|
|
814
|
-
new BrowserUseError({
|
|
815
|
-
code: error.code,
|
|
816
|
-
message: error.message,
|
|
817
|
-
dispatch: "not-dispatched",
|
|
818
|
-
}),
|
|
819
|
-
),
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
if (!ready)
|
|
823
|
-
return yield* invalid(
|
|
824
|
-
"Observed target is stale, disabled or obstructed. No input dispatched. Inspect for a current target.",
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
const input = native(
|
|
828
|
-
command,
|
|
829
|
-
async (page, stage) => {
|
|
830
|
-
inputPage = page;
|
|
831
|
-
const frame = resolveFrame(page, target.frame);
|
|
832
|
-
|
|
833
|
-
if (frame === undefined) return "not-dispatched" as const;
|
|
834
|
-
// Main-frame input works on the ref in place, without resolving and releasing a handle.
|
|
835
|
-
// Focus and its check stay separate reads so focus handlers run before the check.
|
|
836
|
-
if (frame.parentFrame() === null) {
|
|
837
|
-
const realm = frame.isolatedRealm();
|
|
838
|
-
|
|
839
|
-
const point = await realm.evaluate(
|
|
840
|
-
checkDom,
|
|
841
|
-
value.ref,
|
|
842
|
-
target.control,
|
|
843
|
-
false,
|
|
844
|
-
value.kind !== "press",
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
if (point === false) return "not-dispatched" as const;
|
|
848
|
-
stage("dispatch-input");
|
|
849
|
-
if (value.kind === "click") {
|
|
850
|
-
dispatch = "unknown";
|
|
851
|
-
await page.mouse.click(point.x, point.y);
|
|
852
|
-
} else if (value.kind === "select") {
|
|
853
|
-
dispatch = "unknown";
|
|
854
|
-
|
|
855
|
-
// Puppeteer's select: an enabled matching option, then input and change events.
|
|
856
|
-
const selected = await realm.evaluate(
|
|
857
|
-
(ref, value) => {
|
|
858
|
-
const node: unknown = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(
|
|
859
|
-
ref,
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
!(node instanceof HTMLSelectElement) ||
|
|
864
|
-
!Array.from(node.options).some(
|
|
865
|
-
(option) =>
|
|
866
|
-
!option.disabled &&
|
|
867
|
-
!(
|
|
868
|
-
option.parentElement instanceof HTMLOptGroupElement &&
|
|
869
|
-
option.parentElement.disabled
|
|
870
|
-
) &&
|
|
871
|
-
option.value === value,
|
|
872
|
-
)
|
|
873
|
-
)
|
|
874
|
-
return false;
|
|
875
|
-
if (node.multiple)
|
|
876
|
-
for (const option of Array.from(node.options))
|
|
877
|
-
option.selected = option.value === value;
|
|
878
|
-
else {
|
|
879
|
-
for (const option of Array.from(node.options)) option.selected = false;
|
|
880
|
-
const match = Array.from(node.options).find((option) => option.value === value);
|
|
881
|
-
|
|
882
|
-
if (match !== undefined) match.selected = true;
|
|
883
|
-
}
|
|
884
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
885
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
886
|
-
|
|
887
|
-
return true;
|
|
888
|
-
},
|
|
889
|
-
value.ref,
|
|
890
|
-
value.value,
|
|
891
|
-
);
|
|
892
|
-
|
|
893
|
-
if (!selected) {
|
|
894
|
-
dispatch = "not-dispatched";
|
|
895
|
-
|
|
896
|
-
return "not-dispatched" as const;
|
|
897
|
-
}
|
|
898
|
-
} else if (value.kind === "press") {
|
|
899
|
-
const focused =
|
|
900
|
-
(await realm.evaluate((ref) => {
|
|
901
|
-
const node: unknown = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(
|
|
902
|
-
ref,
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
if (!(node instanceof HTMLElement)) return false;
|
|
906
|
-
node.focus();
|
|
907
|
-
|
|
908
|
-
return true;
|
|
909
|
-
}, value.ref)) &&
|
|
910
|
-
(await realm.evaluate((ref) => {
|
|
911
|
-
const node: unknown = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(
|
|
912
|
-
ref,
|
|
913
|
-
);
|
|
267
|
+
if (existing !== undefined) target = existing;
|
|
268
|
+
else {
|
|
269
|
+
const connection = source.connection();
|
|
914
270
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
node.isConnected &&
|
|
918
|
-
Reflect.get(node.getRootNode(), "activeElement") === node
|
|
919
|
-
);
|
|
920
|
-
}, value.ref));
|
|
271
|
+
if (connection === undefined) return invalid("Frame connection is closed");
|
|
272
|
+
const { targetInfo } = await source.send("Target.getTargetInfo");
|
|
921
273
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
} else {
|
|
926
|
-
const focused = await realm.evaluate((ref) => {
|
|
927
|
-
const node: unknown = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(
|
|
928
|
-
ref,
|
|
929
|
-
);
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
!(
|
|
933
|
-
(node instanceof HTMLInputElement &&
|
|
934
|
-
![
|
|
935
|
-
"password",
|
|
936
|
-
"file",
|
|
937
|
-
"checkbox",
|
|
938
|
-
"radio",
|
|
939
|
-
"hidden",
|
|
940
|
-
"submit",
|
|
941
|
-
"button",
|
|
942
|
-
].includes(node.type) &&
|
|
943
|
-
!node.readOnly) ||
|
|
944
|
-
(node instanceof HTMLTextAreaElement && !node.readOnly) ||
|
|
945
|
-
(node instanceof HTMLElement &&
|
|
946
|
-
node.isContentEditable &&
|
|
947
|
-
node.getAttribute("aria-readonly") !== "true")
|
|
948
|
-
)
|
|
949
|
-
)
|
|
950
|
-
return false;
|
|
951
|
-
node.focus();
|
|
952
|
-
|
|
953
|
-
return true;
|
|
954
|
-
}, value.ref);
|
|
955
|
-
|
|
956
|
-
if (!focused) return "not-dispatched" as const;
|
|
957
|
-
|
|
958
|
-
// Focus handlers can replace a field. Never transfer input to the new focus.
|
|
959
|
-
const selected = await realm.evaluate((ref) => {
|
|
960
|
-
const node: unknown = Reflect.get(globalThis, "@effect-agent/native-browser")?.get(
|
|
961
|
-
ref,
|
|
962
|
-
);
|
|
963
|
-
|
|
964
|
-
if (
|
|
965
|
-
!(node instanceof HTMLElement) ||
|
|
966
|
-
!node.isConnected ||
|
|
967
|
-
!(
|
|
968
|
-
node.getRootNode() instanceof Document || node.getRootNode() instanceof ShadowRoot
|
|
969
|
-
) ||
|
|
970
|
-
Reflect.get(node.getRootNode(), "activeElement") !== node
|
|
971
|
-
)
|
|
972
|
-
return false;
|
|
973
|
-
let selected = false;
|
|
974
|
-
|
|
975
|
-
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
|
976
|
-
node.select();
|
|
977
|
-
selected =
|
|
978
|
-
node.value.length === 0 ||
|
|
979
|
-
(node.selectionStart === 0 && node.selectionEnd === node.value.length) ||
|
|
980
|
-
globalThis.getSelection()?.toString() === node.value;
|
|
981
|
-
} else {
|
|
982
|
-
const selection = globalThis.getSelection();
|
|
983
|
-
|
|
984
|
-
if (selection === null) return false;
|
|
985
|
-
const range = document.createRange();
|
|
986
|
-
|
|
987
|
-
range.selectNodeContents(node);
|
|
988
|
-
selection.removeAllRanges();
|
|
989
|
-
selection.addRange(range);
|
|
990
|
-
selected =
|
|
991
|
-
selection.rangeCount === 1 &&
|
|
992
|
-
selection.getRangeAt(0).compareBoundaryPoints(Range.START_TO_START, range) ===
|
|
993
|
-
0 &&
|
|
994
|
-
selection.getRangeAt(0).compareBoundaryPoints(Range.END_TO_END, range) === 0;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
return (
|
|
998
|
-
selected &&
|
|
999
|
-
node.isConnected &&
|
|
1000
|
-
Reflect.get(node.getRootNode(), "activeElement") === node
|
|
1001
|
-
);
|
|
1002
|
-
}, value.ref);
|
|
1003
|
-
|
|
1004
|
-
if (!selected) {
|
|
1005
|
-
refusal =
|
|
1006
|
-
"Native selection could not prepare this field for replacement. No input dispatched.";
|
|
1007
|
-
|
|
1008
|
-
return "not-dispatched" as const;
|
|
274
|
+
// Keep arbitrary commands off Puppeteer's own event-tracking session.
|
|
275
|
+
target = await connection.createSession(targetInfo);
|
|
276
|
+
connections.set(source, target);
|
|
1009
277
|
}
|
|
1010
|
-
dispatch = "unknown";
|
|
1011
|
-
await page.keyboard.sendCharacter(value.value);
|
|
1012
|
-
}
|
|
1013
|
-
dispatch = "acknowledged";
|
|
1014
|
-
|
|
1015
|
-
return "acknowledged" as const;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const handle = await frame
|
|
1019
|
-
.isolatedRealm()
|
|
1020
|
-
.evaluateHandle(
|
|
1021
|
-
(ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null,
|
|
1022
|
-
value.ref,
|
|
1023
|
-
);
|
|
1024
|
-
|
|
1025
|
-
try {
|
|
1026
|
-
const element = handle.asElement();
|
|
1027
|
-
|
|
1028
|
-
if (element === null || !(element instanceof ElementHandle))
|
|
1029
|
-
return "not-dispatched" as const;
|
|
1030
|
-
|
|
1031
|
-
const point = await checkedPoint(
|
|
1032
|
-
element,
|
|
1033
|
-
frame,
|
|
1034
|
-
target.control,
|
|
1035
|
-
false,
|
|
1036
|
-
value.kind !== "press",
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
if (point === false) return "not-dispatched" as const;
|
|
1040
|
-
stage("dispatch-input");
|
|
1041
|
-
if (value.kind === "click") {
|
|
1042
|
-
dispatch = "unknown";
|
|
1043
|
-
await page.mouse.click(point.x, point.y);
|
|
1044
|
-
} else if (value.kind === "select") {
|
|
1045
|
-
const valid = await element.evaluate(
|
|
1046
|
-
(node, value) =>
|
|
1047
|
-
node instanceof HTMLSelectElement &&
|
|
1048
|
-
Array.from(node.options).some(
|
|
1049
|
-
(option) =>
|
|
1050
|
-
!option.disabled &&
|
|
1051
|
-
!(
|
|
1052
|
-
option.parentElement instanceof HTMLOptGroupElement &&
|
|
1053
|
-
option.parentElement.disabled
|
|
1054
|
-
) &&
|
|
1055
|
-
option.value === value,
|
|
1056
|
-
),
|
|
1057
|
-
value.value,
|
|
1058
|
-
);
|
|
1059
|
-
|
|
1060
|
-
if (!valid) return "not-dispatched" as const;
|
|
1061
|
-
dispatch = "unknown";
|
|
1062
|
-
await element.select(value.value);
|
|
1063
|
-
} else if (value.kind === "press") {
|
|
1064
|
-
await element.focus();
|
|
1065
|
-
|
|
1066
|
-
const focused = await element.evaluate(
|
|
1067
|
-
(node) =>
|
|
1068
|
-
node.isConnected && Reflect.get(node.getRootNode(), "activeElement") === node,
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
|
-
if (!focused) return "not-dispatched" as const;
|
|
1072
|
-
dispatch = "unknown";
|
|
1073
|
-
await page.keyboard.press(value.key);
|
|
1074
|
-
} else {
|
|
1075
|
-
const fillable = await element.evaluate(
|
|
1076
|
-
(node) =>
|
|
1077
|
-
(node instanceof HTMLInputElement &&
|
|
1078
|
-
!["password", "file", "checkbox", "radio", "hidden", "submit", "button"].includes(
|
|
1079
|
-
node.type,
|
|
1080
|
-
) &&
|
|
1081
|
-
!node.readOnly) ||
|
|
1082
|
-
(node instanceof HTMLTextAreaElement && !node.readOnly) ||
|
|
1083
|
-
(node instanceof HTMLElement &&
|
|
1084
|
-
node.isContentEditable &&
|
|
1085
|
-
node.getAttribute("aria-readonly") !== "true"),
|
|
1086
|
-
);
|
|
1087
|
-
|
|
1088
|
-
if (!fillable) return "not-dispatched" as const;
|
|
1089
|
-
await element.focus();
|
|
1090
|
-
// Focus handlers can replace a field. Never transfer input to the new focus.
|
|
1091
278
|
if (
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
Reflect.get(node.getRootNode(), "activeElement") === node,
|
|
1098
|
-
))
|
|
279
|
+
primary.page.url() !== originalUrl ||
|
|
280
|
+
page.url() !== pageUrl ||
|
|
281
|
+
frame.detached ||
|
|
282
|
+
frame.url() !== frameUrl ||
|
|
283
|
+
frame.client !== source
|
|
1099
284
|
)
|
|
1100
|
-
return "
|
|
1101
|
-
|
|
1102
|
-
const selected = await element.evaluate((node) => {
|
|
1103
|
-
let selected = false;
|
|
1104
|
-
|
|
1105
|
-
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
|
1106
|
-
node.select();
|
|
1107
|
-
selected =
|
|
1108
|
-
node.value.length === 0 ||
|
|
1109
|
-
(node.selectionStart === 0 && node.selectionEnd === node.value.length) ||
|
|
1110
|
-
globalThis.getSelection()?.toString() === node.value;
|
|
1111
|
-
} else {
|
|
1112
|
-
const selection = globalThis.getSelection();
|
|
1113
|
-
|
|
1114
|
-
if (selection === null) return false;
|
|
1115
|
-
const range = document.createRange();
|
|
1116
|
-
|
|
1117
|
-
range.selectNodeContents(node);
|
|
1118
|
-
selection.removeAllRanges();
|
|
1119
|
-
selection.addRange(range);
|
|
1120
|
-
selected =
|
|
1121
|
-
selection.rangeCount === 1 &&
|
|
1122
|
-
selection.getRangeAt(0).compareBoundaryPoints(Range.START_TO_START, range) ===
|
|
1123
|
-
0 &&
|
|
1124
|
-
selection.getRangeAt(0).compareBoundaryPoints(Range.END_TO_END, range) === 0;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
return (
|
|
1128
|
-
selected &&
|
|
1129
|
-
node.isConnected &&
|
|
1130
|
-
Reflect.get(node.getRootNode(), "activeElement") === node
|
|
1131
|
-
);
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
if (!selected) {
|
|
1135
|
-
refusal =
|
|
1136
|
-
"Native selection could not prepare this field for replacement. No input dispatched.";
|
|
1137
|
-
|
|
1138
|
-
return "not-dispatched" as const;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
dispatch = "unknown";
|
|
1142
|
-
await page.keyboard.sendCharacter(value.value);
|
|
285
|
+
return invalid("Frame changed during attachment; inspect it again");
|
|
1143
286
|
}
|
|
1144
287
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
},
|
|
1152
|
-
undefined,
|
|
1153
|
-
"input",
|
|
1154
|
-
).pipe(
|
|
1155
|
-
Effect.mapError(
|
|
1156
|
-
(error) => new BrowserUseError({ code: error.code, message: error.message, dispatch }),
|
|
1157
|
-
),
|
|
1158
|
-
);
|
|
1159
|
-
|
|
1160
|
-
// The session lock and native deadline remain owned by a controller-scoped fiber.
|
|
1161
|
-
// A dialog suspends input without abandoning SDK work or acknowledging it early.
|
|
1162
|
-
const start = yield* Deferred.make<void>();
|
|
1163
|
-
const dialogReady = yield* Deferred.make<void>();
|
|
1164
|
-
const ref = `op${scopeId}-${nextInput++}`;
|
|
1165
|
-
|
|
1166
|
-
const fiber = yield* Deferred.await(start).pipe(
|
|
1167
|
-
Effect.andThen(input),
|
|
1168
|
-
Effect.forkIn(ownerScope),
|
|
1169
|
-
);
|
|
1170
|
-
|
|
1171
|
-
pendingInput = { ref, fiber };
|
|
1172
|
-
dialogSignal = dialogReady;
|
|
1173
|
-
yield* Deferred.succeed(start, undefined);
|
|
1174
|
-
|
|
1175
|
-
const result = yield* Effect.raceFirst(
|
|
1176
|
-
Fiber.join(fiber),
|
|
1177
|
-
Deferred.await(dialogReady).pipe(Effect.as("unknown" as const)),
|
|
1178
|
-
).pipe(
|
|
1179
|
-
Effect.onInterrupt(() => Fiber.interrupt(fiber)),
|
|
1180
|
-
Effect.result,
|
|
1181
|
-
);
|
|
1182
|
-
|
|
1183
|
-
if (result._tag === "Failure") {
|
|
1184
|
-
clearInput();
|
|
288
|
+
// Bind the native receiver explicitly; decode the foreign response below.
|
|
289
|
+
return Reflect.apply(target.send.bind(target), undefined, [value.method, value.params]);
|
|
290
|
+
},
|
|
291
|
+
value.targetId,
|
|
292
|
+
value.frameId,
|
|
293
|
+
);
|
|
1185
294
|
|
|
1186
|
-
return yield* result.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
value.kind === "fill" ? { frame: target.frame, ref: value.ref } : undefined,
|
|
1194
|
-
).pipe(
|
|
1195
|
-
Effect.mapError(
|
|
1196
|
-
(failure) =>
|
|
1197
|
-
new BrowserUseError({
|
|
1198
|
-
code: failure.code,
|
|
1199
|
-
message: failure.message,
|
|
1200
|
-
dispatch: "acknowledged",
|
|
1201
|
-
}),
|
|
295
|
+
return yield* Schema.decodeUnknownEffect(Schema.Json)(result).pipe(
|
|
296
|
+
Effect.mapError(() =>
|
|
297
|
+
BrowserUseError.make({
|
|
298
|
+
code: "browser",
|
|
299
|
+
message: "Browser returned a non-JSON result",
|
|
300
|
+
dispatch: "possibly-dispatched",
|
|
301
|
+
}),
|
|
1202
302
|
),
|
|
1203
303
|
);
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
let frame: string | undefined;
|
|
1219
|
-
|
|
1220
|
-
for (const value of values) {
|
|
1221
|
-
frame = targets.get(value.ref)?.frame ?? frame;
|
|
1222
|
-
const outcome = yield* action(value).pipe(Effect.result);
|
|
1223
|
-
|
|
1224
|
-
if (outcome._tag === "Failure") {
|
|
1225
|
-
error = outcome.failure.message;
|
|
1226
|
-
if (outcome.failure.dispatch === "acknowledged") completed++;
|
|
1227
|
-
if (outcome.failure.dispatch === "unknown") dispatch = "unknown";
|
|
1228
|
-
else if (completed > 0) dispatch = "acknowledged";
|
|
1229
|
-
break;
|
|
1230
|
-
}
|
|
1231
|
-
if (outcome.success === "not-dispatched") {
|
|
1232
|
-
error = "Observed target changed before input. Inspect before continuing.";
|
|
1233
|
-
break;
|
|
1234
|
-
}
|
|
1235
|
-
if (outcome.success === "unknown") {
|
|
1236
|
-
dispatch = "unknown";
|
|
1237
|
-
error =
|
|
1238
|
-
"Native input is awaiting a JavaScript dialog. Respond to it; do not replay this input.";
|
|
1239
|
-
break;
|
|
1240
|
-
}
|
|
1241
|
-
dispatch = "acknowledged";
|
|
1242
|
-
completed++;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
yield* Effect.annotateCurrentSpan({
|
|
1246
|
-
"browser.completed": completed,
|
|
1247
|
-
"browser.dispatch": dispatch,
|
|
1248
|
-
});
|
|
1249
|
-
|
|
1250
|
-
return yield* after(completed, error, dispatch, frame, true, options.observe !== false);
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
const navigate = Effect.fnUntraced(function* (request: typeof NavigateRequest.Type) {
|
|
1254
|
-
yield* Schema.decodeEffect(InteractiveBrowserTargetUrl)(request.url).pipe(
|
|
1255
|
-
Effect.mapError(() => invalid("Navigation requires a credential-free HTTP(S) URL.")),
|
|
1256
|
-
);
|
|
1257
|
-
yield* charge();
|
|
1258
|
-
targets.clear();
|
|
1259
|
-
|
|
1260
|
-
const outcome = yield* native({ kind: "navigate", request }, (page) =>
|
|
1261
|
-
page.goto(request.url, { waitUntil: "domcontentloaded" }),
|
|
1262
|
-
).pipe(Effect.result);
|
|
1263
|
-
|
|
1264
|
-
return yield* after(
|
|
1265
|
-
outcome._tag === "Success" ? 1 : 0,
|
|
1266
|
-
outcome._tag === "Failure" ? outcome.failure.message : null,
|
|
1267
|
-
outcome._tag === "Success" ? "acknowledged" : (outcome.failure.dispatch ?? "unknown"),
|
|
1268
|
-
);
|
|
1269
|
-
});
|
|
1270
|
-
|
|
1271
|
-
const scroll = Effect.fnUntraced(function* (request: typeof ScrollRequest.Type) {
|
|
1272
|
-
yield* Schema.decodeEffect(ScrollRequest)(request).pipe(
|
|
1273
|
-
Effect.mapError(() => invalid("Invalid scroll request.")),
|
|
1274
|
-
);
|
|
1275
|
-
yield* charge();
|
|
1276
|
-
const target = request.ref === undefined ? undefined : targets.get(request.ref);
|
|
1277
|
-
|
|
1278
|
-
if (request.ref !== undefined && target === undefined)
|
|
1279
|
-
return yield* invalid("Scroll target was not observed.");
|
|
1280
|
-
|
|
1281
|
-
const outcome = yield* native(
|
|
1282
|
-
{ kind: "scroll", request },
|
|
1283
|
-
async (page) => {
|
|
1284
|
-
if (target === undefined) {
|
|
1285
|
-
const viewport = await page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
|
1286
|
-
|
|
1287
|
-
await page.mouse.move(viewport.width / 2, viewport.height / 2);
|
|
1288
|
-
await page.mouse.wheel({ deltaX: request.deltaX, deltaY: request.deltaY });
|
|
1289
|
-
|
|
1290
|
-
return true;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const frame = resolveFrame(page, target.frame);
|
|
1294
|
-
|
|
1295
|
-
if (frame === undefined) return false;
|
|
1296
|
-
|
|
1297
|
-
const handle = await frame
|
|
1298
|
-
.isolatedRealm()
|
|
1299
|
-
.evaluateHandle(
|
|
1300
|
-
(ref) => Reflect.get(globalThis, "@effect-agent/native-browser")?.get(ref) ?? null,
|
|
1301
|
-
request.ref ?? "",
|
|
1302
|
-
);
|
|
1303
|
-
|
|
1304
|
-
try {
|
|
1305
|
-
return await handle.evaluate(
|
|
1306
|
-
(node, x, y) => {
|
|
1307
|
-
if (!(node instanceof HTMLElement) || !node.isConnected) return false;
|
|
1308
|
-
node.scrollBy({ left: x, top: y, behavior: "instant" });
|
|
1309
|
-
|
|
1310
|
-
return true;
|
|
1311
|
-
},
|
|
1312
|
-
request.deltaX,
|
|
1313
|
-
request.deltaY,
|
|
1314
|
-
);
|
|
1315
|
-
} finally {
|
|
1316
|
-
await handle.dispose();
|
|
304
|
+
}),
|
|
305
|
+
tabs: Effect.gen(function* () {
|
|
306
|
+
const result = yield* run({ kind: "tabs" }, async () => {
|
|
307
|
+
const current = await context.pages();
|
|
308
|
+
|
|
309
|
+
if (current.length > limits.maxTabs) return null;
|
|
310
|
+
const result: Array<typeof Tab.Type> = [];
|
|
311
|
+
|
|
312
|
+
for (const page of current) result.push(await tab(await attach(page)));
|
|
313
|
+
for (const [id, attached] of pages) {
|
|
314
|
+
if (attached.page.isClosed()) {
|
|
315
|
+
pages.delete(id);
|
|
316
|
+
connections.delete(attached.page);
|
|
317
|
+
}
|
|
1317
318
|
}
|
|
1318
|
-
},
|
|
1319
|
-
2_000,
|
|
1320
|
-
).pipe(Effect.result);
|
|
1321
|
-
|
|
1322
|
-
return yield* after(
|
|
1323
|
-
outcome._tag === "Success" && outcome.success ? 1 : 0,
|
|
1324
|
-
outcome._tag === "Failure"
|
|
1325
|
-
? outcome.failure.message
|
|
1326
|
-
: outcome.success
|
|
1327
|
-
? null
|
|
1328
|
-
: "Scroll target is stale.",
|
|
1329
|
-
outcome._tag === "Failure"
|
|
1330
|
-
? (outcome.failure.dispatch ?? "unknown")
|
|
1331
|
-
: outcome.success
|
|
1332
|
-
? "acknowledged"
|
|
1333
|
-
: "not-dispatched",
|
|
1334
|
-
);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
const wait = Effect.fnUntraced(function* (request: typeof WaitRequest.Type) {
|
|
1338
|
-
yield* Schema.decodeEffect(WaitRequest)(request).pipe(
|
|
1339
|
-
Effect.mapError(() => invalid("Invalid condition wait.")),
|
|
1340
|
-
);
|
|
1341
|
-
const frameRef = request.frame;
|
|
1342
|
-
|
|
1343
|
-
if (request.state === "text" && request.text === undefined)
|
|
1344
|
-
return yield* invalid("A text wait requires expected text.");
|
|
1345
|
-
|
|
1346
|
-
if (frameRef !== undefined && !frames.has(frameRef))
|
|
1347
|
-
return yield* invalid("Frame was not observed.");
|
|
1348
|
-
yield* native({ kind: "wait", request }, async (page) => {
|
|
1349
|
-
const frame = frameRef === undefined ? page.mainFrame() : resolveFrame(page, frameRef);
|
|
1350
|
-
|
|
1351
|
-
if (frame === undefined) throw new Error("Frame detached");
|
|
1352
|
-
await discoverClosedRoots(frame);
|
|
1353
|
-
|
|
1354
|
-
try {
|
|
1355
|
-
const handle = await frame.isolatedRealm().waitForFunction(
|
|
1356
|
-
waitDom,
|
|
1357
|
-
{
|
|
1358
|
-
timeout: Math.min(request.timeoutMillis, limits.maxWaitMillis ?? request.timeoutMillis),
|
|
1359
|
-
polling: "raf",
|
|
1360
|
-
},
|
|
1361
|
-
request.selector,
|
|
1362
|
-
request.state,
|
|
1363
|
-
request.text,
|
|
1364
|
-
);
|
|
1365
|
-
|
|
1366
|
-
await handle.dispose();
|
|
1367
|
-
} catch (error) {
|
|
1368
|
-
if (
|
|
1369
|
-
limits.maxWaitMillis === null ||
|
|
1370
|
-
!(error instanceof Error) ||
|
|
1371
|
-
error.name !== "TimeoutError"
|
|
1372
|
-
)
|
|
1373
|
-
throw error;
|
|
1374
|
-
}
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
return yield* inspect(frameRef === undefined ? {} : { frame: frameRef });
|
|
1378
|
-
});
|
|
1379
319
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
mediaType: "image/png" as const,
|
|
1383
|
-
base64: await page.screenshot({ type: "png", encoding: "base64" }),
|
|
1384
|
-
};
|
|
1385
|
-
}).pipe(Effect.flatMap(bounded));
|
|
1386
|
-
|
|
1387
|
-
const press = Effect.fnUntraced(function* (request: typeof PressRequest.Type) {
|
|
1388
|
-
yield* Schema.decodeEffect(PressRequest)(request).pipe(
|
|
1389
|
-
Effect.mapError(() => invalid("Invalid native key request.")),
|
|
1390
|
-
);
|
|
1391
|
-
const frame = targets.get(request.ref)?.frame;
|
|
1392
|
-
const result = yield* action({ kind: "press", ...request }).pipe(Effect.result);
|
|
1393
|
-
|
|
1394
|
-
const dispatch =
|
|
1395
|
-
result._tag === "Success" ? result.success : (result.failure.dispatch ?? "not-dispatched");
|
|
1396
|
-
|
|
1397
|
-
return yield* after(
|
|
1398
|
-
dispatch === "acknowledged" ? 1 : 0,
|
|
1399
|
-
result._tag === "Failure"
|
|
1400
|
-
? result.failure.message
|
|
1401
|
-
: result.success === "not-dispatched"
|
|
1402
|
-
? "Target changed before keyboard input."
|
|
1403
|
-
: null,
|
|
1404
|
-
dispatch,
|
|
1405
|
-
frame,
|
|
1406
|
-
true,
|
|
1407
|
-
);
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
const selectTab = Effect.fnUntraced(function* (request: typeof TabRequest.Type) {
|
|
1411
|
-
yield* Schema.decodeEffect(TabRequest)(request).pipe(
|
|
1412
|
-
Effect.mapError(() => invalid("Invalid tab request.")),
|
|
1413
|
-
);
|
|
1414
|
-
const tab = tabs.get(request.ref);
|
|
1415
|
-
|
|
1416
|
-
if (tab === undefined)
|
|
1417
|
-
return yield* invalid("Tab was not observed. Inspect before selecting it.");
|
|
1418
|
-
yield* native({ kind: "select-tab", request }, async (page) => {
|
|
1419
|
-
if (tab.isClosed() || tab.browserContext() !== page.browserContext())
|
|
1420
|
-
throw new Error("Observed tab is unavailable");
|
|
1421
|
-
await tab.bringToFront();
|
|
1422
|
-
activePage = tab;
|
|
1423
|
-
targets.clear();
|
|
1424
|
-
frames.clear();
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
return yield* inspect({});
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
const respondDialog = Effect.fnUntraced(function* (request: typeof DialogRequest.Type) {
|
|
1431
|
-
yield* Schema.decodeEffect(DialogRequest)(request).pipe(
|
|
1432
|
-
Effect.mapError(() => invalid("Invalid dialog response.")),
|
|
1433
|
-
);
|
|
1434
|
-
yield* charge();
|
|
1435
|
-
const outstanding = pendingInput;
|
|
1436
|
-
|
|
1437
|
-
if (outstanding !== undefined && inputPage !== undefined) {
|
|
1438
|
-
const page = inputPage;
|
|
1439
|
-
const pending = pendingDialogs.get(page);
|
|
1440
|
-
|
|
1441
|
-
if (
|
|
1442
|
-
pending === undefined ||
|
|
1443
|
-
pending.ref !== request.ref ||
|
|
1444
|
-
(request.text !== undefined && pending.dialog.type() !== "prompt")
|
|
1445
|
-
)
|
|
1446
|
-
return yield* invalid("Observed dialog is no longer current.");
|
|
1447
|
-
if (outstanding.fiber.pollUnsafe()?._tag === "Failure") {
|
|
1448
|
-
clearInput();
|
|
1449
|
-
const settled = yield* Fiber.join(outstanding.fiber).pipe(Effect.result);
|
|
1450
|
-
|
|
1451
|
-
return {
|
|
1452
|
-
...(yield* after(
|
|
1453
|
-
0,
|
|
1454
|
-
"Pending native input failed. No dialog response dispatched.",
|
|
1455
|
-
"not-dispatched",
|
|
1456
|
-
)),
|
|
1457
|
-
settledInput: {
|
|
1458
|
-
ref: outstanding.ref,
|
|
1459
|
-
dispatch:
|
|
1460
|
-
settled._tag === "Success"
|
|
1461
|
-
? settled.success
|
|
1462
|
-
: (settled.failure.dispatch ?? "unknown"),
|
|
1463
|
-
},
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
const dialogReady = yield* Deferred.make<void>();
|
|
1468
|
-
|
|
1469
|
-
// Re-arm before answering: the handler may open another dialog before
|
|
1470
|
-
// this response settles. The original input keeps its fiber and deadline.
|
|
1471
|
-
dialogSignal = dialogReady;
|
|
1472
|
-
|
|
1473
|
-
const response =
|
|
1474
|
-
outstanding.fiber.pollUnsafe() === undefined
|
|
1475
|
-
? // Suspended input owns the session lock. This is its only allowed interleaving;
|
|
1476
|
-
// host authority is checked immediately before responding on that exact connection.
|
|
1477
|
-
authorize({ kind: "respond-dialog", request }, { pageUrl: page.url() }).pipe(
|
|
1478
|
-
Effect.andThen(
|
|
1479
|
-
Effect.tryPromise({
|
|
1480
|
-
try: () =>
|
|
1481
|
-
request.accept ? pending.dialog.accept(request.text) : pending.dialog.dismiss(),
|
|
1482
|
-
catch: () =>
|
|
1483
|
-
new BrowserUseError({
|
|
1484
|
-
code: "browser",
|
|
1485
|
-
message: "Native dialog response failed. Do not replay it.",
|
|
1486
|
-
dispatch: "unknown",
|
|
1487
|
-
}),
|
|
1488
|
-
}),
|
|
1489
|
-
),
|
|
1490
|
-
)
|
|
1491
|
-
: native({ kind: "respond-dialog", request }, async () => {
|
|
1492
|
-
if (request.accept) await pending.dialog.accept(request.text);
|
|
1493
|
-
else await pending.dialog.dismiss();
|
|
1494
|
-
});
|
|
1495
|
-
|
|
1496
|
-
const result = yield* response.pipe(
|
|
1497
|
-
Effect.withSpan("BrowserUse.respond-dialog.input"),
|
|
1498
|
-
Effect.result,
|
|
1499
|
-
);
|
|
1500
|
-
|
|
1501
|
-
if (result._tag === "Failure")
|
|
1502
|
-
return yield* after(0, result.failure.message, result.failure.dispatch ?? "not-dispatched");
|
|
1503
|
-
if (pendingDialogs.get(page) === pending) pendingDialogs.delete(page);
|
|
320
|
+
return result;
|
|
321
|
+
});
|
|
1504
322
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
323
|
+
if (result === null) return yield* Effect.fail(invalid("Browser tab limit exceeded"));
|
|
324
|
+
|
|
325
|
+
return result;
|
|
326
|
+
}),
|
|
327
|
+
frames: Effect.fnUntraced(function* (request) {
|
|
328
|
+
const value = yield* decode(FramesRequest, request);
|
|
329
|
+
|
|
330
|
+
const frames = yield* run(
|
|
331
|
+
{ kind: "frames", request: value },
|
|
332
|
+
async ({ page }) =>
|
|
333
|
+
page.frames().map((frame) => ({
|
|
334
|
+
frameId: frame._id,
|
|
335
|
+
parentFrameId: frame.parentFrame()?._id ?? null,
|
|
336
|
+
url: frame.url(),
|
|
337
|
+
})),
|
|
338
|
+
value.targetId,
|
|
1511
339
|
);
|
|
1512
340
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
return {
|
|
1519
|
-
...(yield* after(
|
|
1520
|
-
1,
|
|
1521
|
-
settled._tag === "Failure" ? settled.failure.message : null,
|
|
1522
|
-
"acknowledged",
|
|
1523
|
-
)),
|
|
1524
|
-
settledInput: {
|
|
1525
|
-
ref: outstanding.ref,
|
|
1526
|
-
dispatch:
|
|
1527
|
-
settled._tag === "Success" ? settled.success : (settled.failure.dispatch ?? "unknown"),
|
|
1528
|
-
},
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
341
|
+
return yield* decode(Schema.Array(FrameInfo), frames);
|
|
342
|
+
}),
|
|
343
|
+
newTab: Effect.gen(function* () {
|
|
344
|
+
const result = yield* run({ kind: "new-tab" }, async () => {
|
|
345
|
+
if ((await context.pages()).length >= limits.maxTabs) return null;
|
|
1531
346
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
};
|
|
1535
|
-
|
|
1536
|
-
const result = yield* native({ kind: "respond-dialog", request }, async (page) => {
|
|
1537
|
-
const pending = pendingDialogs.get(page);
|
|
1538
|
-
|
|
1539
|
-
if (
|
|
1540
|
-
pending === undefined ||
|
|
1541
|
-
pending.ref !== request.ref ||
|
|
1542
|
-
(request.text !== undefined && pending.dialog.type() !== "prompt")
|
|
1543
|
-
)
|
|
1544
|
-
return false;
|
|
1545
|
-
receipt.dispatch = "unknown";
|
|
1546
|
-
if (request.accept) await pending.dialog.accept(request.text);
|
|
1547
|
-
else await pending.dialog.dismiss();
|
|
1548
|
-
receipt.dispatch = "acknowledged";
|
|
1549
|
-
if (pendingDialogs.get(page) === pending) pendingDialogs.delete(page);
|
|
1550
|
-
|
|
1551
|
-
return true;
|
|
1552
|
-
}).pipe(Effect.result);
|
|
347
|
+
return tab(await attach(await context.newPage()));
|
|
348
|
+
});
|
|
1553
349
|
|
|
1554
|
-
|
|
1555
|
-
receipt.dispatch === "acknowledged" ? 1 : 0,
|
|
1556
|
-
result._tag === "Failure"
|
|
1557
|
-
? result.failure.message
|
|
1558
|
-
: result.success
|
|
1559
|
-
? null
|
|
1560
|
-
: "Observed dialog is no longer current.",
|
|
1561
|
-
receipt.dispatch,
|
|
1562
|
-
);
|
|
1563
|
-
});
|
|
350
|
+
if (result === null) return yield* Effect.fail(invalid("Browser tab limit reached"));
|
|
1564
351
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
});
|
|
352
|
+
return result;
|
|
353
|
+
}),
|
|
354
|
+
closeTab: Effect.fnUntraced(function* (request) {
|
|
355
|
+
const value = yield* decode(CloseTabRequest, request);
|
|
1570
356
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
respondDialog: (request) =>
|
|
1580
|
-
lock.withPermit(respondDialog(request)).pipe(Effect.withSpan("BrowserUse.respond-dialog")),
|
|
1581
|
-
});
|
|
357
|
+
if (value.targetId === primary.targetId)
|
|
358
|
+
return yield* Effect.fail(invalid("The original page belongs to the host"));
|
|
359
|
+
yield* run(
|
|
360
|
+
{ kind: "close-tab", request: value },
|
|
361
|
+
async ({ page }) => {
|
|
362
|
+
await page.close();
|
|
363
|
+
pages.delete(value.targetId);
|
|
364
|
+
connections.delete(page);
|
|
1582
365
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
Layer.succeed(BrowserControl, control),
|
|
1589
|
-
),
|
|
366
|
+
return null;
|
|
367
|
+
},
|
|
368
|
+
value.targetId,
|
|
369
|
+
);
|
|
370
|
+
}),
|
|
1590
371
|
};
|
|
372
|
+
|
|
373
|
+
return { client, layer: Layer.succeed(BrowserCdp, client) };
|
|
1591
374
|
});
|