@narumitw/pi-chrome-devtools 0.50.1 β 0.51.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 +49 -26
- package/package.json +1 -1
- package/src/chrome-devtools.ts +20 -9
- package/src/lazy-tools.ts +143 -0
- package/src/menu.ts +24 -23
- package/src/tool-selector.ts +42 -30
- package/src/tools.ts +0 -5
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@ This package is inspired by [`chrome-devtools-mcp`](https://github.com/ChromeDev
|
|
|
26
26
|
explicit endpoint overrides.
|
|
27
27
|
- Retries briefly while Chrome is starting and reports actionable endpoint errors.
|
|
28
28
|
- Shows statusline activity only while Chrome DevTools tools are running.
|
|
29
|
-
-
|
|
30
|
-
-
|
|
29
|
+
- Keeps one loader tool active and exposes matching browser capabilities only when the agent needs them.
|
|
30
|
+
- Provides a state-first `/chrome-devtools` menu for tool availability, browser status, setup, and help.
|
|
31
|
+
- Stages menu-based availability changes for exact review before one confirmed apply.
|
|
31
32
|
- Uses `@narumitw/pi-tui-kit` for width-safe TUI menus and equivalent RPC dialogs.
|
|
32
|
-
- Persists the
|
|
33
|
+
- Persists the Chrome DevTools lazy-load catalog across Pi restarts.
|
|
33
34
|
|
|
34
35
|
## π¦ Install
|
|
35
36
|
|
|
@@ -143,12 +144,31 @@ removes their temporary profiles. It never closes user-started browsers or remot
|
|
|
143
144
|
|
|
144
145
|
## π οΈ Pi tools
|
|
145
146
|
|
|
147
|
+
- `chrome_devtools_load` β find and load browser capabilities relevant to a task.
|
|
146
148
|
- `chrome_devtools_list_pages` β list inspectable Chrome tabs/pages.
|
|
147
149
|
- `chrome_devtools_select_page` β select the active page for later tool calls.
|
|
148
150
|
- `chrome_devtools_navigate` β navigate a page to a URL; if no page exists, create one first.
|
|
149
151
|
- `chrome_devtools_evaluate` β evaluate JavaScript in the selected page.
|
|
150
152
|
- `chrome_devtools_screenshot` β capture a PNG screenshot and save it as a PNG file.
|
|
151
153
|
|
|
154
|
+
### Lazy tool loading
|
|
155
|
+
|
|
156
|
+
All six tools are registered, but only `chrome_devtools_load` starts active for this extension.
|
|
157
|
+
|
|
158
|
+
The loader accepts a task-oriented `query`, matches it against the five capability tools, and adds matching available tools without removing any active Pi tool.
|
|
159
|
+
|
|
160
|
+
Loaded capability tools remain active for the rest of the session unless the user makes them unavailable through `/chrome-devtools`.
|
|
161
|
+
|
|
162
|
+
Pi uses native deferred tool references on compatible Anthropic and OpenAI models.
|
|
163
|
+
|
|
164
|
+
Other models receive Pi's safe fallback: the newly active definitions appear in the normal tool list on the next model request.
|
|
165
|
+
|
|
166
|
+
The capability tools omit active-only prompt snippets so a lazy load does not rebuild the system prompt prefix.
|
|
167
|
+
|
|
168
|
+
The saved `tools` array controls which capabilities the loader may expose.
|
|
169
|
+
|
|
170
|
+
An empty array leaves the loader active but makes every browser capability unavailable.
|
|
171
|
+
|
|
152
172
|
### Screenshot files
|
|
153
173
|
|
|
154
174
|
`chrome_devtools_screenshot` always saves the captured PNG to disk. If `savePath` is omitted,
|
|
@@ -182,14 +202,14 @@ to read the saved path, for example `read({ path: "artifacts/homepage.png" })`.
|
|
|
182
202
|
/chrome-devtools
|
|
183
203
|
```
|
|
184
204
|
|
|
185
|
-
Opens a menu that shows the
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
Opens a menu that shows the lazy catalog size, whether that catalog is saved, the configured
|
|
206
|
+
endpoint, the observed managed-browser state, and any settings or launch warning before you choose
|
|
207
|
+
an action. The five actions stay on one level:
|
|
188
208
|
|
|
189
|
-
- **Choose browser toolsβ¦** β stage any combination of the five capabilities, then review
|
|
190
|
-
|
|
191
|
-
- **
|
|
192
|
-
bulk change before applying it.
|
|
209
|
+
- **Choose available browser toolsβ¦** β stage any combination of the five capabilities, then review
|
|
210
|
+
the exact available/unavailable result before selecting **Apply tool changes**.
|
|
211
|
+
- **Make all browser tools availableβ¦** or **Make all browser tools unavailableβ¦** β preview the
|
|
212
|
+
context-appropriate bulk change before applying it.
|
|
193
213
|
- **Browser status** β inspect runtime, endpoint, launch mode, and the last launch attempt without
|
|
194
214
|
probing the endpoint or starting Chrome.
|
|
195
215
|
- **Settings & setup** β inspect effective files, sources, trust, reload steps, and recovery guidance.
|
|
@@ -200,8 +220,8 @@ friendly task labels while retaining their raw `chrome_devtools_*` identity in t
|
|
|
200
220
|
Toggles remain a command-local draft. **Review changes** previews the exact effect, **Apply tool
|
|
201
221
|
changes** saves it, and Cancel, Escape, Ctrl+C, disposal, or session replacement discards an
|
|
202
222
|
unconfirmed draft without changing runtime tools or settings. A failed apply restores the previous
|
|
203
|
-
|
|
204
|
-
recover.
|
|
223
|
+
availability and loaded-tool state, preserves the settings file, retains the draft for retry, and
|
|
224
|
+
reports how to recover.
|
|
205
225
|
|
|
206
226
|
Direct subcommands are also available:
|
|
207
227
|
|
|
@@ -221,14 +241,15 @@ Compatibility aliases remain available: `toggle` and `select` mean `tools`, `on`
|
|
|
221
241
|
- `help` shows command usage.
|
|
222
242
|
- `quickstart` shows the configured CDP endpoint, endpoint source, auto-launch mode, browser
|
|
223
243
|
candidates, last launch attempt, and launch hints.
|
|
224
|
-
- `status` shows
|
|
225
|
-
launch mode, last launch attempt, and active non-Chrome tool
|
|
226
|
-
|
|
244
|
+
- `status` shows available and loaded capability counts, loader state, the persisted catalog,
|
|
245
|
+
settings file path, endpoint source, launch mode, last launch attempt, and active non-Chrome tool
|
|
246
|
+
count.
|
|
247
|
+
- `tools` opens the same staged, width-safe availability and review flow used by the menu.
|
|
227
248
|
- `toggle` and `select` are compatibility aliases for `tools`.
|
|
228
|
-
- `enable`
|
|
249
|
+
- `enable` makes all five capability tools available to the loader and saves that catalog; `on` is a
|
|
229
250
|
compatibility alias.
|
|
230
|
-
- `disable`
|
|
231
|
-
compatibility alias. The slash command
|
|
251
|
+
- `disable` makes all five capability tools unavailable and saves the empty catalog; `off` is a
|
|
252
|
+
compatibility alias. The slash command and `chrome_devtools_load` remain available.
|
|
232
253
|
|
|
233
254
|
The menu, `tools`, `help`, `quickstart`, and `status` require TUI or RPC mode so their result is
|
|
234
255
|
observable. TUI uses keyboard navigation and injected Pi keybindings; RPC receives equivalent
|
|
@@ -236,19 +257,20 @@ standard dialogs. In print and JSON modes, interactive and informational routes
|
|
|
236
257
|
instead of silently opening unavailable UI. The immediate `enable`/`disable` routes remain available
|
|
237
258
|
for deterministic non-interactive use.
|
|
238
259
|
|
|
239
|
-
The
|
|
260
|
+
The available capability names are saved to:
|
|
240
261
|
|
|
241
262
|
```text
|
|
242
263
|
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json
|
|
243
264
|
```
|
|
244
265
|
|
|
245
|
-
When the file is missing or invalid, the extension preserves Pi's current
|
|
246
|
-
instead of
|
|
247
|
-
`/reload
|
|
248
|
-
|
|
249
|
-
valid document, and preserve unknown fields. Malformed
|
|
250
|
-
mutation unavailable and block direct saves without
|
|
251
|
-
|
|
266
|
+
When the file is missing or invalid, the extension preserves Pi's current Chrome DevTools
|
|
267
|
+
availability policy instead of replacing it. A valid saved catalog is restored on Pi startup and
|
|
268
|
+
`/reload`, while its capability definitions remain deferred. A missing file is created by the first
|
|
269
|
+
confirmed menu apply or successful direct availability change. Within one Pi process, catalog saves
|
|
270
|
+
run in invocation order, reread the latest valid document, and preserve unknown fields. Malformed
|
|
271
|
+
JSON or invalid recognized fields make menu mutation unavailable and block direct saves without
|
|
272
|
+
replacement; a failed save restores the prior Chrome DevTools availability and loaded-tool state
|
|
273
|
+
while preserving other extensions' current tools.
|
|
252
274
|
|
|
253
275
|
Compatibility: older versions used `pi-chrome-devtools-settings.json`. A legacy-only file remains
|
|
254
276
|
readable with a warning and is never modified automatically; rename it to
|
|
@@ -271,6 +293,7 @@ packages/pi-chrome-devtools/
|
|
|
271
293
|
βββ src/
|
|
272
294
|
β βββ index.ts # Pi package entrypoint
|
|
273
295
|
β βββ chrome-devtools.ts # Extension registration and command orchestration
|
|
296
|
+
β βββ lazy-tools.ts # Deferred capability catalog and loader tool
|
|
274
297
|
β βββ *.ts # Package-local browser, CDP, tool, and storage modules
|
|
275
298
|
βββ README.md
|
|
276
299
|
βββ LICENSE
|
package/package.json
CHANGED
package/src/chrome-devtools.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { shutdownManagedBrowser } from "./browser-manager.js";
|
|
3
|
+
import {
|
|
4
|
+
availableChromeDevtoolsTools,
|
|
5
|
+
configureLazyChromeDevtoolsTools,
|
|
6
|
+
createChromeDevtoolsLoadTool,
|
|
7
|
+
initializeAvailableChromeDevtoolsTools,
|
|
8
|
+
} from "./lazy-tools.js";
|
|
3
9
|
import { applyRuntimeBrowserSettings, state } from "./runtime.js";
|
|
4
10
|
import { loadSettings } from "./settings.js";
|
|
5
11
|
import {
|
|
6
12
|
allChromeDevtoolsTools,
|
|
7
|
-
applyChromeDevtoolsTools,
|
|
8
13
|
buildCommandGuide,
|
|
9
14
|
buildQuickstartMessage,
|
|
10
15
|
buildToolStatusMessage,
|
|
@@ -27,12 +32,12 @@ const COMMAND_COMPLETIONS = [
|
|
|
27
32
|
{ value: "help", label: "help", description: "Show command usage" },
|
|
28
33
|
{ value: "quickstart", label: "quickstart", description: "Show endpoint and launch help" },
|
|
29
34
|
{ value: "status", label: "status", description: "Show tool and settings status" },
|
|
30
|
-
{ value: "tools", label: "tools", description: "
|
|
35
|
+
{ value: "tools", label: "tools", description: "Choose lazy-loadable Chrome DevTools tools" },
|
|
31
36
|
{ value: "toggle", label: "toggle", description: "Alias for tools" },
|
|
32
37
|
{ value: "select", label: "select", description: "Compatibility alias for tools" },
|
|
33
|
-
{ value: "enable", label: "enable", description: "
|
|
38
|
+
{ value: "enable", label: "enable", description: "Make all Chrome DevTools tools available" },
|
|
34
39
|
{ value: "on", label: "on", description: "Compatibility alias for enable" },
|
|
35
|
-
{ value: "disable", label: "disable", description: "
|
|
40
|
+
{ value: "disable", label: "disable", description: "Make all Chrome DevTools tools unavailable" },
|
|
36
41
|
{ value: "off", label: "off", description: "Compatibility alias for disable" },
|
|
37
42
|
];
|
|
38
43
|
export default function chromeDevtools(pi: ExtensionAPI) {
|
|
@@ -41,11 +46,13 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
41
46
|
pi.registerTool(navigateTool);
|
|
42
47
|
pi.registerTool(evaluateTool);
|
|
43
48
|
pi.registerTool(screenshotTool);
|
|
49
|
+
pi.registerTool(createChromeDevtoolsLoadTool(pi));
|
|
44
50
|
|
|
45
51
|
pi.registerCommand("chrome-devtools", {
|
|
46
52
|
description: "Open Chrome DevTools help and tool controls",
|
|
47
53
|
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
48
54
|
handler: async (args, ctx) => {
|
|
55
|
+
initializeAvailableChromeDevtoolsTools(pi);
|
|
49
56
|
const generation = state.sessionGeneration;
|
|
50
57
|
await handleChromeDevtoolsCommand(pi, args, ctx, generation);
|
|
51
58
|
},
|
|
@@ -53,6 +60,7 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
53
60
|
|
|
54
61
|
pi.on("session_start", async (_event, ctx) => {
|
|
55
62
|
const generation = ++state.sessionGeneration;
|
|
63
|
+
initializeAvailableChromeDevtoolsTools(pi);
|
|
56
64
|
replaceSessionController("Chrome DevTools session replaced");
|
|
57
65
|
state.shuttingDown = false;
|
|
58
66
|
state.settingsNotice = undefined;
|
|
@@ -69,9 +77,11 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
69
77
|
for (const warning of settings.warnings) {
|
|
70
78
|
ctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), "warning");
|
|
71
79
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
const availableTools =
|
|
81
|
+
settings.kind === "loaded" && settings.settings.tools
|
|
82
|
+
? settings.settings.tools
|
|
83
|
+
: availableChromeDevtoolsTools(pi);
|
|
84
|
+
configureLazyChromeDevtoolsTools(pi, availableTools);
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
@@ -120,10 +130,10 @@ async function handleChromeDevtoolsCommand(
|
|
|
120
130
|
return;
|
|
121
131
|
}
|
|
122
132
|
case "enable":
|
|
123
|
-
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "
|
|
133
|
+
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "made all available");
|
|
124
134
|
return;
|
|
125
135
|
case "disable":
|
|
126
|
-
await updateChromeDevtoolsTools(pi, ctx, [], "
|
|
136
|
+
await updateChromeDevtoolsTools(pi, ctx, [], "made all unavailable");
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
139
|
|
|
@@ -185,6 +195,7 @@ export {
|
|
|
185
195
|
isLocalDevToolsHost,
|
|
186
196
|
quoteCommandPart,
|
|
187
197
|
} from "./browser-manager.js";
|
|
198
|
+
export { CHROME_DEVTOOLS_LOAD_TOOL_NAME } from "./lazy-tools.js";
|
|
188
199
|
export { parseConfiguredPort } from "./runtime.js";
|
|
189
200
|
export {
|
|
190
201
|
hasParentPathSegment,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
4
|
+
|
|
5
|
+
export const CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
|
|
6
|
+
|
|
7
|
+
const AVAILABLE_TOOLS_STORE = Symbol.for("@narumitw/pi-chrome-devtools.available-tools-store");
|
|
8
|
+
type ChromeDevtoolsGlobal = typeof globalThis & {
|
|
9
|
+
[AVAILABLE_TOOLS_STORE]?: WeakMap<ExtensionAPI, Set<ChromeDevToolsToolName>>;
|
|
10
|
+
};
|
|
11
|
+
const sharedGlobal = globalThis as ChromeDevtoolsGlobal;
|
|
12
|
+
const existingAvailableToolsStore = sharedGlobal[AVAILABLE_TOOLS_STORE];
|
|
13
|
+
const availableToolsByApi =
|
|
14
|
+
existingAvailableToolsStore ?? new WeakMap<ExtensionAPI, Set<ChromeDevToolsToolName>>();
|
|
15
|
+
if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
|
|
16
|
+
|
|
17
|
+
const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
|
|
18
|
+
chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
|
|
19
|
+
chrome_devtools_select_page: "select choose active chrome browser page tab target",
|
|
20
|
+
chrome_devtools_navigate: "navigate open create chrome browser page url website",
|
|
21
|
+
chrome_devtools_evaluate: "evaluate run javascript expression dom inspect chrome browser page",
|
|
22
|
+
chrome_devtools_screenshot: "capture screenshot png image visual chrome browser page",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function initializeAvailableChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
26
|
+
if (availableToolsByApi.has(pi)) return;
|
|
27
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
28
|
+
setAvailableTools(
|
|
29
|
+
pi,
|
|
30
|
+
CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => activeTools.has(name)),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function configureLazyChromeDevtoolsTools(
|
|
35
|
+
pi: ExtensionAPI,
|
|
36
|
+
availableTools: readonly ChromeDevToolsToolName[],
|
|
37
|
+
) {
|
|
38
|
+
setAvailableTools(pi, availableTools);
|
|
39
|
+
const nonCapabilityTools = pi
|
|
40
|
+
.getActiveTools()
|
|
41
|
+
.filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
|
|
42
|
+
pi.setActiveTools(unique([...nonCapabilityTools, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function applyAvailableChromeDevtoolsTools(
|
|
46
|
+
pi: ExtensionAPI,
|
|
47
|
+
availableTools: readonly ChromeDevToolsToolName[],
|
|
48
|
+
) {
|
|
49
|
+
const available = setAvailableTools(pi, availableTools);
|
|
50
|
+
const active = pi
|
|
51
|
+
.getActiveTools()
|
|
52
|
+
.filter(
|
|
53
|
+
(name) =>
|
|
54
|
+
!CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName) ||
|
|
55
|
+
available.has(name as ChromeDevToolsToolName),
|
|
56
|
+
);
|
|
57
|
+
pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function availableChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
61
|
+
const available = availableToolsByApi.get(pi) ?? new Set();
|
|
62
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createChromeDevtoolsLoadTool(pi: ExtensionAPI) {
|
|
66
|
+
return defineTool({
|
|
67
|
+
name: CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
68
|
+
label: "Chrome DevTools: Load Tools",
|
|
69
|
+
description:
|
|
70
|
+
"Find and enable Chrome DevTools browser tools relevant to a task. Loaded tools remain available for the session.",
|
|
71
|
+
promptSnippet: "Load Chrome DevTools browser capabilities on demand",
|
|
72
|
+
promptGuidelines: [
|
|
73
|
+
"Use chrome_devtools_load when a task requires inspecting or controlling a Chrome browser and the needed chrome_devtools_* capability is not active.",
|
|
74
|
+
],
|
|
75
|
+
parameters: Type.Object({
|
|
76
|
+
query: Type.String({
|
|
77
|
+
description: "Browser capability or task to find tools for.",
|
|
78
|
+
maxLength: 500,
|
|
79
|
+
}),
|
|
80
|
+
limit: Type.Optional(
|
|
81
|
+
Type.Integer({
|
|
82
|
+
description: "Maximum tools to load. Defaults to 3.",
|
|
83
|
+
minimum: 1,
|
|
84
|
+
maximum: 5,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
async execute(_toolCallId, params, signal) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
const available = new Set(availableChromeDevtoolsTools(pi));
|
|
91
|
+
const matches = matchChromeDevtoolsTools(params.query, params.limit ?? 3, available);
|
|
92
|
+
const active = pi.getActiveTools();
|
|
93
|
+
const activeSet = new Set(active);
|
|
94
|
+
const added = matches.filter((name) => !activeSet.has(name));
|
|
95
|
+
if (added.length > 0) {
|
|
96
|
+
pi.setActiveTools(unique([...active, ...added]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const text =
|
|
100
|
+
matches.length === 0
|
|
101
|
+
? "No available Chrome DevTools tools matched the query."
|
|
102
|
+
: added.length > 0
|
|
103
|
+
? `Loaded Chrome DevTools tools: ${added.join(", ")}`
|
|
104
|
+
: `Matching Chrome DevTools tools are already loaded: ${matches.join(", ")}`;
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text" as const, text }],
|
|
107
|
+
details: { matches, added },
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function matchChromeDevtoolsTools(
|
|
114
|
+
query: string,
|
|
115
|
+
limit: number,
|
|
116
|
+
available: ReadonlySet<ChromeDevToolsToolName>,
|
|
117
|
+
) {
|
|
118
|
+
const terms = query
|
|
119
|
+
.toLowerCase()
|
|
120
|
+
.split(/[^a-z0-9]+/)
|
|
121
|
+
.filter((term) => term.length >= 2);
|
|
122
|
+
if (terms.length === 0) return [];
|
|
123
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name))
|
|
124
|
+
.map((name, index) => ({
|
|
125
|
+
name,
|
|
126
|
+
index,
|
|
127
|
+
score: terms.reduce((score, term) => score + (SEARCH_TEXT[name].includes(term) ? 1 : 0), 0),
|
|
128
|
+
}))
|
|
129
|
+
.filter((match) => match.score > 0)
|
|
130
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
131
|
+
.slice(0, limit)
|
|
132
|
+
.map((match) => match.name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function setAvailableTools(pi: ExtensionAPI, availableTools: readonly ChromeDevToolsToolName[]) {
|
|
136
|
+
const available = new Set(availableTools);
|
|
137
|
+
availableToolsByApi.set(pi, available);
|
|
138
|
+
return available;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unique(values: readonly string[]) {
|
|
142
|
+
return [...new Set(values)];
|
|
143
|
+
}
|
package/src/menu.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { defineMenu, runMenu, runTask } from "@narumitw/pi-tui-kit";
|
|
3
3
|
import { browserLifecycleState, devToolsEndpoint, launchModeLabel } from "./browser-manager.js";
|
|
4
|
+
import { availableChromeDevtoolsTools } from "./lazy-tools.js";
|
|
4
5
|
import { state } from "./runtime.js";
|
|
5
6
|
import { loadSettings, type SettingsLoadResult } from "./settings.js";
|
|
6
7
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
@@ -75,8 +76,8 @@ export async function showChromeDevtoolsMenu(
|
|
|
75
76
|
items: [
|
|
76
77
|
{
|
|
77
78
|
id: "tools",
|
|
78
|
-
label: "Choose browser toolsβ¦",
|
|
79
|
-
description: "
|
|
79
|
+
label: "Choose available browser toolsβ¦",
|
|
80
|
+
description: "Choose which capabilities the loader may expose.",
|
|
80
81
|
disabled: Boolean(current.mutationBlockedReason),
|
|
81
82
|
disabledReason: current.mutationBlockedReason,
|
|
82
83
|
action: "tools",
|
|
@@ -85,8 +86,8 @@ export async function showChromeDevtoolsMenu(
|
|
|
85
86
|
id: "bulk",
|
|
86
87
|
label:
|
|
87
88
|
current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
88
|
-
? "
|
|
89
|
-
: "
|
|
89
|
+
? "Make all browser tools unavailableβ¦"
|
|
90
|
+
: "Make all browser tools availableβ¦",
|
|
90
91
|
description:
|
|
91
92
|
current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
92
93
|
? "Preview 0 of 5; other active tools stay enabled."
|
|
@@ -250,8 +251,8 @@ export async function showChromeDevtoolsToolWorkflow(
|
|
|
250
251
|
kind: "review",
|
|
251
252
|
title: "Review tool changes",
|
|
252
253
|
lines: [
|
|
253
|
-
`
|
|
254
|
-
`Proposed: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
254
|
+
`Currently available: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
255
|
+
`Proposed availability: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
255
256
|
],
|
|
256
257
|
content: buildToolReview(current),
|
|
257
258
|
format: { kind: "text" },
|
|
@@ -307,7 +308,7 @@ export async function showChromeDevtoolsToolWorkflow(
|
|
|
307
308
|
current.draft = new Set(selectedTools);
|
|
308
309
|
current.applied = true;
|
|
309
310
|
ctx.ui.notify(
|
|
310
|
-
`Saved: ${selectedTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} browser tools
|
|
311
|
+
`Saved: ${selectedTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} browser tools available to lazy-load.`,
|
|
311
312
|
"info",
|
|
312
313
|
);
|
|
313
314
|
return { kind: "close" };
|
|
@@ -363,7 +364,7 @@ function buildMenuSnapshot(pi: ExtensionAPI, settings: SettingsLoadResult): Menu
|
|
|
363
364
|
? "not saved"
|
|
364
365
|
: arraysEqual(activeTools, persistedTools)
|
|
365
366
|
? "saved"
|
|
366
|
-
: "runtime differs from saved
|
|
367
|
+
: "runtime catalog differs from saved catalog";
|
|
367
368
|
const settingsWarning = [
|
|
368
369
|
...new Set([settings.notice, state.settingsNotice].filter(Boolean)),
|
|
369
370
|
].join("\n");
|
|
@@ -379,7 +380,7 @@ function buildMenuSnapshot(pi: ExtensionAPI, settings: SettingsLoadResult): Menu
|
|
|
379
380
|
|
|
380
381
|
function mainStateLines(snapshot: MenuSnapshot) {
|
|
381
382
|
return sanitizeLines([
|
|
382
|
-
`
|
|
383
|
+
`Lazy catalog: ${snapshot.activeTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} available Β· ${snapshot.persistenceLabel}`,
|
|
383
384
|
`Browser: ${browserLifecycleSummary()}`,
|
|
384
385
|
`Endpoint: ${devToolsEndpoint()}`,
|
|
385
386
|
...(snapshot.settingsWarning ? [`Settings warning: ${snapshot.settingsWarning}`] : []),
|
|
@@ -406,7 +407,7 @@ function toolDraftLines(current: ToolWorkflowState) {
|
|
|
406
407
|
return sanitizeLines([`Unavailable: ${current.mutationBlockedReason}`]);
|
|
407
408
|
const changes = symmetricDifferenceSize(current.accepted, current.draft);
|
|
408
409
|
return [
|
|
409
|
-
`
|
|
410
|
+
`Currently available: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
410
411
|
changes === 0
|
|
411
412
|
? "No unapplied changes Β· Escape cancels"
|
|
412
413
|
: `${changes} unapplied ${changes === 1 ? "change" : "changes"} Β· Escape cancels`,
|
|
@@ -415,25 +416,26 @@ function toolDraftLines(current: ToolWorkflowState) {
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
function buildToolReview(current: ToolWorkflowState) {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
419
|
+
const available = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => current.draft.has(name));
|
|
420
|
+
const unavailable = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => !current.draft.has(name));
|
|
420
421
|
return sanitizeChromeDevtoolsDisplay(
|
|
421
422
|
[
|
|
422
|
-
`Current browser tools: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
423
|
-
`Proposed browser tools: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
423
|
+
`Current available browser tools: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
424
|
+
`Proposed available browser tools: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
424
425
|
"",
|
|
425
|
-
"
|
|
426
|
-
...(
|
|
427
|
-
?
|
|
426
|
+
"Available to lazy-load after apply:",
|
|
427
|
+
...(available.length > 0
|
|
428
|
+
? available.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
|
|
428
429
|
: [" - none"]),
|
|
429
430
|
"",
|
|
430
|
-
"
|
|
431
|
-
...(
|
|
432
|
-
?
|
|
431
|
+
"Unavailable after apply:",
|
|
432
|
+
...(unavailable.length > 0
|
|
433
|
+
? unavailable.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
|
|
433
434
|
: [" - none"]),
|
|
434
435
|
"",
|
|
435
436
|
"Other active Pi tools remain unchanged.",
|
|
436
|
-
"
|
|
437
|
+
"Newly available tools remain deferred until chrome_devtools_load selects them.",
|
|
438
|
+
"The accepted availability policy is saved for future sessions.",
|
|
437
439
|
].join("\n"),
|
|
438
440
|
);
|
|
439
441
|
}
|
|
@@ -447,8 +449,7 @@ function updateSnapshotAfterApply(
|
|
|
447
449
|
}
|
|
448
450
|
|
|
449
451
|
function activeChromeTools(pi: ExtensionAPI) {
|
|
450
|
-
|
|
451
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => active.has(toolName));
|
|
452
|
+
return availableChromeDevtoolsTools(pi);
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
function orderedTools(tools: ReadonlySet<ChromeDevToolsToolName>) {
|
package/src/tool-selector.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
launchHint,
|
|
11
11
|
launchModeLabel,
|
|
12
12
|
} from "./browser-manager.js";
|
|
13
|
+
import {
|
|
14
|
+
applyAvailableChromeDevtoolsTools,
|
|
15
|
+
availableChromeDevtoolsTools,
|
|
16
|
+
CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
17
|
+
} from "./lazy-tools.js";
|
|
13
18
|
import { state } from "./runtime.js";
|
|
14
19
|
import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
|
|
15
20
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
@@ -25,8 +30,9 @@ function formatError(error: unknown) {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
interface ToolStatusSummary {
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
availabilityStatus: "enabled" | "disabled" | "partial";
|
|
34
|
+
availableChromeToolCount: number;
|
|
35
|
+
loadedChromeToolCount: number;
|
|
30
36
|
activeNonChromeToolCount: number;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -43,7 +49,7 @@ export async function updateChromeDevtoolsTools(
|
|
|
43
49
|
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
44
50
|
const status = await buildToolStatusMessage(pi);
|
|
45
51
|
if (generation !== state.sessionGeneration) return;
|
|
46
|
-
ctx.ui.notify(`Chrome DevTools
|
|
52
|
+
ctx.ui.notify(`Chrome DevTools lazy catalog ${action}.\n\n${status}`, "info");
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export async function setSelectedChromeDevtoolsTools(
|
|
@@ -92,7 +98,7 @@ async function transactSelectedToolsNow(
|
|
|
92
98
|
expectedActiveTools?: readonly ChromeDevToolsToolName[],
|
|
93
99
|
): Promise<ToolSelectionSaveResult> {
|
|
94
100
|
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
95
|
-
if (expectedActiveTools && !arraysEqual(
|
|
101
|
+
if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
|
|
96
102
|
ctx.ui.notify(
|
|
97
103
|
"Browser tool selection changed while review was open. Review the current state, then apply again.",
|
|
98
104
|
"warning",
|
|
@@ -100,6 +106,7 @@ async function transactSelectedToolsNow(
|
|
|
100
106
|
return "active-tools-changed";
|
|
101
107
|
}
|
|
102
108
|
const previousActiveTools = pi.getActiveTools();
|
|
109
|
+
const previousAvailableTools = availableChromeDevtoolsTools(pi);
|
|
103
110
|
try {
|
|
104
111
|
applyChromeDevtoolsTools(pi, selectedTools);
|
|
105
112
|
await persistSettings(selectedTools);
|
|
@@ -107,10 +114,14 @@ async function transactSelectedToolsNow(
|
|
|
107
114
|
} catch (error) {
|
|
108
115
|
let rollbackError: unknown;
|
|
109
116
|
try {
|
|
110
|
-
|
|
117
|
+
applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
|
|
118
|
+
const currentNonChromeTools = pi
|
|
119
|
+
.getActiveTools()
|
|
120
|
+
.filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
|
|
121
|
+
const previousLoadedChromeTools = previousActiveTools.filter((name) =>
|
|
111
122
|
CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
|
|
112
|
-
)
|
|
113
|
-
|
|
123
|
+
);
|
|
124
|
+
pi.setActiveTools(unique([...currentNonChromeTools, ...previousLoadedChromeTools]));
|
|
114
125
|
} catch (caught) {
|
|
115
126
|
rollbackError = caught;
|
|
116
127
|
}
|
|
@@ -127,11 +138,6 @@ async function transactSelectedToolsNow(
|
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
function activeChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
131
|
-
const active = new Set(pi.getActiveTools());
|
|
132
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => active.has(toolName));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
141
|
function arraysEqual<T>(left: readonly T[], right: readonly T[]) {
|
|
136
142
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
137
143
|
}
|
|
@@ -140,29 +146,32 @@ export function applyChromeDevtoolsTools(
|
|
|
140
146
|
pi: ExtensionAPI,
|
|
141
147
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
142
148
|
) {
|
|
143
|
-
|
|
144
|
-
const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
145
|
-
const activeNonChromeToolNames = activeToolNames.filter((name) => !chromeToolNames.has(name));
|
|
146
|
-
pi.setActiveTools(unique([...activeNonChromeToolNames, ...selectedTools]));
|
|
149
|
+
applyAvailableChromeDevtoolsTools(pi, selectedTools);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
150
153
|
const chromeToolNames = new Set<string>(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
151
154
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
152
|
-
const
|
|
155
|
+
const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
|
|
153
156
|
activeToolNames.has(name),
|
|
154
157
|
).length;
|
|
158
|
+
const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
|
|
155
159
|
const activeNonChromeToolCount = Array.from(activeToolNames).filter(
|
|
156
|
-
(name) => !chromeToolNames.has(name),
|
|
160
|
+
(name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
157
161
|
).length;
|
|
158
|
-
const
|
|
159
|
-
|
|
162
|
+
const availabilityStatus =
|
|
163
|
+
availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
160
164
|
? "enabled"
|
|
161
|
-
:
|
|
165
|
+
: availableChromeToolCount === 0
|
|
162
166
|
? "disabled"
|
|
163
167
|
: "partial";
|
|
164
168
|
|
|
165
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
availabilityStatus,
|
|
171
|
+
availableChromeToolCount,
|
|
172
|
+
loadedChromeToolCount,
|
|
173
|
+
activeNonChromeToolCount,
|
|
174
|
+
};
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
@@ -170,8 +179,10 @@ export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
|
170
179
|
const persistedSetting = await persistedSettingLabel();
|
|
171
180
|
return sanitizeChromeDevtoolsDisplay(
|
|
172
181
|
[
|
|
173
|
-
`Chrome DevTools tools: ${formatRuntimeStatus(summary)}`,
|
|
174
|
-
`
|
|
182
|
+
`Chrome DevTools tools available to lazy-load: ${formatRuntimeStatus(summary)}`,
|
|
183
|
+
`Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
184
|
+
`Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
|
|
185
|
+
`Persisted lazy catalog: ${persistedSetting}`,
|
|
175
186
|
...browserSettingsStatusLines(),
|
|
176
187
|
...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
|
|
177
188
|
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
@@ -276,10 +287,10 @@ export function buildCommandGuide() {
|
|
|
276
287
|
"/chrome-devtools help β show command usage",
|
|
277
288
|
"/chrome-devtools quickstart β show endpoint and launch help",
|
|
278
289
|
"/chrome-devtools status β show tool and settings status",
|
|
279
|
-
"/chrome-devtools tools β
|
|
290
|
+
"/chrome-devtools tools β choose tools available to lazy-load",
|
|
280
291
|
"/chrome-devtools toggle|select β compatibility aliases for tools",
|
|
281
|
-
"/chrome-devtools enable|on β
|
|
282
|
-
"/chrome-devtools disable|off β
|
|
292
|
+
"/chrome-devtools enable|on β make all Chrome DevTools tools available to lazy-load",
|
|
293
|
+
"/chrome-devtools disable|off β make all Chrome DevTools capability tools unavailable",
|
|
283
294
|
].join("\n");
|
|
284
295
|
}
|
|
285
296
|
|
|
@@ -292,7 +303,7 @@ export function orderedChromeDevtoolsTools(selectedTools: ReadonlySet<ChromeDevT
|
|
|
292
303
|
}
|
|
293
304
|
|
|
294
305
|
function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
295
|
-
return `${summary.
|
|
306
|
+
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
async function persistedSettingLabel() {
|
|
@@ -308,9 +319,10 @@ async function persistedSettingLabel() {
|
|
|
308
319
|
|
|
309
320
|
function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
|
|
310
321
|
if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
|
|
311
|
-
return `all
|
|
322
|
+
return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
312
323
|
}
|
|
313
|
-
if (tools.length === 0)
|
|
324
|
+
if (tools.length === 0)
|
|
325
|
+
return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
314
326
|
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
315
327
|
}
|
|
316
328
|
|
package/src/tools.ts
CHANGED
|
@@ -18,7 +18,6 @@ export const listPagesTool = defineTool({
|
|
|
18
18
|
name: CHROME_DEVTOOLS_TOOL_NAMES[0],
|
|
19
19
|
label: "Chrome DevTools: List Pages",
|
|
20
20
|
description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
|
|
21
|
-
promptSnippet: "List Chrome tabs/pages available over Chrome DevTools Protocol",
|
|
22
21
|
parameters: Type.Object({}),
|
|
23
22
|
renderCall: renderToolCall("list pages"),
|
|
24
23
|
renderResult: renderTextResult,
|
|
@@ -34,7 +33,6 @@ export const selectPageTool = defineTool({
|
|
|
34
33
|
name: CHROME_DEVTOOLS_TOOL_NAMES[1],
|
|
35
34
|
label: "Chrome DevTools: Select Page",
|
|
36
35
|
description: "Select the active Chrome page for later chrome_devtools_* tool calls.",
|
|
37
|
-
promptSnippet: "Select the Chrome tab/page to inspect or control",
|
|
38
36
|
parameters: Type.Object({
|
|
39
37
|
pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." }),
|
|
40
38
|
}),
|
|
@@ -56,7 +54,6 @@ export const navigateTool = defineTool({
|
|
|
56
54
|
label: "Chrome DevTools: Navigate",
|
|
57
55
|
description:
|
|
58
56
|
"Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.",
|
|
59
|
-
promptSnippet: "Navigate the selected or first Chrome tab to a URL, creating one if needed",
|
|
60
57
|
parameters: Type.Object({
|
|
61
58
|
url: Type.String({ description: "URL to navigate to." }),
|
|
62
59
|
pageId: Type.Optional(
|
|
@@ -88,7 +85,6 @@ export const evaluateTool = defineTool({
|
|
|
88
85
|
name: CHROME_DEVTOOLS_TOOL_NAMES[3],
|
|
89
86
|
label: "Chrome DevTools: Evaluate",
|
|
90
87
|
description: "Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.",
|
|
91
|
-
promptSnippet: "Evaluate JavaScript in the selected Chrome tab",
|
|
92
88
|
parameters: Type.Object({
|
|
93
89
|
expression: Type.String({ description: "JavaScript expression to evaluate." }),
|
|
94
90
|
pageId: Type.Optional(
|
|
@@ -121,7 +117,6 @@ export const screenshotTool = defineTool({
|
|
|
121
117
|
name: CHROME_DEVTOOLS_TOOL_NAMES[4],
|
|
122
118
|
label: "Chrome DevTools: Screenshot",
|
|
123
119
|
description: "Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.",
|
|
124
|
-
promptSnippet: "Capture a screenshot from the selected Chrome tab",
|
|
125
120
|
parameters: Type.Object({
|
|
126
121
|
pageId: Type.Optional(
|
|
127
122
|
Type.String({ description: "Optional page id. Defaults to selected or first page." }),
|