@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
@@ -3,404 +3,369 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
3
3
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
4
4
  import { shutdownManagedBrowser, syncManagedBrowserSettings } from "./browser-manager.js";
5
5
  import {
6
- applyAvailableChromeDevtoolsTools,
7
- configuredChromeDevtoolsTools,
8
- setChromeDevtoolsSessionOwner,
6
+ applyAvailableChromeDevtoolsTools,
7
+ configuredChromeDevtoolsTools,
8
+ setChromeDevtoolsSessionOwner,
9
9
  } from "./lazy-tools.js";
10
10
  import {
11
- applyRuntimeBrowserSettings,
12
- applyRuntimeWebMcpSetting,
13
- invalidateWebMcpOperations,
14
- state,
15
- webMcpEnabled,
11
+ applyRuntimeBrowserSettings,
12
+ applyRuntimeWebMcpSetting,
13
+ invalidateWebMcpOperations,
14
+ state,
15
+ webMcpEnabled,
16
16
  } from "./runtime.js";
17
17
  import {
18
- type BrowserSettingsPatch,
19
- type BrowserSettingsSource,
20
- loadSettings,
21
- parseBrowserEndpoint,
22
- type SettingsLoadResult,
23
- saveBrowserSettings,
24
- saveWebMcpSettings,
18
+ type BrowserSettingsPatch,
19
+ type BrowserSettingsSource,
20
+ loadSettings,
21
+ parseBrowserEndpoint,
22
+ type SettingsLoadResult,
23
+ saveBrowserSettings,
24
+ saveWebMcpSettings,
25
25
  } from "./settings.js";
26
26
  import { sanitizeChromeDevtoolsDisplay } from "./tool-selector.js";
27
27
 
28
28
  type BrowserSettingsScreen = "settings" | "endpoint" | "executable" | "extensions";
29
29
  type BrowserSettingsAction =
30
- | "open-endpoint"
31
- | "save-endpoint"
32
- | "set-auto-launch"
33
- | "set-webmcp"
34
- | "open-executable"
35
- | "save-executable"
36
- | "show-extensions";
30
+ | "open-endpoint"
31
+ | "save-endpoint"
32
+ | "set-auto-launch"
33
+ | "set-webmcp"
34
+ | "open-executable"
35
+ | "save-executable"
36
+ | "show-extensions";
37
37
 
38
38
  interface BrowserSettingsMenuState {
39
- load: SettingsLoadResult;
39
+ load: SettingsLoadResult;
40
40
  }
41
41
 
42
42
  type CommandContext = ExtensionCommandContext;
43
43
 
44
44
  export async function showChromeDevtoolsBrowserSettings(
45
- pi: ExtensionAPI,
46
- ctx: CommandContext,
47
- generation: number,
45
+ pi: ExtensionAPI,
46
+ ctx: CommandContext,
47
+ generation: number,
48
48
  ): Promise<{ closeParent: boolean }> {
49
- setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
50
- const ownerSignal = state.sessionController.signal;
51
- const isCurrent = () => generation === state.sessionGeneration && !ownerSignal.aborted;
52
- const projectTrusted = ctx.isProjectTrusted();
53
- const menu = defineMenu<BrowserSettingsMenuState, BrowserSettingsScreen, BrowserSettingsAction>({
54
- start: "settings",
55
- screens: {
56
- settings: ({ state: current }) => {
57
- if (current.load.userFile.kind === "invalid") return invalidSettingsScreen(current.load);
58
- const browser = current.load.effectiveBrowser;
59
- return {
60
- kind: "settings",
61
- title: "Chrome DevTools Browser settings",
62
- lines: settingsContextLines(current.load),
63
- items: [
64
- {
65
- id: "endpoint",
66
- label: "DevTools endpoint",
67
- description:
68
- 'HTTP origin with an explicit port; enter "default" to restore 127.0.0.1:9222.',
69
- currentValue: sanitizeChromeDevtoolsDisplay(browser.endpoint),
70
- action: "open-endpoint",
71
- },
72
- {
73
- id: "auto-launch",
74
- label: "Auto-launch",
75
- description:
76
- "Launch an isolated managed browser when a local endpoint is unavailable.",
77
- currentValue: browser.autoLaunchEnabled ? "On" : "Off",
78
- values: ["On", "Off"],
79
- action: "set-auto-launch",
80
- },
81
- {
82
- id: "executable",
83
- label: "Browser executable",
84
- description:
85
- 'Absolute executable path; enter "automatic" to restore browser discovery.',
86
- currentValue: sanitizeChromeDevtoolsDisplay(browser.executablePath ?? "Automatic"),
87
- action: "open-executable",
88
- },
89
- {
90
- id: "webmcp",
91
- label: "WebMCP · Experimental",
92
- description:
93
- "Allow user-confirmed calls to page-provided WebMCP tools in this browser session.",
94
- currentValue: current.load.effectiveWebMcpEnabled ? "On" : "Off",
95
- values: ["On", "Off"],
96
- action: "set-webmcp",
97
- },
98
- {
99
- id: "extensions",
100
- label: "Unpacked extensions",
101
- description: "Inspect trusted extension paths configured in user or project JSON.",
102
- currentValue: `${browser.extensionPaths.length} configured`,
103
- action: "show-extensions",
104
- },
105
- ],
106
- };
107
- },
108
- endpoint: ({ state: current }) => ({
109
- kind: "input",
110
- title: "DevTools endpoint",
111
- lines: [
112
- sanitizeChromeDevtoolsDisplay(
113
- `Current effective endpoint: ${current.load.effectiveBrowser.endpoint}`,
114
- ),
115
- 'Enter an HTTP origin with an explicit port, or "default".',
116
- ],
117
- placeholder: "http://127.0.0.1:9222",
118
- action: "save-endpoint",
119
- hint: "back",
120
- }),
121
- executable: ({ state: current }) => ({
122
- kind: "input",
123
- title: "Browser executable",
124
- lines: [
125
- sanitizeChromeDevtoolsDisplay(
126
- `Current effective executable: ${current.load.effectiveBrowser.executablePath ?? "automatic discovery"}`,
127
- ),
128
- 'Enter an absolute path, or "automatic".',
129
- ],
130
- placeholder: "absolute browser executable path",
131
- action: "save-executable",
132
- hint: "back",
133
- }),
134
- extensions: ({ state: current }) => ({
135
- kind: "detail",
136
- title: "Unpacked extensions",
137
- lines: extensionDetailLines(current.load),
138
- hint: "back",
139
- }),
140
- },
141
- actions: {
142
- "open-endpoint": () => ({ kind: "to", screen: "endpoint" }),
143
- "save-endpoint": async ({ value, signal }) => {
144
- if (value === undefined) return { kind: "rejected" };
145
- let endpoint: string | null;
146
- try {
147
- const trimmed = value.trim();
148
- endpoint =
149
- trimmed.toLowerCase() === "default" ? null : parseBrowserEndpoint(trimmed).endpoint;
150
- } catch (error) {
151
- notifySaveFailure(ctx, error);
152
- return { kind: "rejected" };
153
- }
154
- const saved = await saveAndApplyBrowserPatch(
155
- ctx,
156
- generation,
157
- ownerSignal,
158
- signal,
159
- projectTrusted,
160
- { endpoint },
161
- );
162
- if (!saved) return { kind: "rejected" };
163
- notifyEffectiveSave(
164
- ctx,
165
- "endpoint",
166
- saved.effectiveBrowser.endpoint,
167
- saved.effectiveBrowser.endpointSource,
168
- );
169
- return { kind: "back" };
170
- },
171
- "set-auto-launch": async ({ value, signal }) => {
172
- if (value !== "On" && value !== "Off") return { kind: "rejected" };
173
- const saved = await saveAndApplyBrowserPatch(
174
- ctx,
175
- generation,
176
- ownerSignal,
177
- signal,
178
- projectTrusted,
179
- { autoLaunch: value === "On" },
180
- );
181
- if (!saved) return { kind: "rejected" };
182
- notifyEffectiveSave(
183
- ctx,
184
- "auto-launch",
185
- saved.effectiveBrowser.autoLaunchEnabled ? "On" : "Off",
186
- saved.effectiveBrowser.autoLaunchSource,
187
- );
188
- return { kind: "stay" };
189
- },
190
- "set-webmcp": async ({ value, signal }) => {
191
- if (value !== "On" && value !== "Off") return { kind: "rejected" };
192
- const saved = await saveAndApplyWebMcpSetting(
193
- pi,
194
- ctx,
195
- generation,
196
- ownerSignal,
197
- signal,
198
- projectTrusted,
199
- value === "On",
200
- );
201
- if (!saved) return { kind: "rejected" };
202
- ctx.ui.notify(
203
- saved.effectiveWebMcpEnabled
204
- ? "Experimental WebMCP enabled. Every page-provided tool call requires confirmation. Choose gateway availability under Browser tools."
205
- : "Experimental WebMCP disabled. Active WebMCP work was aborted and both gateway tools are unavailable.",
206
- "warning",
207
- );
208
- return { kind: "stay" };
209
- },
210
- "open-executable": () => ({ kind: "to", screen: "executable" }),
211
- "save-executable": async ({ value, signal }) => {
212
- if (value === undefined) return { kind: "rejected" };
213
- const trimmed = value.trim();
214
- const reset = ["automatic", "default", "none"].includes(trimmed.toLowerCase());
215
- if (!reset && (!trimmed || !isAbsolute(trimmed))) {
216
- notifySaveFailure(ctx, new Error('Enter an absolute path or "automatic".'));
217
- return { kind: "rejected" };
218
- }
219
- const executablePath = reset ? null : trimmed;
220
- const saved = await saveAndApplyBrowserPatch(
221
- ctx,
222
- generation,
223
- ownerSignal,
224
- signal,
225
- projectTrusted,
226
- { executablePath },
227
- );
228
- if (!saved) return { kind: "rejected" };
229
- notifyEffectiveSave(
230
- ctx,
231
- "browser executable",
232
- saved.effectiveBrowser.executablePath ?? "automatic discovery",
233
- saved.effectiveBrowser.executablePathSource,
234
- );
235
- return { kind: "back" };
236
- },
237
- "show-extensions": () => ({ kind: "to", screen: "extensions" }),
238
- },
239
- });
240
- const result = await runMenu(ctx, menu, {
241
- getState: async ({ signal }) => {
242
- signal.throwIfAborted();
243
- const load = await loadSettings({ cwd: ctx.cwd, projectTrusted });
244
- signal.throwIfAborted();
245
- return { load };
246
- },
247
- signal: ownerSignal,
248
- isCurrent,
249
- onError: (currentCtx, error) =>
250
- currentCtx.ui.notify(
251
- sanitizeChromeDevtoolsDisplay(
252
- `Chrome DevTools browser settings failed: ${formatError(error)}`,
253
- ),
254
- "error",
255
- ),
256
- });
257
- return { closeParent: result.kind === "closed" && result.reason === "close" };
49
+ setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
50
+ const ownerSignal = state.sessionController.signal;
51
+ const isCurrent = () => generation === state.sessionGeneration && !ownerSignal.aborted;
52
+ const projectTrusted = ctx.isProjectTrusted();
53
+ const menu = defineMenu<BrowserSettingsMenuState, BrowserSettingsScreen, BrowserSettingsAction>({
54
+ start: "settings",
55
+ screens: {
56
+ settings: ({ state: current }) => {
57
+ if (current.load.userFile.kind === "invalid") return invalidSettingsScreen(current.load);
58
+ const browser = current.load.effectiveBrowser;
59
+ return {
60
+ kind: "settings",
61
+ title: "Chrome DevTools Browser settings",
62
+ lines: settingsContextLines(current.load),
63
+ items: [
64
+ {
65
+ id: "endpoint",
66
+ label: "DevTools endpoint",
67
+ description: 'HTTP origin with an explicit port; enter "default" to restore 127.0.0.1:9222.',
68
+ currentValue: sanitizeChromeDevtoolsDisplay(browser.endpoint),
69
+ action: "open-endpoint",
70
+ },
71
+ {
72
+ id: "auto-launch",
73
+ label: "Auto-launch",
74
+ description: "Launch an isolated managed browser when a local endpoint is unavailable.",
75
+ currentValue: browser.autoLaunchEnabled ? "On" : "Off",
76
+ values: ["On", "Off"],
77
+ action: "set-auto-launch",
78
+ },
79
+ {
80
+ id: "executable",
81
+ label: "Browser executable",
82
+ description: 'Absolute executable path; enter "automatic" to restore browser discovery.',
83
+ currentValue: sanitizeChromeDevtoolsDisplay(browser.executablePath ?? "Automatic"),
84
+ action: "open-executable",
85
+ },
86
+ {
87
+ id: "webmcp",
88
+ label: "WebMCP · Experimental",
89
+ description: "Allow user-confirmed calls to page-provided WebMCP tools in this browser session.",
90
+ currentValue: current.load.effectiveWebMcpEnabled ? "On" : "Off",
91
+ values: ["On", "Off"],
92
+ action: "set-webmcp",
93
+ },
94
+ {
95
+ id: "extensions",
96
+ label: "Unpacked extensions",
97
+ description: "Inspect trusted extension paths configured in user or project JSON.",
98
+ currentValue: `${browser.extensionPaths.length} configured`,
99
+ action: "show-extensions",
100
+ },
101
+ ],
102
+ };
103
+ },
104
+ endpoint: ({ state: current }) => ({
105
+ kind: "input",
106
+ title: "DevTools endpoint",
107
+ lines: [
108
+ sanitizeChromeDevtoolsDisplay(`Current effective endpoint: ${current.load.effectiveBrowser.endpoint}`),
109
+ 'Enter an HTTP origin with an explicit port, or "default".',
110
+ ],
111
+ placeholder: "http://127.0.0.1:9222",
112
+ action: "save-endpoint",
113
+ hint: "back",
114
+ }),
115
+ executable: ({ state: current }) => ({
116
+ kind: "input",
117
+ title: "Browser executable",
118
+ lines: [
119
+ sanitizeChromeDevtoolsDisplay(
120
+ `Current effective executable: ${current.load.effectiveBrowser.executablePath ?? "automatic discovery"}`,
121
+ ),
122
+ 'Enter an absolute path, or "automatic".',
123
+ ],
124
+ placeholder: "absolute browser executable path",
125
+ action: "save-executable",
126
+ hint: "back",
127
+ }),
128
+ extensions: ({ state: current }) => ({
129
+ kind: "detail",
130
+ title: "Unpacked extensions",
131
+ lines: extensionDetailLines(current.load),
132
+ hint: "back",
133
+ }),
134
+ },
135
+ actions: {
136
+ "open-endpoint": () => ({ kind: "to", screen: "endpoint" }),
137
+ "save-endpoint": async ({ value, signal }) => {
138
+ if (value === undefined) return { kind: "rejected" };
139
+ let endpoint: string | null;
140
+ try {
141
+ const trimmed = value.trim();
142
+ endpoint = trimmed.toLowerCase() === "default" ? null : parseBrowserEndpoint(trimmed).endpoint;
143
+ } catch (error) {
144
+ notifySaveFailure(ctx, error);
145
+ return { kind: "rejected" };
146
+ }
147
+ const saved = await saveAndApplyBrowserPatch(ctx, generation, ownerSignal, signal, projectTrusted, {
148
+ endpoint,
149
+ });
150
+ if (!saved) return { kind: "rejected" };
151
+ notifyEffectiveSave(ctx, "endpoint", saved.effectiveBrowser.endpoint, saved.effectiveBrowser.endpointSource);
152
+ return { kind: "back" };
153
+ },
154
+ "set-auto-launch": async ({ value, signal }) => {
155
+ if (value !== "On" && value !== "Off") return { kind: "rejected" };
156
+ const saved = await saveAndApplyBrowserPatch(ctx, generation, ownerSignal, signal, projectTrusted, {
157
+ autoLaunch: value === "On",
158
+ });
159
+ if (!saved) return { kind: "rejected" };
160
+ notifyEffectiveSave(
161
+ ctx,
162
+ "auto-launch",
163
+ saved.effectiveBrowser.autoLaunchEnabled ? "On" : "Off",
164
+ saved.effectiveBrowser.autoLaunchSource,
165
+ );
166
+ return { kind: "stay" };
167
+ },
168
+ "set-webmcp": async ({ value, signal }) => {
169
+ if (value !== "On" && value !== "Off") return { kind: "rejected" };
170
+ const saved = await saveAndApplyWebMcpSetting(
171
+ pi,
172
+ ctx,
173
+ generation,
174
+ ownerSignal,
175
+ signal,
176
+ projectTrusted,
177
+ value === "On",
178
+ );
179
+ if (!saved) return { kind: "rejected" };
180
+ ctx.ui.notify(
181
+ saved.effectiveWebMcpEnabled
182
+ ? "Experimental WebMCP enabled. Every page-provided tool call requires confirmation. Choose gateway availability under Browser tools."
183
+ : "Experimental WebMCP disabled. Active WebMCP work was aborted and both gateway tools are unavailable.",
184
+ "warning",
185
+ );
186
+ return { kind: "stay" };
187
+ },
188
+ "open-executable": () => ({ kind: "to", screen: "executable" }),
189
+ "save-executable": async ({ value, signal }) => {
190
+ if (value === undefined) return { kind: "rejected" };
191
+ const trimmed = value.trim();
192
+ const reset = ["automatic", "default", "none"].includes(trimmed.toLowerCase());
193
+ if (!reset && (!trimmed || !isAbsolute(trimmed))) {
194
+ notifySaveFailure(ctx, new Error('Enter an absolute path or "automatic".'));
195
+ return { kind: "rejected" };
196
+ }
197
+ const executablePath = reset ? null : trimmed;
198
+ const saved = await saveAndApplyBrowserPatch(ctx, generation, ownerSignal, signal, projectTrusted, {
199
+ executablePath,
200
+ });
201
+ if (!saved) return { kind: "rejected" };
202
+ notifyEffectiveSave(
203
+ ctx,
204
+ "browser executable",
205
+ saved.effectiveBrowser.executablePath ?? "automatic discovery",
206
+ saved.effectiveBrowser.executablePathSource,
207
+ );
208
+ return { kind: "back" };
209
+ },
210
+ "show-extensions": () => ({ kind: "to", screen: "extensions" }),
211
+ },
212
+ });
213
+ const result = await runMenu(ctx, menu, {
214
+ getState: async ({ signal }) => {
215
+ signal.throwIfAborted();
216
+ const load = await loadSettings({ cwd: ctx.cwd, projectTrusted });
217
+ signal.throwIfAborted();
218
+ return { load };
219
+ },
220
+ signal: ownerSignal,
221
+ isCurrent,
222
+ onError: (currentCtx, error) =>
223
+ currentCtx.ui.notify(
224
+ sanitizeChromeDevtoolsDisplay(`Chrome DevTools browser settings failed: ${formatError(error)}`),
225
+ "error",
226
+ ),
227
+ });
228
+ return { closeParent: result.kind === "closed" && result.reason === "close" };
258
229
  }
259
230
 
260
231
  async function saveAndApplyBrowserPatch(
261
- ctx: CommandContext,
262
- generation: number,
263
- ownerSignal: AbortSignal,
264
- actionSignal: AbortSignal,
265
- projectTrusted: boolean,
266
- patch: BrowserSettingsPatch,
232
+ ctx: CommandContext,
233
+ generation: number,
234
+ ownerSignal: AbortSignal,
235
+ actionSignal: AbortSignal,
236
+ projectTrusted: boolean,
237
+ patch: BrowserSettingsPatch,
267
238
  ) {
268
- try {
269
- await ctx.waitForIdle();
270
- if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
271
- await saveBrowserSettings(patch);
272
- if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
273
- invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools browser configuration changed");
274
- await shutdownManagedBrowser(undefined, { owner: ctx.sessionManager });
275
- if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
276
- const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
277
- if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
278
- applyRuntimeBrowserSettings(loaded.effectiveBrowser, loaded.paths, projectTrusted);
279
- syncManagedBrowserSettings(ctx.sessionManager, loaded.effectiveBrowser);
280
- state.settingsNotice = loaded.notice;
281
- return loaded;
282
- } catch (error) {
283
- if (isCurrent(generation, ownerSignal, actionSignal)) notifySaveFailure(ctx, error);
284
- return false;
285
- }
239
+ try {
240
+ await ctx.waitForIdle();
241
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
242
+ await saveBrowserSettings(patch);
243
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
244
+ invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools browser configuration changed");
245
+ await shutdownManagedBrowser(undefined, { owner: ctx.sessionManager });
246
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
247
+ const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
248
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
249
+ applyRuntimeBrowserSettings(loaded.effectiveBrowser, loaded.paths, projectTrusted);
250
+ syncManagedBrowserSettings(ctx.sessionManager, loaded.effectiveBrowser);
251
+ state.settingsNotice = loaded.notice;
252
+ return loaded;
253
+ } catch (error) {
254
+ if (isCurrent(generation, ownerSignal, actionSignal)) notifySaveFailure(ctx, error);
255
+ return false;
256
+ }
286
257
  }
287
258
 
288
259
  async function saveAndApplyWebMcpSetting(
289
- pi: ExtensionAPI,
290
- ctx: CommandContext,
291
- generation: number,
292
- ownerSignal: AbortSignal,
293
- actionSignal: AbortSignal,
294
- projectTrusted: boolean,
295
- enabled: boolean,
260
+ pi: ExtensionAPI,
261
+ ctx: CommandContext,
262
+ generation: number,
263
+ ownerSignal: AbortSignal,
264
+ actionSignal: AbortSignal,
265
+ projectTrusted: boolean,
266
+ enabled: boolean,
296
267
  ) {
297
- const previousEnabled = webMcpEnabled(ctx.sessionManager);
298
- const previousTools = configuredChromeDevtoolsTools(pi);
299
- try {
300
- await ctx.waitForIdle();
301
- if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
302
- applyRuntimeWebMcpSetting(enabled, ctx.sessionManager);
303
- applyAvailableChromeDevtoolsTools(pi, previousTools);
304
- await saveWebMcpSettings(enabled);
305
- if (!isOwnerCurrent(generation, ownerSignal)) return false;
306
- const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
307
- if (!isOwnerCurrent(generation, ownerSignal)) return false;
308
- state.settingsNotice = loaded.notice;
309
- return loaded;
310
- } catch (error) {
311
- if (!isOwnerCurrent(generation, ownerSignal)) return false;
312
- let rollbackError: unknown;
313
- try {
314
- applyRuntimeWebMcpSetting(previousEnabled, ctx.sessionManager);
315
- applyAvailableChromeDevtoolsTools(pi, previousTools);
316
- } catch (caught) {
317
- rollbackError = caught;
318
- }
319
- if (isCurrent(generation, ownerSignal, actionSignal)) {
320
- notifySaveFailure(
321
- ctx,
322
- rollbackError
323
- ? new Error(
324
- `${formatError(error)}; WebMCP rollback failed: ${formatError(rollbackError)}`,
325
- )
326
- : error,
327
- );
328
- }
329
- return false;
330
- }
268
+ const previousEnabled = webMcpEnabled(ctx.sessionManager);
269
+ const previousTools = configuredChromeDevtoolsTools(pi);
270
+ try {
271
+ await ctx.waitForIdle();
272
+ if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
273
+ applyRuntimeWebMcpSetting(enabled, ctx.sessionManager);
274
+ applyAvailableChromeDevtoolsTools(pi, previousTools);
275
+ await saveWebMcpSettings(enabled);
276
+ if (!isOwnerCurrent(generation, ownerSignal)) return false;
277
+ const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
278
+ if (!isOwnerCurrent(generation, ownerSignal)) return false;
279
+ state.settingsNotice = loaded.notice;
280
+ return loaded;
281
+ } catch (error) {
282
+ if (!isOwnerCurrent(generation, ownerSignal)) return false;
283
+ let rollbackError: unknown;
284
+ try {
285
+ applyRuntimeWebMcpSetting(previousEnabled, ctx.sessionManager);
286
+ applyAvailableChromeDevtoolsTools(pi, previousTools);
287
+ } catch (caught) {
288
+ rollbackError = caught;
289
+ }
290
+ if (isCurrent(generation, ownerSignal, actionSignal)) {
291
+ notifySaveFailure(
292
+ ctx,
293
+ rollbackError
294
+ ? new Error(`${formatError(error)}; WebMCP rollback failed: ${formatError(rollbackError)}`)
295
+ : error,
296
+ );
297
+ }
298
+ return false;
299
+ }
331
300
  }
332
301
 
333
302
  function isOwnerCurrent(generation: number, ownerSignal: AbortSignal) {
334
- return generation === state.sessionGeneration && !ownerSignal.aborted;
303
+ return generation === state.sessionGeneration && !ownerSignal.aborted;
335
304
  }
336
305
 
337
306
  function isCurrent(generation: number, ownerSignal: AbortSignal, actionSignal: AbortSignal) {
338
- return isOwnerCurrent(generation, ownerSignal) && !actionSignal.aborted;
307
+ return isOwnerCurrent(generation, ownerSignal) && !actionSignal.aborted;
339
308
  }
340
309
 
341
310
  function invalidSettingsScreen(load: SettingsLoadResult) {
342
- const reason =
343
- load.userFile.kind === "invalid"
344
- ? load.userFile.reason
345
- : "The user settings file is unavailable.";
346
- return {
347
- kind: "detail" as const,
348
- title: "Chrome DevTools Browser settings · Read only",
349
- lines: [
350
- sanitizeChromeDevtoolsDisplay(
351
- `Fix the active user settings before saving; the file will not be overwritten. ${reason}`,
352
- ),
353
- ],
354
- hint: "back" as const,
355
- };
311
+ const reason = load.userFile.kind === "invalid" ? load.userFile.reason : "The user settings file is unavailable.";
312
+ return {
313
+ kind: "detail" as const,
314
+ title: "Chrome DevTools Browser settings · Read only",
315
+ lines: [
316
+ sanitizeChromeDevtoolsDisplay(
317
+ `Fix the active user settings before saving; the file will not be overwritten. ${reason}`,
318
+ ),
319
+ ],
320
+ hint: "back" as const,
321
+ };
356
322
  }
357
323
 
358
324
  function settingsContextLines(load: SettingsLoadResult) {
359
- const browser = load.effectiveBrowser;
360
- return [
361
- `User settings · ${load.paths.user}`,
362
- `Effective sources: endpoint ${browser.endpointSource} · auto-launch ${browser.autoLaunchSource} · executable ${browser.executablePathSource}`,
363
- ...(load.notice ? [`Warning: ${load.notice}`] : []),
364
- ].map((line) => sanitizeChromeDevtoolsDisplay(line));
325
+ const browser = load.effectiveBrowser;
326
+ return [
327
+ `User settings · ${load.paths.user}`,
328
+ `Effective sources: endpoint ${browser.endpointSource} · auto-launch ${browser.autoLaunchSource} · executable ${browser.executablePathSource}`,
329
+ ...(load.notice ? [`Warning: ${load.notice}`] : []),
330
+ ].map((line) => sanitizeChromeDevtoolsDisplay(line));
365
331
  }
366
332
 
367
333
  function extensionDetailLines(load: SettingsLoadResult) {
368
- const browser = load.effectiveBrowser;
369
- return [
370
- `Effective source: ${browser.extensionPathsSource}`,
371
- ...(browser.extensionPaths.length > 0
372
- ? browser.extensionPaths.map((extensionPath) => `- ${extensionPath}`)
373
- : ["No unpacked extensions are configured."]),
374
- "Edit browser.extensionPaths in the user JSON or a trusted project JSON, then run /reload.",
375
- "Unpacked extensions execute privileged browser code; load only paths you trust.",
376
- ].map((line) => sanitizeChromeDevtoolsDisplay(line));
334
+ const browser = load.effectiveBrowser;
335
+ return [
336
+ `Effective source: ${browser.extensionPathsSource}`,
337
+ ...(browser.extensionPaths.length > 0
338
+ ? browser.extensionPaths.map((extensionPath) => `- ${extensionPath}`)
339
+ : ["No unpacked extensions are configured."]),
340
+ "Edit browser.extensionPaths in the user JSON or a trusted project JSON, then run /reload.",
341
+ "Unpacked extensions execute privileged browser code; load only paths you trust.",
342
+ ].map((line) => sanitizeChromeDevtoolsDisplay(line));
377
343
  }
378
344
 
379
345
  function notifyEffectiveSave(
380
- ctx: CommandContext,
381
- setting: string,
382
- effectiveValue: string,
383
- source: BrowserSettingsSource,
346
+ ctx: CommandContext,
347
+ setting: string,
348
+ effectiveValue: string,
349
+ source: BrowserSettingsSource,
384
350
  ) {
385
- const overrideNotice =
386
- source === "environment" ? " The deprecated environment override remains effective." : "";
387
- ctx.ui.notify(
388
- sanitizeChromeDevtoolsDisplay(
389
- `Chrome DevTools ${setting} saved. Effective ${setting}: ${effectiveValue} (${source}).${overrideNotice}`,
390
- ),
391
- "info",
392
- );
351
+ const overrideNotice = source === "environment" ? " The deprecated environment override remains effective." : "";
352
+ ctx.ui.notify(
353
+ sanitizeChromeDevtoolsDisplay(
354
+ `Chrome DevTools ${setting} saved. Effective ${setting}: ${effectiveValue} (${source}).${overrideNotice}`,
355
+ ),
356
+ "info",
357
+ );
393
358
  }
394
359
 
395
360
  function notifySaveFailure(ctx: CommandContext, error: unknown) {
396
- ctx.ui.notify(
397
- sanitizeChromeDevtoolsDisplay(
398
- `Chrome DevTools browser settings save failed; previous settings remain active: ${formatError(error)}`,
399
- ),
400
- "warning",
401
- );
361
+ ctx.ui.notify(
362
+ sanitizeChromeDevtoolsDisplay(
363
+ `Chrome DevTools browser settings save failed; previous settings remain active: ${formatError(error)}`,
364
+ ),
365
+ "warning",
366
+ );
402
367
  }
403
368
 
404
369
  function formatError(error: unknown) {
405
- return error instanceof Error ? error.message : String(error);
370
+ return error instanceof Error ? error.message : String(error);
406
371
  }