pi-browser-harness 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +102 -1
- package/package.json +13 -7
- package/skills/pi-browser-harness/SKILL.md +84 -0
- package/src/cdp/commands.ts +204 -14
- package/src/cdp/console-buffer.ts +37 -23
- package/src/cdp/discovery.ts +72 -37
- package/src/cdp/events.ts +8 -3
- package/src/cdp/network-buffer.ts +38 -32
- package/src/cdp/ownership.ts +22 -4
- package/src/cdp/record-store.ts +2 -2
- package/src/cdp/session.ts +320 -217
- package/src/cdp/target-factory.ts +34 -15
- package/src/cdp/types.ts +17 -1
- package/src/client.ts +76 -43
- package/src/daemon/bridge.ts +115 -191
- package/src/daemon/connection.ts +323 -0
- package/src/daemon/index.ts +77 -48
- package/src/daemon/lines.ts +40 -0
- package/src/daemon/main.ts +12 -0
- package/src/daemon/protocol.ts +55 -25
- package/src/daemon/server.ts +100 -63
- package/src/daemon/socket.ts +10 -0
- package/src/daemon/spawn.ts +84 -27
- package/src/daemon/transport.ts +88 -52
- package/src/domains/ax-tree.ts +8 -8
- package/src/domains/cdp-call.ts +14 -7
- package/src/domains/click.ts +2 -7
- package/src/domains/concurrency.ts +50 -0
- package/src/domains/console.ts +20 -66
- package/src/domains/drag.ts +26 -28
- package/src/domains/element-call.ts +102 -68
- package/src/domains/files.ts +64 -11
- package/src/domains/form-batch.ts +13 -11
- package/src/domains/form.ts +79 -116
- package/src/domains/headers.ts +89 -0
- package/src/domains/history.ts +7 -7
- package/src/domains/io.ts +35 -0
- package/src/domains/isolated-tab.ts +6 -12
- package/src/domains/js.ts +68 -53
- package/src/domains/keyboard.ts +16 -18
- package/src/domains/navigate.ts +102 -69
- package/src/domains/network.ts +56 -98
- package/src/domains/page/fill.page.ts +139 -0
- package/src/domains/page/form.page.ts +61 -0
- package/src/domains/page/inject.ts +37 -0
- package/src/domains/page/keyboard.page.ts +16 -0
- package/src/domains/page/live-value.page.ts +10 -0
- package/src/domains/page/read-page.page.ts +51 -0
- package/src/domains/page/serp.page.ts +39 -0
- package/src/domains/page.ts +19 -58
- package/src/domains/poll.ts +42 -0
- package/src/domains/readpage/read-page.ts +48 -54
- package/src/domains/ref-resolve.ts +43 -29
- package/src/domains/ref-signature.ts +11 -0
- package/src/domains/render.ts +12 -23
- package/src/domains/screenshot-capture.ts +4 -2
- package/src/domains/screenshot.ts +13 -14
- package/src/domains/scroll.ts +4 -5
- package/src/domains/search/google-serp.ts +1 -1
- package/src/domains/search/web-search.ts +38 -34
- package/src/domains/setup.ts +0 -2
- package/src/domains/snapshot.ts +72 -77
- package/src/domains/tab-resolve.ts +36 -0
- package/src/domains/tabs.ts +19 -78
- package/src/domains/target.ts +1 -1
- package/src/domains/viewport.ts +0 -1
- package/src/index.ts +100 -39
- package/src/profile/bind.ts +45 -14
- package/src/profile/browser-process.ts +50 -12
- package/src/profile/command.ts +20 -12
- package/src/profile/launch.ts +13 -9
- package/src/profile/list.ts +9 -13
- package/src/profile/store.ts +2 -1
- package/src/prompt.ts +42 -117
- package/src/registry.ts +68 -4
- package/src/schemas/parse.ts +2 -2
- package/src/setup.ts +28 -19
- package/src/state.ts +33 -2
- package/src/util/debug.ts +7 -1
- package/src/util/guards.ts +3 -0
- package/src/util/mutex.ts +1 -1
- package/src/util/paths.ts +12 -1
- package/src/util/result.ts +8 -0
- package/src/util/sharp-shim.ts +18 -11
- package/src/util/sleep.ts +18 -0
- package/src/util/tool.ts +163 -114
- package/src/util/truncate.ts +1 -1
- package/tsconfig.json +2 -1
- package/src/domains/fill-engine.ts +0 -80
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import type { BrowserClient } from "../client";
|
|
5
5
|
import { type Result, err, ok } from "../util/result";
|
|
6
|
+
import { safeJs } from "../util/js-template";
|
|
6
7
|
import { type CdpError, cdpError } from "./errors";
|
|
8
|
+
import { reconcileOwnership } from "./ownership";
|
|
7
9
|
|
|
8
10
|
const SPAWN_DEADLINE_MS = 8_000;
|
|
9
11
|
const SPAWN_POLL_MS = 100;
|
|
@@ -26,30 +28,46 @@ const listPageTargets = async (client: BrowserClient): Promise<Result<ReadonlyAr
|
|
|
26
28
|
return ok(r.data.targetInfos.filter((t) => t.type === "page"));
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
export type BorrowedSession = { readonly sessionId: string; readonly release: () => Promise<void> };
|
|
32
|
+
|
|
33
|
+
// Reuses the tracked, domain-enabled session when the harness holds one for the target; otherwise attaches a raw throwaway session that `release` detaches, so no session outlives its single use.
|
|
34
|
+
export const borrowSession = async (client: BrowserClient, targetId: string): Promise<Result<BorrowedSession, CdpError>> => {
|
|
35
|
+
const tracked = client.session().sessionFor(targetId);
|
|
36
|
+
if (tracked !== undefined) return ok({ sessionId: tracked, release: () => Promise.resolve() });
|
|
37
|
+
const attached = await client.session().attach(targetId);
|
|
38
|
+
if (!attached.success) return attached;
|
|
39
|
+
const sessionId = attached.data;
|
|
40
|
+
return ok({
|
|
41
|
+
sessionId,
|
|
42
|
+
release: async () => {
|
|
43
|
+
await client.session().detach(sessionId);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
33
46
|
};
|
|
34
47
|
|
|
35
48
|
const spawnTabViaOpener = async (
|
|
36
49
|
client: BrowserClient,
|
|
37
50
|
openerTargetId: string,
|
|
38
51
|
): Promise<Result<string, CdpError>> => {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
52
|
+
const borrowed = await borrowSession(client, openerTargetId);
|
|
53
|
+
if (!borrowed.success) return borrowed;
|
|
41
54
|
|
|
42
55
|
const token = `pi-${randomUUID()}`;
|
|
43
56
|
// userGesture: true is what gets this past the popup blocker.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
let opened;
|
|
58
|
+
try {
|
|
59
|
+
opened = await client.session().callOnTarget(
|
|
60
|
+
"Runtime.evaluate",
|
|
61
|
+
{
|
|
62
|
+
expression: safeJs`!!window.open(${`about:blank#${token}`}, '_blank')`,
|
|
63
|
+
returnByValue: true,
|
|
64
|
+
userGesture: true,
|
|
65
|
+
},
|
|
66
|
+
borrowed.data.sessionId,
|
|
67
|
+
);
|
|
68
|
+
} finally {
|
|
69
|
+
await borrowed.data.release();
|
|
70
|
+
}
|
|
53
71
|
if (!opened.success) return opened;
|
|
54
72
|
if (opened.data.result.value !== true) {
|
|
55
73
|
return err(cdpError(
|
|
@@ -93,6 +111,7 @@ const ensureHarnessWindowUncoordinated = async (
|
|
|
93
111
|
const live = new Set(targets.data.map((t) => t.targetId));
|
|
94
112
|
|
|
95
113
|
const ownership = client.ownership();
|
|
114
|
+
reconcileOwnership(ownership, live);
|
|
96
115
|
const pinnedCtx = client.profileContextId();
|
|
97
116
|
// With a pin set but no context resolved yet, a recorded tab's profile cannot be verified, so seed a new window rather than risk adopting one from the wrong profile.
|
|
98
117
|
const canReuse = client.profilePin() === null || pinnedCtx !== undefined;
|
package/src/cdp/types.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type CdpEvent = {
|
|
|
11
11
|
export type CdpRawMessage = {
|
|
12
12
|
readonly id?: number;
|
|
13
13
|
readonly method?: string;
|
|
14
|
-
readonly params?: Record<string, unknown
|
|
14
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
15
15
|
readonly result?: unknown;
|
|
16
16
|
readonly error?: { readonly message: string; readonly code?: number };
|
|
17
17
|
readonly sessionId?: string;
|
|
@@ -30,6 +30,15 @@ export type TabInfo = {
|
|
|
30
30
|
readonly owned?: boolean;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
// The identity of an interactive element as seen in one snapshot. Kept on the tab session per ref so the next
|
|
34
|
+
// mutation can report what changed without re-running a full snapshot.
|
|
35
|
+
export type RefSignature = {
|
|
36
|
+
readonly role: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly value: string;
|
|
39
|
+
readonly state: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
33
42
|
export type PageInfo = {
|
|
34
43
|
readonly url: string;
|
|
35
44
|
readonly title: string;
|
|
@@ -41,6 +50,11 @@ export type PageInfo = {
|
|
|
41
50
|
readonly pageHeight: number;
|
|
42
51
|
};
|
|
43
52
|
|
|
53
|
+
// Discriminated on `kind`.
|
|
54
|
+
export type PageInfoResult =
|
|
55
|
+
| ({ readonly kind: "page" } & PageInfo)
|
|
56
|
+
| { readonly kind: "dialog"; readonly dialog: DialogInfo };
|
|
57
|
+
|
|
44
58
|
export type DaemonStatus = {
|
|
45
59
|
readonly alive: boolean;
|
|
46
60
|
readonly sessionId: string | null;
|
|
@@ -56,6 +70,8 @@ export const isCdpRawMessage = (v: unknown): v is CdpRawMessage => {
|
|
|
56
70
|
if (method !== undefined && typeof method !== "string") return false;
|
|
57
71
|
const sessionId = v["sessionId"];
|
|
58
72
|
if (sessionId !== undefined && typeof sessionId !== "string") return false;
|
|
73
|
+
const params = v["params"];
|
|
74
|
+
if (params !== undefined && !isRecord(params)) return false;
|
|
59
75
|
const errVal = v["error"];
|
|
60
76
|
if (errVal !== undefined) {
|
|
61
77
|
if (!isRecord(errVal)) return false;
|
package/src/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DAEMON_RECONNECT_METHOD } from "./daemon/protocol";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import { Compile } from "typebox/compile";
|
|
3
4
|
import { parseJson } from "./schemas/parse";
|
|
@@ -8,11 +9,11 @@ import { discoverEndpoint } from "./cdp/discovery";
|
|
|
8
9
|
import { type CdpError, cdpError } from "./cdp/errors";
|
|
9
10
|
import type { CdpTransport } from "./cdp/types";
|
|
10
11
|
import { type CdpSession, createCdpSession } from "./cdp/session";
|
|
11
|
-
import { type OwnershipRegistry, createOwnershipRegistry } from "./cdp/ownership";
|
|
12
|
+
import { type OwnershipRegistry, createOwnershipRegistry, reconcileOwnership } from "./cdp/ownership";
|
|
12
13
|
import { ensureHarnessWindow, openHarnessTab } from "./cdp/target-factory";
|
|
13
14
|
import { seedProfileWindow } from "./profile/bind";
|
|
14
15
|
import type { ProfilePin } from "./profile/store";
|
|
15
|
-
import type { DaemonStatus, DialogInfo, PageInfo, TabInfo } from "./cdp/types";
|
|
16
|
+
import type { DaemonStatus, DialogInfo, PageInfo, PageInfoResult, TabInfo } from "./cdp/types";
|
|
16
17
|
|
|
17
18
|
export type BrowserClientOptions = {
|
|
18
19
|
readonly namespace: string;
|
|
@@ -35,12 +36,14 @@ export type BrowserClient = {
|
|
|
35
36
|
readonly namespace: string;
|
|
36
37
|
ensureAlive(): Promise<Result<void, CdpError>>;
|
|
37
38
|
status(): DaemonStatus;
|
|
38
|
-
|
|
39
|
+
// `reconnectNow` is for explicit human demand (/browser-setup, /browser-reload-daemon): it asks the
|
|
40
|
+
// daemon to dial Chrome immediately even if it is cooling down after a failed attempt.
|
|
41
|
+
start(opts?: { readonly reconnectNow?: boolean }): Promise<Result<void, CdpError>>;
|
|
39
42
|
stop(): Promise<void>;
|
|
40
43
|
detach(): Promise<void>;
|
|
41
44
|
closeOwnedTabs(): Promise<void>;
|
|
42
45
|
evaluateJs(expression: string, sessionId?: string): Promise<Result<unknown, CdpError>>;
|
|
43
|
-
pageInfo(): Promise<Result<
|
|
46
|
+
pageInfo(): Promise<Result<PageInfoResult, CdpError>>;
|
|
44
47
|
takeDialog(): DialogInfo | null;
|
|
45
48
|
listTabs(includeInternal?: boolean): Promise<Result<ReadonlyArray<TabInfo>, CdpError>>;
|
|
46
49
|
switchTab(targetId: string): Promise<Result<void, CdpError>>;
|
|
@@ -123,7 +126,8 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
123
126
|
});
|
|
124
127
|
const mutationMutex = createMutex();
|
|
125
128
|
let lastHealth = 0;
|
|
126
|
-
let
|
|
129
|
+
let healthCheck: Promise<Result<void, CdpError>> | null = null;
|
|
130
|
+
const pageCaches = new Map<string, { readonly info: PageInfo; readonly at: number }>();
|
|
127
131
|
let remote: BrowserClientOptions["remote"] | null = opts.remote ?? null;
|
|
128
132
|
let userDataDir: string | undefined;
|
|
129
133
|
let profilePin: ProfilePin | null = opts.profilePin ?? null;
|
|
@@ -137,7 +141,7 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
137
141
|
return ok(undefined);
|
|
138
142
|
};
|
|
139
143
|
|
|
140
|
-
const start = async (): Promise<Result<void, CdpError>> => {
|
|
144
|
+
const start = async (opts: { readonly reconnectNow?: boolean } = {}): Promise<Result<void, CdpError>> => {
|
|
141
145
|
if (transport.state() === "open" && session.current()) return ok(undefined);
|
|
142
146
|
let wsUrl: string;
|
|
143
147
|
if (remote?.cdpUrl) {
|
|
@@ -150,9 +154,18 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
150
154
|
}
|
|
151
155
|
const connected = await transport.connect(wsUrl, { timeoutMs: 10_000 });
|
|
152
156
|
if (!connected.success) return connected;
|
|
157
|
+
if (opts.reconnectNow) {
|
|
158
|
+
// Only the daemon understands this; a direct Chrome transport answers "method not found",
|
|
159
|
+
// which is fine because it has no cooldown to clear.
|
|
160
|
+
const dialed = await transport.request(DAEMON_RECONNECT_METHOD, {}, { timeoutMs: 15_000 });
|
|
161
|
+
if (!dialed.success && dialed.error.kind !== "remote_error") {
|
|
162
|
+
await transport.close();
|
|
163
|
+
return dialed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
153
166
|
|
|
154
167
|
const newBrowserId: string = wsUrl.split("/").pop() ?? "unknown";
|
|
155
|
-
if (
|
|
168
|
+
if (remote?.browserId !== newBrowserId) {
|
|
156
169
|
profileContextId = undefined;
|
|
157
170
|
}
|
|
158
171
|
remote = { cdpUrl: wsUrl, browserId: newBrowserId };
|
|
@@ -177,33 +190,57 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
177
190
|
lastHealth = 0;
|
|
178
191
|
};
|
|
179
192
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
const restart = async (): Promise<Result<void, CdpError>> => {
|
|
194
|
+
await stop();
|
|
195
|
+
return start();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const reattach = async (): Promise<Result<void, CdpError>> => {
|
|
199
|
+
const seeded = await seedIfPinned();
|
|
200
|
+
if (!seeded.success) return seeded;
|
|
201
|
+
const reattached = await session.attachFirstPage();
|
|
202
|
+
if (reattached.success) {
|
|
203
|
+
pageCaches.clear();
|
|
204
|
+
return ok(undefined);
|
|
184
205
|
}
|
|
206
|
+
return restart();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Two probes: the browser one proves the socket works, the page one proves the current session does.
|
|
210
|
+
// Only transport-level failures (closed socket, timeout) mean the connection is dead. A page probe that
|
|
211
|
+
// fails for a page-level reason (navigation in flight, a throwing page) still proves the session routes,
|
|
212
|
+
// so it counts as alive; only a dead session id triggers a re-attach.
|
|
213
|
+
const checkHealth = async (): Promise<Result<void, CdpError>> => {
|
|
214
|
+
if (transport.state() !== "open" || !session.current()) return restart();
|
|
185
215
|
if (Date.now() - lastHealth < HEALTH_TTL_MS) return ok(undefined);
|
|
186
|
-
const probe = await
|
|
187
|
-
if (!probe.success)
|
|
188
|
-
await stop();
|
|
189
|
-
return start();
|
|
190
|
-
}
|
|
216
|
+
const probe = await session.callBrowser("Target.getTargets", {}, { timeoutMs: 2_000 });
|
|
217
|
+
if (!probe.success) return restart();
|
|
191
218
|
lastHealth = Date.now();
|
|
192
219
|
const jsProbe = await session.call("Runtime.evaluate", {
|
|
193
220
|
expression: "1", returnByValue: true,
|
|
194
221
|
}, { timeoutMs: 2_000 });
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
if (jsProbe.success) return ok(undefined);
|
|
223
|
+
switch (jsProbe.error.kind) {
|
|
224
|
+
case "session_not_found":
|
|
225
|
+
return reattach();
|
|
226
|
+
case "transport_closed":
|
|
227
|
+
case "timeout":
|
|
228
|
+
return restart();
|
|
229
|
+
case "discovery_failed":
|
|
230
|
+
case "invalid_response":
|
|
231
|
+
case "remote_error":
|
|
201
232
|
return ok(undefined);
|
|
202
|
-
}
|
|
203
|
-
await stop();
|
|
204
|
-
return start();
|
|
205
233
|
}
|
|
206
|
-
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Concurrent callers share one in-flight check, so two tools racing after a disconnect cannot both restart the transport.
|
|
237
|
+
const ensureAlive = (): Promise<Result<void, CdpError>> => {
|
|
238
|
+
if (healthCheck) return healthCheck;
|
|
239
|
+
const running = checkHealth().finally(() => {
|
|
240
|
+
healthCheck = null;
|
|
241
|
+
});
|
|
242
|
+
healthCheck = running;
|
|
243
|
+
return running;
|
|
207
244
|
};
|
|
208
245
|
|
|
209
246
|
const evaluateJs = async (expression: string, sessionId?: string): Promise<Result<unknown, CdpError>> => {
|
|
@@ -236,10 +273,12 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
236
273
|
return ok(info.data);
|
|
237
274
|
};
|
|
238
275
|
|
|
239
|
-
const pageInfo = async (): Promise<Result<
|
|
276
|
+
const pageInfo = async (): Promise<Result<PageInfoResult, CdpError>> => {
|
|
240
277
|
const d = session.takeDialog();
|
|
241
|
-
if (d) return ok({ dialog: d });
|
|
242
|
-
|
|
278
|
+
if (d) return ok({ kind: "dialog", dialog: d });
|
|
279
|
+
const info = await readPageInfo();
|
|
280
|
+
if (!info.success) return info;
|
|
281
|
+
return ok({ kind: "page", ...info.data });
|
|
243
282
|
};
|
|
244
283
|
|
|
245
284
|
const listTabs = async (includeInternal = true): Promise<Result<ReadonlyArray<TabInfo>, CdpError>> => {
|
|
@@ -255,11 +294,7 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
255
294
|
owned: ownership.has(t.targetId),
|
|
256
295
|
}));
|
|
257
296
|
const live = new Set(tabs.map((t) => t.targetId));
|
|
258
|
-
|
|
259
|
-
const survivors = owned.filter((id) => live.has(id));
|
|
260
|
-
if (survivors.length !== owned.length) ownership.replaceAll(survivors);
|
|
261
|
-
const hw = ownership.harnessWindow();
|
|
262
|
-
if (hw && !live.has(hw)) ownership.setHarnessWindow(undefined);
|
|
297
|
+
reconcileOwnership(ownership, live);
|
|
263
298
|
for (const tid of pageCaches.keys()) {
|
|
264
299
|
if (!live.has(tid)) pageCaches.delete(tid);
|
|
265
300
|
}
|
|
@@ -276,9 +311,7 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
276
311
|
};
|
|
277
312
|
|
|
278
313
|
const newTab = async (url?: string): Promise<Result<string, CdpError>> => {
|
|
279
|
-
|
|
280
|
-
if (!tabsResult.success) return tabsResult;
|
|
281
|
-
|
|
314
|
+
// ensureHarnessWindow lists live targets and reconciles ownership itself, so no listing is needed here.
|
|
282
315
|
const window = await ensureHarnessWindow(api);
|
|
283
316
|
if (!window.success) return window;
|
|
284
317
|
let tabId: string;
|
|
@@ -308,8 +341,9 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
308
341
|
|
|
309
342
|
const closeOwnedTabs = async (): Promise<void> => {
|
|
310
343
|
for (const id of ownership.list()) {
|
|
311
|
-
await session.callBrowser("Target.closeTarget", { targetId: id })
|
|
312
|
-
|
|
344
|
+
const closed = await session.callBrowser("Target.closeTarget", { targetId: id });
|
|
345
|
+
// A remote error means the browser answered (typically "no target with given id"): the tab is gone either way. A transport failure means nothing was closed, so keep the record for the next attempt.
|
|
346
|
+
if (closed.success || closed.error.kind === "remote_error") ownership.remove(id);
|
|
313
347
|
}
|
|
314
348
|
ownership.setHarnessWindow(undefined);
|
|
315
349
|
ownership.setHarnessWindowId(undefined);
|
|
@@ -323,9 +357,8 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
323
357
|
});
|
|
324
358
|
|
|
325
359
|
const detach = async (): Promise<void> => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
await transport.request("Target.detachFromTarget", { sessionId: cur.sessionId }, { sessionId: null });
|
|
360
|
+
if (transport.state() !== "open") return;
|
|
361
|
+
await session.detachAll();
|
|
329
362
|
};
|
|
330
363
|
|
|
331
364
|
const api: BrowserClient = {
|
|
@@ -344,7 +377,7 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
344
377
|
setProfilePin: (pin) => {
|
|
345
378
|
const changed = pin === null
|
|
346
379
|
? profilePin !== null
|
|
347
|
-
: profilePin
|
|
380
|
+
: profilePin?.userDataDir !== pin.userDataDir || profilePin.profileDir !== pin.profileDir;
|
|
348
381
|
profilePin = pin;
|
|
349
382
|
if (changed) {
|
|
350
383
|
profileContextId = undefined;
|