@narumitw/pi-chrome-devtools 0.35.0 → 0.39.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 +2 -2
- package/package.json +4 -3
- package/src/chrome-devtools.ts +57 -25
- package/src/runtime.ts +2 -0
- package/src/tool-selector.ts +67 -171
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ This package is inspired by [`chrome-devtools-mcp`](https://github.com/ChromeDev
|
|
|
25
25
|
- Retries briefly while Chrome is starting and reports actionable endpoint errors.
|
|
26
26
|
- Shows statusline activity only while Chrome DevTools tools are running.
|
|
27
27
|
- Provides a `/chrome-devtools` menu with quick-start help and tool controls.
|
|
28
|
-
-
|
|
28
|
+
- Uses `@narumitw/pi-tui-kit` for width-safe menus and individual tool selection.
|
|
29
29
|
- Persists the selected Chrome DevTools tools across Pi restarts.
|
|
30
30
|
|
|
31
31
|
## 📦 Install
|
|
@@ -155,7 +155,7 @@ Direct subcommands are also available:
|
|
|
155
155
|
candidates, last launch attempt, and launch hints.
|
|
156
156
|
- `status` shows runtime tool state, persisted selection, settings file path, endpoint source,
|
|
157
157
|
launch mode, last launch attempt, and active non-Chrome tool count.
|
|
158
|
-
- `tools` opens a
|
|
158
|
+
- `tools` opens a width-safe selector for choosing individual `chrome_devtools_*` tools.
|
|
159
159
|
- `toggle` is an alias for `tools`.
|
|
160
160
|
- `enable` enables all `chrome_devtools_*` tools for future turns.
|
|
161
161
|
- `disable` disables all `chrome_devtools_*` tools for future turns. The slash command remains
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-chrome-devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Pi extension that exposes Chrome DevTools Protocol tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@narumitw/pi-tui-kit": "<1",
|
|
32
33
|
"typebox": "^1.3.8"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@biomejs/biome": "2.5.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
36
|
+
"@biomejs/biome": "2.5.6",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
37
38
|
"typescript": "7.0.2"
|
|
38
39
|
},
|
|
39
40
|
"repository": {
|
package/src/chrome-devtools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
3
|
import { shutdownManagedBrowser } from "./browser-manager.js";
|
|
3
4
|
import { state } from "./runtime.js";
|
|
4
5
|
import { loadSettings } from "./settings.js";
|
|
@@ -59,6 +60,7 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
59
60
|
|
|
60
61
|
pi.on("session_start", async (_event, ctx) => {
|
|
61
62
|
const generation = ++state.sessionGeneration;
|
|
63
|
+
replaceSessionController("Chrome DevTools session replaced");
|
|
62
64
|
state.shuttingDown = false;
|
|
63
65
|
state.settingsNotice = undefined;
|
|
64
66
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
@@ -77,6 +79,7 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
77
79
|
|
|
78
80
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
79
81
|
state.sessionGeneration += 1;
|
|
82
|
+
replaceSessionController("Chrome DevTools session shut down");
|
|
80
83
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
81
84
|
const browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });
|
|
82
85
|
await waitForChromeDevtoolsSettings();
|
|
@@ -134,31 +137,60 @@ async function showMenu(pi: ExtensionAPI, ctx: CommandContext, generation: numbe
|
|
|
134
137
|
return;
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
type Screen = "main";
|
|
141
|
+
type Action = keyof typeof MENU_OPTIONS;
|
|
142
|
+
const menu = defineMenu<undefined, Screen, Action>({
|
|
143
|
+
start: "main",
|
|
144
|
+
screens: {
|
|
145
|
+
main: () => ({
|
|
146
|
+
kind: "actions",
|
|
147
|
+
title: "Chrome DevTools",
|
|
148
|
+
items: Object.entries(MENU_OPTIONS).map(([id, label]) => ({
|
|
149
|
+
id,
|
|
150
|
+
label,
|
|
151
|
+
action: id as Action,
|
|
152
|
+
})),
|
|
153
|
+
hint: "close",
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
actions: {
|
|
157
|
+
quickstart: async () => {
|
|
158
|
+
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
159
|
+
return { kind: "close" };
|
|
160
|
+
},
|
|
161
|
+
help: async () => {
|
|
162
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
163
|
+
return { kind: "close" };
|
|
164
|
+
},
|
|
165
|
+
status: async () => {
|
|
166
|
+
const status = await buildToolStatusMessage(pi);
|
|
167
|
+
if (generation === state.sessionGeneration) ctx.ui.notify(status, "info");
|
|
168
|
+
return { kind: "close" };
|
|
169
|
+
},
|
|
170
|
+
tools: async () => {
|
|
171
|
+
await showToolSelector(pi, ctx);
|
|
172
|
+
return { kind: "close" };
|
|
173
|
+
},
|
|
174
|
+
enable: async () => {
|
|
175
|
+
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "enabled all");
|
|
176
|
+
return { kind: "close" };
|
|
177
|
+
},
|
|
178
|
+
disable: async () => {
|
|
179
|
+
await updateChromeDevtoolsTools(pi, ctx, [], "disabled all");
|
|
180
|
+
return { kind: "close" };
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
await runMenu(ctx, menu, {
|
|
185
|
+
getState: () => undefined,
|
|
186
|
+
signal: state.sessionController.signal,
|
|
187
|
+
isCurrent: () => generation === state.sessionGeneration,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function replaceSessionController(reason: string) {
|
|
192
|
+
state.sessionController.abort(new DOMException(reason, "AbortError"));
|
|
193
|
+
state.sessionController = new AbortController();
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
export function parseCommand(args: string): CommandAction | "unknown" {
|
package/src/runtime.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface ChromeDevToolsState {
|
|
|
32
32
|
lastLaunchAttempt?: BrowserLaunchAttempt;
|
|
33
33
|
shuttingDown: boolean;
|
|
34
34
|
sessionGeneration: number;
|
|
35
|
+
sessionController: AbortController;
|
|
35
36
|
settingsNotice?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -84,4 +85,5 @@ export const state: ChromeDevToolsState = {
|
|
|
84
85
|
browserExecutable: process.env.PI_CHROME_DEVTOOLS_BROWSER,
|
|
85
86
|
shuttingDown: false,
|
|
86
87
|
sessionGeneration: 0,
|
|
88
|
+
sessionController: new AbortController(),
|
|
87
89
|
};
|
package/src/tool-selector.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
3
|
import {
|
|
3
4
|
browserCandidateHint,
|
|
4
5
|
devToolsEndpoint,
|
|
@@ -13,13 +14,9 @@ import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
|
|
|
13
14
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
14
15
|
|
|
15
16
|
type CommandContext = ExtensionCommandContext;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type ToolSelectorAction = "enableAll" | "disableAll" | "done";
|
|
20
|
-
type ToolSelectorRow =
|
|
21
|
-
| { kind: "tool"; toolName: ChromeDevToolsToolName }
|
|
22
|
-
| { kind: "action"; action: ToolSelectorAction; label: string };
|
|
17
|
+
type ToolSelectorScreen = "tools";
|
|
18
|
+
type ToolSelectorAction = "toggle" | "enableAll" | "disableAll";
|
|
19
|
+
|
|
23
20
|
function unique<T>(values: T[]) {
|
|
24
21
|
return Array.from(new Set(values));
|
|
25
22
|
}
|
|
@@ -41,156 +38,68 @@ interface ToolStatusSummary {
|
|
|
41
38
|
export async function showToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
42
39
|
const generation = state.sessionGeneration;
|
|
43
40
|
if (!ctx.hasUI) return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!row) return;
|
|
76
|
-
|
|
77
|
-
if (row.kind === "tool") {
|
|
78
|
-
if (selectedTools.has(row.toolName)) selectedTools.delete(row.toolName);
|
|
79
|
-
else selectedTools.add(row.toolName);
|
|
80
|
-
commitSelectedTools();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (row.action === "enableAll") {
|
|
85
|
-
selectedTools = new Set(allChromeDevtoolsTools());
|
|
86
|
-
commitSelectedTools();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (row.action === "disableAll") {
|
|
90
|
-
selectedTools = new Set();
|
|
91
|
-
commitSelectedTools();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
done("closed");
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
invalidate() {},
|
|
100
|
-
render() {
|
|
101
|
-
return [
|
|
102
|
-
theme.fg("accent", theme.bold(toolSelectorTitle(selectedTools))),
|
|
103
|
-
"",
|
|
104
|
-
...rows.map((row, index) => {
|
|
105
|
-
const label = formatToolSelectorRow(row, selectedTools);
|
|
106
|
-
if (index === selectedIndex) {
|
|
107
|
-
return `${theme.fg("accent", "›")} ${theme.fg("accent", label)}`;
|
|
108
|
-
}
|
|
109
|
-
return ` ${label}`;
|
|
110
|
-
}),
|
|
111
|
-
"",
|
|
112
|
-
theme.fg("dim", "↑↓ navigate • Enter/Space toggle • Esc close"),
|
|
113
|
-
];
|
|
114
|
-
},
|
|
115
|
-
handleInput(data: string) {
|
|
116
|
-
if (keybindings.matches(data, "tui.select.up")) {
|
|
117
|
-
moveSelection(-1);
|
|
118
|
-
tui.requestRender();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (keybindings.matches(data, "tui.select.down")) {
|
|
122
|
-
moveSelection(1);
|
|
123
|
-
tui.requestRender();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
127
|
-
selectedIndex = 0;
|
|
128
|
-
tui.requestRender();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
132
|
-
selectedIndex = rows.length - 1;
|
|
133
|
-
tui.requestRender();
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
|
|
137
|
-
activateSelectedRow();
|
|
138
|
-
tui.requestRender();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
142
|
-
done("closed");
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
};
|
|
41
|
+
const menu = defineMenu<undefined, ToolSelectorScreen, ToolSelectorAction>({
|
|
42
|
+
start: "tools",
|
|
43
|
+
screens: {
|
|
44
|
+
tools: () => {
|
|
45
|
+
const selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
|
|
46
|
+
return {
|
|
47
|
+
kind: "multiSelect",
|
|
48
|
+
title: toolSelectorTitle(selectedTools),
|
|
49
|
+
items: CHROME_DEVTOOLS_TOOL_NAMES.map((toolName) => ({
|
|
50
|
+
id: toolName,
|
|
51
|
+
label: toolName,
|
|
52
|
+
selected: selectedTools.has(toolName),
|
|
53
|
+
})),
|
|
54
|
+
action: "toggle",
|
|
55
|
+
actions: [
|
|
56
|
+
{
|
|
57
|
+
id: "enable-all",
|
|
58
|
+
label: "Enable all Chrome DevTools tools",
|
|
59
|
+
action: "enableAll",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "disable-all",
|
|
63
|
+
label: "Disable all Chrome DevTools tools",
|
|
64
|
+
action: "disableAll",
|
|
65
|
+
},
|
|
66
|
+
{ id: "done", label: "Done", close: true },
|
|
67
|
+
],
|
|
68
|
+
hint: "close",
|
|
69
|
+
doneLabel: "Done",
|
|
70
|
+
};
|
|
71
|
+
},
|
|
146
72
|
},
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else if (row.action === "enableAll") {
|
|
178
|
-
selectedTools = new Set(allChromeDevtoolsTools());
|
|
179
|
-
} else if (row.action === "disableAll") {
|
|
180
|
-
selectedTools = new Set();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const saved = await transactSelectedTools(
|
|
184
|
-
pi,
|
|
185
|
-
ctx,
|
|
186
|
-
orderedChromeDevtoolsTools(selectedTools),
|
|
187
|
-
generation,
|
|
188
|
-
);
|
|
189
|
-
if (generation !== state.sessionGeneration) return;
|
|
190
|
-
if (!saved) selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (generation !== state.sessionGeneration) return;
|
|
73
|
+
actions: {
|
|
74
|
+
toggle: async ({ itemId, selected }) => {
|
|
75
|
+
if (!isChromeDevtoolsToolName(itemId)) return { kind: "rejected" };
|
|
76
|
+
const selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
|
|
77
|
+
if (selected) selectedTools.add(itemId);
|
|
78
|
+
else selectedTools.delete(itemId);
|
|
79
|
+
const saved = await transactSelectedTools(
|
|
80
|
+
pi,
|
|
81
|
+
ctx,
|
|
82
|
+
orderedChromeDevtoolsTools(selectedTools),
|
|
83
|
+
generation,
|
|
84
|
+
);
|
|
85
|
+
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
86
|
+
},
|
|
87
|
+
enableAll: async () => {
|
|
88
|
+
const saved = await transactSelectedTools(pi, ctx, allChromeDevtoolsTools(), generation);
|
|
89
|
+
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
90
|
+
},
|
|
91
|
+
disableAll: async () => {
|
|
92
|
+
const saved = await transactSelectedTools(pi, ctx, [], generation);
|
|
93
|
+
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const result = await runMenu(ctx, menu, {
|
|
98
|
+
getState: () => undefined,
|
|
99
|
+
signal: state.sessionController.signal,
|
|
100
|
+
isCurrent: () => generation === state.sessionGeneration,
|
|
101
|
+
});
|
|
102
|
+
if (result.kind !== "closed" || generation !== state.sessionGeneration) return;
|
|
194
103
|
const status = await buildToolStatusMessage(pi);
|
|
195
104
|
if (generation !== state.sessionGeneration) return;
|
|
196
105
|
ctx.ui.notify(status, "info");
|
|
@@ -348,21 +257,8 @@ function toolSelectorTitle(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
|
|
|
348
257
|
return `Chrome DevTools tools (${selectedTools.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}). Non-built-in tools run at user risk.`;
|
|
349
258
|
}
|
|
350
259
|
|
|
351
|
-
function
|
|
352
|
-
return
|
|
353
|
-
...CHROME_DEVTOOLS_TOOL_NAMES.map((toolName) => ({ kind: "tool" as const, toolName })),
|
|
354
|
-
{ kind: "action", action: "enableAll", label: TOOL_SELECTOR_ENABLE_ALL },
|
|
355
|
-
{ kind: "action", action: "disableAll", label: TOOL_SELECTOR_DISABLE_ALL },
|
|
356
|
-
{ kind: "action", action: "done", label: TOOL_SELECTOR_DONE },
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function formatToolSelectorRow(
|
|
361
|
-
row: ToolSelectorRow,
|
|
362
|
-
selectedTools: ReadonlySet<ChromeDevToolsToolName>,
|
|
363
|
-
) {
|
|
364
|
-
if (row.kind === "action") return row.label;
|
|
365
|
-
return `${selectedTools.has(row.toolName) ? "[x]" : "[ ]"} ${row.toolName}`;
|
|
260
|
+
function isChromeDevtoolsToolName(value: string): value is ChromeDevToolsToolName {
|
|
261
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.includes(value as ChromeDevToolsToolName);
|
|
366
262
|
}
|
|
367
263
|
|
|
368
264
|
function getActiveChromeDevtoolsTools(pi: ExtensionAPI) {
|