@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/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?: NormalizedBrowserSection;
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 effectiveBrowser = resolveEffectiveBrowser(
131
- user.normalized?.browser,
132
- project.normalized?.browser,
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: NormalizedBrowserSection | undefined,
170
- project: NormalizedBrowserSection | undefined,
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?.executablePath;
174
- const extensionPaths = project?.extensionPaths ?? user?.extensionPaths ?? [];
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?.executablePath
248
+ : user.executablePath
181
249
  ? "user"
182
250
  : "default",
183
251
  extensionPathsSource: project?.extensionPaths
184
252
  ? "project"
185
- : user?.extensionPaths
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 = projectExecutableWarnings(parsed, scope, filePath);
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 projectExecutableWarnings(
310
+ function projectMachineSettingsWarnings(
232
311
  document: Record<string, unknown>,
233
312
  scope: "project" | "user",
234
313
  filePath: string,
235
314
  ) {
236
- if (
237
- scope !== "project" ||
238
- !isRecord(document.browser) ||
239
- document.browser.executablePath === undefined
240
- ) {
241
- return [];
242
- }
243
- return [
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<NormalizedBrowserSection> {
281
- const normalized: NormalizedBrowserSection = {};
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
- const operation = settingsSaveQueue.then(() => saveSettingsNow(settings, operations));
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 saveSettingsNow(
390
- settings: ChromeDevToolsSettings,
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 {
@@ -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
- runtimeStatus: "enabled" | "disabled" | "partial";
29
- activeChromeToolCount: number;
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 tools ${action}.\n\n${status}`, "info");
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(activeChromeDevtoolsTools(pi), expectedActiveTools)) {
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
- const previousChromeTools = previousActiveTools.filter((name) =>
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
- ) as ChromeDevToolsToolName[];
113
- applyChromeDevtoolsTools(pi, previousChromeTools);
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
- const activeToolNames = pi.getActiveTools();
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 activeChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
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 runtimeStatus =
159
- activeChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
162
+ const availabilityStatus =
163
+ availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
160
164
  ? "enabled"
161
- : activeChromeToolCount === 0
165
+ : availableChromeToolCount === 0
162
166
  ? "disabled"
163
167
  : "partial";
164
168
 
165
- return { runtimeStatus, activeChromeToolCount, activeNonChromeToolCount };
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
- `Persisted selection: ${persistedSetting}`,
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
- "Settings changes apply to a new managed browser after /reload or session replacement.",
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 tools — select individual Chrome DevTools tools",
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 — enable all Chrome DevTools tools immediately",
282
- "/chrome-devtools disable|off — disable all Chrome DevTools tools immediately",
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.runtimeStatus} (${summary.activeChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} active)`;
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 enabled (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
324
+ return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
312
325
  }
313
- if (tools.length === 0) return `all disabled (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
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." }),