@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 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
- - Provides a Plan-mode-style selector for choosing individual Chrome DevTools tools.
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 Plan-mode-style selector for choosing individual `chrome_devtools_*` tools.
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.35.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.5",
36
- "@earendil-works/pi-coding-agent": "0.82.1",
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": {
@@ -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
- const choice = await ctx.ui.select("Chrome DevTools", Object.values(MENU_OPTIONS));
138
- if (generation !== state.sessionGeneration) return;
139
- switch (choice) {
140
- case MENU_OPTIONS.quickstart:
141
- ctx.ui.notify(buildQuickstartMessage(), "info");
142
- return;
143
- case MENU_OPTIONS.help:
144
- ctx.ui.notify(buildCommandGuide(), "info");
145
- return;
146
- case MENU_OPTIONS.status: {
147
- const status = await buildToolStatusMessage(pi);
148
- if (generation !== state.sessionGeneration) return;
149
- ctx.ui.notify(status, "info");
150
- return;
151
- }
152
- case MENU_OPTIONS.tools:
153
- await showToolSelector(pi, ctx);
154
- return;
155
- case MENU_OPTIONS.enable:
156
- await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "enabled all");
157
- return;
158
- case MENU_OPTIONS.disable:
159
- await updateChromeDevtoolsTools(pi, ctx, [], "disabled all");
160
- return;
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
  };
@@ -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
- const TOOL_SELECTOR_DONE = "Done";
17
- const TOOL_SELECTOR_ENABLE_ALL = "Enable all Chrome DevTools tools";
18
- const TOOL_SELECTOR_DISABLE_ALL = "Disable all Chrome DevTools tools";
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
- if (ctx.mode !== "tui") {
45
- await showDialogToolSelector(pi, ctx, generation);
46
- return;
47
- }
48
-
49
- let selectedTools = new Set<ChromeDevToolsToolName>(getActiveChromeDevtoolsTools(pi));
50
- let persistQueue = Promise.resolve();
51
- let requestedRevision = 0;
52
- let requestRender: () => void = () => undefined;
53
- const commitSelectedTools = () => {
54
- const nextSelectedTools = orderedChromeDevtoolsTools(selectedTools);
55
- const revision = ++requestedRevision;
56
- persistQueue = persistQueue.then(async () => {
57
- const saved = await transactSelectedTools(pi, ctx, nextSelectedTools, generation);
58
- if (!saved && generation === state.sessionGeneration && revision === requestedRevision) {
59
- selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
60
- requestRender();
61
- }
62
- });
63
- };
64
-
65
- const customResult = await ctx.ui.custom<"closed" | undefined>(
66
- (tui, theme, keybindings, done) => {
67
- requestRender = () => tui.requestRender();
68
- const rows = chromeDevtoolsToolSelectorRows();
69
- let selectedIndex = 0;
70
- const moveSelection = (delta: number) => {
71
- selectedIndex = (selectedIndex + delta + rows.length) % rows.length;
72
- };
73
- const activateSelectedRow = () => {
74
- const row = rows[selectedIndex];
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
- if (generation !== state.sessionGeneration) return;
150
- if (customResult !== "closed") {
151
- await showDialogToolSelector(pi, ctx, generation);
152
- return;
153
- }
154
-
155
- await persistQueue;
156
- if (generation !== state.sessionGeneration) return;
157
- const status = await buildToolStatusMessage(pi);
158
- if (generation !== state.sessionGeneration) return;
159
- ctx.ui.notify(status, "info");
160
- }
161
-
162
- async function showDialogToolSelector(pi: ExtensionAPI, ctx: CommandContext, generation: number) {
163
- let selectedTools = new Set<ChromeDevToolsToolName>(getActiveChromeDevtoolsTools(pi));
164
- while (true) {
165
- const rows = chromeDevtoolsToolSelectorRows();
166
- const choices = rows.map((row) => formatToolSelectorRow(row, selectedTools));
167
- const choice = await ctx.ui.select(toolSelectorTitle(selectedTools), choices);
168
- if (generation !== state.sessionGeneration || !choice) break;
169
-
170
- const row = rows[choices.indexOf(choice)];
171
- if (!row) continue;
172
- if (row.kind === "action" && row.action === "done") break;
173
-
174
- if (row.kind === "tool") {
175
- if (selectedTools.has(row.toolName)) selectedTools.delete(row.toolName);
176
- else selectedTools.add(row.toolName);
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 chromeDevtoolsToolSelectorRows(): ToolSelectorRow[] {
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) {