@narumitw/pi-chrome-devtools 0.52.3 → 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 +62 -9
- package/dist/chunks/{browser-settings-menu-VWCNG4HB.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-RH6KESL3.js → chunk-GVBXT3FB.js} +369 -514
- 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-PLD3S3EO.js → menu-UBHW6UP2.js} +72 -39
- 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 +158 -195
- 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 +51 -11
- package/src/lazy-tools.ts +37 -4
- package/src/menu.ts +80 -39
- 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 +67 -30
- 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-VWCNG4HB.js.map +0 -7
- package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
- package/dist/chunks/menu-PLD3S3EO.js.map +0 -7
package/src/chrome-devtools.ts
CHANGED
|
@@ -1,14 +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
8
|
requireEagerChromeDevtoolsToolExposure,
|
|
9
|
+
setChromeDevtoolsSessionOwner,
|
|
9
10
|
supportsNativeDeferredToolLoading,
|
|
10
11
|
} from "./lazy-tools.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
applyRuntimeBrowserSettings,
|
|
14
|
+
applyRuntimeWebMcpSetting,
|
|
15
|
+
invalidateWebMcpOperations,
|
|
16
|
+
setWebMcpSessionOwner,
|
|
17
|
+
state,
|
|
18
|
+
webMcpEnabled,
|
|
19
|
+
} from "./runtime.js";
|
|
12
20
|
import { loadSettings, waitForSettingsWrites } from "./settings.js";
|
|
13
21
|
import {
|
|
14
22
|
allChromeDevtoolsTools,
|
|
@@ -25,6 +33,8 @@ import {
|
|
|
25
33
|
navigateTool,
|
|
26
34
|
screenshotTool,
|
|
27
35
|
selectPageTool,
|
|
36
|
+
webMcpCallTool,
|
|
37
|
+
webMcpListToolsTool,
|
|
28
38
|
} from "./tools.js";
|
|
29
39
|
|
|
30
40
|
type CommandAction =
|
|
@@ -57,12 +67,15 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
57
67
|
pi.registerTool(navigateTool);
|
|
58
68
|
pi.registerTool(evaluateTool);
|
|
59
69
|
pi.registerTool(screenshotTool);
|
|
70
|
+
pi.registerTool(webMcpListToolsTool);
|
|
71
|
+
pi.registerTool(webMcpCallTool);
|
|
60
72
|
pi.registerTool(createChromeDevtoolsLoadTool(pi));
|
|
61
73
|
|
|
62
74
|
pi.registerCommand("chrome-devtools", {
|
|
63
75
|
description: "Open Chrome DevTools help and tool controls",
|
|
64
76
|
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
65
77
|
handler: async (args, ctx) => {
|
|
78
|
+
setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
|
|
66
79
|
initializeAvailableChromeDevtoolsTools(pi);
|
|
67
80
|
const generation = state.sessionGeneration;
|
|
68
81
|
await handleChromeDevtoolsCommand(pi, args, ctx, generation);
|
|
@@ -71,12 +84,19 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
71
84
|
|
|
72
85
|
pi.on("session_start", async (_event, ctx) => {
|
|
73
86
|
const generation = ++state.sessionGeneration;
|
|
87
|
+
setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
|
|
74
88
|
initializeAvailableChromeDevtoolsTools(pi);
|
|
89
|
+
setWebMcpSessionOwner(ctx.sessionManager);
|
|
75
90
|
replaceSessionController("Chrome DevTools session replaced");
|
|
91
|
+
invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools session replaced");
|
|
76
92
|
state.shuttingDown = false;
|
|
77
93
|
state.settingsNotice = undefined;
|
|
78
94
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
79
|
-
await shutdownManagedBrowser(
|
|
95
|
+
await shutdownManagedBrowser(undefined, {
|
|
96
|
+
cancelLaunch: true,
|
|
97
|
+
owner: ctx.sessionManager,
|
|
98
|
+
});
|
|
99
|
+
startManagedBrowserSession(ctx.sessionManager);
|
|
80
100
|
if (generation !== state.sessionGeneration) return;
|
|
81
101
|
state.activePageId = undefined;
|
|
82
102
|
state.lastLaunchAttempt = undefined;
|
|
@@ -84,10 +104,17 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
84
104
|
const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
85
105
|
if (generation !== state.sessionGeneration) return;
|
|
86
106
|
applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
|
|
107
|
+
applyRuntimeWebMcpSetting(settings.effectiveWebMcpEnabled, ctx.sessionManager);
|
|
87
108
|
state.settingsNotice = settings.notice;
|
|
88
109
|
for (const warning of settings.warnings) {
|
|
89
110
|
ctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), "warning");
|
|
90
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
|
+
}
|
|
91
118
|
const availableTools =
|
|
92
119
|
settings.kind === "loaded" && settings.settings.tools
|
|
93
120
|
? settings.settings.tools
|
|
@@ -95,7 +122,11 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
95
122
|
configureChromeDevtoolsToolExposure(pi, availableTools, ctx.model);
|
|
96
123
|
});
|
|
97
124
|
|
|
98
|
-
pi.on("model_select", (event) => {
|
|
125
|
+
pi.on("model_select", (event, ctx) => {
|
|
126
|
+
invalidateWebMcpOperations(
|
|
127
|
+
ctx.sessionManager,
|
|
128
|
+
"Chrome DevTools model and tool exposure changed",
|
|
129
|
+
);
|
|
99
130
|
if (!supportsNativeDeferredToolLoading(event.model)) {
|
|
100
131
|
requireEagerChromeDevtoolsToolExposure(pi);
|
|
101
132
|
}
|
|
@@ -104,8 +135,12 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
104
135
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
105
136
|
state.sessionGeneration += 1;
|
|
106
137
|
replaceSessionController("Chrome DevTools session shut down");
|
|
138
|
+
invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools session shut down");
|
|
107
139
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
108
|
-
const browserShutdown = shutdownManagedBrowser(undefined, {
|
|
140
|
+
const browserShutdown = shutdownManagedBrowser(undefined, {
|
|
141
|
+
cancelLaunch: true,
|
|
142
|
+
owner: ctx.sessionManager,
|
|
143
|
+
});
|
|
109
144
|
await waitForChromeDevtoolsSettings();
|
|
110
145
|
await waitForSettingsWrites();
|
|
111
146
|
await browserShutdown;
|
|
@@ -125,15 +160,15 @@ async function handleChromeDevtoolsCommand(
|
|
|
125
160
|
return;
|
|
126
161
|
case "help":
|
|
127
162
|
requireObservableUi(ctx, "help");
|
|
128
|
-
ctx.ui.notify(buildCommandGuide(), "info");
|
|
163
|
+
ctx.ui.notify(buildCommandGuide(ctx.sessionManager), "info");
|
|
129
164
|
return;
|
|
130
165
|
case "quickstart":
|
|
131
166
|
requireObservableUi(ctx, "quickstart");
|
|
132
|
-
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
167
|
+
ctx.ui.notify(buildQuickstartMessage(ctx.sessionManager), "info");
|
|
133
168
|
return;
|
|
134
169
|
case "status": {
|
|
135
170
|
requireObservableUi(ctx, "status");
|
|
136
|
-
const status = await buildToolStatusMessage(pi);
|
|
171
|
+
const status = await buildToolStatusMessage(pi, ctx.sessionManager);
|
|
137
172
|
if (generation !== state.sessionGeneration) return;
|
|
138
173
|
ctx.ui.notify(status, "info");
|
|
139
174
|
return;
|
|
@@ -144,7 +179,7 @@ async function handleChromeDevtoolsCommand(
|
|
|
144
179
|
}
|
|
145
180
|
const { showChromeDevtoolsBrowserSettings } = await import("./browser-settings-menu.js");
|
|
146
181
|
if (generation !== state.sessionGeneration) return;
|
|
147
|
-
await showChromeDevtoolsBrowserSettings(ctx, generation);
|
|
182
|
+
await showChromeDevtoolsBrowserSettings(pi, ctx, generation);
|
|
148
183
|
return;
|
|
149
184
|
}
|
|
150
185
|
case "tools": {
|
|
@@ -157,7 +192,12 @@ async function handleChromeDevtoolsCommand(
|
|
|
157
192
|
return;
|
|
158
193
|
}
|
|
159
194
|
case "enable":
|
|
160
|
-
await updateChromeDevtoolsTools(
|
|
195
|
+
await updateChromeDevtoolsTools(
|
|
196
|
+
pi,
|
|
197
|
+
ctx,
|
|
198
|
+
allChromeDevtoolsTools(ctx.sessionManager),
|
|
199
|
+
"made all available",
|
|
200
|
+
);
|
|
161
201
|
return;
|
|
162
202
|
case "disable":
|
|
163
203
|
await updateChromeDevtoolsTools(pi, ctx, [], "made all unavailable");
|
|
@@ -170,7 +210,7 @@ async function handleChromeDevtoolsCommand(
|
|
|
170
210
|
ctx.ui.notify(
|
|
171
211
|
`Unknown /chrome-devtools command: ${args.trim()}
|
|
172
212
|
|
|
173
|
-
${buildCommandGuide()}`,
|
|
213
|
+
${buildCommandGuide(ctx.sessionManager)}`,
|
|
174
214
|
"warning",
|
|
175
215
|
);
|
|
176
216
|
}
|
package/src/lazy-tools.ts
CHANGED
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import {
|
|
7
|
+
import { webMcpEnabled } from "./runtime.js";
|
|
8
|
+
import {
|
|
9
|
+
CHROME_DEVTOOLS_TOOL_NAMES,
|
|
10
|
+
type ChromeDevToolsToolName,
|
|
11
|
+
isWebMcpToolName,
|
|
12
|
+
} from "./tool-names.js";
|
|
8
13
|
|
|
9
14
|
export const CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
|
|
10
15
|
|
|
@@ -18,6 +23,7 @@ const availableToolsByApi =
|
|
|
18
23
|
existingAvailableToolsStore ?? new WeakMap<ExtensionAPI, Set<ChromeDevToolsToolName>>();
|
|
19
24
|
if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
|
|
20
25
|
const lazyExposureByApi = new WeakMap<ExtensionAPI, boolean>();
|
|
26
|
+
const sessionOwnerByApi = new WeakMap<ExtensionAPI, object>();
|
|
21
27
|
|
|
22
28
|
const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
|
|
23
29
|
chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
|
|
@@ -25,8 +31,16 @@ const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
|
|
|
25
31
|
chrome_devtools_navigate: "navigate open create chrome browser page url website",
|
|
26
32
|
chrome_devtools_evaluate: "evaluate run javascript expression dom inspect chrome browser page",
|
|
27
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",
|
|
28
38
|
};
|
|
29
39
|
|
|
40
|
+
export function setChromeDevtoolsSessionOwner(pi: ExtensionAPI, owner: object) {
|
|
41
|
+
sessionOwnerByApi.set(pi, owner);
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
export function initializeAvailableChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
31
45
|
if (availableToolsByApi.has(pi)) return;
|
|
32
46
|
const activeTools = new Set(pi.getActiveTools());
|
|
@@ -41,7 +55,8 @@ export function configureChromeDevtoolsToolExposure(
|
|
|
41
55
|
availableTools: readonly ChromeDevToolsToolName[],
|
|
42
56
|
model?: ExtensionContext["model"],
|
|
43
57
|
) {
|
|
44
|
-
|
|
58
|
+
setAvailableTools(pi, availableTools);
|
|
59
|
+
const available = effectiveAvailableTools(pi);
|
|
45
60
|
const lazyExposure = supportsNativeDeferredToolLoading(model);
|
|
46
61
|
lazyExposureByApi.set(pi, lazyExposure);
|
|
47
62
|
const exposedTools = lazyExposure
|
|
@@ -66,7 +81,8 @@ export function applyAvailableChromeDevtoolsTools(
|
|
|
66
81
|
pi: ExtensionAPI,
|
|
67
82
|
availableTools: readonly ChromeDevToolsToolName[],
|
|
68
83
|
) {
|
|
69
|
-
|
|
84
|
+
setAvailableTools(pi, availableTools);
|
|
85
|
+
const available = effectiveAvailableTools(pi);
|
|
70
86
|
const lazyExposure = lazyExposureByApi.get(pi) === true;
|
|
71
87
|
const active = pi
|
|
72
88
|
.getActiveTools()
|
|
@@ -110,6 +126,11 @@ export function supportsNativeDeferredToolLoading(model: ExtensionContext["model
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
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) {
|
|
113
134
|
const available = availableToolsByApi.get(pi) ?? new Set();
|
|
114
135
|
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
115
136
|
}
|
|
@@ -137,8 +158,9 @@ export function createChromeDevtoolsLoadTool(pi: ExtensionAPI) {
|
|
|
137
158
|
}),
|
|
138
159
|
),
|
|
139
160
|
}),
|
|
140
|
-
async execute(_toolCallId, params, signal) {
|
|
161
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
141
162
|
signal?.throwIfAborted();
|
|
163
|
+
setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
|
|
142
164
|
const available = new Set(availableChromeDevtoolsTools(pi));
|
|
143
165
|
const matches = matchChromeDevtoolsTools(params.query, params.limit ?? 3, available);
|
|
144
166
|
const active = pi.getActiveTools();
|
|
@@ -190,6 +212,17 @@ function setAvailableTools(pi: ExtensionAPI, availableTools: readonly ChromeDevT
|
|
|
190
212
|
return available;
|
|
191
213
|
}
|
|
192
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
|
+
|
|
193
226
|
function compatBoolean(value: unknown, key: string) {
|
|
194
227
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
195
228
|
const record = value as Record<string, unknown>;
|
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,50 +407,54 @@ 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
|
-
`Tool catalog: ${snapshot.activeTools.length} of ${
|
|
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
459
|
"Available after apply:",
|
|
425
460
|
...(available.length > 0
|
|
@@ -478,6 +513,12 @@ function sanitizeLines(lines: readonly string[]) {
|
|
|
478
513
|
return sanitizeChromeDevtoolsDisplay(lines.join("\n")).split("\n");
|
|
479
514
|
}
|
|
480
515
|
|
|
516
|
+
function availableCatalogNames(owner: object): ChromeDevToolsToolName[] {
|
|
517
|
+
return webMcpEnabled(owner)
|
|
518
|
+
? [...CHROME_DEVTOOLS_TOOL_NAMES]
|
|
519
|
+
: [...CORE_CHROME_DEVTOOLS_TOOL_NAMES];
|
|
520
|
+
}
|
|
521
|
+
|
|
481
522
|
function formatError(error: unknown) {
|
|
482
523
|
return error instanceof Error ? error.message : String(error);
|
|
483
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
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface ManagedBrowser {
|
|
|
57
57
|
exited: boolean;
|
|
58
58
|
ready: boolean;
|
|
59
59
|
ownerGeneration: number;
|
|
60
|
+
sessionOwner?: object;
|
|
60
61
|
cleanupPromise?: Promise<void>;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -98,6 +99,97 @@ export const state: ChromeDevToolsState = {
|
|
|
98
99
|
sessionController: new AbortController(),
|
|
99
100
|
};
|
|
100
101
|
|
|
102
|
+
interface WebMcpSessionRuntime {
|
|
103
|
+
controller: AbortController;
|
|
104
|
+
enabled: boolean;
|
|
105
|
+
controllers: Set<AbortController>;
|
|
106
|
+
generation: number;
|
|
107
|
+
sessionGeneration: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const webMcpRuntimeByOwner = new WeakMap<object, WebMcpSessionRuntime>();
|
|
111
|
+
|
|
112
|
+
export function setWebMcpSessionOwner(owner: object) {
|
|
113
|
+
const runtime = webMcpRuntime(owner);
|
|
114
|
+
runtime.enabled = false;
|
|
115
|
+
runtime.sessionGeneration += 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function beginWebMcpOperation(owner: object, toolSignal?: AbortSignal) {
|
|
119
|
+
const runtime = webMcpRuntime(owner);
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
runtime.controllers.add(controller);
|
|
122
|
+
const signals = [controller.signal, runtime.controller.signal];
|
|
123
|
+
if (toolSignal) signals.push(toolSignal);
|
|
124
|
+
const signal = AbortSignal.any(signals);
|
|
125
|
+
return {
|
|
126
|
+
signal,
|
|
127
|
+
owner,
|
|
128
|
+
sessionGeneration: runtime.sessionGeneration,
|
|
129
|
+
webMcpGeneration: runtime.generation,
|
|
130
|
+
dispose() {
|
|
131
|
+
runtime.controllers.delete(controller);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function invalidateWebMcpOperations(owner: object, reason: string) {
|
|
137
|
+
const runtime = webMcpRuntime(owner);
|
|
138
|
+
runtime.generation += 1;
|
|
139
|
+
const controllers = [...runtime.controllers];
|
|
140
|
+
runtime.controllers.clear();
|
|
141
|
+
const error = new DOMException(reason, "AbortError");
|
|
142
|
+
runtime.controller.abort(error);
|
|
143
|
+
runtime.controller = new AbortController();
|
|
144
|
+
for (const controller of controllers) controller.abort(error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function currentWebMcpGeneration(owner: object) {
|
|
148
|
+
return webMcpRuntime(owner).generation;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function webMcpSessionSignal(owner: object) {
|
|
152
|
+
return webMcpRuntime(owner).controller.signal;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function applyRuntimeWebMcpSetting(enabled: boolean, owner: object) {
|
|
156
|
+
const runtime = webMcpRuntime(owner);
|
|
157
|
+
if (runtime.enabled !== enabled) {
|
|
158
|
+
invalidateWebMcpOperations(owner, `WebMCP ${enabled ? "enabled" : "disabled"}`);
|
|
159
|
+
}
|
|
160
|
+
runtime.enabled = enabled;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function webMcpEnabled(owner: object) {
|
|
164
|
+
return webMcpRuntime(owner).enabled;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function webMcpOperationIsCurrent(operation: {
|
|
168
|
+
owner: object;
|
|
169
|
+
sessionGeneration: number;
|
|
170
|
+
webMcpGeneration: number;
|
|
171
|
+
}) {
|
|
172
|
+
const runtime = webMcpRuntime(operation.owner);
|
|
173
|
+
return (
|
|
174
|
+
operation.sessionGeneration === runtime.sessionGeneration &&
|
|
175
|
+
operation.webMcpGeneration === runtime.generation
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function webMcpRuntime(owner: object) {
|
|
180
|
+
const existing = webMcpRuntimeByOwner.get(owner);
|
|
181
|
+
if (existing) return existing;
|
|
182
|
+
const created: WebMcpSessionRuntime = {
|
|
183
|
+
controller: new AbortController(),
|
|
184
|
+
controllers: new Set(),
|
|
185
|
+
enabled: false,
|
|
186
|
+
generation: 0,
|
|
187
|
+
sessionGeneration: 0,
|
|
188
|
+
};
|
|
189
|
+
webMcpRuntimeByOwner.set(owner, created);
|
|
190
|
+
return created;
|
|
191
|
+
}
|
|
192
|
+
|
|
101
193
|
export function applyRuntimeBrowserSettings(
|
|
102
194
|
browser: EffectiveBrowserSettings,
|
|
103
195
|
paths: { user: string; project?: string },
|