@narumitw/pi-chrome-devtools 0.53.4 → 0.54.1
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 +109 -0
- package/README.md +34 -16
- package/dist/chunks/{browser-settings-menu-FVMCCUPM.ts → browser-settings-menu-7M3VM7QN.ts} +25 -3
- package/dist/chunks/browser-settings-menu-7M3VM7QN.ts.map +7 -0
- package/dist/chunks/{chunk-V7N3WI5B.ts → chunk-JKPJYUVG.ts} +340 -108
- package/dist/chunks/chunk-JKPJYUVG.ts.map +7 -0
- package/dist/chunks/{chunk-4B3EQHOH.ts → chunk-JLEJ6WFO.ts} +2 -2
- package/dist/chunks/{chunk-LSJSDY2M.ts → chunk-WTMZID3G.ts} +382 -246
- package/dist/chunks/chunk-WTMZID3G.ts.map +7 -0
- package/dist/chunks/{menu-MTPSLDHZ.ts → menu-HGG26E3X.ts} +7 -7
- package/dist/chunks/{menu-MTPSLDHZ.ts.map → menu-HGG26E3X.ts.map} +1 -1
- package/dist/chunks/{tools-OVNH45IJ.ts → tools-O5N4KIAK.ts} +3 -3
- package/dist/index.ts +45 -21
- package/dist/index.ts.map +2 -2
- package/docs/browser-setup.md +7 -1
- package/package.json +6 -5
- package/src/browser-candidates.ts +184 -0
- package/src/browser-manager.ts +93 -276
- package/src/browser-settings-menu.ts +23 -0
- package/src/browser-startup.ts +122 -0
- package/src/chrome-devtools.ts +48 -15
- package/src/lazy-tools.ts +358 -48
- package/src/menu.ts +3 -3
- package/src/runtime.ts +2 -2
- package/src/settings.ts +65 -1
- package/src/tool-selector.ts +95 -40
- package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +0 -7
- package/dist/chunks/chunk-LSJSDY2M.ts.map +0 -7
- package/dist/chunks/chunk-V7N3WI5B.ts.map +0 -7
- /package/dist/chunks/{chunk-4B3EQHOH.ts.map → chunk-JLEJ6WFO.ts.map} +0 -0
- /package/dist/chunks/{tools-OVNH45IJ.ts.map → tools-O5N4KIAK.ts.map} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
import type { EventEmitter } from "node:events";
|
|
3
|
+
|
|
4
|
+
const MAX_STARTUP_STDERR_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
function handleErrorsUntilClose(emitter: EventEmitter) {
|
|
7
|
+
// Cancellation or readiness can settle before an already queued process/pipe error.
|
|
8
|
+
// Keep the resource safe until close, then remove this lifetime-only handler.
|
|
9
|
+
const ignoreError = () => undefined;
|
|
10
|
+
emitter.on("error", ignoreError);
|
|
11
|
+
emitter.once("close", () => emitter.off("error", ignoreError));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function browserWebSocketIdentity(value: unknown, port: number): string | undefined {
|
|
15
|
+
if (typeof value !== "string") return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(value);
|
|
18
|
+
if (
|
|
19
|
+
url.protocol !== "ws:" ||
|
|
20
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
|
|
21
|
+
Number(url.port || "80") !== port ||
|
|
22
|
+
url.username ||
|
|
23
|
+
url.password ||
|
|
24
|
+
url.search ||
|
|
25
|
+
url.hash ||
|
|
26
|
+
!/^\/devtools\/browser\/[^/]+$/.test(url.pathname)
|
|
27
|
+
) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return url.pathname;
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A free-port probe cannot reserve the port through Chrome's startup. Use the owned child's
|
|
37
|
+
// announcement, not an arbitrary responding HTTP server, as the browser identity authority.
|
|
38
|
+
export function waitForBrowserStartup(
|
|
39
|
+
child: ChildProcess,
|
|
40
|
+
port: number,
|
|
41
|
+
waitMs: number,
|
|
42
|
+
signal: AbortSignal,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
handleErrorsUntilClose(child);
|
|
45
|
+
const stderr = child.stderr;
|
|
46
|
+
if (stderr && !stderr.closed) handleErrorsUntilClose(stderr);
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
if (!stderr) return Promise.reject(new Error("Auto-launched browser has no startup stderr stream."));
|
|
49
|
+
|
|
50
|
+
return new Promise<string>((resolve, reject) => {
|
|
51
|
+
let spawned = false;
|
|
52
|
+
let identity: string | undefined;
|
|
53
|
+
let receivedBytes = 0;
|
|
54
|
+
let pendingLine = "";
|
|
55
|
+
let settled = false;
|
|
56
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
57
|
+
|
|
58
|
+
const settle = (error?: Error) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
if (!error && (!spawned || !identity)) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
child.off("spawn", onSpawn);
|
|
64
|
+
child.off("error", onError);
|
|
65
|
+
child.off("exit", onExit);
|
|
66
|
+
child.off("close", onExit);
|
|
67
|
+
stderr.off("data", onData);
|
|
68
|
+
stderr.off("end", onEnd);
|
|
69
|
+
stderr.off("error", onError);
|
|
70
|
+
signal.removeEventListener("abort", onAbort);
|
|
71
|
+
// Keep the pipe drained after startup without retaining or displaying browser logs.
|
|
72
|
+
stderr.resume();
|
|
73
|
+
if (error) reject(error);
|
|
74
|
+
else resolve(identity as string);
|
|
75
|
+
};
|
|
76
|
+
const onError = (error: Error) => settle(error);
|
|
77
|
+
const onAbort = () => settle(new Error("Chrome DevTools browser startup cancelled.", { cause: signal.reason }));
|
|
78
|
+
const onExit = () => settle(new Error("Auto-launched browser exited before DevTools became available."));
|
|
79
|
+
const onSpawn = () => {
|
|
80
|
+
spawned = true;
|
|
81
|
+
timeout = setTimeout(
|
|
82
|
+
() => settle(new Error("Timed out waiting for auto-launched browser DevTools startup announcement.")),
|
|
83
|
+
waitMs,
|
|
84
|
+
);
|
|
85
|
+
settle();
|
|
86
|
+
};
|
|
87
|
+
const inspectLine = (line: string) => {
|
|
88
|
+
const match = /^DevTools listening on (\S+)$/.exec(line.trim());
|
|
89
|
+
if (!match) return;
|
|
90
|
+
identity = browserWebSocketIdentity(match[1], port);
|
|
91
|
+
if (!identity) settle(new Error("Auto-launched browser announced an unexpected DevTools endpoint."));
|
|
92
|
+
else settle();
|
|
93
|
+
};
|
|
94
|
+
const onData = (data: Buffer | string) => {
|
|
95
|
+
receivedBytes += Buffer.byteLength(data);
|
|
96
|
+
if (receivedBytes > MAX_STARTUP_STDERR_BYTES) {
|
|
97
|
+
settle(new Error("Auto-launched browser startup stderr exceeded 64 KB before readiness."));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pendingLine += data.toString();
|
|
101
|
+
const lines = pendingLine.split("\n");
|
|
102
|
+
pendingLine = lines.pop() ?? "";
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
inspectLine(line);
|
|
105
|
+
if (settled) break;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const onEnd = () => {
|
|
109
|
+
inspectLine(pendingLine);
|
|
110
|
+
if (!identity) settle(new Error("Auto-launched browser closed stderr before its DevTools startup announcement."));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
child.once("spawn", onSpawn);
|
|
114
|
+
child.once("error", onError);
|
|
115
|
+
child.once("exit", onExit);
|
|
116
|
+
child.once("close", onExit);
|
|
117
|
+
stderr.on("data", onData);
|
|
118
|
+
stderr.once("end", onEnd);
|
|
119
|
+
stderr.once("error", onError);
|
|
120
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
});
|
|
122
|
+
}
|
package/src/chrome-devtools.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { shutdownManagedBrowser, startManagedBrowserSession, syncManagedBrowserS
|
|
|
3
3
|
import { setActivePageId } from "./cdp-client.js";
|
|
4
4
|
import {
|
|
5
5
|
availableChromeDevtoolsTools,
|
|
6
|
+
chromeDevtoolsToolMode,
|
|
6
7
|
configureChromeDevtoolsToolExposure,
|
|
7
|
-
createChromeDevtoolsLoadTool,
|
|
8
8
|
initializeAvailableChromeDevtoolsTools,
|
|
9
|
+
previousChromeDevtoolsTools,
|
|
10
|
+
registerChromeDevtoolsCapabilities,
|
|
9
11
|
requireEagerChromeDevtoolsToolExposure,
|
|
10
12
|
setChromeDevtoolsSessionOwner,
|
|
11
13
|
supportsNativeDeferredToolLoading,
|
|
@@ -55,14 +57,15 @@ const COMMAND_COMPLETIONS = [
|
|
|
55
57
|
{ value: "off", label: "off", description: "Compatibility alias for disable" },
|
|
56
58
|
];
|
|
57
59
|
export default function chromeDevtools(pi: ExtensionAPI) {
|
|
58
|
-
pi
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
registerChromeDevtoolsCapabilities(pi, [
|
|
61
|
+
listPagesTool,
|
|
62
|
+
selectPageTool,
|
|
63
|
+
navigateTool,
|
|
64
|
+
evaluateTool,
|
|
65
|
+
screenshotTool,
|
|
66
|
+
webMcpListToolsTool,
|
|
67
|
+
webMcpCallTool,
|
|
68
|
+
]);
|
|
66
69
|
|
|
67
70
|
pi.registerCommand("chrome-devtools", {
|
|
68
71
|
description: "Open Chrome DevTools help and tool controls",
|
|
@@ -77,7 +80,11 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
77
80
|
|
|
78
81
|
pi.on("session_start", async (_event, ctx) => {
|
|
79
82
|
const generation = ++state.sessionGeneration;
|
|
80
|
-
setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
|
|
83
|
+
setChromeDevtoolsSessionOwner(pi, ctx.sessionManager, ctx.sessionManager.getBranch());
|
|
84
|
+
// Capture before default initialization: unknown invalid files must not
|
|
85
|
+
// broaden availability, while a restored/current policy survives reload.
|
|
86
|
+
const previousAvailableTools = previousChromeDevtoolsTools(pi);
|
|
87
|
+
const previousMode = chromeDevtoolsToolMode(pi);
|
|
81
88
|
initializeAvailableChromeDevtoolsTools(pi);
|
|
82
89
|
setWebMcpSessionOwner(ctx.sessionManager);
|
|
83
90
|
replaceSessionController("Chrome DevTools session replaced");
|
|
@@ -95,6 +102,8 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
95
102
|
state.activePageId = undefined;
|
|
96
103
|
state.lastLaunchAttempt = undefined;
|
|
97
104
|
const projectTrusted = ctx.isProjectTrusted();
|
|
105
|
+
await waitForChromeDevtoolsSettings();
|
|
106
|
+
if (generation !== state.sessionGeneration) return;
|
|
98
107
|
const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
99
108
|
if (generation !== state.sessionGeneration) return;
|
|
100
109
|
applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
|
|
@@ -110,17 +119,41 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
110
119
|
"warning",
|
|
111
120
|
);
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
// Tool mode/catalog are user-owned; a valid project browser section can
|
|
123
|
+
// make the aggregate result loaded even when its user source is invalid.
|
|
124
|
+
const preserveToolPolicy = settings.userFile.kind === "invalid";
|
|
125
|
+
const availableTools = preserveToolPolicy
|
|
126
|
+
? previousAvailableTools
|
|
127
|
+
: settings.kind === "loaded" && settings.settings.tools
|
|
115
128
|
? settings.settings.tools
|
|
116
|
-
:
|
|
117
|
-
configureChromeDevtoolsToolExposure(
|
|
129
|
+
: allChromeDevtoolsTools(ctx.sessionManager);
|
|
130
|
+
const provenanceWarning = configureChromeDevtoolsToolExposure(
|
|
131
|
+
pi,
|
|
132
|
+
availableTools,
|
|
133
|
+
ctx.model,
|
|
134
|
+
preserveToolPolicy ? previousMode : settings.kind === "loaded" ? settings.settings.toolMode : "codemode",
|
|
135
|
+
);
|
|
136
|
+
if (generation !== state.sessionGeneration) return;
|
|
137
|
+
if (provenanceWarning) ctx.ui.notify(provenanceWarning, "warning");
|
|
138
|
+
const active = new Set(pi.getActiveTools());
|
|
139
|
+
if (
|
|
140
|
+
chromeDevtoolsToolMode(pi) === "codemode" &&
|
|
141
|
+
!active.has("codemode") &&
|
|
142
|
+
availableChromeDevtoolsTools(pi).some((name) => !active.has(name))
|
|
143
|
+
) {
|
|
144
|
+
ctx.ui.notify(
|
|
145
|
+
"Chrome DevTools capabilities require Pi's codemode tool, but it is not active. Add '+codemode' to defaultTools and reload, or choose direct/lazy mode in Chrome DevTools settings. Chrome DevTools does not activate host tools automatically.",
|
|
146
|
+
"warning",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
118
149
|
});
|
|
119
150
|
|
|
120
151
|
pi.on("model_select", (event, ctx) => {
|
|
152
|
+
const generation = state.sessionGeneration;
|
|
121
153
|
invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools model and tool exposure changed");
|
|
122
154
|
if (!supportsNativeDeferredToolLoading(event.model)) {
|
|
123
|
-
requireEagerChromeDevtoolsToolExposure(pi);
|
|
155
|
+
const provenanceWarning = requireEagerChromeDevtoolsToolExposure(pi);
|
|
156
|
+
if (generation === state.sessionGeneration && provenanceWarning) ctx.ui.notify(provenanceWarning, "warning");
|
|
124
157
|
}
|
|
125
158
|
});
|
|
126
159
|
|