@narumitw/pi-chrome-devtools 0.52.2 → 0.53.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.
Files changed (35) hide show
  1. package/README.md +78 -17
  2. package/dist/chunks/{browser-settings-menu-VI2I3TXI.js → browser-settings-menu-N5O65I7S.js} +81 -7
  3. package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
  4. package/dist/chunks/chunk-B7B63PWB.js +465 -0
  5. package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
  6. package/dist/chunks/{chunk-CC37WUUE.js → chunk-GVBXT3FB.js} +369 -461
  7. package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
  8. package/dist/chunks/chunk-RTECHAEZ.js +373 -0
  9. package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
  10. package/dist/chunks/{menu-LLR455UX.js → menu-UBHW6UP2.js} +75 -41
  11. package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
  12. package/dist/chunks/tools-V7OR5KJ3.js +945 -0
  13. package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
  14. package/dist/index.ts +166 -196
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +282 -109
  18. package/src/browser-settings-menu.ts +98 -4
  19. package/src/cdp-client.ts +329 -83
  20. package/src/chrome-devtools.ts +61 -13
  21. package/src/lazy-tools.ts +105 -8
  22. package/src/menu.ts +83 -41
  23. package/src/render.ts +12 -1
  24. package/src/runtime.ts +92 -0
  25. package/src/settings.ts +47 -10
  26. package/src/tool-names.ts +16 -1
  27. package/src/tool-selector.ts +79 -36
  28. package/src/tools.ts +86 -10
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +402 -0
  31. package/src/webmcp/protocol.ts +414 -0
  32. package/src/webmcp/tools.ts +190 -0
  33. package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +0 -7
  34. package/dist/chunks/chunk-CC37WUUE.js.map +0 -7
  35. package/dist/chunks/menu-LLR455UX.js.map +0 -7
@@ -1,12 +1,22 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { shutdownManagedBrowser } from "./browser-manager.js";
2
+ import { shutdownManagedBrowser, startManagedBrowserSession } from "./browser-manager.js";
3
3
  import {
4
4
  availableChromeDevtoolsTools,
5
- configureLazyChromeDevtoolsTools,
5
+ configureChromeDevtoolsToolExposure,
6
6
  createChromeDevtoolsLoadTool,
7
7
  initializeAvailableChromeDevtoolsTools,
8
+ requireEagerChromeDevtoolsToolExposure,
9
+ setChromeDevtoolsSessionOwner,
10
+ supportsNativeDeferredToolLoading,
8
11
  } from "./lazy-tools.js";
9
- import { applyRuntimeBrowserSettings, state } from "./runtime.js";
12
+ import {
13
+ applyRuntimeBrowserSettings,
14
+ applyRuntimeWebMcpSetting,
15
+ invalidateWebMcpOperations,
16
+ setWebMcpSessionOwner,
17
+ state,
18
+ webMcpEnabled,
19
+ } from "./runtime.js";
10
20
  import { loadSettings, waitForSettingsWrites } from "./settings.js";
11
21
  import {
12
22
  allChromeDevtoolsTools,
@@ -23,6 +33,8 @@ import {
23
33
  navigateTool,
24
34
  screenshotTool,
25
35
  selectPageTool,
36
+ webMcpCallTool,
37
+ webMcpListToolsTool,
26
38
  } from "./tools.js";
27
39
 
28
40
  type CommandAction =
@@ -41,7 +53,7 @@ const COMMAND_COMPLETIONS = [
41
53
  { value: "quickstart", label: "quickstart", description: "Show endpoint and launch help" },
42
54
  { value: "status", label: "status", description: "Show tool and settings status" },
43
55
  { value: "settings", label: "settings", description: "Edit browser connection settings" },
44
- { value: "tools", label: "tools", description: "Choose lazy-loadable Chrome DevTools tools" },
56
+ { value: "tools", label: "tools", description: "Choose available Chrome DevTools tools" },
45
57
  { value: "toggle", label: "toggle", description: "Alias for tools" },
46
58
  { value: "select", label: "select", description: "Compatibility alias for tools" },
47
59
  { value: "enable", label: "enable", description: "Make all Chrome DevTools tools available" },
@@ -55,12 +67,15 @@ export default function chromeDevtools(pi: ExtensionAPI) {
55
67
  pi.registerTool(navigateTool);
56
68
  pi.registerTool(evaluateTool);
57
69
  pi.registerTool(screenshotTool);
70
+ pi.registerTool(webMcpListToolsTool);
71
+ pi.registerTool(webMcpCallTool);
58
72
  pi.registerTool(createChromeDevtoolsLoadTool(pi));
59
73
 
60
74
  pi.registerCommand("chrome-devtools", {
61
75
  description: "Open Chrome DevTools help and tool controls",
62
76
  getArgumentCompletions: (prefix) => commandCompletions(prefix),
63
77
  handler: async (args, ctx) => {
78
+ setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
64
79
  initializeAvailableChromeDevtoolsTools(pi);
65
80
  const generation = state.sessionGeneration;
66
81
  await handleChromeDevtoolsCommand(pi, args, ctx, generation);
@@ -69,12 +84,19 @@ export default function chromeDevtools(pi: ExtensionAPI) {
69
84
 
70
85
  pi.on("session_start", async (_event, ctx) => {
71
86
  const generation = ++state.sessionGeneration;
87
+ setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
72
88
  initializeAvailableChromeDevtoolsTools(pi);
89
+ setWebMcpSessionOwner(ctx.sessionManager);
73
90
  replaceSessionController("Chrome DevTools session replaced");
91
+ invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools session replaced");
74
92
  state.shuttingDown = false;
75
93
  state.settingsNotice = undefined;
76
94
  ctx.ui.setStatus(STATUS_KEY, undefined);
77
- await shutdownManagedBrowser();
95
+ await shutdownManagedBrowser(undefined, {
96
+ cancelLaunch: true,
97
+ owner: ctx.sessionManager,
98
+ });
99
+ startManagedBrowserSession(ctx.sessionManager);
78
100
  if (generation !== state.sessionGeneration) return;
79
101
  state.activePageId = undefined;
80
102
  state.lastLaunchAttempt = undefined;
@@ -82,22 +104,43 @@ export default function chromeDevtools(pi: ExtensionAPI) {
82
104
  const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
83
105
  if (generation !== state.sessionGeneration) return;
84
106
  applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
107
+ applyRuntimeWebMcpSetting(settings.effectiveWebMcpEnabled, ctx.sessionManager);
85
108
  state.settingsNotice = settings.notice;
86
109
  for (const warning of settings.warnings) {
87
110
  ctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), "warning");
88
111
  }
112
+ if (webMcpEnabled(ctx.sessionManager)) {
113
+ ctx.ui.notify(
114
+ "Experimental WebMCP is enabled. Page-provided tools use the visible browser session and require confirmation for every call.",
115
+ "warning",
116
+ );
117
+ }
89
118
  const availableTools =
90
119
  settings.kind === "loaded" && settings.settings.tools
91
120
  ? settings.settings.tools
92
121
  : availableChromeDevtoolsTools(pi);
93
- configureLazyChromeDevtoolsTools(pi, availableTools);
122
+ configureChromeDevtoolsToolExposure(pi, availableTools, ctx.model);
123
+ });
124
+
125
+ pi.on("model_select", (event, ctx) => {
126
+ invalidateWebMcpOperations(
127
+ ctx.sessionManager,
128
+ "Chrome DevTools model and tool exposure changed",
129
+ );
130
+ if (!supportsNativeDeferredToolLoading(event.model)) {
131
+ requireEagerChromeDevtoolsToolExposure(pi);
132
+ }
94
133
  });
95
134
 
96
135
  pi.on("session_shutdown", async (_event, ctx) => {
97
136
  state.sessionGeneration += 1;
98
137
  replaceSessionController("Chrome DevTools session shut down");
138
+ invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools session shut down");
99
139
  ctx.ui.setStatus(STATUS_KEY, undefined);
100
- const browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });
140
+ const browserShutdown = shutdownManagedBrowser(undefined, {
141
+ cancelLaunch: true,
142
+ owner: ctx.sessionManager,
143
+ });
101
144
  await waitForChromeDevtoolsSettings();
102
145
  await waitForSettingsWrites();
103
146
  await browserShutdown;
@@ -117,15 +160,15 @@ async function handleChromeDevtoolsCommand(
117
160
  return;
118
161
  case "help":
119
162
  requireObservableUi(ctx, "help");
120
- ctx.ui.notify(buildCommandGuide(), "info");
163
+ ctx.ui.notify(buildCommandGuide(ctx.sessionManager), "info");
121
164
  return;
122
165
  case "quickstart":
123
166
  requireObservableUi(ctx, "quickstart");
124
- ctx.ui.notify(buildQuickstartMessage(), "info");
167
+ ctx.ui.notify(buildQuickstartMessage(ctx.sessionManager), "info");
125
168
  return;
126
169
  case "status": {
127
170
  requireObservableUi(ctx, "status");
128
- const status = await buildToolStatusMessage(pi);
171
+ const status = await buildToolStatusMessage(pi, ctx.sessionManager);
129
172
  if (generation !== state.sessionGeneration) return;
130
173
  ctx.ui.notify(status, "info");
131
174
  return;
@@ -136,7 +179,7 @@ async function handleChromeDevtoolsCommand(
136
179
  }
137
180
  const { showChromeDevtoolsBrowserSettings } = await import("./browser-settings-menu.js");
138
181
  if (generation !== state.sessionGeneration) return;
139
- await showChromeDevtoolsBrowserSettings(ctx, generation);
182
+ await showChromeDevtoolsBrowserSettings(pi, ctx, generation);
140
183
  return;
141
184
  }
142
185
  case "tools": {
@@ -149,7 +192,12 @@ async function handleChromeDevtoolsCommand(
149
192
  return;
150
193
  }
151
194
  case "enable":
152
- await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "made all available");
195
+ await updateChromeDevtoolsTools(
196
+ pi,
197
+ ctx,
198
+ allChromeDevtoolsTools(ctx.sessionManager),
199
+ "made all available",
200
+ );
153
201
  return;
154
202
  case "disable":
155
203
  await updateChromeDevtoolsTools(pi, ctx, [], "made all unavailable");
@@ -162,7 +210,7 @@ async function handleChromeDevtoolsCommand(
162
210
  ctx.ui.notify(
163
211
  `Unknown /chrome-devtools command: ${args.trim()}
164
212
 
165
- ${buildCommandGuide()}`,
213
+ ${buildCommandGuide(ctx.sessionManager)}`,
166
214
  "warning",
167
215
  );
168
216
  }
package/src/lazy-tools.ts CHANGED
@@ -1,6 +1,15 @@
1
- import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ defineTool,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import { Type } from "typebox";
3
- import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
7
+ import { webMcpEnabled } from "./runtime.js";
8
+ import {
9
+ CHROME_DEVTOOLS_TOOL_NAMES,
10
+ type ChromeDevToolsToolName,
11
+ isWebMcpToolName,
12
+ } from "./tool-names.js";
4
13
 
5
14
  export const CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
6
15
 
@@ -13,6 +22,8 @@ const existingAvailableToolsStore = sharedGlobal[AVAILABLE_TOOLS_STORE];
13
22
  const availableToolsByApi =
14
23
  existingAvailableToolsStore ?? new WeakMap<ExtensionAPI, Set<ChromeDevToolsToolName>>();
15
24
  if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
25
+ const lazyExposureByApi = new WeakMap<ExtensionAPI, boolean>();
26
+ const sessionOwnerByApi = new WeakMap<ExtensionAPI, object>();
16
27
 
17
28
  const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
18
29
  chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
@@ -20,8 +31,16 @@ const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
20
31
  chrome_devtools_navigate: "navigate open create chrome browser page url website",
21
32
  chrome_devtools_evaluate: "evaluate run javascript expression dom inspect chrome browser page",
22
33
  chrome_devtools_screenshot: "capture screenshot png image visual chrome browser page",
34
+ chrome_devtools_webmcp_list_tools:
35
+ "list discover page provided website webmcp tools capabilities experimental",
36
+ chrome_devtools_webmcp_call_tool:
37
+ "call invoke page provided website webmcp tool confirmation experimental",
23
38
  };
24
39
 
40
+ export function setChromeDevtoolsSessionOwner(pi: ExtensionAPI, owner: object) {
41
+ sessionOwnerByApi.set(pi, owner);
42
+ }
43
+
25
44
  export function initializeAvailableChromeDevtoolsTools(pi: ExtensionAPI) {
26
45
  if (availableToolsByApi.has(pi)) return;
27
46
  const activeTools = new Set(pi.getActiveTools());
@@ -31,33 +50,87 @@ export function initializeAvailableChromeDevtoolsTools(pi: ExtensionAPI) {
31
50
  );
32
51
  }
33
52
 
34
- export function configureLazyChromeDevtoolsTools(
53
+ export function configureChromeDevtoolsToolExposure(
35
54
  pi: ExtensionAPI,
36
55
  availableTools: readonly ChromeDevToolsToolName[],
56
+ model?: ExtensionContext["model"],
37
57
  ) {
38
58
  setAvailableTools(pi, availableTools);
59
+ const available = effectiveAvailableTools(pi);
60
+ const lazyExposure = supportsNativeDeferredToolLoading(model);
61
+ lazyExposureByApi.set(pi, lazyExposure);
62
+ const exposedTools = lazyExposure
63
+ ? []
64
+ : CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
39
65
  const nonCapabilityTools = pi
40
66
  .getActiveTools()
41
67
  .filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
42
- pi.setActiveTools(unique([...nonCapabilityTools, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
68
+ pi.setActiveTools(
69
+ unique([...nonCapabilityTools, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...exposedTools]),
70
+ );
71
+ }
72
+
73
+ export function requireEagerChromeDevtoolsToolExposure(pi: ExtensionAPI) {
74
+ lazyExposureByApi.set(pi, false);
75
+ const active = pi.getActiveTools();
76
+ const available = availableChromeDevtoolsTools(pi);
77
+ pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...available]));
43
78
  }
44
79
 
45
80
  export function applyAvailableChromeDevtoolsTools(
46
81
  pi: ExtensionAPI,
47
82
  availableTools: readonly ChromeDevToolsToolName[],
48
83
  ) {
49
- const available = setAvailableTools(pi, availableTools);
84
+ setAvailableTools(pi, availableTools);
85
+ const available = effectiveAvailableTools(pi);
86
+ const lazyExposure = lazyExposureByApi.get(pi) === true;
50
87
  const active = pi
51
88
  .getActiveTools()
52
89
  .filter(
53
90
  (name) =>
54
91
  !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName) ||
55
- available.has(name as ChromeDevToolsToolName),
92
+ (lazyExposure && available.has(name as ChromeDevToolsToolName)),
93
+ );
94
+ const eagerTools = lazyExposure
95
+ ? []
96
+ : CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
97
+ pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...eagerTools]));
98
+ }
99
+
100
+ export function chromeDevtoolsToolExposureMode(pi: ExtensionAPI) {
101
+ return lazyExposureByApi.get(pi) === true ? "native deferred" : "eager";
102
+ }
103
+
104
+ export function supportsNativeDeferredToolLoading(model: ExtensionContext["model"]): boolean {
105
+ if (!model) return false;
106
+ if (model.api === "anthropic-messages") {
107
+ const configured = compatBoolean(model.compat, "supportsToolReferences");
108
+ if (configured !== undefined) return configured;
109
+ if (model.provider !== "anthropic" || model.id.includes("haiku")) return false;
110
+ const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
111
+ if (!version) return false;
112
+ const major = Number(version[1]);
113
+ const minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;
114
+ return major > 4 || (major === 4 && minor >= 5);
115
+ }
116
+ if (model.api === "openai-completions") {
117
+ return compatString(model.compat, "deferredToolsMode") === "kimi";
118
+ }
119
+ if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
120
+ return (
121
+ compatBoolean(model.compat, "supportsAdditionalTools") === true ||
122
+ compatBoolean(model.compat, "supportsToolSearch") === true
56
123
  );
57
- pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
124
+ }
125
+ return false;
58
126
  }
59
127
 
60
128
  export function availableChromeDevtoolsTools(pi: ExtensionAPI) {
129
+ const available = effectiveAvailableTools(pi);
130
+ return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
131
+ }
132
+
133
+ export function configuredChromeDevtoolsTools(pi: ExtensionAPI) {
61
134
  const available = availableToolsByApi.get(pi) ?? new Set();
62
135
  return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
63
136
  }
@@ -85,8 +158,9 @@ export function createChromeDevtoolsLoadTool(pi: ExtensionAPI) {
85
158
  }),
86
159
  ),
87
160
  }),
88
- async execute(_toolCallId, params, signal) {
161
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
89
162
  signal?.throwIfAborted();
163
+ setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
90
164
  const available = new Set(availableChromeDevtoolsTools(pi));
91
165
  const matches = matchChromeDevtoolsTools(params.query, params.limit ?? 3, available);
92
166
  const active = pi.getActiveTools();
@@ -138,6 +212,29 @@ function setAvailableTools(pi: ExtensionAPI, availableTools: readonly ChromeDevT
138
212
  return available;
139
213
  }
140
214
 
215
+ function effectiveAvailableTools(pi: ExtensionAPI) {
216
+ const configured = availableToolsByApi.get(pi) ?? new Set();
217
+ const owner = sessionOwnerByApi.get(pi);
218
+ const enabled = owner !== undefined && webMcpEnabled(owner);
219
+ return new Set(
220
+ CHROME_DEVTOOLS_TOOL_NAMES.filter(
221
+ (name) => configured.has(name) && (enabled || !isWebMcpToolName(name)),
222
+ ),
223
+ );
224
+ }
225
+
226
+ function compatBoolean(value: unknown, key: string) {
227
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
228
+ const record = value as Record<string, unknown>;
229
+ return typeof record[key] === "boolean" ? record[key] : undefined;
230
+ }
231
+
232
+ function compatString(value: unknown, key: string) {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
234
+ const record = value as Record<string, unknown>;
235
+ return typeof record[key] === "string" ? record[key] : undefined;
236
+ }
237
+
141
238
  function unique(values: readonly string[]) {
142
239
  return [...new Set(values)];
143
240
  }
package/src/menu.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { defineMenu, runMenu, runTask } from "@narumitw/pi-tui-kit";
3
- import { browserLifecycleState, devToolsEndpoint, launchModeLabel } from "./browser-manager.js";
3
+ import {
4
+ browserLifecycleState,
5
+ devToolsEndpoint,
6
+ launchAttemptLines,
7
+ launchModeLabel,
8
+ } from "./browser-manager.js";
4
9
  import { availableChromeDevtoolsTools } from "./lazy-tools.js";
5
- import { state } from "./runtime.js";
10
+ import { state, webMcpEnabled } from "./runtime.js";
6
11
  import { loadSettings, type SettingsLoadResult } from "./settings.js";
7
- import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
12
+ import {
13
+ CHROME_DEVTOOLS_TOOL_NAMES,
14
+ type ChromeDevToolsToolName,
15
+ CORE_CHROME_DEVTOOLS_TOOL_NAMES,
16
+ } from "./tool-names.js";
8
17
  import {
9
18
  buildBrowserStatusMessage,
10
19
  buildCommandGuide,
@@ -54,6 +63,14 @@ const TOOL_PRESENTATION: Record<ChromeDevToolsToolName, { label: string; descrip
54
63
  label: "Capture a screenshot",
55
64
  description: "Save a PNG of the selected page (chrome_devtools_screenshot).",
56
65
  },
66
+ chrome_devtools_webmcp_list_tools: {
67
+ label: "List page WebMCP tools · Experimental",
68
+ description: "Discover page-provided WebMCP capabilities (chrome_devtools_webmcp_list_tools).",
69
+ },
70
+ chrome_devtools_webmcp_call_tool: {
71
+ label: "Call a page WebMCP tool · Experimental",
72
+ description: "Invoke a listed page tool after confirmation (chrome_devtools_webmcp_call_tool).",
73
+ },
57
74
  };
58
75
 
59
76
  export async function showChromeDevtoolsMenu(
@@ -71,7 +88,7 @@ export async function showChromeDevtoolsMenu(
71
88
  main: ({ state: current }) => ({
72
89
  kind: "actions",
73
90
  title: "Chrome DevTools",
74
- lines: mainStateLines(current),
91
+ lines: mainStateLines(current, ctx.sessionManager),
75
92
  items: [
76
93
  {
77
94
  id: "tools",
@@ -84,13 +101,13 @@ export async function showChromeDevtoolsMenu(
84
101
  {
85
102
  id: "bulk",
86
103
  label:
87
- current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
104
+ current.activeTools.length === availableCatalogNames(ctx.sessionManager).length
88
105
  ? "Make all browser tools unavailable…"
89
106
  : "Make all browser tools available…",
90
107
  description:
91
- current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
92
- ? "Preview 0 of 5; other active tools stay enabled."
93
- : "Preview 5 of 5; other active tools stay enabled.",
108
+ current.activeTools.length === availableCatalogNames(ctx.sessionManager).length
109
+ ? `Preview 0 of ${availableCatalogNames(ctx.sessionManager).length}; other active tools stay enabled.`
110
+ : `Preview ${availableCatalogNames(ctx.sessionManager).length} of ${availableCatalogNames(ctx.sessionManager).length}; other active tools stay enabled.`,
94
111
  disabled: Boolean(current.mutationBlockedReason),
95
112
  disabledReason: current.mutationBlockedReason,
96
113
  action: "bulk",
@@ -119,7 +136,7 @@ export async function showChromeDevtoolsMenu(
119
136
  browserStatus: () => ({
120
137
  kind: "review",
121
138
  title: "Browser status",
122
- content: buildBrowserStatusMessage(),
139
+ content: buildBrowserStatusMessage(ctx.sessionManager),
123
140
  format: { kind: "text" },
124
141
  viewportSize: "adaptive",
125
142
  hint: "back",
@@ -127,7 +144,7 @@ export async function showChromeDevtoolsMenu(
127
144
  help: () => ({
128
145
  kind: "review",
129
146
  title: "Chrome DevTools help",
130
- content: buildCommandGuide(),
147
+ content: buildCommandGuide(ctx.sessionManager),
131
148
  format: { kind: "text" },
132
149
  viewportSize: "adaptive",
133
150
  hint: "back",
@@ -143,9 +160,9 @@ export async function showChromeDevtoolsMenu(
143
160
  },
144
161
  bulk: async ({ state: current }) => {
145
162
  const selectedTools =
146
- current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
163
+ current.activeTools.length === availableCatalogNames(ctx.sessionManager).length
147
164
  ? []
148
- : [...CHROME_DEVTOOLS_TOOL_NAMES];
165
+ : availableCatalogNames(ctx.sessionManager);
149
166
  const result = await showChromeDevtoolsToolWorkflow(pi, ctx, generation, {
150
167
  snapshot: current,
151
168
  initialDraft: selectedTools,
@@ -154,11 +171,14 @@ export async function showChromeDevtoolsMenu(
154
171
  if (result?.applied && isCurrent()) updateSnapshotAfterApply(current, result.selectedTools);
155
172
  return result?.closeParent ? { kind: "close" } : { kind: "stay" };
156
173
  },
157
- settings: async () => {
174
+ settings: async ({ state: current }) => {
158
175
  const { showChromeDevtoolsBrowserSettings } = await import("./browser-settings-menu.js");
159
176
  if (!isCurrent()) return { kind: "stay" };
160
- const result = await showChromeDevtoolsBrowserSettings(ctx, generation);
177
+ const result = await showChromeDevtoolsBrowserSettings(pi, ctx, generation);
161
178
  if (!isCurrent()) return { kind: "stay" };
179
+ const refreshed = await loadChromeDevtoolsMenuSnapshot(pi, ctx);
180
+ if (!isCurrent()) return { kind: "stay" };
181
+ Object.assign(current, refreshed);
162
182
  return result.closeParent ? { kind: "close" } : { kind: "stay" };
163
183
  },
164
184
  },
@@ -204,9 +224,9 @@ export async function showChromeDevtoolsToolWorkflow(
204
224
  screens: {
205
225
  tools: ({ state: current }) => ({
206
226
  kind: "multiSelect",
207
- title: `Browser tools (${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length})`,
208
- lines: toolDraftLines(current),
209
- items: CHROME_DEVTOOLS_TOOL_NAMES.map((toolName) => ({
227
+ title: `Browser tools (${current.draft.size}/${availableCatalogNames(ctx.sessionManager).length})`,
228
+ lines: toolDraftLines(current, ctx.sessionManager),
229
+ items: availableCatalogNames(ctx.sessionManager).map((toolName) => ({
210
230
  id: toolName,
211
231
  label: TOOL_PRESENTATION[toolName].label,
212
232
  description: TOOL_PRESENTATION[toolName].description,
@@ -249,10 +269,10 @@ export async function showChromeDevtoolsToolWorkflow(
249
269
  kind: "review",
250
270
  title: "Review tool changes",
251
271
  lines: [
252
- `Currently available: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
253
- `Proposed availability: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
272
+ `Currently available: ${current.accepted.size}/${availableCatalogNames(ctx.sessionManager).length}`,
273
+ `Proposed availability: ${current.draft.size}/${availableCatalogNames(ctx.sessionManager).length}`,
254
274
  ],
255
- content: buildToolReview(current),
275
+ content: buildToolReview(current, ctx.sessionManager),
256
276
  format: { kind: "text" },
257
277
  viewportSize: "adaptive",
258
278
  ...(current.mutationBlockedReason || setsEqual(current.accepted, current.draft)
@@ -276,7 +296,7 @@ export async function showChromeDevtoolsToolWorkflow(
276
296
  return { kind: "stay" };
277
297
  },
278
298
  selectAll: ({ state: current }) => {
279
- current.draft = new Set(CHROME_DEVTOOLS_TOOL_NAMES);
299
+ current.draft = new Set(availableCatalogNames(ctx.sessionManager));
280
300
  return { kind: "stay" };
281
301
  },
282
302
  selectNone: ({ state: current }) => {
@@ -306,7 +326,7 @@ export async function showChromeDevtoolsToolWorkflow(
306
326
  current.draft = new Set(selectedTools);
307
327
  current.applied = true;
308
328
  ctx.ui.notify(
309
- `Saved: ${selectedTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} browser tools available to lazy-load.`,
329
+ `Saved: ${selectedTools.length} of ${availableCatalogNames(ctx.sessionManager).length} browser tools available.`,
310
330
  "info",
311
331
  );
312
332
  return { kind: "close" };
@@ -349,6 +369,17 @@ async function loadSnapshotWithFeedback(pi: ExtensionAPI, ctx: CommandContext, g
349
369
  return result.kind === "completed" ? result.value : undefined;
350
370
  }
351
371
 
372
+ export async function loadChromeDevtoolsMenuSnapshot(
373
+ pi: ExtensionAPI,
374
+ ctx: ExtensionCommandContext,
375
+ ) {
376
+ const settings = await loadSettings({
377
+ cwd: ctx.cwd,
378
+ projectTrusted: ctx.isProjectTrusted(),
379
+ });
380
+ return buildMenuSnapshot(pi, settings);
381
+ }
382
+
352
383
  function buildMenuSnapshot(pi: ExtensionAPI, settings: SettingsLoadResult): MenuSnapshot {
353
384
  const activeTools = activeChromeTools(pi);
354
385
  const persistedTools =
@@ -376,52 +407,56 @@ function buildMenuSnapshot(pi: ExtensionAPI, settings: SettingsLoadResult): Menu
376
407
  };
377
408
  }
378
409
 
379
- function mainStateLines(snapshot: MenuSnapshot) {
410
+ function mainStateLines(snapshot: MenuSnapshot, owner: object) {
411
+ const launchError = launchAttemptLines(owner).find((line) =>
412
+ line.startsWith("Last launch error:"),
413
+ );
380
414
  return sanitizeLines([
381
- `Lazy catalog: ${snapshot.activeTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} available · ${snapshot.persistenceLabel}`,
382
- `Browser: ${browserLifecycleSummary()}`,
383
- `Endpoint: ${devToolsEndpoint()}`,
415
+ `Tool catalog: ${snapshot.activeTools.length} of ${availableCatalogNames(owner).length} available · ${snapshot.persistenceLabel}`,
416
+ `WebMCP: ${webMcpEnabled(owner) ? "enabled · experimental · confirmation required for every call" : "disabled · experimental"}`,
417
+ `Browser: ${browserLifecycleSummary(owner)}`,
418
+ `Endpoint: ${devToolsEndpoint(owner)}`,
384
419
  ...(snapshot.settingsWarning ? [`Settings warning: ${snapshot.settingsWarning}`] : []),
385
- ...(state.lastLaunchAttempt?.lastError
386
- ? [`Launch warning: ${state.lastLaunchAttempt.lastError}`]
387
- : []),
420
+ ...(launchError ? [`Launch warning: ${launchError.slice("Last launch error: ".length)}`] : []),
388
421
  ]);
389
422
  }
390
423
 
391
- function browserLifecycleSummary() {
392
- const lifecycle = browserLifecycleState();
424
+ function browserLifecycleSummary(owner: object) {
425
+ const lifecycle = browserLifecycleState(owner);
393
426
  if (lifecycle === "starting") return "starting managed browser";
394
427
  if (lifecycle === "running") return "managed browser running";
395
428
  if (lifecycle === "exited") return "managed browser exited · open Browser status to recover";
396
429
  if (lifecycle === "failed") return "last launch failed · open Browser status to recover";
397
- const launchMode = launchModeLabel();
430
+ const launchMode = launchModeLabel(owner);
398
431
  return launchMode.startsWith("attach first")
399
432
  ? "not started · attaches or launches on first use"
400
433
  : `not started · ${launchMode}`;
401
434
  }
402
435
 
403
- function toolDraftLines(current: ToolWorkflowState) {
436
+ function toolDraftLines(current: ToolWorkflowState, owner: object) {
404
437
  if (current.mutationBlockedReason)
405
438
  return sanitizeLines([`Unavailable: ${current.mutationBlockedReason}`]);
406
439
  const changes = symmetricDifferenceSize(current.accepted, current.draft);
407
440
  return [
408
- `Currently available: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
441
+ `Currently available: ${current.accepted.size}/${availableCatalogNames(owner).length}`,
409
442
  changes === 0
410
443
  ? "No unapplied changes · Escape cancels"
411
444
  : `${changes} unapplied ${changes === 1 ? "change" : "changes"} · Escape cancels`,
445
+ `WebMCP gateways: ${webMcpEnabled(owner) ? "available for selection · experimental" : "hidden while experimental WebMCP is disabled"}`,
412
446
  "Changes are not applied until Review changes and Apply tool changes.",
413
447
  ];
414
448
  }
415
449
 
416
- function buildToolReview(current: ToolWorkflowState) {
417
- const available = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => current.draft.has(name));
418
- const unavailable = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => !current.draft.has(name));
450
+ function buildToolReview(current: ToolWorkflowState, owner: object) {
451
+ const catalog = availableCatalogNames(owner);
452
+ const available = catalog.filter((name) => current.draft.has(name));
453
+ const unavailable = catalog.filter((name) => !current.draft.has(name));
419
454
  return sanitizeChromeDevtoolsDisplay(
420
455
  [
421
- `Current available browser tools: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
422
- `Proposed available browser tools: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
456
+ `Current available browser tools: ${current.accepted.size}/${catalog.length}`,
457
+ `Proposed available browser tools: ${current.draft.size}/${catalog.length}`,
423
458
  "",
424
- "Available to lazy-load after apply:",
459
+ "Available after apply:",
425
460
  ...(available.length > 0
426
461
  ? available.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
427
462
  : [" - none"]),
@@ -432,7 +467,8 @@ function buildToolReview(current: ToolWorkflowState) {
432
467
  : [" - none"]),
433
468
  "",
434
469
  "Other active Pi tools remain unchanged.",
435
- "Newly available tools remain deferred until chrome_devtools_load selects them.",
470
+ "Native-capable models defer these tools until chrome_devtools_load selects them.",
471
+ "Other models expose available tools eagerly before the next model request.",
436
472
  "The accepted availability policy is saved for future sessions.",
437
473
  ].join("\n"),
438
474
  );
@@ -477,6 +513,12 @@ function sanitizeLines(lines: readonly string[]) {
477
513
  return sanitizeChromeDevtoolsDisplay(lines.join("\n")).split("\n");
478
514
  }
479
515
 
516
+ function availableCatalogNames(owner: object): ChromeDevToolsToolName[] {
517
+ return webMcpEnabled(owner)
518
+ ? [...CHROME_DEVTOOLS_TOOL_NAMES]
519
+ : [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
520
+ }
521
+
480
522
  function formatError(error: unknown) {
481
523
  return error instanceof Error ? error.message : String(error);
482
524
  }
package/src/render.ts CHANGED
@@ -2,12 +2,15 @@ import type { AgentToolResult, ToolRenderResultOptions } from "@earendil-works/p
2
2
 
3
3
  const STATUS_KEY = "chrome-devtools";
4
4
  interface StatusContext {
5
+ sessionManager: object;
5
6
  ui: { setStatus: (key: string, value: string | undefined) => void };
6
7
  }
7
8
  interface RenderTheme {
8
9
  bold(text: string): string;
9
10
  fg(color: string, text: string): string;
10
11
  }
12
+ const activeStatuses = new WeakMap<object, Array<{ status: string; token: symbol }>>();
13
+
11
14
  interface RenderComponent {
12
15
  invalidate(): void;
13
16
  render(width: number): string[];
@@ -101,10 +104,18 @@ export async function withStatus<T>(
101
104
  status: string,
102
105
  callback: () => Promise<T>,
103
106
  ) {
107
+ const token = Symbol(status);
108
+ const statuses = activeStatuses.get(ctx.sessionManager) ?? [];
109
+ statuses.push({ status, token });
110
+ activeStatuses.set(ctx.sessionManager, statuses);
104
111
  ctx.ui.setStatus(STATUS_KEY, status);
105
112
  try {
106
113
  return await callback();
107
114
  } finally {
108
- ctx.ui.setStatus(STATUS_KEY, undefined);
115
+ const index = statuses.findIndex((entry) => entry.token === token);
116
+ if (index >= 0) statuses.splice(index, 1);
117
+ const remaining = statuses.at(-1)?.status;
118
+ if (statuses.length === 0) activeStatuses.delete(ctx.sessionManager);
119
+ ctx.ui.setStatus(STATUS_KEY, remaining);
109
120
  }
110
121
  }