@narumitw/pi-chrome-devtools 0.50.0 β†’ 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 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
- - Provides a state-first `/chrome-devtools` menu for tool access, browser status, setup, and help.
30
- - Stages menu-based tool changes for exact review before one confirmed apply.
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 selected Chrome DevTools tools across Pi restarts.
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 current browser-tool count, whether that selection is saved, the
186
- configured endpoint, the observed managed-browser state, and any settings or launch warning before
187
- you choose an action. The five actions stay on one level:
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 the exact
190
- enabled/disabled result before selecting **Apply tool changes**.
191
- - **Enable all browser tools…** or **Disable all browser tools…** β€” preview the context-appropriate
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
- active-tool state, preserves the settings file, retains the draft for retry, and reports how to
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 runtime tool state, persisted selection, settings file path, endpoint source,
225
- launch mode, last launch attempt, and active non-Chrome tool count.
226
- - `tools` opens the same staged, width-safe selection and review flow used by the menu.
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` immediately enables and saves all `chrome_devtools_*` tools for future turns; `on` is a
249
+ - `enable` makes all five capability tools available to the loader and saves that catalog; `on` is a
229
250
  compatibility alias.
230
- - `disable` immediately disables and saves all `chrome_devtools_*` tools for future turns; `off` is a
231
- compatibility alias. The slash command remains available.
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 selected tool names are saved to:
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 active-tool policy
246
- instead of enabling tools by itself. A valid saved selection is restored on Pi startup and
247
- `/reload`. A missing file is created by the first confirmed menu apply or successful direct
248
- selection change. Within one Pi process, selection saves run in invocation order, reread the latest
249
- valid document, and preserve unknown fields. Malformed JSON or invalid recognized fields make menu
250
- mutation unavailable and block direct saves without replacement; a failed save restores the prior
251
- Chrome DevTools tool selection while preserving other extensions' current tools.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-chrome-devtools",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Pi extension that exposes Chrome DevTools Protocol tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "typescript": "7.0.2"
43
43
  },
44
44
  "dependencies": {
45
- "@narumitw/pi-tui-kit": "^0.49.1"
45
+ "@narumitw/pi-tui-kit": "^0.51.0"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",
@@ -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: "Select Chrome DevTools tools" },
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: "Enable all Chrome DevTools tools" },
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: "Disable all Chrome DevTools tools" },
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
- if (settings.kind === "loaded" && settings.settings.tools) {
73
- applyChromeDevtoolsTools(pi, settings.settings.tools);
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(), "enabled all");
133
+ await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "made all available");
124
134
  return;
125
135
  case "disable":
126
- await updateChromeDevtoolsTools(pi, ctx, [], "disabled all");
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: "Select what Pi may do in Chrome.",
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
- ? "Disable all browser tools…"
89
- : "Enable all browser tools…",
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
- `Current: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
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 enabled.`,
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 selection";
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
- `Tools: ${snapshot.activeTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} enabled Β· ${snapshot.persistenceLabel}`,
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
- `Current accepted: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
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 enabled = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => current.draft.has(name));
419
- const disabled = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => !current.draft.has(name));
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
- "Enabled after apply:",
426
- ...(enabled.length > 0
427
- ? enabled.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
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
- "Disabled after apply:",
431
- ...(disabled.length > 0
432
- ? disabled.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
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
- "The accepted selection is saved for future sessions.",
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
- const active = new Set(pi.getActiveTools());
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>) {
@@ -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
- runtimeStatus: "enabled" | "disabled" | "partial";
29
- activeChromeToolCount: number;
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 tools ${action}.\n\n${status}`, "info");
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(activeChromeDevtoolsTools(pi), expectedActiveTools)) {
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
- const previousChromeTools = previousActiveTools.filter((name) =>
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
- ) as ChromeDevToolsToolName[];
113
- applyChromeDevtoolsTools(pi, previousChromeTools);
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
- const activeToolNames = pi.getActiveTools();
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 activeChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter((name) =>
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 runtimeStatus =
159
- activeChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
162
+ const availabilityStatus =
163
+ availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length
160
164
  ? "enabled"
161
- : activeChromeToolCount === 0
165
+ : availableChromeToolCount === 0
162
166
  ? "disabled"
163
167
  : "partial";
164
168
 
165
- return { runtimeStatus, activeChromeToolCount, activeNonChromeToolCount };
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
- `Persisted selection: ${persistedSetting}`,
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 β€” select individual 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 β€” enable all Chrome DevTools tools immediately",
282
- "/chrome-devtools disable|off β€” disable all Chrome DevTools tools immediately",
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.runtimeStatus} (${summary.activeChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} active)`;
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 enabled (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
322
+ return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
312
323
  }
313
- if (tools.length === 0) return `all disabled (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
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." }),