@narumitw/pi-chrome-devtools 0.50.1 → 0.52.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/README.md +107 -61
- package/package.json +1 -1
- package/src/browser-manager.ts +3 -5
- package/src/browser-settings-menu.ts +311 -0
- package/src/chrome-devtools.ts +42 -11
- package/src/lazy-tools.ts +143 -0
- package/src/menu.ts +36 -37
- package/src/runtime.ts +28 -23
- package/src/settings.ts +204 -38
- package/src/tool-selector.ts +45 -31
- package/src/tools.ts +0 -5
package/src/settings.ts
CHANGED
|
@@ -23,11 +23,36 @@ export interface ChromeDevToolsSettings {
|
|
|
23
23
|
updatedAt: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const DEFAULT_BROWSER_HOST = "127.0.0.1";
|
|
27
|
+
export const DEFAULT_BROWSER_PORT = 9222;
|
|
28
|
+
export const DEFAULT_BROWSER_ENDPOINT = `http://${DEFAULT_BROWSER_HOST}:${DEFAULT_BROWSER_PORT}`;
|
|
29
|
+
|
|
26
30
|
export type BrowserSettingsSource = "default" | "environment" | "project" | "user";
|
|
27
31
|
|
|
32
|
+
export interface UserBrowserSettings {
|
|
33
|
+
endpoint?: string;
|
|
34
|
+
autoLaunch?: boolean;
|
|
35
|
+
executablePath?: string;
|
|
36
|
+
extensionPaths?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BrowserSettingsPatch {
|
|
40
|
+
endpoint?: string | null;
|
|
41
|
+
autoLaunch?: boolean | null;
|
|
42
|
+
executablePath?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
export interface EffectiveBrowserSettings {
|
|
46
|
+
endpoint: string;
|
|
47
|
+
host: string;
|
|
48
|
+
port: number;
|
|
49
|
+
hostConfigured: boolean;
|
|
50
|
+
portConfigured: boolean;
|
|
51
|
+
autoLaunchEnabled: boolean;
|
|
29
52
|
executablePath?: string;
|
|
30
53
|
extensionPaths: string[];
|
|
54
|
+
endpointSource: BrowserSettingsSource;
|
|
55
|
+
autoLaunchSource: BrowserSettingsSource;
|
|
31
56
|
executablePathSource: BrowserSettingsSource;
|
|
32
57
|
extensionPathsSource: BrowserSettingsSource;
|
|
33
58
|
}
|
|
@@ -43,9 +68,14 @@ export interface SettingsLoadOptions {
|
|
|
43
68
|
projectTrusted?: boolean;
|
|
44
69
|
}
|
|
45
70
|
|
|
71
|
+
export type UserSettingsFileStatus =
|
|
72
|
+
| { kind: "missing" | "valid" }
|
|
73
|
+
| { kind: "invalid"; reason: string };
|
|
74
|
+
|
|
46
75
|
interface SettingsLoadBase {
|
|
47
76
|
effectiveBrowser: EffectiveBrowserSettings;
|
|
48
77
|
paths: { user: string; project?: string };
|
|
78
|
+
userFile: UserSettingsFileStatus;
|
|
49
79
|
warnings: string[];
|
|
50
80
|
notice?: string;
|
|
51
81
|
}
|
|
@@ -60,15 +90,10 @@ export interface SettingsFileOperations {
|
|
|
60
90
|
rename(source: string, destination: string): Promise<void>;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
interface NormalizedBrowserSection {
|
|
64
|
-
executablePath?: string;
|
|
65
|
-
extensionPaths?: string[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
93
|
interface NormalizedSettingsDocument {
|
|
69
94
|
tools?: ChromeDevToolsToolName[];
|
|
70
95
|
updatedAt?: number;
|
|
71
|
-
browser?:
|
|
96
|
+
browser?: UserBrowserSettings;
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
interface SettingsDocumentResult {
|
|
@@ -86,6 +111,10 @@ const DEFAULT_FILE_OPERATIONS: SettingsFileOperations = {
|
|
|
86
111
|
|
|
87
112
|
let settingsSaveQueue = Promise.resolve();
|
|
88
113
|
|
|
114
|
+
export async function waitForSettingsWrites() {
|
|
115
|
+
await settingsSaveQueue;
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
export async function loadSettings(options: SettingsLoadOptions = {}): Promise<SettingsLoadResult> {
|
|
90
119
|
await settingsSaveQueue;
|
|
91
120
|
const userPath = settingsFilePath();
|
|
@@ -127,10 +156,11 @@ export async function loadSettings(options: SettingsLoadOptions = {}): Promise<S
|
|
|
127
156
|
warnings.push(...project.warnings);
|
|
128
157
|
}
|
|
129
158
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
const environmentWarning = deprecatedEnvironmentWarning();
|
|
160
|
+
if (environmentWarning) warnings.push(environmentWarning);
|
|
161
|
+
|
|
162
|
+
const userBrowser = user.normalized?.browser ?? {};
|
|
163
|
+
const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
|
|
134
164
|
const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
|
|
135
165
|
const recognized =
|
|
136
166
|
settings.tools !== undefined ||
|
|
@@ -140,9 +170,17 @@ export async function loadSettings(options: SettingsLoadOptions = {}): Promise<S
|
|
|
140
170
|
.filter((result) => result.kind === "invalid")
|
|
141
171
|
.map((result) => result.reason)
|
|
142
172
|
.filter((reason): reason is string => Boolean(reason));
|
|
173
|
+
const userFile: UserSettingsFileStatus =
|
|
174
|
+
user.kind === "invalid"
|
|
175
|
+
? {
|
|
176
|
+
kind: "invalid",
|
|
177
|
+
reason: user.reason ?? `${userPath}: invalid settings`,
|
|
178
|
+
}
|
|
179
|
+
: { kind: user.kind };
|
|
143
180
|
const base = {
|
|
144
181
|
effectiveBrowser,
|
|
145
182
|
paths,
|
|
183
|
+
userFile,
|
|
146
184
|
warnings,
|
|
147
185
|
...(warnings.length > 0 ? { notice: warnings.join("\n") } : {}),
|
|
148
186
|
};
|
|
@@ -166,28 +204,69 @@ function resolveSettings(
|
|
|
166
204
|
}
|
|
167
205
|
|
|
168
206
|
function resolveEffectiveBrowser(
|
|
169
|
-
user:
|
|
170
|
-
project:
|
|
207
|
+
user: UserBrowserSettings,
|
|
208
|
+
project: UserBrowserSettings | undefined,
|
|
171
209
|
): EffectiveBrowserSettings {
|
|
210
|
+
const configuredEndpoint = parseBrowserEndpoint(user.endpoint ?? DEFAULT_BROWSER_ENDPOINT);
|
|
211
|
+
const environmentHost = process.env.PI_CHROME_DEVTOOLS_HOST;
|
|
212
|
+
const environmentPortValue = process.env.PI_CHROME_DEVTOOLS_PORT;
|
|
213
|
+
const environmentPort = parseConfiguredPort(environmentPortValue);
|
|
214
|
+
const environmentEndpointConfigured =
|
|
215
|
+
environmentHost !== undefined || environmentPort !== undefined;
|
|
216
|
+
const host = environmentHost ?? configuredEndpoint.host;
|
|
217
|
+
const port = environmentPort ?? configuredEndpoint.port;
|
|
218
|
+
const environmentAutoLaunch = process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH;
|
|
172
219
|
const environmentExecutable = process.env.PI_CHROME_DEVTOOLS_BROWSER;
|
|
173
|
-
const executablePath = environmentExecutable || user
|
|
174
|
-
const extensionPaths = project?.extensionPaths ?? user
|
|
220
|
+
const executablePath = environmentExecutable || user.executablePath;
|
|
221
|
+
const extensionPaths = project?.extensionPaths ?? user.extensionPaths ?? [];
|
|
222
|
+
const endpointSource: BrowserSettingsSource = environmentEndpointConfigured
|
|
223
|
+
? "environment"
|
|
224
|
+
: user.endpoint
|
|
225
|
+
? "user"
|
|
226
|
+
: "default";
|
|
175
227
|
return {
|
|
228
|
+
endpoint: formatBrowserEndpoint(host, port),
|
|
229
|
+
host,
|
|
230
|
+
port,
|
|
231
|
+
hostConfigured: environmentHost !== undefined || user.endpoint !== undefined,
|
|
232
|
+
portConfigured: environmentPort !== undefined || user.endpoint !== undefined,
|
|
233
|
+
autoLaunchEnabled:
|
|
234
|
+
environmentAutoLaunch === undefined
|
|
235
|
+
? (user.autoLaunch ?? true)
|
|
236
|
+
: environmentAutoLaunch !== "0",
|
|
176
237
|
...(executablePath ? { executablePath } : {}),
|
|
177
238
|
extensionPaths: [...extensionPaths],
|
|
239
|
+
endpointSource,
|
|
240
|
+
autoLaunchSource:
|
|
241
|
+
environmentAutoLaunch !== undefined
|
|
242
|
+
? "environment"
|
|
243
|
+
: user.autoLaunch !== undefined
|
|
244
|
+
? "user"
|
|
245
|
+
: "default",
|
|
178
246
|
executablePathSource: environmentExecutable
|
|
179
247
|
? "environment"
|
|
180
|
-
: user
|
|
248
|
+
: user.executablePath
|
|
181
249
|
? "user"
|
|
182
250
|
: "default",
|
|
183
251
|
extensionPathsSource: project?.extensionPaths
|
|
184
252
|
? "project"
|
|
185
|
-
: user
|
|
253
|
+
: user.extensionPaths
|
|
186
254
|
? "user"
|
|
187
255
|
: "default",
|
|
188
256
|
};
|
|
189
257
|
}
|
|
190
258
|
|
|
259
|
+
function deprecatedEnvironmentWarning() {
|
|
260
|
+
const configuredNames = [
|
|
261
|
+
"PI_CHROME_DEVTOOLS_HOST",
|
|
262
|
+
"PI_CHROME_DEVTOOLS_PORT",
|
|
263
|
+
"PI_CHROME_DEVTOOLS_AUTO_LAUNCH",
|
|
264
|
+
"PI_CHROME_DEVTOOLS_BROWSER",
|
|
265
|
+
].filter((name) => process.env[name] !== undefined);
|
|
266
|
+
if (configuredNames.length === 0) return undefined;
|
|
267
|
+
return `Chrome DevTools environment settings are deprecated and will be removed in a future version: ${configuredNames.join(", ")}. Move them to browser.endpoint, browser.autoLaunch, and browser.executablePath in ${settingsFilePath()}. Environment values remain active overrides during the deprecation period.`;
|
|
268
|
+
}
|
|
269
|
+
|
|
191
270
|
async function readSettingsDocument(
|
|
192
271
|
filePath: string,
|
|
193
272
|
scope: "project" | "user",
|
|
@@ -214,7 +293,7 @@ async function readSettingsDocument(
|
|
|
214
293
|
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
215
294
|
}
|
|
216
295
|
|
|
217
|
-
const scopeWarnings =
|
|
296
|
+
const scopeWarnings = projectMachineSettingsWarnings(parsed, scope, filePath);
|
|
218
297
|
try {
|
|
219
298
|
const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
|
|
220
299
|
return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
|
|
@@ -228,21 +307,19 @@ async function readSettingsDocument(
|
|
|
228
307
|
}
|
|
229
308
|
}
|
|
230
309
|
|
|
231
|
-
function
|
|
310
|
+
function projectMachineSettingsWarnings(
|
|
232
311
|
document: Record<string, unknown>,
|
|
233
312
|
scope: "project" | "user",
|
|
234
313
|
filePath: string,
|
|
235
314
|
) {
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
`Chrome DevTools project browser.executablePath ignored in ${filePath}; configure the machine-owned executable in ${settingsFilePath()}.`,
|
|
245
|
-
];
|
|
315
|
+
if (scope !== "project" || !isRecord(document.browser)) return [];
|
|
316
|
+
const browser = document.browser;
|
|
317
|
+
return ["endpoint", "autoLaunch", "executablePath"]
|
|
318
|
+
.filter((field) => browser[field] !== undefined)
|
|
319
|
+
.map(
|
|
320
|
+
(field) =>
|
|
321
|
+
`Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`,
|
|
322
|
+
);
|
|
246
323
|
}
|
|
247
324
|
|
|
248
325
|
async function normalizeSettingsDocument(
|
|
@@ -277,8 +354,20 @@ async function normalizeBrowserSection(
|
|
|
277
354
|
scope: "project" | "user",
|
|
278
355
|
_filePath: string,
|
|
279
356
|
cwd?: string,
|
|
280
|
-
): Promise<
|
|
281
|
-
const normalized:
|
|
357
|
+
): Promise<UserBrowserSettings> {
|
|
358
|
+
const normalized: UserBrowserSettings = {};
|
|
359
|
+
if (scope === "user" && browser.endpoint !== undefined) {
|
|
360
|
+
if (typeof browser.endpoint !== "string") {
|
|
361
|
+
throw new Error("expected browser.endpoint to be an HTTP URL with an explicit port");
|
|
362
|
+
}
|
|
363
|
+
normalized.endpoint = parseBrowserEndpoint(browser.endpoint).endpoint;
|
|
364
|
+
}
|
|
365
|
+
if (scope === "user" && browser.autoLaunch !== undefined) {
|
|
366
|
+
if (typeof browser.autoLaunch !== "boolean") {
|
|
367
|
+
throw new Error("expected browser.autoLaunch to be a boolean");
|
|
368
|
+
}
|
|
369
|
+
normalized.autoLaunch = browser.autoLaunch;
|
|
370
|
+
}
|
|
282
371
|
if (scope === "user" && browser.executablePath !== undefined) {
|
|
283
372
|
if (typeof browser.executablePath !== "string" || browser.executablePath.length === 0) {
|
|
284
373
|
throw new Error("expected browser.executablePath to be a non-empty absolute path");
|
|
@@ -350,6 +439,52 @@ async function validateUnpackedExtensionPath(extensionPath: string) {
|
|
|
350
439
|
return canonicalPath;
|
|
351
440
|
}
|
|
352
441
|
|
|
442
|
+
export function parseBrowserEndpoint(value: string) {
|
|
443
|
+
const trimmedValue = value.trim();
|
|
444
|
+
const explicitPort = trimmedValue.match(/^http:\/\/(?:\[[^\]]+\]|[^/:?#]+):(\d+)\/?$/i)?.[1];
|
|
445
|
+
let endpoint: URL;
|
|
446
|
+
try {
|
|
447
|
+
endpoint = new URL(trimmedValue);
|
|
448
|
+
} catch {
|
|
449
|
+
throw new Error("browser.endpoint must be a valid HTTP URL with an explicit port");
|
|
450
|
+
}
|
|
451
|
+
if (
|
|
452
|
+
endpoint.protocol !== "http:" ||
|
|
453
|
+
endpoint.username ||
|
|
454
|
+
endpoint.password ||
|
|
455
|
+
!endpoint.hostname ||
|
|
456
|
+
!explicitPort ||
|
|
457
|
+
(endpoint.pathname !== "/" && endpoint.pathname !== "") ||
|
|
458
|
+
endpoint.search ||
|
|
459
|
+
endpoint.hash
|
|
460
|
+
) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
"browser.endpoint must be an HTTP origin with an explicit port and no credentials, path, query, or fragment",
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const port = parseConfiguredPort(explicitPort);
|
|
466
|
+
if (port === undefined) {
|
|
467
|
+
throw new Error("browser.endpoint must use a port from 1 through 65535");
|
|
468
|
+
}
|
|
469
|
+
const host = endpoint.hostname;
|
|
470
|
+
return { endpoint: formatBrowserEndpoint(host, port), host, port };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function parseConfiguredPort(value: string | undefined) {
|
|
474
|
+
if (value === undefined) return undefined;
|
|
475
|
+
const trimmedValue = value.trim();
|
|
476
|
+
if (!/^\d+$/.test(trimmedValue)) return undefined;
|
|
477
|
+
const port = Number(trimmedValue);
|
|
478
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) return undefined;
|
|
479
|
+
return port;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function formatBrowserEndpoint(host: string, port: number) {
|
|
483
|
+
const formattedHost =
|
|
484
|
+
host.startsWith("[") && host.endsWith("]") ? host : host.includes(":") ? `[${host}]` : host;
|
|
485
|
+
return `http://${formattedHost}:${port}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
353
488
|
export function normalizeChromeDevtoolsSettings(
|
|
354
489
|
value: unknown,
|
|
355
490
|
): ChromeDevToolsSettings | undefined {
|
|
@@ -381,13 +516,48 @@ export function saveSettings(
|
|
|
381
516
|
settings: ChromeDevToolsSettings,
|
|
382
517
|
operations: Partial<SettingsFileOperations> = {},
|
|
383
518
|
): Promise<void> {
|
|
384
|
-
|
|
519
|
+
return queueSettingsMutation(
|
|
520
|
+
(current) => ({
|
|
521
|
+
...current,
|
|
522
|
+
tools: [...settings.tools],
|
|
523
|
+
updatedAt: settings.updatedAt,
|
|
524
|
+
}),
|
|
525
|
+
operations,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function saveBrowserSettings(
|
|
530
|
+
patch: BrowserSettingsPatch,
|
|
531
|
+
operations: Partial<SettingsFileOperations> = {},
|
|
532
|
+
): Promise<void> {
|
|
533
|
+
return queueSettingsMutation(async (current) => {
|
|
534
|
+
const browser = isRecord(current.browser) ? { ...current.browser } : {};
|
|
535
|
+
for (const field of ["endpoint", "autoLaunch", "executablePath"] as const) {
|
|
536
|
+
const value = patch[field];
|
|
537
|
+
if (value === undefined) continue;
|
|
538
|
+
if (value === null) delete browser[field];
|
|
539
|
+
else browser[field] = value;
|
|
540
|
+
}
|
|
541
|
+
await normalizeBrowserSection(browser, "user", settingsFilePath());
|
|
542
|
+
return { ...current, browser };
|
|
543
|
+
}, operations);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function queueSettingsMutation(
|
|
547
|
+
mutate: (
|
|
548
|
+
current: Record<string, unknown>,
|
|
549
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>,
|
|
550
|
+
operations: Partial<SettingsFileOperations>,
|
|
551
|
+
) {
|
|
552
|
+
const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations));
|
|
385
553
|
settingsSaveQueue = operation.catch(() => undefined);
|
|
386
554
|
return operation;
|
|
387
555
|
}
|
|
388
556
|
|
|
389
|
-
async function
|
|
390
|
-
|
|
557
|
+
async function saveSettingsMutationNow(
|
|
558
|
+
mutate: (
|
|
559
|
+
current: Record<string, unknown>,
|
|
560
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>,
|
|
391
561
|
operations: Partial<SettingsFileOperations>,
|
|
392
562
|
): Promise<void> {
|
|
393
563
|
const filePath = settingsFilePath();
|
|
@@ -397,11 +567,7 @@ async function saveSettingsNow(
|
|
|
397
567
|
if (current.kind === "invalid") {
|
|
398
568
|
throw new Error(`Cannot save Chrome DevTools settings until you repair ${current.reason}`);
|
|
399
569
|
}
|
|
400
|
-
const nextDocument = {
|
|
401
|
-
...(current.document ?? {}),
|
|
402
|
-
tools: [...settings.tools],
|
|
403
|
-
updatedAt: settings.updatedAt,
|
|
404
|
-
};
|
|
570
|
+
const nextDocument = await mutate(current.document ?? {});
|
|
405
571
|
await mkdir(dirname(filePath), { recursive: true });
|
|
406
572
|
const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
407
573
|
try {
|
package/src/tool-selector.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
launchHint,
|
|
11
11
|
launchModeLabel,
|
|
12
12
|
} from "./browser-manager.js";
|
|
13
|
+
import {
|
|
14
|
+
applyAvailableChromeDevtoolsTools,
|
|
15
|
+
availableChromeDevtoolsTools,
|
|
16
|
+
CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
17
|
+
} from "./lazy-tools.js";
|
|
13
18
|
import { state } from "./runtime.js";
|
|
14
19
|
import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
|
|
15
20
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
@@ -25,8 +30,9 @@ function formatError(error: unknown) {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
interface ToolStatusSummary {
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
availabilityStatus: "enabled" | "disabled" | "partial";
|
|
34
|
+
availableChromeToolCount: number;
|
|
35
|
+
loadedChromeToolCount: number;
|
|
30
36
|
activeNonChromeToolCount: number;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -43,7 +49,7 @@ export async function updateChromeDevtoolsTools(
|
|
|
43
49
|
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
44
50
|
const status = await buildToolStatusMessage(pi);
|
|
45
51
|
if (generation !== state.sessionGeneration) return;
|
|
46
|
-
ctx.ui.notify(`Chrome DevTools
|
|
52
|
+
ctx.ui.notify(`Chrome DevTools lazy catalog ${action}.\n\n${status}`, "info");
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export async function setSelectedChromeDevtoolsTools(
|
|
@@ -92,7 +98,7 @@ async function transactSelectedToolsNow(
|
|
|
92
98
|
expectedActiveTools?: readonly ChromeDevToolsToolName[],
|
|
93
99
|
): Promise<ToolSelectionSaveResult> {
|
|
94
100
|
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
95
|
-
if (expectedActiveTools && !arraysEqual(
|
|
101
|
+
if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
|
|
96
102
|
ctx.ui.notify(
|
|
97
103
|
"Browser tool selection changed while review was open. Review the current state, then apply again.",
|
|
98
104
|
"warning",
|
|
@@ -100,6 +106,7 @@ async function transactSelectedToolsNow(
|
|
|
100
106
|
return "active-tools-changed";
|
|
101
107
|
}
|
|
102
108
|
const previousActiveTools = pi.getActiveTools();
|
|
109
|
+
const previousAvailableTools = availableChromeDevtoolsTools(pi);
|
|
103
110
|
try {
|
|
104
111
|
applyChromeDevtoolsTools(pi, selectedTools);
|
|
105
112
|
await persistSettings(selectedTools);
|
|
@@ -107,10 +114,14 @@ async function transactSelectedToolsNow(
|
|
|
107
114
|
} catch (error) {
|
|
108
115
|
let rollbackError: unknown;
|
|
109
116
|
try {
|
|
110
|
-
|
|
117
|
+
applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
|
|
118
|
+
const currentNonChromeTools = pi
|
|
119
|
+
.getActiveTools()
|
|
120
|
+
.filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
|
|
121
|
+
const previousLoadedChromeTools = previousActiveTools.filter((name) =>
|
|
111
122
|
CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
|
|
112
|
-
)
|
|
113
|
-
|
|
123
|
+
);
|
|
124
|
+
pi.setActiveTools(unique([...currentNonChromeTools, ...previousLoadedChromeTools]));
|
|
114
125
|
} catch (caught) {
|
|
115
126
|
rollbackError = caught;
|
|
116
127
|
}
|
|
@@ -127,11 +138,6 @@ async function transactSelectedToolsNow(
|
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
function activeChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
131
|
-
const active = new Set(pi.getActiveTools());
|
|
132
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => active.has(toolName));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
141
|
function arraysEqual<T>(left: readonly T[], right: readonly T[]) {
|
|
136
142
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
137
143
|
}
|
|
@@ -140,29 +146,32 @@ export function applyChromeDevtoolsTools(
|
|
|
140
146
|
pi: ExtensionAPI,
|
|
141
147
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
142
148
|
) {
|
|
143
|
-
|
|
144
|
-
const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
145
|
-
const activeNonChromeToolNames = activeToolNames.filter((name) => !chromeToolNames.has(name));
|
|
146
|
-
pi.setActiveTools(unique([...activeNonChromeToolNames, ...selectedTools]));
|
|
149
|
+
applyAvailableChromeDevtoolsTools(pi, selectedTools);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
150
153
|
const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
151
154
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
152
|
-
const
|
|
155
|
+
const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
|
|
153
156
|
activeToolNames.has(name),
|
|
154
157
|
).length;
|
|
158
|
+
const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
|
|
155
159
|
const activeNonChromeToolCount = Array.from(activeToolNames).filter(
|
|
156
|
-
(name) => !chromeToolNames.has(name),
|
|
160
|
+
(name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
157
161
|
).length;
|
|
158
|
-
const
|
|
159
|
-
|
|
162
|
+
const availabilityStatus =
|
|
163
|
+
availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
160
164
|
? "enabled"
|
|
161
|
-
:
|
|
165
|
+
: availableChromeToolCount === 0
|
|
162
166
|
? "disabled"
|
|
163
167
|
: "partial";
|
|
164
168
|
|
|
165
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
availabilityStatus,
|
|
171
|
+
availableChromeToolCount,
|
|
172
|
+
loadedChromeToolCount,
|
|
173
|
+
activeNonChromeToolCount,
|
|
174
|
+
};
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
@@ -170,8 +179,10 @@ export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
|
170
179
|
const persistedSetting = await persistedSettingLabel();
|
|
171
180
|
return sanitizeChromeDevtoolsDisplay(
|
|
172
181
|
[
|
|
173
|
-
`Chrome DevTools tools: ${formatRuntimeStatus(summary)}`,
|
|
174
|
-
`
|
|
182
|
+
`Chrome DevTools tools available to lazy-load: ${formatRuntimeStatus(summary)}`,
|
|
183
|
+
`Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
184
|
+
`Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
|
|
185
|
+
`Persisted lazy catalog: ${persistedSetting}`,
|
|
175
186
|
...browserSettingsStatusLines(),
|
|
176
187
|
...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
|
|
177
188
|
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
@@ -257,10 +268,11 @@ function browserSettingsStatusLines() {
|
|
|
257
268
|
`Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`,
|
|
258
269
|
]
|
|
259
270
|
: []),
|
|
271
|
+
`Auto-launch: ${state.autoLaunchEnabled ? "on" : "off"} (${state.autoLaunchSource})`,
|
|
260
272
|
`Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
|
|
261
273
|
`Unpacked extensions (${state.extensionPathsSource}):`,
|
|
262
274
|
...extensionLines,
|
|
263
|
-
"
|
|
275
|
+
"Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
|
|
264
276
|
...(state.extensionPaths.length > 0
|
|
265
277
|
? [
|
|
266
278
|
"Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code.",
|
|
@@ -276,10 +288,11 @@ export function buildCommandGuide() {
|
|
|
276
288
|
"/chrome-devtools help — show command usage",
|
|
277
289
|
"/chrome-devtools quickstart — show endpoint and launch help",
|
|
278
290
|
"/chrome-devtools status — show tool and settings status",
|
|
279
|
-
"/chrome-devtools
|
|
291
|
+
"/chrome-devtools settings — edit browser connection settings",
|
|
292
|
+
"/chrome-devtools tools — choose tools available to lazy-load",
|
|
280
293
|
"/chrome-devtools toggle|select — compatibility aliases for tools",
|
|
281
|
-
"/chrome-devtools enable|on —
|
|
282
|
-
"/chrome-devtools disable|off —
|
|
294
|
+
"/chrome-devtools enable|on — make all Chrome DevTools tools available to lazy-load",
|
|
295
|
+
"/chrome-devtools disable|off — make all Chrome DevTools capability tools unavailable",
|
|
283
296
|
].join("\n");
|
|
284
297
|
}
|
|
285
298
|
|
|
@@ -292,7 +305,7 @@ export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevT
|
|
|
292
305
|
}
|
|
293
306
|
|
|
294
307
|
function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
295
|
-
return `${summary.
|
|
308
|
+
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
async function persistedSettingLabel() {
|
|
@@ -308,9 +321,10 @@ async function persistedSettingLabel() {
|
|
|
308
321
|
|
|
309
322
|
function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
|
|
310
323
|
if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
|
|
311
|
-
return `all
|
|
324
|
+
return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
312
325
|
}
|
|
313
|
-
if (tools.length === 0)
|
|
326
|
+
if (tools.length === 0)
|
|
327
|
+
return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
314
328
|
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
315
329
|
}
|
|
316
330
|
|
package/src/tools.ts
CHANGED
|
@@ -18,7 +18,6 @@ export const listPagesTool = defineTool({
|
|
|
18
18
|
name: CHROME_DEVTOOLS_TOOL_NAMES[0],
|
|
19
19
|
label: "Chrome DevTools: List Pages",
|
|
20
20
|
description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
|
|
21
|
-
promptSnippet: "List Chrome tabs/pages available over Chrome DevTools Protocol",
|
|
22
21
|
parameters: Type.Object({}),
|
|
23
22
|
renderCall: renderToolCall("list pages"),
|
|
24
23
|
renderResult: renderTextResult,
|
|
@@ -34,7 +33,6 @@ export const selectPageTool = defineTool({
|
|
|
34
33
|
name: CHROME_DEVTOOLS_TOOL_NAMES[1],
|
|
35
34
|
label: "Chrome DevTools: Select Page",
|
|
36
35
|
description: "Select the active Chrome page for later chrome_devtools_* tool calls.",
|
|
37
|
-
promptSnippet: "Select the Chrome tab/page to inspect or control",
|
|
38
36
|
parameters: Type.Object({
|
|
39
37
|
pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." }),
|
|
40
38
|
}),
|
|
@@ -56,7 +54,6 @@ export const navigateTool = defineTool({
|
|
|
56
54
|
label: "Chrome DevTools: Navigate",
|
|
57
55
|
description:
|
|
58
56
|
"Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.",
|
|
59
|
-
promptSnippet: "Navigate the selected or first Chrome tab to a URL, creating one if needed",
|
|
60
57
|
parameters: Type.Object({
|
|
61
58
|
url: Type.String({ description: "URL to navigate to." }),
|
|
62
59
|
pageId: Type.Optional(
|
|
@@ -88,7 +85,6 @@ export const evaluateTool = defineTool({
|
|
|
88
85
|
name: CHROME_DEVTOOLS_TOOL_NAMES[3],
|
|
89
86
|
label: "Chrome DevTools: Evaluate",
|
|
90
87
|
description: "Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.",
|
|
91
|
-
promptSnippet: "Evaluate JavaScript in the selected Chrome tab",
|
|
92
88
|
parameters: Type.Object({
|
|
93
89
|
expression: Type.String({ description: "JavaScript expression to evaluate." }),
|
|
94
90
|
pageId: Type.Optional(
|
|
@@ -121,7 +117,6 @@ export const screenshotTool = defineTool({
|
|
|
121
117
|
name: CHROME_DEVTOOLS_TOOL_NAMES[4],
|
|
122
118
|
label: "Chrome DevTools: Screenshot",
|
|
123
119
|
description: "Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.",
|
|
124
|
-
promptSnippet: "Capture a screenshot from the selected Chrome tab",
|
|
125
120
|
parameters: Type.Object({
|
|
126
121
|
pageId: Type.Optional(
|
|
127
122
|
Type.String({ description: "Optional page id. Defaults to selected or first page." }),
|