pi-browser-harness 0.10.2 → 0.11.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 +28 -0
- package/README.md +5 -9
- package/package.json +5 -2
- package/skills/pi-browser-harness/SKILL.md +1 -1
- package/src/cdp/commands.ts +256 -0
- package/src/cdp/console-buffer.ts +10 -85
- package/src/cdp/discovery.ts +28 -71
- package/src/cdp/errors.ts +4 -0
- package/src/cdp/events.ts +170 -0
- package/src/cdp/network-buffer.ts +42 -109
- package/src/cdp/ownership.ts +0 -17
- package/src/cdp/record-store.ts +61 -0
- package/src/cdp/session.ts +205 -193
- package/src/cdp/target-factory.ts +10 -80
- package/src/cdp/types.ts +21 -12
- package/src/client.ts +39 -95
- package/src/daemon/bridge.ts +150 -203
- package/src/daemon/index.ts +0 -28
- package/src/daemon/protocol.ts +6 -68
- package/src/daemon/queue.ts +85 -0
- package/src/daemon/server.ts +24 -99
- package/src/daemon/spawn.ts +18 -68
- package/src/{cdp/daemon-transport.ts → daemon/transport.ts} +18 -51
- package/src/domains/ax-tree.ts +179 -0
- package/src/domains/box.ts +51 -0
- package/src/domains/cdp-call.ts +55 -0
- package/src/domains/click.ts +19 -32
- package/src/domains/console.ts +48 -2
- package/src/domains/dialog.ts +8 -5
- package/src/domains/drag.ts +8 -7
- package/src/domains/element-call.ts +117 -0
- package/src/domains/files.ts +22 -26
- package/src/domains/fill-engine.ts +80 -0
- package/src/domains/form-batch.ts +65 -0
- package/src/domains/form.ts +116 -105
- package/src/domains/history.ts +13 -9
- package/src/domains/isolated-tab.ts +26 -55
- package/src/domains/js.ts +29 -42
- package/src/domains/keyboard.ts +36 -27
- package/src/domains/navigate.ts +16 -23
- package/src/domains/network.ts +60 -10
- package/src/domains/page.ts +24 -3
- package/src/domains/readpage/read-page.ts +74 -19
- package/src/domains/readpage/readability.ts +0 -24
- package/src/domains/ref-resolve.ts +24 -58
- package/src/domains/render.ts +6 -27
- package/src/domains/screenshot-capture.ts +80 -0
- package/src/domains/screenshot.ts +9 -86
- package/src/domains/scroll.ts +10 -11
- package/src/domains/search/google-serp.ts +0 -29
- package/src/domains/search/web-search.ts +48 -20
- package/src/domains/setup.ts +1 -11
- package/src/domains/snapshot.ts +35 -276
- package/src/domains/tabs.ts +14 -7
- package/src/domains/target.ts +35 -0
- package/src/domains/viewport.ts +5 -3
- package/src/index.ts +8 -58
- package/src/profile/bind.ts +8 -57
- package/src/profile/browser-process.ts +75 -126
- package/src/profile/command.ts +50 -29
- package/src/profile/launch.ts +22 -40
- package/src/profile/list.ts +14 -54
- package/src/profile/paths.ts +2 -65
- package/src/profile/store.ts +35 -56
- package/src/registry.ts +30 -33
- package/src/schemas/parse.ts +23 -0
- package/src/setup.ts +0 -25
- package/src/state.ts +16 -16
- package/src/util/debug.ts +8 -0
- package/src/util/guards.ts +30 -0
- package/src/util/js-template.ts +1 -16
- package/src/util/mutex.ts +0 -5
- package/src/util/result.ts +0 -6
- package/src/util/sharp-shim.ts +12 -19
- package/src/util/tool.ts +17 -42
- package/src/util/truncate.ts +6 -0
- package/tsconfig.json +1 -16
- package/skills/deep-research/SKILL.md +0 -66
- package/src/cdp/event-queue.ts +0 -128
- package/src/cdp/transport.ts +0 -156
- package/src/cdp/window-binding.ts +0 -15
- package/src/deep-research.ts +0 -41
- package/src/domains/forms.ts +0 -355
- package/src/domains/readpage/capture.ts +0 -46
- package/src/domains/search/extract.ts +0 -44
- package/src/profile/picker.ts +0 -73
- package/src/util/keycodes.ts +0 -10
- package/src/util/time.ts +0 -29
package/src/domains/tabs.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { type Result, err, ok } from "../util/result";
|
|
3
3
|
import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool";
|
|
4
|
+
import { cdpCallBrowser } from "./cdp-call";
|
|
4
5
|
|
|
5
6
|
const ListTabsArgs = Type.Object({
|
|
6
7
|
includeInternal: Type.Optional(Type.Boolean({ default: true, description: "Include chrome:// pages" })),
|
|
@@ -24,6 +25,7 @@ export const listTabsTool = defineBrowserTool({
|
|
|
24
25
|
"Internal tabs (chrome://) included by default; pass includeInternal=false to exclude them.",
|
|
25
26
|
],
|
|
26
27
|
parameters: ListTabsArgs,
|
|
28
|
+
concurrency: "parallel",
|
|
27
29
|
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
28
30
|
const r = await client.listTabs(args.includeInternal ?? true);
|
|
29
31
|
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
|
|
@@ -50,12 +52,13 @@ export const currentTabTool = defineBrowserTool({
|
|
|
50
52
|
promptSnippet: "Get current tab info",
|
|
51
53
|
promptGuidelines: ["Returns targetId, url, title, and whether the tab is owned by this harness session."],
|
|
52
54
|
parameters: Type.Object({}),
|
|
55
|
+
concurrency: "parallel",
|
|
53
56
|
async handler(_a, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
54
57
|
const cur = client.current();
|
|
55
58
|
if (!cur) return err({ kind: "invalid_state", message: "No tab attached." });
|
|
56
|
-
const ti = await client
|
|
57
|
-
if (!ti.success) return
|
|
58
|
-
const info =
|
|
59
|
+
const ti = await cdpCallBrowser(client, "Target.getTargetInfo", { targetId: cur.targetId });
|
|
60
|
+
if (!ti.success) return ti;
|
|
61
|
+
const info = ti.data.targetInfo;
|
|
59
62
|
const owned = client.owns(info.targetId);
|
|
60
63
|
return ok({
|
|
61
64
|
text: `Current tab${owned ? " (owned)" : " (NOT owned by this session)"}:\n ${info.targetId}\n ${info.url}\n ${info.title}`,
|
|
@@ -83,9 +86,8 @@ export const switchTabTool = defineBrowserTool({
|
|
|
83
86
|
"Refuses tabs not owned by this session — use browser_new_tab to open a controllable tab instead.",
|
|
84
87
|
],
|
|
85
88
|
parameters: SwitchTabArgs,
|
|
89
|
+
concurrency: "serialized",
|
|
86
90
|
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
87
|
-
// Resolve to a concrete targetId (exact, or unique hex prefix). We resolve
|
|
88
|
-
// first so the ownership check sees the canonical id, not the prefix.
|
|
89
91
|
let resolved = args.targetId;
|
|
90
92
|
if (!client.owns(resolved)) {
|
|
91
93
|
const isHexPrefix = /^[0-9A-Fa-f]{8,}$/.test(args.targetId);
|
|
@@ -104,7 +106,9 @@ export const switchTabTool = defineBrowserTool({
|
|
|
104
106
|
details: { matches: matches.map((t) => ({ targetId: t.targetId, url: t.url })) },
|
|
105
107
|
});
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
const first = matches[0];
|
|
110
|
+
if (first === undefined) return err({ kind: "invalid_state", message: "no matching tab" });
|
|
111
|
+
resolved = first.targetId;
|
|
108
112
|
}
|
|
109
113
|
if (!client.owns(resolved)) {
|
|
110
114
|
return err({
|
|
@@ -139,6 +143,7 @@ export const newTabTool = defineBrowserTool({
|
|
|
139
143
|
"Pass url to navigate immediately.",
|
|
140
144
|
],
|
|
141
145
|
parameters: NewTabArgs,
|
|
146
|
+
concurrency: "serialized",
|
|
142
147
|
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
143
148
|
const r = await client.newTab(args.url);
|
|
144
149
|
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
|
|
@@ -165,6 +170,7 @@ export const closeTabTool = defineBrowserTool({
|
|
|
165
170
|
"Accepts exact targetId or a unique hex prefix (>=8 chars).",
|
|
166
171
|
],
|
|
167
172
|
parameters: CloseTabArgs,
|
|
173
|
+
concurrency: "serialized",
|
|
168
174
|
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
169
175
|
let resolved = args.targetId;
|
|
170
176
|
if (!client.owns(resolved)) {
|
|
@@ -172,7 +178,8 @@ export const closeTabTool = defineBrowserTool({
|
|
|
172
178
|
if (isHexPrefix) {
|
|
173
179
|
const owned = client.ownership().list();
|
|
174
180
|
const matches = owned.filter((id) => id.startsWith(args.targetId));
|
|
175
|
-
|
|
181
|
+
const first = matches[0];
|
|
182
|
+
if (matches.length === 1 && first !== undefined) resolved = first;
|
|
176
183
|
else if (matches.length > 1) {
|
|
177
184
|
return err({
|
|
178
185
|
kind: "invalid_state",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BrowserClient } from "../client";
|
|
2
|
+
import { type Result, err, ok } from "../util/result";
|
|
3
|
+
import type { ToolErr } from "../util/tool";
|
|
4
|
+
import { resolveRefToBox } from "./ref-resolve";
|
|
5
|
+
|
|
6
|
+
export type PointArgs = {
|
|
7
|
+
readonly ref?: string | undefined;
|
|
8
|
+
readonly x?: number | undefined;
|
|
9
|
+
readonly y?: number | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ResolvedTarget = {
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly y: number;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const resolveTarget = async (
|
|
19
|
+
client: BrowserClient,
|
|
20
|
+
args: PointArgs,
|
|
21
|
+
): Promise<Result<ResolvedTarget, ToolErr>> => {
|
|
22
|
+
if (args.ref !== undefined) {
|
|
23
|
+
const box = await resolveRefToBox(client, args.ref);
|
|
24
|
+
if (!box.success) return box;
|
|
25
|
+
return ok({
|
|
26
|
+
x: box.data.cx,
|
|
27
|
+
y: box.data.cy,
|
|
28
|
+
label: `[${args.ref}] (${box.data.cx}, ${box.data.cy})`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (args.x !== undefined && args.y !== undefined) {
|
|
32
|
+
return ok({ x: args.x, y: args.y, label: `(${args.x}, ${args.y})` });
|
|
33
|
+
}
|
|
34
|
+
return err({ kind: "invalid_state", message: "Provide either `ref` or both `x` and `y`." });
|
|
35
|
+
};
|
package/src/domains/viewport.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { type Result,
|
|
2
|
+
import { type Result, ok } from "../util/result";
|
|
3
3
|
import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool";
|
|
4
|
+
import { cdpCall } from "./cdp-call";
|
|
4
5
|
|
|
5
6
|
const ViewportArgs = Type.Object({
|
|
6
7
|
width: Type.Integer({ minimum: 100, maximum: 8000, description: "Viewport CSS pixel width" }),
|
|
@@ -20,14 +21,15 @@ export const viewportResizeTool = defineBrowserTool({
|
|
|
20
21
|
"Pass deviceScaleFactor=2 to simulate retina displays.",
|
|
21
22
|
],
|
|
22
23
|
parameters: ViewportArgs,
|
|
24
|
+
concurrency: "serialized",
|
|
23
25
|
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
|
|
24
|
-
const r = await client
|
|
26
|
+
const r = await cdpCall(client, "Emulation.setDeviceMetricsOverride", {
|
|
25
27
|
width: args.width,
|
|
26
28
|
height: args.height,
|
|
27
29
|
deviceScaleFactor: args.deviceScaleFactor ?? 1,
|
|
28
30
|
mobile: false,
|
|
29
31
|
});
|
|
30
|
-
if (!r.success) return
|
|
32
|
+
if (!r.success) return r;
|
|
31
33
|
return ok({
|
|
32
34
|
text: `Viewport set to ${args.width}x${args.height} @${args.deviceScaleFactor ?? 1}x`,
|
|
33
35
|
details: {
|
package/src/index.ts
CHANGED
|
@@ -1,41 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-browser-harness — browser control extension for pi.
|
|
3
|
-
*
|
|
4
|
-
* Gives pi agents full control of a real Chrome browser via CDP.
|
|
5
|
-
* Connects to the user's running Chrome (chrome://inspect/#remote-debugging),
|
|
6
|
-
* registers browser_* tools, and injects browser control guidance into the
|
|
7
|
-
* system prompt.
|
|
8
|
-
*
|
|
9
|
-
* Install:
|
|
10
|
-
* pi install npm:pi-browser-harness
|
|
11
|
-
* # or copy to .pi/extensions/pi-browser-harness/
|
|
12
|
-
*
|
|
13
|
-
* Commands:
|
|
14
|
-
* /browser-setup — guided setup wizard
|
|
15
|
-
* /browser-profile — choose which browser profile the agent uses
|
|
16
|
-
* /browser-status — show client status and current page
|
|
17
|
-
* /browser-reload-daemon — restart the browser client
|
|
18
|
-
* /deep-research <q> — research a question on the web, write a cited report
|
|
19
|
-
*
|
|
20
|
-
* Flags:
|
|
21
|
-
* --browser-namespace <name> — override namespace (default: auto)
|
|
22
|
-
* --browser-debug-clicks — enable debug click overlay
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
26
2
|
import { type BrowserClient, createBrowserClient } from "./client";
|
|
27
3
|
import { getBrowserSystemPrompt } from "./prompt";
|
|
28
4
|
import { registerSetupCommand } from "./setup";
|
|
29
5
|
import { registerProfileCommand } from "./profile/command";
|
|
30
6
|
import { readPin } from "./profile/store";
|
|
31
|
-
import { registerDeepResearchCommand } from "./deep-research";
|
|
32
7
|
import { type BrowserState, defaultState, persistState, restoreState } from "./state";
|
|
33
8
|
import { registerAllTools } from "./registry";
|
|
34
9
|
import { cleanupTempDirs } from "./util/truncate";
|
|
35
|
-
import { createDaemonTransport } from "./
|
|
10
|
+
import { createDaemonTransport } from "./daemon/transport";
|
|
11
|
+
import { setDebugClicks } from "./util/debug";
|
|
12
|
+
import { asBoolean, asString } from "./util/guards";
|
|
36
13
|
|
|
37
14
|
export default function browserHarnessExtension(pi: ExtensionAPI): void {
|
|
38
|
-
const flagNs = pi.getFlag("browser-namespace")
|
|
15
|
+
const flagNs = asString(pi.getFlag("browser-namespace"));
|
|
39
16
|
const namespace = flagNs ?? `pi-${Math.random().toString(36).slice(2, 10)}`;
|
|
40
17
|
|
|
41
18
|
let state: BrowserState = defaultState(namespace);
|
|
@@ -106,16 +83,11 @@ export default function browserHarnessExtension(pi: ExtensionAPI): void {
|
|
|
106
83
|
|
|
107
84
|
pi.on("session_start", async (_event, ctx) => {
|
|
108
85
|
state = restoreState(ctx, state.namespace);
|
|
86
|
+
setDebugClicks(asBoolean(pi.getFlag("browser-debug-clicks")) ?? false);
|
|
109
87
|
|
|
110
|
-
//
|
|
111
|
-
// session termination. Re-read it every session start: another pi session
|
|
112
|
-
// may have changed it since this client was created.
|
|
88
|
+
// Re-read the on-disk profile pin every session start: another pi session may have changed it since this client was created.
|
|
113
89
|
const profilePin = await readPin();
|
|
114
90
|
|
|
115
|
-
// Create the client lazily on first session. The daemon transport
|
|
116
|
-
// is created but NOT connected — that only happens when the user
|
|
117
|
-
// runs /browser-setup or a browser tool call finds an existing socket.
|
|
118
|
-
// This means zero Chrome prompts for non-browser pi sessions.
|
|
119
91
|
if (!client) {
|
|
120
92
|
const transport = createDaemonTransport(state.namespace);
|
|
121
93
|
|
|
@@ -140,53 +112,31 @@ export default function browserHarnessExtension(pi: ExtensionAPI): void {
|
|
|
140
112
|
? { harnessWindowId: snap.harnessWindowId }
|
|
141
113
|
: {}),
|
|
142
114
|
};
|
|
143
|
-
|
|
144
|
-
// including after session replacement when pi's ctx is stale.
|
|
145
|
-
// Persistence is best-effort — failure must not crash the event consumer.
|
|
146
|
-
// ponytail: try/catch is the simplest guard against stale-ctx.
|
|
147
|
-
try { persistState(pi, state); } catch { /* stale ctx — safe to ignore */ }
|
|
115
|
+
try { persistState(pi, state); } catch {}
|
|
148
116
|
},
|
|
149
117
|
});
|
|
150
118
|
} else {
|
|
151
|
-
// Client carried over from a previous session — adopt whatever pin is on
|
|
152
|
-
// disk now, which another pi session may have changed.
|
|
153
119
|
client.setProfilePin(profilePin);
|
|
154
120
|
}
|
|
155
121
|
|
|
156
|
-
// Do NOT call client.start() here — browser control is on-demand.
|
|
157
|
-
// The first browser tool call or /browser-setup triggers the actual
|
|
158
|
-
// daemon socket connection and Chrome prompt.
|
|
159
|
-
|
|
160
122
|
if (!toolsRegistered) {
|
|
161
123
|
registerAllTools(pi, client);
|
|
162
124
|
toolsRegistered = true;
|
|
163
125
|
}
|
|
164
126
|
registerSetupCommand(pi, client);
|
|
165
127
|
registerProfileCommand(pi, client);
|
|
166
|
-
registerDeepResearchCommand(pi);
|
|
167
128
|
});
|
|
168
129
|
|
|
169
130
|
pi.on("session_shutdown", async () => {
|
|
170
131
|
if (client) {
|
|
171
132
|
try {
|
|
172
|
-
// Detach from the page target (removes the "Chrome is being controlled"
|
|
173
|
-
// banner) but keep the transport alive for the next session.
|
|
174
133
|
await client.detach();
|
|
175
|
-
// Close every tab this session opened so no stale harness tabs linger
|
|
176
|
-
// in the user's browser between sessions. This also clears the persisted
|
|
177
|
-
// ownership + window binding via onOwnershipChange, so the next session
|
|
178
|
-
// starts with a fresh dedicated window rather than reattaching to leftovers.
|
|
179
134
|
await client.closeOwnedTabs();
|
|
180
135
|
} catch (e) {
|
|
181
136
|
console.warn("[pi-browser-harness] browser teardown failed during shutdown:", e);
|
|
182
137
|
}
|
|
183
|
-
//
|
|
184
|
-
// Do NOT call client.stop() or null out client — the daemon connection
|
|
185
|
-
// persists and eliminates the per-session "Allow Remote Debugging" prompt.
|
|
186
|
-
// Only Chrome restart or daemon death triggers a new prompt.
|
|
138
|
+
// Do NOT stop the client or null it out: the surviving daemon connection is what eliminates the per-session "Allow Remote Debugging" prompt.
|
|
187
139
|
}
|
|
188
|
-
// Persist last — closeOwnedTabs mutates `state` through onOwnershipChange,
|
|
189
|
-
// so this captures the cleared ownership set.
|
|
190
140
|
persistState(pi, state);
|
|
191
141
|
await cleanupTempDirs();
|
|
192
142
|
});
|
package/src/profile/bind.ts
CHANGED
|
@@ -1,62 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Binding the harness session to a pinned profile.
|
|
3
|
-
*
|
|
4
|
-
* Sequence, all of it verified against a live browser:
|
|
5
|
-
* 1. resolve the browser binary and any explicit --user-data-dir
|
|
6
|
-
* 2. launch a window into the profile, marked with a file:// sentinel
|
|
7
|
-
* 3. poll Target.getTargets for the sentinel page — its browserContextId IS
|
|
8
|
-
* the profile's context, which is otherwise undiscoverable (CDP exposes no
|
|
9
|
-
* profile identity)
|
|
10
|
-
* 4. adopt that page as the harness window, blank it, drop the sentinel file
|
|
11
|
-
*
|
|
12
|
-
* If the sentinel never appears the bind FAILS LOUDLY. Falling through to a
|
|
13
|
-
* normal createTarget would put the agent in the focus-derived default profile
|
|
14
|
-
* — the exact bug this feature exists to remove — so the caller gets an
|
|
15
|
-
* actionable error instead.
|
|
16
|
-
*
|
|
17
|
-
* The resolved context id is session-scoped state, never persisted: Chrome
|
|
18
|
-
* mints new context ids on every browser run.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
1
|
import type { BrowserClient } from "../client";
|
|
2
|
+
import type { ResultOf } from "../cdp/commands";
|
|
22
3
|
import { type CdpError, cdpError } from "../cdp/errors";
|
|
23
4
|
import { type Result, err, ok } from "../util/result";
|
|
24
5
|
import { detectRunningBrowser, resolveBrowserExecutable } from "./browser-process";
|
|
25
6
|
import { openProfileWindow } from "./launch";
|
|
26
7
|
import type { ProfilePin } from "./store";
|
|
27
8
|
|
|
28
|
-
/** The launch delegates through ProcessSingleton, so allow for a cold profile. */
|
|
29
9
|
const SEED_DEADLINE_MS = 15_000;
|
|
30
10
|
const SEED_POLL_MS = 500;
|
|
31
11
|
|
|
32
|
-
type PageTarget =
|
|
33
|
-
readonly targetId: string;
|
|
34
|
-
readonly type: string;
|
|
35
|
-
readonly url: string;
|
|
36
|
-
readonly browserContextId?: string;
|
|
37
|
-
};
|
|
12
|
+
type PageTarget = ResultOf<"Target.getTargets">["targetInfos"][number];
|
|
38
13
|
|
|
39
14
|
const pageTargets = async (client: BrowserClient): Promise<ReadonlyArray<PageTarget>> => {
|
|
40
15
|
const r = await client.session().callBrowser("Target.getTargets");
|
|
41
16
|
if (!r.success) return [];
|
|
42
|
-
|
|
43
|
-
return data.targetInfos.filter((t) => t.type === "page");
|
|
17
|
+
return r.data.targetInfos.filter((t) => t.type === "page");
|
|
44
18
|
};
|
|
45
19
|
|
|
46
20
|
const manualFallbackHint = (pin: ProfilePin): string =>
|
|
47
21
|
`open a window for "${pin.label}" yourself (browser profile menu → ${pin.profileDir}), then retry. ` +
|
|
48
22
|
`Run /browser-profile to pick a different profile, or clear the selection to use whichever window is focused.`;
|
|
49
23
|
|
|
50
|
-
/**
|
|
51
|
-
* Open (and adopt) a harness window inside the pinned profile. Returns the
|
|
52
|
-
* seed tab's target id.
|
|
53
|
-
*/
|
|
54
24
|
export const seedProfileWindow = async (
|
|
55
25
|
client: BrowserClient,
|
|
56
26
|
pin: ProfilePin,
|
|
57
27
|
): Promise<Result<string, CdpError>> => {
|
|
58
|
-
// A pin belongs to one user-data-dir. If the harness is now attached to a
|
|
59
|
-
// different browser, silently seeding the "same" profile name would be wrong.
|
|
60
28
|
const connectedDir = client.userDataDir();
|
|
61
29
|
if (connectedDir && connectedDir !== pin.userDataDir) {
|
|
62
30
|
return err(cdpError(
|
|
@@ -66,9 +34,6 @@ export const seedProfileWindow = async (
|
|
|
66
34
|
));
|
|
67
35
|
}
|
|
68
36
|
|
|
69
|
-
// Identify the browser by the dir we're connected to. With several Chromium
|
|
70
|
-
// browsers running, launching into the wrong one would open a window the
|
|
71
|
-
// harness cannot see — in a browser the user never pointed us at.
|
|
72
37
|
const targetUserDataDir = connectedDir ?? pin.userDataDir;
|
|
73
38
|
const running = await detectRunningBrowser(targetUserDataDir);
|
|
74
39
|
const exePath = await resolveBrowserExecutable(running);
|
|
@@ -82,11 +47,7 @@ export const seedProfileWindow = async (
|
|
|
82
47
|
const launched = await openProfileWindow({
|
|
83
48
|
exePath,
|
|
84
49
|
profileDir: pin.profileDir,
|
|
85
|
-
//
|
|
86
|
-
// ProcessSingleton keys on this path: passing the value the target process
|
|
87
|
-
// itself reports guarantees delegation, whereas supplying a default dir we
|
|
88
|
-
// reconstructed could differ by case or normalisation and start a SECOND
|
|
89
|
-
// browser instead.
|
|
50
|
+
// ProcessSingleton keys on the user-data-dir path, so forward only the value the running browser reports: a reconstructed one may differ by case and start a SECOND browser.
|
|
90
51
|
...(running.explicitUserDataDir ? { explicitUserDataDir: running.explicitUserDataDir } : {}),
|
|
91
52
|
});
|
|
92
53
|
if (!launched.success) {
|
|
@@ -116,22 +77,12 @@ export const seedProfileWindow = async (
|
|
|
116
77
|
const ownership = client.ownership();
|
|
117
78
|
ownership.setHarnessWindow(seed.targetId);
|
|
118
79
|
ownership.add(seed.targetId);
|
|
119
|
-
const win = await client.session().
|
|
120
|
-
if (win.success)
|
|
121
|
-
const windowId = (win.data as { windowId?: number }).windowId;
|
|
122
|
-
if (typeof windowId === "number") ownership.setHarnessWindowId(windowId);
|
|
123
|
-
}
|
|
80
|
+
const win = await client.session().windowId(seed.targetId);
|
|
81
|
+
if (win.success) ownership.setHarnessWindowId(win.data);
|
|
124
82
|
|
|
125
|
-
|
|
126
|
-
// then remove the sentinel file. Best-effort: a failure here costs nothing
|
|
127
|
-
// beyond the placeholder page staying visible.
|
|
128
|
-
const attached = await client.session().callBrowser("Target.attachToTarget", {
|
|
129
|
-
targetId: seed.targetId,
|
|
130
|
-
flatten: true,
|
|
131
|
-
});
|
|
83
|
+
const attached = await client.session().attach(seed.targetId);
|
|
132
84
|
if (attached.success) {
|
|
133
|
-
|
|
134
|
-
await client.session().callOnTarget("Page.navigate", { url: "about:blank" }, sessionId);
|
|
85
|
+
await client.session().callOnTarget("Page.navigate", { url: "about:blank" }, attached.data);
|
|
135
86
|
}
|
|
136
87
|
await cleanup();
|
|
137
88
|
|