@narumitw/pi-chrome-devtools 0.50.1 → 0.52.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 +107 -61
- package/package.json +1 -1
- package/src/browser-manager.ts +3 -5
- package/src/browser-settings-menu.ts +311 -0
- package/src/chrome-devtools.ts +42 -11
- package/src/lazy-tools.ts +143 -0
- package/src/menu.ts +36 -37
- package/src/runtime.ts +28 -23
- package/src/settings.ts +204 -38
- package/src/tool-selector.ts +45 -31
- package/src/tools.ts +0 -5
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ 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, editable browser settings,
|
|
31
|
+
browser status, setup, and help.
|
|
32
|
+
- Stages menu-based availability changes for exact review before one confirmed apply.
|
|
31
33
|
- Uses `@narumitw/pi-tui-kit` for width-safe TUI menus and equivalent RPC dialogs.
|
|
32
|
-
- Persists the
|
|
34
|
+
- Persists the Chrome DevTools lazy-load catalog across Pi restarts.
|
|
33
35
|
|
|
34
36
|
## 📦 Install
|
|
35
37
|
|
|
@@ -51,15 +53,33 @@ pi -e ./packages/pi-chrome-devtools
|
|
|
51
53
|
|
|
52
54
|
## 🚀 Browser startup
|
|
53
55
|
|
|
54
|
-
Without unpacked extensions, the extension first tries
|
|
55
|
-
`127.0.0.1:9222`. If that local endpoint is unavailable
|
|
56
|
-
Chromium-family browser with an isolated temp profile and retries
|
|
57
|
-
are reused and are never terminated by the extension.
|
|
56
|
+
Without unpacked extensions, the extension first tries `browser.endpoint`, defaulting to
|
|
57
|
+
`http://127.0.0.1:9222`. If that local endpoint is unavailable and `browser.autoLaunch` is `true`, it
|
|
58
|
+
lazily launches an extension-owned Chromium-family browser with an isolated temp profile and retries
|
|
59
|
+
the CDP request. Existing endpoints are reused and are never terminated by the extension.
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
Configure the canonical user file at
|
|
62
|
+
`${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"browser": {
|
|
67
|
+
"endpoint": "http://127.0.0.1:9222",
|
|
68
|
+
"autoLaunch": true,
|
|
69
|
+
"executablePath": "/absolute/path/to/chromium"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`browser.endpoint` must be an HTTP origin with an explicit port and no credentials, path, query, or
|
|
75
|
+
fragment. Omitting it keeps attach-first behavior on `127.0.0.1:9222` and lets a managed launch use
|
|
76
|
+
Chrome's dynamic DevTools port mode (`--remote-debugging-port=0`). Explicitly saving an endpoint pins
|
|
77
|
+
managed launches to that port. `browser.autoLaunch` defaults to `true`. `browser.executablePath` is
|
|
78
|
+
optional and must be an absolute path; when absent, normal browser discovery applies.
|
|
79
|
+
|
|
80
|
+
The configured endpoint must expose the standard CDP HTTP discovery routes such as `/json/version`
|
|
81
|
+
and `/json/list`. Chrome's newer built-in permission flow can listen on port `9222` while returning
|
|
82
|
+
`404` from those routes; setting the same HTTP origin does not by itself make that flow compatible.
|
|
63
83
|
|
|
64
84
|
### Unpacked extensions
|
|
65
85
|
|
|
@@ -67,8 +87,7 @@ fall back to the default attach-first behavior.
|
|
|
67
87
|
> An unpacked extension executes privileged browser code. Load only code you trust. Project settings
|
|
68
88
|
> are honored only when Pi reports the project as trusted.
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
`${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json`:
|
|
90
|
+
Add trusted unpacked-extension paths to the same canonical user file:
|
|
72
91
|
|
|
73
92
|
```json
|
|
74
93
|
{
|
|
@@ -100,9 +119,10 @@ A trusted project can replace the user extension list in
|
|
|
100
119
|
```
|
|
101
120
|
|
|
102
121
|
Project `extensionPaths` replace, rather than append to, the user array. A project file cannot
|
|
103
|
-
override `browser.
|
|
104
|
-
Effective precedence is defaults, user settings,
|
|
105
|
-
|
|
122
|
+
override `browser.endpoint`, `browser.autoLaunch`, or `browser.executablePath`; browser connection
|
|
123
|
+
settings remain machine-owned user configuration. Effective precedence is defaults, user settings,
|
|
124
|
+
trusted project extension paths, then deprecated environment overrides. No new environment variable
|
|
125
|
+
is required.
|
|
106
126
|
|
|
107
127
|
When `extensionPaths` is non-empty, the extension skips attach-first behavior and starts an isolated,
|
|
108
128
|
extension-owned managed browser with `--disable-extensions-except` and `--load-extension`. It fails
|
|
@@ -115,24 +135,17 @@ old managed browser is closed before the new configuration is applied. Missing f
|
|
|
115
135
|
existing no-extension behavior. Invalid JSON, invalid browser values, and missing manifests are left
|
|
116
136
|
unchanged and ignored with an actionable warning.
|
|
117
137
|
|
|
118
|
-
###
|
|
119
|
-
|
|
120
|
-
`PI_CHROME_DEVTOOLS_BROWSER` remains the explicit runtime executable override. Without unpacked
|
|
121
|
-
extensions, browser discovery still checks platform-specific Chrome, Chromium, Brave, and Microsoft
|
|
122
|
-
Edge candidates. Disable auto-launch to keep the manual flow:
|
|
138
|
+
### Deprecated environment overrides and manual endpoints
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
PI_CHROME_DEVTOOLS_AUTO_LAUNCH
|
|
126
|
-
|
|
140
|
+
The existing `PI_CHROME_DEVTOOLS_HOST`, `PI_CHROME_DEVTOOLS_PORT`,
|
|
141
|
+
`PI_CHROME_DEVTOOLS_AUTO_LAUNCH`, and `PI_CHROME_DEVTOOLS_BROWSER` variables remain temporary
|
|
142
|
+
compatibility overrides. They still take precedence over JSON, but every session that sees one emits
|
|
143
|
+
a deprecation warning. Move their values to `browser.endpoint`, `browser.autoLaunch`, and
|
|
144
|
+
`browser.executablePath`; the variables will be removed in a future version.
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
PI_CHROME_DEVTOOLS_BROWSER=/usr/bin/chromium pi -e ./packages/pi-chrome-devtools
|
|
132
|
-
PI_CHROME_DEVTOOLS_HOST=127.0.0.1 PI_CHROME_DEVTOOLS_PORT=9223 pi -e ./packages/pi-chrome-devtools
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Manual launch remains available when no unpacked extensions are configured:
|
|
146
|
+
Without unpacked extensions, browser discovery still checks platform-specific Chrome, Chromium,
|
|
147
|
+
Brave, and Microsoft Edge candidates. Manual launch remains available when no unpacked extensions are
|
|
148
|
+
configured:
|
|
136
149
|
|
|
137
150
|
```bash
|
|
138
151
|
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/pi-chrome-devtools
|
|
@@ -143,12 +156,31 @@ removes their temporary profiles. It never closes user-started browsers or remot
|
|
|
143
156
|
|
|
144
157
|
## 🛠️ Pi tools
|
|
145
158
|
|
|
159
|
+
- `chrome_devtools_load` — find and load browser capabilities relevant to a task.
|
|
146
160
|
- `chrome_devtools_list_pages` — list inspectable Chrome tabs/pages.
|
|
147
161
|
- `chrome_devtools_select_page` — select the active page for later tool calls.
|
|
148
162
|
- `chrome_devtools_navigate` — navigate a page to a URL; if no page exists, create one first.
|
|
149
163
|
- `chrome_devtools_evaluate` — evaluate JavaScript in the selected page.
|
|
150
164
|
- `chrome_devtools_screenshot` — capture a PNG screenshot and save it as a PNG file.
|
|
151
165
|
|
|
166
|
+
### Lazy tool loading
|
|
167
|
+
|
|
168
|
+
All six tools are registered, but only `chrome_devtools_load` starts active for this extension.
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
Loaded capability tools remain active for the rest of the session unless the user makes them unavailable through `/chrome-devtools`.
|
|
173
|
+
|
|
174
|
+
Pi uses native deferred tool references on compatible Anthropic and OpenAI models.
|
|
175
|
+
|
|
176
|
+
Other models receive Pi's safe fallback: the newly active definitions appear in the normal tool list on the next model request.
|
|
177
|
+
|
|
178
|
+
The capability tools omit active-only prompt snippets so a lazy load does not rebuild the system prompt prefix.
|
|
179
|
+
|
|
180
|
+
The saved `tools` array controls which capabilities the loader may expose.
|
|
181
|
+
|
|
182
|
+
An empty array leaves the loader active but makes every browser capability unavailable.
|
|
183
|
+
|
|
152
184
|
### Screenshot files
|
|
153
185
|
|
|
154
186
|
`chrome_devtools_screenshot` always saves the captured PNG to disk. If `savePath` is omitted,
|
|
@@ -182,17 +214,19 @@ to read the saved path, for example `read({ path: "artifacts/homepage.png" })`.
|
|
|
182
214
|
/chrome-devtools
|
|
183
215
|
```
|
|
184
216
|
|
|
185
|
-
Opens a menu that shows the
|
|
186
|
-
|
|
187
|
-
|
|
217
|
+
Opens a menu that shows the lazy catalog size, whether that catalog is saved, the configured
|
|
218
|
+
endpoint, the observed managed-browser state, and any settings or launch warning before you choose
|
|
219
|
+
an action. The five actions stay on one level:
|
|
188
220
|
|
|
189
|
-
- **Choose browser tools…** — stage any combination of the five capabilities, then review
|
|
190
|
-
|
|
191
|
-
- **
|
|
192
|
-
bulk change before applying it.
|
|
221
|
+
- **Choose available browser tools…** — stage any combination of the five capabilities, then review
|
|
222
|
+
the exact available/unavailable result before selecting **Apply tool changes**.
|
|
223
|
+
- **Make all browser tools available…** or **Make all browser tools unavailable…** — preview the
|
|
224
|
+
context-appropriate bulk change before applying it.
|
|
193
225
|
- **Browser status** — inspect runtime, endpoint, launch mode, and the last launch attempt without
|
|
194
226
|
probing the endpoint or starting Chrome.
|
|
195
|
-
- **
|
|
227
|
+
- **Browser settings** — immediately save the endpoint, auto-launch policy, or browser executable;
|
|
228
|
+
inspect unpacked-extension paths and effective sources. A deprecated environment override remains
|
|
229
|
+
effective until removed even when its underlying JSON value changes.
|
|
196
230
|
- **Help** — view command usage and return to the menu.
|
|
197
231
|
|
|
198
232
|
In the tool screen, **Select all** and **Select none** are unambiguous shortcuts; individual rows use
|
|
@@ -200,8 +234,8 @@ friendly task labels while retaining their raw `chrome_devtools_*` identity in t
|
|
|
200
234
|
Toggles remain a command-local draft. **Review changes** previews the exact effect, **Apply tool
|
|
201
235
|
changes** saves it, and Cancel, Escape, Ctrl+C, disposal, or session replacement discards an
|
|
202
236
|
unconfirmed draft without changing runtime tools or settings. A failed apply restores the previous
|
|
203
|
-
|
|
204
|
-
recover.
|
|
237
|
+
availability and loaded-tool state, preserves the settings file, retains the draft for retry, and
|
|
238
|
+
reports how to recover.
|
|
205
239
|
|
|
206
240
|
Direct subcommands are also available:
|
|
207
241
|
|
|
@@ -209,6 +243,7 @@ Direct subcommands are also available:
|
|
|
209
243
|
/chrome-devtools help
|
|
210
244
|
/chrome-devtools quickstart
|
|
211
245
|
/chrome-devtools status
|
|
246
|
+
/chrome-devtools settings
|
|
212
247
|
/chrome-devtools tools
|
|
213
248
|
/chrome-devtools toggle
|
|
214
249
|
/chrome-devtools enable
|
|
@@ -221,34 +256,44 @@ Compatibility aliases remain available: `toggle` and `select` mean `tools`, `on`
|
|
|
221
256
|
- `help` shows command usage.
|
|
222
257
|
- `quickstart` shows the configured CDP endpoint, endpoint source, auto-launch mode, browser
|
|
223
258
|
candidates, last launch attempt, and launch hints.
|
|
224
|
-
- `status` shows
|
|
225
|
-
launch mode, last launch attempt, and active non-Chrome tool
|
|
226
|
-
|
|
259
|
+
- `status` shows available and loaded capability counts, loader state, the persisted catalog,
|
|
260
|
+
settings file path, endpoint source, launch mode, last launch attempt, and active non-Chrome tool
|
|
261
|
+
count.
|
|
262
|
+
- `settings` opens the same immediate-save browser settings flow used by the menu.
|
|
263
|
+
- `tools` opens the same staged, width-safe availability and review flow used by the menu.
|
|
227
264
|
- `toggle` and `select` are compatibility aliases for `tools`.
|
|
228
|
-
- `enable`
|
|
265
|
+
- `enable` makes all five capability tools available to the loader and saves that catalog; `on` is a
|
|
229
266
|
compatibility alias.
|
|
230
|
-
- `disable`
|
|
231
|
-
compatibility alias. The slash command
|
|
267
|
+
- `disable` makes all five capability tools unavailable and saves the empty catalog; `off` is a
|
|
268
|
+
compatibility alias. The slash command and `chrome_devtools_load` remain available.
|
|
269
|
+
|
|
270
|
+
The menu, `settings`, `tools`, `help`, `quickstart`, and `status` require TUI or RPC mode so their
|
|
271
|
+
result is observable. TUI uses keyboard navigation and injected Pi keybindings; RPC receives
|
|
272
|
+
equivalent standard dialogs. In print and JSON modes, interactive and informational routes reject
|
|
273
|
+
explicitly instead of silently opening unavailable UI. The immediate `enable`/`disable` routes remain
|
|
274
|
+
available for deterministic non-interactive use.
|
|
232
275
|
|
|
233
|
-
|
|
234
|
-
observable. TUI uses keyboard navigation and injected Pi keybindings; RPC receives equivalent
|
|
235
|
-
standard dialogs. In print and JSON modes, interactive and informational routes reject explicitly
|
|
236
|
-
instead of silently opening unavailable UI. The immediate `enable`/`disable` routes remain available
|
|
237
|
-
for deterministic non-interactive use.
|
|
276
|
+
## ⚙️ Settings
|
|
238
277
|
|
|
239
|
-
The
|
|
278
|
+
The available capability names are saved to:
|
|
240
279
|
|
|
241
280
|
```text
|
|
242
281
|
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json
|
|
243
282
|
```
|
|
244
283
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
The same file owns `browser.endpoint`, `browser.autoLaunch`, `browser.executablePath`, and
|
|
285
|
+
`browser.extensionPaths`. Browser connection fields are machine-owned user settings; trusted project
|
|
286
|
+
files may replace only `browser.extensionPaths`. Confirmed menu changes apply before the next browser
|
|
287
|
+
connection and close only an extension-owned managed browser. Manual JSON edits and unpacked-extension
|
|
288
|
+
changes apply after `/reload` or session replacement.
|
|
289
|
+
|
|
290
|
+
When the file is missing or invalid, the extension preserves Pi's current Chrome DevTools
|
|
291
|
+
availability policy instead of replacing it. A valid saved catalog is restored on Pi startup and
|
|
292
|
+
`/reload`, while its capability definitions remain deferred. A missing file is created by the first
|
|
293
|
+
confirmed browser or tool setting. Within one Pi process, all browser and tool saves run in invocation
|
|
294
|
+
order, reread the latest valid document, publish by temporary-file rename, and preserve unknown
|
|
295
|
+
fields. Malformed JSON or invalid recognized fields make menu mutation unavailable and block direct
|
|
296
|
+
saves without replacement; a failed save restores the prior displayed and effective state.
|
|
252
297
|
|
|
253
298
|
Compatibility: older versions used `pi-chrome-devtools-settings.json`. A legacy-only file remains
|
|
254
299
|
readable with a warning and is never modified automatically; rename it to
|
|
@@ -271,6 +316,7 @@ packages/pi-chrome-devtools/
|
|
|
271
316
|
├── src/
|
|
272
317
|
│ ├── index.ts # Pi package entrypoint
|
|
273
318
|
│ ├── chrome-devtools.ts # Extension registration and command orchestration
|
|
319
|
+
│ ├── lazy-tools.ts # Deferred capability catalog and loader tool
|
|
274
320
|
│ └── *.ts # Package-local browser, CDP, tool, and storage modules
|
|
275
321
|
├── README.md
|
|
276
322
|
├── LICENSE
|
package/package.json
CHANGED
package/src/browser-manager.ts
CHANGED
|
@@ -143,7 +143,7 @@ async function prepareManagedBrowserLaunch(
|
|
|
143
143
|
throwIfBrowserLaunchCancelled(generation, signal);
|
|
144
144
|
if (!available) {
|
|
145
145
|
throw new DevToolsEndpointError(
|
|
146
|
-
`Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free
|
|
146
|
+
`Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
|
|
147
147
|
);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -609,9 +609,7 @@ export function formatHostForUrl(host: string) {
|
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
export function endpointSourceLabel() {
|
|
612
|
-
|
|
613
|
-
const portSource = state.portConfigured ? "PI_CHROME_DEVTOOLS_PORT" : "default/dynamic port";
|
|
614
|
-
return `${hostSource}; ${portSource}`;
|
|
612
|
+
return state.endpointSource;
|
|
615
613
|
}
|
|
616
614
|
|
|
617
615
|
export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
|
|
@@ -710,7 +708,7 @@ export function quoteCommandPart(value: string) {
|
|
|
710
708
|
}
|
|
711
709
|
|
|
712
710
|
export function endpointConfigHint() {
|
|
713
|
-
return "
|
|
711
|
+
return "Configure browser.endpoint, browser.executablePath, and browser.autoLaunch in pi-chrome-devtools.json or the /chrome-devtools Settings flow.";
|
|
714
712
|
}
|
|
715
713
|
|
|
716
714
|
export function isLocalDevToolsHost(host: string) {
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
4
|
+
import { shutdownManagedBrowser } from "./browser-manager.js";
|
|
5
|
+
import { applyRuntimeBrowserSettings, state } from "./runtime.js";
|
|
6
|
+
import {
|
|
7
|
+
type BrowserSettingsPatch,
|
|
8
|
+
type BrowserSettingsSource,
|
|
9
|
+
loadSettings,
|
|
10
|
+
parseBrowserEndpoint,
|
|
11
|
+
type SettingsLoadResult,
|
|
12
|
+
saveBrowserSettings,
|
|
13
|
+
} from "./settings.js";
|
|
14
|
+
import { sanitizeChromeDevtoolsDisplay } from "./tool-selector.js";
|
|
15
|
+
|
|
16
|
+
type BrowserSettingsScreen = "settings" | "endpoint" | "executable" | "extensions";
|
|
17
|
+
type BrowserSettingsAction =
|
|
18
|
+
| "open-endpoint"
|
|
19
|
+
| "save-endpoint"
|
|
20
|
+
| "set-auto-launch"
|
|
21
|
+
| "open-executable"
|
|
22
|
+
| "save-executable"
|
|
23
|
+
| "show-extensions";
|
|
24
|
+
|
|
25
|
+
interface BrowserSettingsMenuState {
|
|
26
|
+
load: SettingsLoadResult;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type CommandContext = ExtensionCommandContext;
|
|
30
|
+
|
|
31
|
+
export async function showChromeDevtoolsBrowserSettings(
|
|
32
|
+
ctx: CommandContext,
|
|
33
|
+
generation: number,
|
|
34
|
+
): Promise<{ closeParent: boolean }> {
|
|
35
|
+
const ownerSignal = state.sessionController.signal;
|
|
36
|
+
const isCurrent = () => generation === state.sessionGeneration && !ownerSignal.aborted;
|
|
37
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
38
|
+
const menu = defineMenu<BrowserSettingsMenuState, BrowserSettingsScreen, BrowserSettingsAction>({
|
|
39
|
+
start: "settings",
|
|
40
|
+
screens: {
|
|
41
|
+
settings: ({ state: current }) => {
|
|
42
|
+
if (current.load.userFile.kind === "invalid") return invalidSettingsScreen(current.load);
|
|
43
|
+
const browser = current.load.effectiveBrowser;
|
|
44
|
+
return {
|
|
45
|
+
kind: "settings",
|
|
46
|
+
title: "Chrome DevTools Browser settings",
|
|
47
|
+
lines: settingsContextLines(current.load),
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
id: "endpoint",
|
|
51
|
+
label: "DevTools endpoint",
|
|
52
|
+
description:
|
|
53
|
+
'HTTP origin with an explicit port; enter "default" to restore 127.0.0.1:9222.',
|
|
54
|
+
currentValue: sanitizeChromeDevtoolsDisplay(browser.endpoint),
|
|
55
|
+
action: "open-endpoint",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "auto-launch",
|
|
59
|
+
label: "Auto-launch",
|
|
60
|
+
description:
|
|
61
|
+
"Launch an isolated managed browser when a local endpoint is unavailable.",
|
|
62
|
+
currentValue: browser.autoLaunchEnabled ? "On" : "Off",
|
|
63
|
+
values: ["On", "Off"],
|
|
64
|
+
action: "set-auto-launch",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "executable",
|
|
68
|
+
label: "Browser executable",
|
|
69
|
+
description:
|
|
70
|
+
'Absolute executable path; enter "automatic" to restore browser discovery.',
|
|
71
|
+
currentValue: sanitizeChromeDevtoolsDisplay(browser.executablePath ?? "Automatic"),
|
|
72
|
+
action: "open-executable",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "extensions",
|
|
76
|
+
label: "Unpacked extensions",
|
|
77
|
+
description: "Inspect trusted extension paths configured in user or project JSON.",
|
|
78
|
+
currentValue: `${browser.extensionPaths.length} configured`,
|
|
79
|
+
action: "show-extensions",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
endpoint: ({ state: current }) => ({
|
|
85
|
+
kind: "input",
|
|
86
|
+
title: "DevTools endpoint",
|
|
87
|
+
lines: [
|
|
88
|
+
sanitizeChromeDevtoolsDisplay(
|
|
89
|
+
`Current effective endpoint: ${current.load.effectiveBrowser.endpoint}`,
|
|
90
|
+
),
|
|
91
|
+
'Enter an HTTP origin with an explicit port, or "default".',
|
|
92
|
+
],
|
|
93
|
+
placeholder: "http://127.0.0.1:9222",
|
|
94
|
+
action: "save-endpoint",
|
|
95
|
+
hint: "back",
|
|
96
|
+
}),
|
|
97
|
+
executable: ({ state: current }) => ({
|
|
98
|
+
kind: "input",
|
|
99
|
+
title: "Browser executable",
|
|
100
|
+
lines: [
|
|
101
|
+
sanitizeChromeDevtoolsDisplay(
|
|
102
|
+
`Current effective executable: ${current.load.effectiveBrowser.executablePath ?? "automatic discovery"}`,
|
|
103
|
+
),
|
|
104
|
+
'Enter an absolute path, or "automatic".',
|
|
105
|
+
],
|
|
106
|
+
placeholder: "absolute browser executable path",
|
|
107
|
+
action: "save-executable",
|
|
108
|
+
hint: "back",
|
|
109
|
+
}),
|
|
110
|
+
extensions: ({ state: current }) => ({
|
|
111
|
+
kind: "detail",
|
|
112
|
+
title: "Unpacked extensions",
|
|
113
|
+
lines: extensionDetailLines(current.load),
|
|
114
|
+
hint: "back",
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
actions: {
|
|
118
|
+
"open-endpoint": () => ({ kind: "to", screen: "endpoint" }),
|
|
119
|
+
"save-endpoint": async ({ value, signal }) => {
|
|
120
|
+
if (value === undefined) return { kind: "rejected" };
|
|
121
|
+
let endpoint: string | null;
|
|
122
|
+
try {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
endpoint =
|
|
125
|
+
trimmed.toLowerCase() === "default" ? null : parseBrowserEndpoint(trimmed).endpoint;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
notifySaveFailure(ctx, error);
|
|
128
|
+
return { kind: "rejected" };
|
|
129
|
+
}
|
|
130
|
+
const saved = await saveAndApplyBrowserPatch(
|
|
131
|
+
ctx,
|
|
132
|
+
generation,
|
|
133
|
+
ownerSignal,
|
|
134
|
+
signal,
|
|
135
|
+
projectTrusted,
|
|
136
|
+
{ endpoint },
|
|
137
|
+
);
|
|
138
|
+
if (!saved) return { kind: "rejected" };
|
|
139
|
+
notifyEffectiveSave(
|
|
140
|
+
ctx,
|
|
141
|
+
"endpoint",
|
|
142
|
+
saved.effectiveBrowser.endpoint,
|
|
143
|
+
saved.effectiveBrowser.endpointSource,
|
|
144
|
+
);
|
|
145
|
+
return { kind: "back" };
|
|
146
|
+
},
|
|
147
|
+
"set-auto-launch": async ({ value, signal }) => {
|
|
148
|
+
if (value !== "On" && value !== "Off") return { kind: "rejected" };
|
|
149
|
+
const saved = await saveAndApplyBrowserPatch(
|
|
150
|
+
ctx,
|
|
151
|
+
generation,
|
|
152
|
+
ownerSignal,
|
|
153
|
+
signal,
|
|
154
|
+
projectTrusted,
|
|
155
|
+
{ autoLaunch: value === "On" },
|
|
156
|
+
);
|
|
157
|
+
if (!saved) return { kind: "rejected" };
|
|
158
|
+
notifyEffectiveSave(
|
|
159
|
+
ctx,
|
|
160
|
+
"auto-launch",
|
|
161
|
+
saved.effectiveBrowser.autoLaunchEnabled ? "On" : "Off",
|
|
162
|
+
saved.effectiveBrowser.autoLaunchSource,
|
|
163
|
+
);
|
|
164
|
+
return { kind: "stay" };
|
|
165
|
+
},
|
|
166
|
+
"open-executable": () => ({ kind: "to", screen: "executable" }),
|
|
167
|
+
"save-executable": async ({ value, signal }) => {
|
|
168
|
+
if (value === undefined) return { kind: "rejected" };
|
|
169
|
+
const trimmed = value.trim();
|
|
170
|
+
const reset = ["automatic", "default", "none"].includes(trimmed.toLowerCase());
|
|
171
|
+
if (!reset && (!trimmed || !isAbsolute(trimmed))) {
|
|
172
|
+
notifySaveFailure(ctx, new Error('Enter an absolute path or "automatic".'));
|
|
173
|
+
return { kind: "rejected" };
|
|
174
|
+
}
|
|
175
|
+
const executablePath = reset ? null : trimmed;
|
|
176
|
+
const saved = await saveAndApplyBrowserPatch(
|
|
177
|
+
ctx,
|
|
178
|
+
generation,
|
|
179
|
+
ownerSignal,
|
|
180
|
+
signal,
|
|
181
|
+
projectTrusted,
|
|
182
|
+
{ executablePath },
|
|
183
|
+
);
|
|
184
|
+
if (!saved) return { kind: "rejected" };
|
|
185
|
+
notifyEffectiveSave(
|
|
186
|
+
ctx,
|
|
187
|
+
"browser executable",
|
|
188
|
+
saved.effectiveBrowser.executablePath ?? "automatic discovery",
|
|
189
|
+
saved.effectiveBrowser.executablePathSource,
|
|
190
|
+
);
|
|
191
|
+
return { kind: "back" };
|
|
192
|
+
},
|
|
193
|
+
"show-extensions": () => ({ kind: "to", screen: "extensions" }),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const result = await runMenu(ctx, menu, {
|
|
197
|
+
getState: async ({ signal }) => {
|
|
198
|
+
signal.throwIfAborted();
|
|
199
|
+
const load = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
200
|
+
signal.throwIfAborted();
|
|
201
|
+
return { load };
|
|
202
|
+
},
|
|
203
|
+
signal: ownerSignal,
|
|
204
|
+
isCurrent,
|
|
205
|
+
onError: (currentCtx, error) =>
|
|
206
|
+
currentCtx.ui.notify(
|
|
207
|
+
sanitizeChromeDevtoolsDisplay(
|
|
208
|
+
`Chrome DevTools browser settings failed: ${formatError(error)}`,
|
|
209
|
+
),
|
|
210
|
+
"error",
|
|
211
|
+
),
|
|
212
|
+
});
|
|
213
|
+
return { closeParent: result.kind === "closed" && result.reason === "close" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function saveAndApplyBrowserPatch(
|
|
217
|
+
ctx: CommandContext,
|
|
218
|
+
generation: number,
|
|
219
|
+
ownerSignal: AbortSignal,
|
|
220
|
+
actionSignal: AbortSignal,
|
|
221
|
+
projectTrusted: boolean,
|
|
222
|
+
patch: BrowserSettingsPatch,
|
|
223
|
+
) {
|
|
224
|
+
try {
|
|
225
|
+
await ctx.waitForIdle();
|
|
226
|
+
if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
|
|
227
|
+
await saveBrowserSettings(patch);
|
|
228
|
+
if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
|
|
229
|
+
await shutdownManagedBrowser();
|
|
230
|
+
if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
|
|
231
|
+
const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
232
|
+
if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
|
|
233
|
+
applyRuntimeBrowserSettings(loaded.effectiveBrowser, loaded.paths, projectTrusted);
|
|
234
|
+
state.settingsNotice = loaded.notice;
|
|
235
|
+
return loaded;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (isCurrent(generation, ownerSignal, actionSignal)) notifySaveFailure(ctx, error);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isCurrent(generation: number, ownerSignal: AbortSignal, actionSignal: AbortSignal) {
|
|
243
|
+
return generation === state.sessionGeneration && !ownerSignal.aborted && !actionSignal.aborted;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function invalidSettingsScreen(load: SettingsLoadResult) {
|
|
247
|
+
const reason =
|
|
248
|
+
load.userFile.kind === "invalid"
|
|
249
|
+
? load.userFile.reason
|
|
250
|
+
: "The user settings file is unavailable.";
|
|
251
|
+
return {
|
|
252
|
+
kind: "detail" as const,
|
|
253
|
+
title: "Chrome DevTools Browser settings · Read only",
|
|
254
|
+
lines: [
|
|
255
|
+
sanitizeChromeDevtoolsDisplay(
|
|
256
|
+
`Fix the active user settings before saving; the file will not be overwritten. ${reason}`,
|
|
257
|
+
),
|
|
258
|
+
],
|
|
259
|
+
hint: "back" as const,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function settingsContextLines(load: SettingsLoadResult) {
|
|
264
|
+
const browser = load.effectiveBrowser;
|
|
265
|
+
return [
|
|
266
|
+
`User settings · ${load.paths.user}`,
|
|
267
|
+
`Effective sources: endpoint ${browser.endpointSource} · auto-launch ${browser.autoLaunchSource} · executable ${browser.executablePathSource}`,
|
|
268
|
+
...(load.notice ? [`Warning: ${load.notice}`] : []),
|
|
269
|
+
].map((line) => sanitizeChromeDevtoolsDisplay(line));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function extensionDetailLines(load: SettingsLoadResult) {
|
|
273
|
+
const browser = load.effectiveBrowser;
|
|
274
|
+
return [
|
|
275
|
+
`Effective source: ${browser.extensionPathsSource}`,
|
|
276
|
+
...(browser.extensionPaths.length > 0
|
|
277
|
+
? browser.extensionPaths.map((extensionPath) => `- ${extensionPath}`)
|
|
278
|
+
: ["No unpacked extensions are configured."]),
|
|
279
|
+
"Edit browser.extensionPaths in the user JSON or a trusted project JSON, then run /reload.",
|
|
280
|
+
"Unpacked extensions execute privileged browser code; load only paths you trust.",
|
|
281
|
+
].map((line) => sanitizeChromeDevtoolsDisplay(line));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function notifyEffectiveSave(
|
|
285
|
+
ctx: CommandContext,
|
|
286
|
+
setting: string,
|
|
287
|
+
effectiveValue: string,
|
|
288
|
+
source: BrowserSettingsSource,
|
|
289
|
+
) {
|
|
290
|
+
const overrideNotice =
|
|
291
|
+
source === "environment" ? " The deprecated environment override remains effective." : "";
|
|
292
|
+
ctx.ui.notify(
|
|
293
|
+
sanitizeChromeDevtoolsDisplay(
|
|
294
|
+
`Chrome DevTools ${setting} saved. Effective ${setting}: ${effectiveValue} (${source}).${overrideNotice}`,
|
|
295
|
+
),
|
|
296
|
+
"info",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function notifySaveFailure(ctx: CommandContext, error: unknown) {
|
|
301
|
+
ctx.ui.notify(
|
|
302
|
+
sanitizeChromeDevtoolsDisplay(
|
|
303
|
+
`Chrome DevTools browser settings save failed; previous settings remain active: ${formatError(error)}`,
|
|
304
|
+
),
|
|
305
|
+
"warning",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function formatError(error: unknown) {
|
|
310
|
+
return error instanceof Error ? error.message : String(error);
|
|
311
|
+
}
|