@narumitw/pi-chrome-devtools 0.53.1 → 0.53.3

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.
Files changed (41) hide show
  1. package/README.md +89 -230
  2. package/dist/chunks/{browser-settings-menu-REIU7C3Q.js → browser-settings-menu-SR7KLZT6.js} +16 -42
  3. package/dist/chunks/browser-settings-menu-SR7KLZT6.js.map +7 -0
  4. package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4KMN4SFF.js} +2 -2
  5. package/dist/chunks/chunk-4KMN4SFF.js.map +7 -0
  6. package/dist/chunks/{chunk-SRT3UUIP.js → chunk-F5VG35UI.js} +23 -74
  7. package/dist/chunks/chunk-F5VG35UI.js.map +7 -0
  8. package/dist/chunks/{chunk-YJVYJ2L2.js → chunk-MZCGM6D3.js} +43 -48
  9. package/dist/chunks/chunk-MZCGM6D3.js.map +7 -0
  10. package/dist/chunks/{menu-H5HMVGHI.js → menu-YR4VRT7D.js} +10 -23
  11. package/dist/chunks/menu-YR4VRT7D.js.map +7 -0
  12. package/dist/chunks/{tools-6P7EUX37.js → tools-6R7Z6HWJ.js} +17 -59
  13. package/dist/chunks/tools-6R7Z6HWJ.js.map +7 -0
  14. package/dist/index.ts +23 -48
  15. package/dist/index.ts.map +2 -2
  16. package/docs/browser-setup.md +147 -0
  17. package/package.json +54 -55
  18. package/src/browser-manager.ts +872 -942
  19. package/src/browser-settings-menu.ts +315 -350
  20. package/src/cdp-client.ts +381 -398
  21. package/src/chrome-devtools.ts +205 -232
  22. package/src/display.ts +35 -0
  23. package/src/lazy-tools.ts +158 -181
  24. package/src/menu.ts +409 -446
  25. package/src/render.ts +74 -84
  26. package/src/runtime.ts +137 -140
  27. package/src/screenshot.ts +170 -186
  28. package/src/settings.ts +472 -523
  29. package/src/tool-names.ts +8 -14
  30. package/src/tool-selector.ts +245 -289
  31. package/src/tools.ts +218 -226
  32. package/src/webmcp/discovery.ts +184 -188
  33. package/src/webmcp/policy.ts +296 -329
  34. package/src/webmcp/protocol.ts +324 -337
  35. package/src/webmcp/tools.ts +136 -147
  36. package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +0 -7
  37. package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
  38. package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
  39. package/dist/chunks/chunk-YJVYJ2L2.js.map +0 -7
  40. package/dist/chunks/menu-H5HMVGHI.js.map +0 -7
  41. package/dist/chunks/tools-6P7EUX37.js.map +0 -7
@@ -1,380 +1,336 @@
1
- import { stripVTControlCharacters } from "node:util";
2
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
2
  import {
4
- browserCandidateHint,
5
- browserLifecycleState,
6
- browserSettingsForOwner,
7
- devToolsEndpoint,
8
- endpointConfigHint,
9
- endpointSourceLabel,
10
- launchAttemptLines,
11
- launchHint,
12
- launchModeLabel,
13
- managedBrowserForOwner,
3
+ browserCandidateHint,
4
+ browserLifecycleState,
5
+ browserSettingsForOwner,
6
+ devToolsEndpoint,
7
+ endpointConfigHint,
8
+ endpointSourceLabel,
9
+ launchAttemptLines,
10
+ launchHint,
11
+ launchModeLabel,
12
+ managedBrowserForOwner,
14
13
  } from "./browser-manager.js";
14
+ import { sanitizeChromeDevtoolsDisplay } from "./display.js";
15
15
  import {
16
- applyAvailableChromeDevtoolsTools,
17
- availableChromeDevtoolsTools,
18
- CHROME_DEVTOOLS_LOAD_TOOL_NAME,
19
- chromeDevtoolsToolExposureMode,
16
+ applyAvailableChromeDevtoolsTools,
17
+ availableChromeDevtoolsTools,
18
+ CHROME_DEVTOOLS_LOAD_TOOL_NAME,
19
+ chromeDevtoolsToolExposureMode,
20
20
  } from "./lazy-tools.js";
21
21
  import { invalidateWebMcpOperations, state, webMcpEnabled } from "./runtime.js";
22
22
  import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
23
23
  import {
24
- CHROME_DEVTOOLS_TOOL_NAMES,
25
- type ChromeDevToolsToolName,
26
- CORE_CHROME_DEVTOOLS_TOOL_NAMES,
27
- isWebMcpToolName,
24
+ CHROME_DEVTOOLS_TOOL_NAMES,
25
+ type ChromeDevToolsToolName,
26
+ CORE_CHROME_DEVTOOLS_TOOL_NAMES,
27
+ isWebMcpToolName,
28
28
  } from "./tool-names.js";
29
29
 
30
+ export { sanitizeChromeDevtoolsDisplay };
31
+
30
32
  type CommandContext = ExtensionCommandContext;
31
33
 
32
34
  function unique<T>(values: T[]) {
33
- return Array.from(new Set(values));
35
+ return Array.from(new Set(values));
34
36
  }
35
37
 
36
38
  function formatError(error: unknown) {
37
- return error instanceof Error ? error.message : String(error);
39
+ return error instanceof Error ? error.message : String(error);
38
40
  }
39
41
 
40
42
  interface ToolStatusSummary {
41
- availabilityStatus: "enabled" | "disabled" | "partial";
42
- availableChromeToolCount: number;
43
- loadedChromeToolCount: number;
44
- activeNonChromeToolCount: number;
45
- capabilityCount: number;
43
+ availabilityStatus: "enabled" | "disabled" | "partial";
44
+ availableChromeToolCount: number;
45
+ loadedChromeToolCount: number;
46
+ activeNonChromeToolCount: number;
47
+ capabilityCount: number;
46
48
  }
47
49
 
48
50
  type ToolSelectionSaveResult = "saved" | "active-tools-changed" | "failed";
49
51
 
50
52
  export async function updateChromeDevtoolsTools(
51
- pi: ExtensionAPI,
52
- ctx: CommandContext,
53
- selectedTools: readonly ChromeDevToolsToolName[],
54
- action: string,
53
+ pi: ExtensionAPI,
54
+ ctx: CommandContext,
55
+ selectedTools: readonly ChromeDevToolsToolName[],
56
+ action: string,
55
57
  ) {
56
- const generation = state.sessionGeneration;
57
- const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
58
- if (result !== "saved" || generation !== state.sessionGeneration) return;
59
- const status = await buildToolStatusMessage(pi, ctx.sessionManager);
60
- if (generation !== state.sessionGeneration) return;
61
- ctx.ui.notify(`Chrome DevTools tool catalog ${action}.\n\n${status}`, "info");
58
+ const generation = state.sessionGeneration;
59
+ const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
60
+ if (result !== "saved" || generation !== state.sessionGeneration) return;
61
+ const status = await buildToolStatusMessage(pi, ctx.sessionManager);
62
+ if (generation !== state.sessionGeneration) return;
63
+ ctx.ui.notify(`Chrome DevTools tool catalog ${action}.\n\n${status}`, "info");
62
64
  }
63
65
 
64
66
  export async function setSelectedChromeDevtoolsTools(
65
- pi: ExtensionAPI,
66
- ctx: CommandContext,
67
- selectedTools: readonly ChromeDevToolsToolName[],
68
- expectedActiveTools: readonly ChromeDevToolsToolName[],
67
+ pi: ExtensionAPI,
68
+ ctx: CommandContext,
69
+ selectedTools: readonly ChromeDevToolsToolName[],
70
+ expectedActiveTools: readonly ChromeDevToolsToolName[],
69
71
  ): Promise<ToolSelectionSaveResult> {
70
- return transactSelectedTools(
71
- pi,
72
- ctx,
73
- selectedTools,
74
- state.sessionGeneration,
75
- expectedActiveTools,
76
- );
72
+ return transactSelectedTools(pi, ctx, selectedTools, state.sessionGeneration, expectedActiveTools);
77
73
  }
78
74
 
79
75
  let toolTransactionQueue = Promise.resolve();
80
76
 
81
77
  export async function waitForChromeDevtoolsSettings(): Promise<void> {
82
- await toolTransactionQueue;
78
+ await toolTransactionQueue;
83
79
  }
84
80
 
85
81
  function transactSelectedTools(
86
- pi: ExtensionAPI,
87
- ctx: CommandContext,
88
- selectedTools: readonly ChromeDevToolsToolName[],
89
- expectedGeneration: number,
90
- expectedActiveTools?: readonly ChromeDevToolsToolName[],
82
+ pi: ExtensionAPI,
83
+ ctx: CommandContext,
84
+ selectedTools: readonly ChromeDevToolsToolName[],
85
+ expectedGeneration: number,
86
+ expectedActiveTools?: readonly ChromeDevToolsToolName[],
91
87
  ): Promise<ToolSelectionSaveResult> {
92
- const operation = toolTransactionQueue.then(() =>
93
- transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools),
94
- );
95
- toolTransactionQueue = operation.then(
96
- () => undefined,
97
- () => undefined,
98
- );
99
- return operation;
88
+ const operation = toolTransactionQueue.then(() =>
89
+ transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools),
90
+ );
91
+ toolTransactionQueue = operation.then(
92
+ () => undefined,
93
+ () => undefined,
94
+ );
95
+ return operation;
100
96
  }
101
97
 
102
98
  async function transactSelectedToolsNow(
103
- pi: ExtensionAPI,
104
- ctx: CommandContext,
105
- selectedTools: readonly ChromeDevToolsToolName[],
106
- expectedGeneration: number,
107
- expectedActiveTools?: readonly ChromeDevToolsToolName[],
99
+ pi: ExtensionAPI,
100
+ ctx: CommandContext,
101
+ selectedTools: readonly ChromeDevToolsToolName[],
102
+ expectedGeneration: number,
103
+ expectedActiveTools?: readonly ChromeDevToolsToolName[],
108
104
  ): Promise<ToolSelectionSaveResult> {
109
- if (expectedGeneration !== state.sessionGeneration) return "failed";
110
- if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
111
- ctx.ui.notify(
112
- "Browser tool selection changed while review was open. Review the current state, then apply again.",
113
- "warning",
114
- );
115
- return "active-tools-changed";
116
- }
117
- const previousActiveTools = pi.getActiveTools();
118
- const previousAvailableTools = availableChromeDevtoolsTools(pi);
119
- try {
120
- const previousWebMcpTools = previousAvailableTools.filter(isWebMcpToolName);
121
- const selectedWebMcpTools = selectedTools.filter(isWebMcpToolName);
122
- if (!arraysEqual(previousWebMcpTools, selectedWebMcpTools)) {
123
- invalidateWebMcpOperations(
124
- ctx.sessionManager,
125
- "Chrome DevTools WebMCP gateway availability changed",
126
- );
127
- }
128
- applyChromeDevtoolsTools(pi, selectedTools);
129
- await persistSettings(selectedTools);
130
- return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
131
- } catch (error) {
132
- let rollbackError: unknown;
133
- try {
134
- applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
135
- const currentNonChromeTools = pi
136
- .getActiveTools()
137
- .filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
138
- const previousLoadedChromeTools = previousActiveTools.filter((name) =>
139
- CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
140
- );
141
- const restoredChromeTools =
142
- chromeDevtoolsToolExposureMode(pi) === "eager"
143
- ? previousAvailableTools
144
- : previousLoadedChromeTools;
145
- pi.setActiveTools(unique([...currentNonChromeTools, ...restoredChromeTools]));
146
- } catch (caught) {
147
- rollbackError = caught;
148
- }
149
- if (expectedGeneration !== state.sessionGeneration) return "failed";
150
- ctx.ui.notify(
151
- sanitizeChromeDevtoolsDisplay(
152
- rollbackError
153
- ? `Chrome DevTools settings save failed: ${formatError(error)}; active-tool rollback failed: ${formatError(rollbackError)}`
154
- : `Chrome DevTools settings save failed; active tools restored: ${formatError(error)}`,
155
- ),
156
- "warning",
157
- );
158
- return "failed";
159
- }
105
+ if (expectedGeneration !== state.sessionGeneration) return "failed";
106
+ if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
107
+ ctx.ui.notify(
108
+ "Browser tool selection changed while review was open. Review the current state, then apply again.",
109
+ "warning",
110
+ );
111
+ return "active-tools-changed";
112
+ }
113
+ const previousActiveTools = pi.getActiveTools();
114
+ const previousAvailableTools = availableChromeDevtoolsTools(pi);
115
+ try {
116
+ const previousWebMcpTools = previousAvailableTools.filter(isWebMcpToolName);
117
+ const selectedWebMcpTools = selectedTools.filter(isWebMcpToolName);
118
+ if (!arraysEqual(previousWebMcpTools, selectedWebMcpTools)) {
119
+ invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools WebMCP gateway availability changed");
120
+ }
121
+ applyChromeDevtoolsTools(pi, selectedTools);
122
+ await persistSettings(selectedTools);
123
+ return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
124
+ } catch (error) {
125
+ let rollbackError: unknown;
126
+ try {
127
+ applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
128
+ const currentNonChromeTools = pi
129
+ .getActiveTools()
130
+ .filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
131
+ const previousLoadedChromeTools = previousActiveTools.filter((name) =>
132
+ CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
133
+ );
134
+ const restoredChromeTools =
135
+ chromeDevtoolsToolExposureMode(pi) === "eager" ? previousAvailableTools : previousLoadedChromeTools;
136
+ pi.setActiveTools(unique([...currentNonChromeTools, ...restoredChromeTools]));
137
+ } catch (caught) {
138
+ rollbackError = caught;
139
+ }
140
+ if (expectedGeneration !== state.sessionGeneration) return "failed";
141
+ ctx.ui.notify(
142
+ sanitizeChromeDevtoolsDisplay(
143
+ rollbackError
144
+ ? `Chrome DevTools settings save failed: ${formatError(error)}; active-tool rollback failed: ${formatError(rollbackError)}`
145
+ : `Chrome DevTools settings save failed; active tools restored: ${formatError(error)}`,
146
+ ),
147
+ "warning",
148
+ );
149
+ return "failed";
150
+ }
160
151
  }
161
152
 
162
153
  function arraysEqual<T>(left: readonly T[], right: readonly T[]) {
163
- return left.length === right.length && left.every((value, index) => value === right[index]);
154
+ return left.length === right.length && left.every((value, index) => value === right[index]);
164
155
  }
165
156
 
166
- export function applyChromeDevtoolsTools(
167
- pi: ExtensionAPI,
168
- selectedTools: readonly ChromeDevToolsToolName[],
169
- ) {
170
- applyAvailableChromeDevtoolsTools(pi, selectedTools);
157
+ export function applyChromeDevtoolsTools(pi: ExtensionAPI, selectedTools: readonly ChromeDevToolsToolName[]) {
158
+ applyAvailableChromeDevtoolsTools(pi, selectedTools);
171
159
  }
172
160
 
173
161
  function getToolStatusSummary(pi: ExtensionAPI, owner: object): ToolStatusSummary {
174
- const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
175
- const activeToolNames = new Set(pi.getActiveTools());
176
- const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
177
- activeToolNames.has(name),
178
- ).length;
179
- const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
180
- const activeNonChromeToolCount = Array.from(activeToolNames).filter(
181
- (name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
182
- ).length;
183
- const capabilityCount = effectiveCatalog(owner).length;
184
- const availabilityStatus =
185
- availableChromeToolCount === capabilityCount
186
- ? "enabled"
187
- : availableChromeToolCount === 0
188
- ? "disabled"
189
- : "partial";
162
+ const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
163
+ const activeToolNames = new Set(pi.getActiveTools());
164
+ const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => activeToolNames.has(name)).length;
165
+ const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
166
+ const activeNonChromeToolCount = Array.from(activeToolNames).filter(
167
+ (name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
168
+ ).length;
169
+ const capabilityCount = effectiveCatalog(owner).length;
170
+ const availabilityStatus =
171
+ availableChromeToolCount === capabilityCount ? "enabled" : availableChromeToolCount === 0 ? "disabled" : "partial";
190
172
 
191
- return {
192
- availabilityStatus,
193
- availableChromeToolCount,
194
- loadedChromeToolCount,
195
- activeNonChromeToolCount,
196
- capabilityCount,
197
- };
173
+ return {
174
+ availabilityStatus,
175
+ availableChromeToolCount,
176
+ loadedChromeToolCount,
177
+ activeNonChromeToolCount,
178
+ capabilityCount,
179
+ };
198
180
  }
199
181
 
200
182
  export async function buildToolStatusMessage(pi: ExtensionAPI, owner: object) {
201
- const summary = getToolStatusSummary(pi, owner);
202
- const persistedSetting = await persistedSettingLabel();
203
- return sanitizeChromeDevtoolsDisplay(
204
- [
205
- `Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
206
- `Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
207
- `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${summary.capabilityCount}`,
208
- `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
209
- `Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
210
- `Persisted tool catalog: ${persistedSetting}`,
211
- ...browserSettingsStatusLines(owner),
212
- ...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
213
- `Other active tools preserved: ${summary.activeNonChromeToolCount}`,
214
- `Endpoint: ${devToolsEndpoint(owner)}`,
215
- `Endpoint source: ${endpointSourceLabel(owner)}`,
216
- `Launch mode: ${launchModeLabel(owner)}`,
217
- ...launchAttemptLines(owner),
218
- ].join("\n"),
219
- );
183
+ const summary = getToolStatusSummary(pi, owner);
184
+ const persistedSetting = await persistedSettingLabel();
185
+ return sanitizeChromeDevtoolsDisplay(
186
+ [
187
+ `Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
188
+ `Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
189
+ `Loaded capability tools this session: ${summary.loadedChromeToolCount}/${summary.capabilityCount}`,
190
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
191
+ `Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
192
+ `Persisted tool catalog: ${persistedSetting}`,
193
+ ...browserSettingsStatusLines(owner),
194
+ ...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
195
+ `Other active tools preserved: ${summary.activeNonChromeToolCount}`,
196
+ `Endpoint: ${devToolsEndpoint(owner)}`,
197
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
198
+ `Launch mode: ${launchModeLabel(owner)}`,
199
+ ...launchAttemptLines(owner),
200
+ ].join("\n"),
201
+ );
220
202
  }
221
203
 
222
204
  export function buildQuickstartMessage(owner: object) {
223
- return buildSettingsSetupMessage(owner);
205
+ return buildSettingsSetupMessage(owner);
224
206
  }
225
207
 
226
208
  export function buildBrowserStatusMessage(owner?: object) {
227
- const browser = browserSettingsForOwner(owner);
228
- const lifecycle = browserLifecycleState(owner);
229
- const webMcpIsEnabled = owner ? webMcpEnabled(owner) : false;
230
- const browserState =
231
- lifecycle === "starting"
232
- ? "starting managed browser"
233
- : lifecycle === "running"
234
- ? "managed browser running"
235
- : lifecycle === "exited"
236
- ? "managed browser exited"
237
- : lifecycle === "failed"
238
- ? "last launch failed"
239
- : "not started; connection has not been checked";
240
- const needsRecovery = lifecycle === "exited" || lifecycle === "failed";
241
- return sanitizeChromeDevtoolsDisplay(
242
- [
243
- `Browser: ${browserState}`,
244
- "Viewing this status does not probe the endpoint or launch Chrome.",
245
- `Endpoint: ${devToolsEndpoint(owner)}`,
246
- `Endpoint source: ${endpointSourceLabel(owner)}`,
247
- `Launch mode: ${launchModeLabel(owner)}`,
248
- `Unpacked extensions: ${browser.extensionPaths.length} (${browser.extensionPathsSource})`,
249
- `WebMCP: ${webMcpIsEnabled ? "enabled · experimental" : "disabled · experimental"}`,
250
- ...(webMcpIsEnabled && !managedBrowserForOwner(owner)?.ready
251
- ? [
252
- "WebMCP warning: attached browser profiles may contain everyday authenticated sessions and sensitive state.",
253
- ]
254
- : []),
255
- ...(browser.extensionPaths.length > 0
256
- ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
257
- : []),
258
- ...launchAttemptLines(owner),
259
- ...(needsRecovery ? [launchHint(owner), endpointConfigHint()] : []),
260
- ].join("\n"),
261
- );
209
+ const browser = browserSettingsForOwner(owner);
210
+ const lifecycle = browserLifecycleState(owner);
211
+ const webMcpIsEnabled = owner ? webMcpEnabled(owner) : false;
212
+ const browserState =
213
+ lifecycle === "starting"
214
+ ? "starting managed browser"
215
+ : lifecycle === "running"
216
+ ? "managed browser running"
217
+ : lifecycle === "exited"
218
+ ? "managed browser exited"
219
+ : lifecycle === "failed"
220
+ ? "last launch failed"
221
+ : "not started; connection has not been checked";
222
+ const needsRecovery = lifecycle === "exited" || lifecycle === "failed";
223
+ return sanitizeChromeDevtoolsDisplay(
224
+ [
225
+ `Browser: ${browserState}`,
226
+ "Viewing this status does not probe the endpoint or launch Chrome.",
227
+ `Endpoint: ${devToolsEndpoint(owner)}`,
228
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
229
+ `Launch mode: ${launchModeLabel(owner)}`,
230
+ `Unpacked extensions: ${browser.extensionPaths.length} (${browser.extensionPathsSource})`,
231
+ `WebMCP: ${webMcpIsEnabled ? "enabled · experimental" : "disabled · experimental"}`,
232
+ ...(webMcpIsEnabled && !managedBrowserForOwner(owner)?.ready
233
+ ? ["WebMCP warning: attached browser profiles may contain everyday authenticated sessions and sensitive state."]
234
+ : []),
235
+ ...(browser.extensionPaths.length > 0
236
+ ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
237
+ : []),
238
+ ...launchAttemptLines(owner),
239
+ ...(needsRecovery ? [launchHint(owner), endpointConfigHint()] : []),
240
+ ].join("\n"),
241
+ );
262
242
  }
263
243
 
264
244
  export function buildSettingsSetupMessage(owner: object) {
265
- return sanitizeChromeDevtoolsDisplay(
266
- [
267
- `Chrome DevTools endpoint: ${devToolsEndpoint(owner)}`,
268
- `Endpoint source: ${endpointSourceLabel(owner)}`,
269
- `Launch mode: ${launchModeLabel(owner)}`,
270
- ...browserSettingsStatusLines(owner),
271
- launchHint(owner),
272
- browserCandidateHint(owner),
273
- ...launchAttemptLines(owner),
274
- endpointConfigHint(),
275
- ].join("\n"),
276
- );
277
- }
278
-
279
- export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_000) {
280
- const withoutBidi = stripVTControlCharacters(value).replace(
281
- /[\u202a-\u202e\u2066-\u2069]/gu,
282
- "�",
283
- );
284
- const sanitized = Array.from(withoutBidi, (character) => {
285
- const codePoint = character.codePointAt(0) ?? 0;
286
- const unsafeControl =
287
- (codePoint >= 0 && codePoint <= 8) ||
288
- (codePoint >= 11 && codePoint <= 31) ||
289
- (codePoint >= 127 && codePoint <= 159);
290
- return unsafeControl ? "�" : character;
291
- }).join("");
292
- if (sanitized.length <= maxCharacters) return sanitized;
293
- return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}…`;
245
+ return sanitizeChromeDevtoolsDisplay(
246
+ [
247
+ `Chrome DevTools endpoint: ${devToolsEndpoint(owner)}`,
248
+ `Endpoint source: ${endpointSourceLabel(owner)}`,
249
+ `Launch mode: ${launchModeLabel(owner)}`,
250
+ ...browserSettingsStatusLines(owner),
251
+ launchHint(owner),
252
+ browserCandidateHint(owner),
253
+ ...launchAttemptLines(owner),
254
+ endpointConfigHint(),
255
+ ].join("\n"),
256
+ );
294
257
  }
295
258
 
296
259
  function browserSettingsStatusLines(owner: object) {
297
- const browser = browserSettingsForOwner(owner);
298
- const extensionPaths = browser.extensionPaths;
299
- const extensionLines =
300
- extensionPaths.length > 0
301
- ? extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
302
- : [" - none"];
303
- return [
304
- `Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
305
- ...(state.projectSettingsFilePath
306
- ? [
307
- `Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`,
308
- ]
309
- : []),
310
- `Auto-launch: ${browser.autoLaunchEnabled ? "on" : "off"} (${browser.autoLaunchSource})`,
311
- `Browser executable: ${browser.executablePath ?? "automatic discovery"} (${browser.executablePathSource})`,
312
- `Unpacked extensions (${browser.extensionPathsSource}):`,
313
- ...extensionLines,
314
- `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · every call requires confirmation" : "disabled · experimental"}`,
315
- "Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
316
- ...(extensionPaths.length > 0
317
- ? [
318
- "Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code.",
319
- ]
320
- : []),
321
- ];
260
+ const browser = browserSettingsForOwner(owner);
261
+ const extensionPaths = browser.extensionPaths;
262
+ const extensionLines =
263
+ extensionPaths.length > 0 ? extensionPaths.map((extensionPath) => ` - ${extensionPath}`) : [" - none"];
264
+ return [
265
+ `Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
266
+ ...(state.projectSettingsFilePath
267
+ ? [
268
+ `Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`,
269
+ ]
270
+ : []),
271
+ `Auto-launch: ${browser.autoLaunchEnabled ? "on" : "off"} (${browser.autoLaunchSource})`,
272
+ `Browser executable: ${browser.executablePath ?? "automatic discovery"} (${browser.executablePathSource})`,
273
+ `Unpacked extensions (${browser.extensionPathsSource}):`,
274
+ ...extensionLines,
275
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · every call requires confirmation" : "disabled · experimental"}`,
276
+ "Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
277
+ ...(extensionPaths.length > 0
278
+ ? ["Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code."]
279
+ : []),
280
+ ];
322
281
  }
323
282
 
324
283
  export function buildCommandGuide(owner: object) {
325
- return [
326
- "Chrome DevTools commands:",
327
- `WebMCP: ${webMcpEnabled(owner) ? "enabled" : "disabled"} · experimental · every page-provided call requires confirmation`,
328
- "/chrome-devtools — open this menu",
329
- "/chrome-devtools help — show command usage",
330
- "/chrome-devtools quickstart — show endpoint and launch help",
331
- "/chrome-devtools status — show tool and settings status",
332
- "/chrome-devtools settings — edit browser connection settings",
333
- "/chrome-devtools tools — choose available Chrome DevTools tools",
334
- "/chrome-devtools toggle|select — compatibility aliases for tools",
335
- "/chrome-devtools enable|on — make all Chrome DevTools tools available",
336
- "/chrome-devtools disable|off — make all Chrome DevTools capability tools unavailable",
337
- ].join("\n");
284
+ return [
285
+ "Chrome DevTools commands:",
286
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled" : "disabled"} · experimental · every page-provided call requires confirmation`,
287
+ "/chrome-devtools — open this menu",
288
+ "/chrome-devtools help — show command usage",
289
+ "/chrome-devtools quickstart — show endpoint and launch help",
290
+ "/chrome-devtools status — show tool and settings status",
291
+ "/chrome-devtools settings — edit browser connection settings",
292
+ "/chrome-devtools tools — choose available Chrome DevTools tools",
293
+ "/chrome-devtools toggle|select — compatibility aliases for tools",
294
+ "/chrome-devtools enable|on — make all Chrome DevTools tools available",
295
+ "/chrome-devtools disable|off — make all Chrome DevTools capability tools unavailable",
296
+ ].join("\n");
338
297
  }
339
298
 
340
299
  export function allChromeDevtoolsTools(owner: object) {
341
- return effectiveCatalog(owner);
300
+ return effectiveCatalog(owner);
342
301
  }
343
302
 
344
303
  export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
345
- return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
304
+ return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
346
305
  }
347
306
 
348
307
  function formatRuntimeStatus(summary: ToolStatusSummary) {
349
- return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${summary.capabilityCount} available)`;
308
+ return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${summary.capabilityCount} available)`;
350
309
  }
351
310
 
352
311
  async function persistedSettingLabel() {
353
- const settings = await loadSettings();
354
- if (settings.kind === "loaded" && settings.settings.tools) {
355
- return formatPersistedSelection(settings.settings.tools);
356
- }
357
- if (settings.kind === "invalid") {
358
- return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
359
- }
360
- return "none; current active-tool policy preserved";
312
+ const settings = await loadSettings();
313
+ if (settings.kind === "loaded" && settings.settings.tools) {
314
+ return formatPersistedSelection(settings.settings.tools);
315
+ }
316
+ if (settings.kind === "invalid") {
317
+ return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
318
+ }
319
+ return "none; current active-tool policy preserved";
361
320
  }
362
321
 
363
322
  function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
364
- if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
365
- return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
366
- }
367
- if (tools.length === 0)
368
- return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
369
- return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
323
+ if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
324
+ return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
325
+ }
326
+ if (tools.length === 0) return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
327
+ return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
370
328
  }
371
329
 
372
330
  function effectiveCatalog(owner: object): ChromeDevToolsToolName[] {
373
- return webMcpEnabled(owner)
374
- ? [...CHROME_DEVTOOLS_TOOL_NAMES]
375
- : [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
331
+ return webMcpEnabled(owner) ? [...CHROME_DEVTOOLS_TOOL_NAMES] : [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
376
332
  }
377
333
 
378
334
  async function persistSettings(selectedTools: readonly ChromeDevToolsToolName[]) {
379
- await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
335
+ await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
380
336
  }