@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.
- package/README.md +78 -17
- package/dist/chunks/{browser-settings-menu-VI2I3TXI.js → browser-settings-menu-N5O65I7S.js} +81 -7
- package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
- package/dist/chunks/chunk-B7B63PWB.js +465 -0
- package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
- package/dist/chunks/{chunk-CC37WUUE.js → chunk-GVBXT3FB.js} +369 -461
- package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
- package/dist/chunks/chunk-RTECHAEZ.js +373 -0
- package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
- package/dist/chunks/{menu-LLR455UX.js → menu-UBHW6UP2.js} +75 -41
- package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
- package/dist/chunks/tools-V7OR5KJ3.js +945 -0
- package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
- package/dist/index.ts +166 -196
- package/dist/index.ts.map +4 -4
- package/package.json +4 -3
- package/src/browser-manager.ts +282 -109
- package/src/browser-settings-menu.ts +98 -4
- package/src/cdp-client.ts +329 -83
- package/src/chrome-devtools.ts +61 -13
- package/src/lazy-tools.ts +105 -8
- package/src/menu.ts +83 -41
- package/src/render.ts +12 -1
- package/src/runtime.ts +92 -0
- package/src/settings.ts +47 -10
- package/src/tool-names.ts +16 -1
- package/src/tool-selector.ts +79 -36
- package/src/tools.ts +86 -10
- package/src/webmcp/discovery.ts +235 -0
- package/src/webmcp/policy.ts +402 -0
- package/src/webmcp/protocol.ts +414 -0
- package/src/webmcp/tools.ts +190 -0
- package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +0 -7
- package/dist/chunks/chunk-CC37WUUE.js.map +0 -7
- package/dist/chunks/menu-LLR455UX.js.map +0 -7
package/src/chrome-devtools.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
+
configureChromeDevtoolsToolExposure,
|
|
6
6
|
createChromeDevtoolsLoadTool,
|
|
7
7
|
initializeAvailableChromeDevtoolsTools,
|
|
8
|
+
requireEagerChromeDevtoolsToolExposure,
|
|
9
|
+
setChromeDevtoolsSessionOwner,
|
|
10
|
+
supportsNativeDeferredToolLoading,
|
|
8
11
|
} from "./lazy-tools.js";
|
|
9
|
-
import {
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 ===
|
|
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 ===
|
|
92
|
-
?
|
|
93
|
-
:
|
|
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 ===
|
|
163
|
+
current.activeTools.length === availableCatalogNames(ctx.sessionManager).length
|
|
147
164
|
? []
|
|
148
|
-
:
|
|
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}/${
|
|
208
|
-
lines: toolDraftLines(current),
|
|
209
|
-
items:
|
|
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}/${
|
|
253
|
-
`Proposed availability: ${current.draft.size}/${
|
|
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(
|
|
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 ${
|
|
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
|
-
`
|
|
382
|
-
`
|
|
383
|
-
`
|
|
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
|
-
...(
|
|
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}/${
|
|
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
|
|
418
|
-
const
|
|
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}/${
|
|
422
|
-
`Proposed available browser tools: ${current.draft.size}/${
|
|
456
|
+
`Current available browser tools: ${current.accepted.size}/${catalog.length}`,
|
|
457
|
+
`Proposed available browser tools: ${current.draft.size}/${catalog.length}`,
|
|
423
458
|
"",
|
|
424
|
-
"Available
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
}
|