@narumitw/pi-chrome-devtools 0.49.4 → 0.50.1
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 +115 -38
- package/package.json +2 -2
- package/src/browser-manager.ts +366 -57
- package/src/chrome-devtools.ts +46 -81
- package/src/menu.ts +483 -0
- package/src/runtime.ts +27 -0
- package/src/settings.ts +337 -82
- package/src/tool-selector.ts +145 -130
package/src/tool-selector.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
1
2
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
browserCandidateHint,
|
|
5
|
+
browserLifecycleState,
|
|
4
6
|
devToolsEndpoint,
|
|
5
7
|
endpointConfigHint,
|
|
6
8
|
endpointSourceLabel,
|
|
@@ -13,17 +15,11 @@ import { loadSettings, saveSettings, settingsFilePath } from "./settings.js";
|
|
|
13
15
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
14
16
|
|
|
15
17
|
type CommandContext = ExtensionCommandContext;
|
|
16
|
-
type ToolSelectorScreen = "tools";
|
|
17
|
-
type ToolSelectorAction = "toggle" | "enableAll" | "disableAll";
|
|
18
18
|
|
|
19
19
|
function unique<T>(values: T[]) {
|
|
20
20
|
return Array.from(new Set(values));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function recordSettingsNotice(settings: { notice?: string }) {
|
|
24
|
-
if (settings.notice) state.settingsNotice = settings.notice;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
function formatError(error: unknown) {
|
|
28
24
|
return error instanceof Error ? error.message : String(error);
|
|
29
25
|
}
|
|
@@ -34,79 +30,7 @@ interface ToolStatusSummary {
|
|
|
34
30
|
activeNonChromeToolCount: number;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
const generation = state.sessionGeneration;
|
|
39
|
-
if (!ctx.hasUI) return;
|
|
40
|
-
const menuSignal = state.sessionController.signal;
|
|
41
|
-
const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
|
|
42
|
-
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
43
|
-
if (!isCurrent()) return;
|
|
44
|
-
const menu = defineMenu<undefined, ToolSelectorScreen, ToolSelectorAction>({
|
|
45
|
-
start: "tools",
|
|
46
|
-
screens: {
|
|
47
|
-
tools: () => {
|
|
48
|
-
const selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
|
|
49
|
-
return {
|
|
50
|
-
kind: "multiSelect",
|
|
51
|
-
title: toolSelectorTitle(selectedTools),
|
|
52
|
-
items: CHROME_DEVTOOLS_TOOL_NAMES.map((toolName) => ({
|
|
53
|
-
id: toolName,
|
|
54
|
-
label: toolName,
|
|
55
|
-
selected: selectedTools.has(toolName),
|
|
56
|
-
})),
|
|
57
|
-
action: "toggle",
|
|
58
|
-
actions: [
|
|
59
|
-
{
|
|
60
|
-
id: "enable-all",
|
|
61
|
-
label: "Enable all Chrome DevTools tools",
|
|
62
|
-
action: "enableAll",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "disable-all",
|
|
66
|
-
label: "Disable all Chrome DevTools tools",
|
|
67
|
-
action: "disableAll",
|
|
68
|
-
},
|
|
69
|
-
{ id: "done", label: "Done", close: true },
|
|
70
|
-
],
|
|
71
|
-
hint: "close",
|
|
72
|
-
doneLabel: "Done",
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
actions: {
|
|
77
|
-
toggle: async ({ itemId, selected }) => {
|
|
78
|
-
if (!isChromeDevtoolsToolName(itemId)) return { kind: "rejected" };
|
|
79
|
-
const selectedTools = new Set(getActiveChromeDevtoolsTools(pi));
|
|
80
|
-
if (selected) selectedTools.add(itemId);
|
|
81
|
-
else selectedTools.delete(itemId);
|
|
82
|
-
const saved = await transactSelectedTools(
|
|
83
|
-
pi,
|
|
84
|
-
ctx,
|
|
85
|
-
orderedChromeDevtoolsTools(selectedTools),
|
|
86
|
-
generation,
|
|
87
|
-
);
|
|
88
|
-
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
89
|
-
},
|
|
90
|
-
enableAll: async () => {
|
|
91
|
-
const saved = await transactSelectedTools(pi, ctx, allChromeDevtoolsTools(), generation);
|
|
92
|
-
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
93
|
-
},
|
|
94
|
-
disableAll: async () => {
|
|
95
|
-
const saved = await transactSelectedTools(pi, ctx, [], generation);
|
|
96
|
-
return saved ? { kind: "stay" } : { kind: "rejected" };
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
const result = await runMenu(ctx, menu, {
|
|
101
|
-
getState: () => undefined,
|
|
102
|
-
signal: menuSignal,
|
|
103
|
-
isCurrent,
|
|
104
|
-
});
|
|
105
|
-
if (result.kind !== "closed" || generation !== state.sessionGeneration) return;
|
|
106
|
-
const status = await buildToolStatusMessage(pi);
|
|
107
|
-
if (generation !== state.sessionGeneration) return;
|
|
108
|
-
ctx.ui.notify(status, "info");
|
|
109
|
-
}
|
|
33
|
+
type ToolSelectionSaveResult = "saved" | "active-tools-changed" | "failed";
|
|
110
34
|
|
|
111
35
|
export async function updateChromeDevtoolsTools(
|
|
112
36
|
pi: ExtensionAPI,
|
|
@@ -115,8 +39,8 @@ export async function updateChromeDevtoolsTools(
|
|
|
115
39
|
action: string,
|
|
116
40
|
) {
|
|
117
41
|
const generation = state.sessionGeneration;
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
42
|
+
const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
|
|
43
|
+
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
120
44
|
const status = await buildToolStatusMessage(pi);
|
|
121
45
|
if (generation !== state.sessionGeneration) return;
|
|
122
46
|
ctx.ui.notify(`Chrome DevTools tools ${action}.\n\n${status}`, "info");
|
|
@@ -126,8 +50,15 @@ export async function setSelectedChromeDevtoolsTools(
|
|
|
126
50
|
pi: ExtensionAPI,
|
|
127
51
|
ctx: CommandContext,
|
|
128
52
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
129
|
-
|
|
130
|
-
|
|
53
|
+
expectedActiveTools: readonly ChromeDevToolsToolName[],
|
|
54
|
+
): Promise<ToolSelectionSaveResult> {
|
|
55
|
+
return transactSelectedTools(
|
|
56
|
+
pi,
|
|
57
|
+
ctx,
|
|
58
|
+
selectedTools,
|
|
59
|
+
state.sessionGeneration,
|
|
60
|
+
expectedActiveTools,
|
|
61
|
+
);
|
|
131
62
|
}
|
|
132
63
|
|
|
133
64
|
let toolTransactionQueue = Promise.resolve();
|
|
@@ -141,9 +72,10 @@ function transactSelectedTools(
|
|
|
141
72
|
ctx: CommandContext,
|
|
142
73
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
143
74
|
expectedGeneration: number,
|
|
144
|
-
|
|
75
|
+
expectedActiveTools?: readonly ChromeDevToolsToolName[],
|
|
76
|
+
): Promise<ToolSelectionSaveResult> {
|
|
145
77
|
const operation = toolTransactionQueue.then(() =>
|
|
146
|
-
transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration),
|
|
78
|
+
transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools),
|
|
147
79
|
);
|
|
148
80
|
toolTransactionQueue = operation.then(
|
|
149
81
|
() => undefined,
|
|
@@ -157,13 +89,21 @@ async function transactSelectedToolsNow(
|
|
|
157
89
|
ctx: CommandContext,
|
|
158
90
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
159
91
|
expectedGeneration: number,
|
|
160
|
-
|
|
161
|
-
|
|
92
|
+
expectedActiveTools?: readonly ChromeDevToolsToolName[],
|
|
93
|
+
): Promise<ToolSelectionSaveResult> {
|
|
94
|
+
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
95
|
+
if (expectedActiveTools && !arraysEqual(activeChromeDevtoolsTools(pi), expectedActiveTools)) {
|
|
96
|
+
ctx.ui.notify(
|
|
97
|
+
"Browser tool selection changed while review was open. Review the current state, then apply again.",
|
|
98
|
+
"warning",
|
|
99
|
+
);
|
|
100
|
+
return "active-tools-changed";
|
|
101
|
+
}
|
|
162
102
|
const previousActiveTools = pi.getActiveTools();
|
|
163
103
|
try {
|
|
164
104
|
applyChromeDevtoolsTools(pi, selectedTools);
|
|
165
105
|
await persistSettings(selectedTools);
|
|
166
|
-
return expectedGeneration === state.sessionGeneration;
|
|
106
|
+
return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
|
|
167
107
|
} catch (error) {
|
|
168
108
|
let rollbackError: unknown;
|
|
169
109
|
try {
|
|
@@ -174,17 +114,28 @@ async function transactSelectedToolsNow(
|
|
|
174
114
|
} catch (caught) {
|
|
175
115
|
rollbackError = caught;
|
|
176
116
|
}
|
|
177
|
-
if (expectedGeneration !== state.sessionGeneration) return
|
|
117
|
+
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
178
118
|
ctx.ui.notify(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
119
|
+
sanitizeChromeDevtoolsDisplay(
|
|
120
|
+
rollbackError
|
|
121
|
+
? `Chrome DevTools settings save failed: ${formatError(error)}; active-tool rollback failed: ${formatError(rollbackError)}`
|
|
122
|
+
: `Chrome DevTools settings save failed; active tools restored: ${formatError(error)}`,
|
|
123
|
+
),
|
|
182
124
|
"warning",
|
|
183
125
|
);
|
|
184
|
-
return
|
|
126
|
+
return "failed";
|
|
185
127
|
}
|
|
186
128
|
}
|
|
187
129
|
|
|
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
|
+
function arraysEqual<T>(left: readonly T[], right: readonly T[]) {
|
|
136
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
137
|
+
}
|
|
138
|
+
|
|
188
139
|
export function applyChromeDevtoolsTools(
|
|
189
140
|
pi: ExtensionAPI,
|
|
190
141
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
@@ -217,29 +168,105 @@ function getToolStatusSummary(pi: ExtensionAPI): ToolStatusSummary {
|
|
|
217
168
|
export async function buildToolStatusMessage(pi: ExtensionAPI) {
|
|
218
169
|
const summary = getToolStatusSummary(pi);
|
|
219
170
|
const persistedSetting = await persistedSettingLabel();
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
171
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
172
|
+
[
|
|
173
|
+
`Chrome DevTools tools: ${formatRuntimeStatus(summary)}`,
|
|
174
|
+
`Persisted selection: ${persistedSetting}`,
|
|
175
|
+
...browserSettingsStatusLines(),
|
|
176
|
+
...(state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : []),
|
|
177
|
+
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
178
|
+
`Endpoint: ${devToolsEndpoint()}`,
|
|
179
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
180
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
181
|
+
...launchAttemptLines(),
|
|
182
|
+
].join("\n"),
|
|
183
|
+
);
|
|
231
184
|
}
|
|
232
185
|
|
|
233
186
|
export function buildQuickstartMessage() {
|
|
187
|
+
return buildSettingsSetupMessage();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function buildBrowserStatusMessage() {
|
|
191
|
+
const lifecycle = browserLifecycleState();
|
|
192
|
+
const browserState =
|
|
193
|
+
lifecycle === "starting"
|
|
194
|
+
? "starting managed browser"
|
|
195
|
+
: lifecycle === "running"
|
|
196
|
+
? "managed browser running"
|
|
197
|
+
: lifecycle === "exited"
|
|
198
|
+
? "managed browser exited"
|
|
199
|
+
: lifecycle === "failed"
|
|
200
|
+
? "last launch failed"
|
|
201
|
+
: "not started; connection has not been checked";
|
|
202
|
+
const needsRecovery = lifecycle === "exited" || lifecycle === "failed";
|
|
203
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
204
|
+
[
|
|
205
|
+
`Browser: ${browserState}`,
|
|
206
|
+
"Viewing this status does not probe the endpoint or launch Chrome.",
|
|
207
|
+
`Endpoint: ${devToolsEndpoint()}`,
|
|
208
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
209
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
210
|
+
`Unpacked extensions: ${state.extensionPaths.length} (${state.extensionPathsSource})`,
|
|
211
|
+
...(state.extensionPaths.length > 0
|
|
212
|
+
? ["Unpacked extensions execute trusted browser code in an isolated managed browser."]
|
|
213
|
+
: []),
|
|
214
|
+
...launchAttemptLines(),
|
|
215
|
+
...(needsRecovery ? [launchHint(), endpointConfigHint()] : []),
|
|
216
|
+
].join("\n"),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildSettingsSetupMessage() {
|
|
221
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
222
|
+
[
|
|
223
|
+
`Chrome DevTools endpoint: ${devToolsEndpoint()}`,
|
|
224
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
225
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
226
|
+
...browserSettingsStatusLines(),
|
|
227
|
+
launchHint(),
|
|
228
|
+
browserCandidateHint(),
|
|
229
|
+
...launchAttemptLines(),
|
|
230
|
+
endpointConfigHint(),
|
|
231
|
+
].join("\n"),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function sanitizeChromeDevtoolsDisplay(value: string, maxCharacters = 50_000) {
|
|
236
|
+
const sanitized = Array.from(stripVTControlCharacters(value), (character) => {
|
|
237
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
238
|
+
const unsafeControl =
|
|
239
|
+
(codePoint >= 0 && codePoint <= 8) ||
|
|
240
|
+
(codePoint >= 11 && codePoint <= 31) ||
|
|
241
|
+
(codePoint >= 127 && codePoint <= 159);
|
|
242
|
+
return unsafeControl ? "�" : character;
|
|
243
|
+
}).join("");
|
|
244
|
+
if (sanitized.length <= maxCharacters) return sanitized;
|
|
245
|
+
return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}…`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function browserSettingsStatusLines() {
|
|
249
|
+
const extensionLines =
|
|
250
|
+
state.extensionPaths.length > 0
|
|
251
|
+
? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`)
|
|
252
|
+
: [" - none"];
|
|
234
253
|
return [
|
|
235
|
-
`
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
254
|
+
`Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
|
|
255
|
+
...(state.projectSettingsFilePath
|
|
256
|
+
? [
|
|
257
|
+
`Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`,
|
|
258
|
+
]
|
|
259
|
+
: []),
|
|
260
|
+
`Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
|
|
261
|
+
`Unpacked extensions (${state.extensionPathsSource}):`,
|
|
262
|
+
...extensionLines,
|
|
263
|
+
"Settings changes apply to a new managed browser after /reload or session replacement.",
|
|
264
|
+
...(state.extensionPaths.length > 0
|
|
265
|
+
? [
|
|
266
|
+
"Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code.",
|
|
267
|
+
]
|
|
268
|
+
: []),
|
|
269
|
+
];
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
export function buildCommandGuide() {
|
|
@@ -250,25 +277,12 @@ export function buildCommandGuide() {
|
|
|
250
277
|
"/chrome-devtools quickstart — show endpoint and launch help",
|
|
251
278
|
"/chrome-devtools status — show tool and settings status",
|
|
252
279
|
"/chrome-devtools tools — select individual Chrome DevTools tools",
|
|
253
|
-
"/chrome-devtools toggle —
|
|
254
|
-
"/chrome-devtools enable — enable all Chrome DevTools tools",
|
|
255
|
-
"/chrome-devtools disable — disable all Chrome DevTools tools",
|
|
280
|
+
"/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",
|
|
256
283
|
].join("\n");
|
|
257
284
|
}
|
|
258
285
|
|
|
259
|
-
function toolSelectorTitle(selectedTools: ReadonlySet<ChromeDevToolsToolName>) {
|
|
260
|
-
return `Chrome DevTools tools (${selectedTools.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}). Non-built-in tools run at user risk.`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function isChromeDevtoolsToolName(value: string): value is ChromeDevToolsToolName {
|
|
264
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.includes(value as ChromeDevToolsToolName);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function getActiveChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
268
|
-
const activeToolNames = new Set(pi.getActiveTools());
|
|
269
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => activeToolNames.has(toolName));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
286
|
export function allChromeDevtoolsTools() {
|
|
273
287
|
return [...CHROME_DEVTOOLS_TOOL_NAMES];
|
|
274
288
|
}
|
|
@@ -283,8 +297,9 @@ function formatRuntimeStatus(summary: ToolStatusSummary) {
|
|
|
283
297
|
|
|
284
298
|
async function persistedSettingLabel() {
|
|
285
299
|
const settings = await loadSettings();
|
|
286
|
-
|
|
287
|
-
|
|
300
|
+
if (settings.kind === "loaded" && settings.settings.tools) {
|
|
301
|
+
return formatPersistedSelection(settings.settings.tools);
|
|
302
|
+
}
|
|
288
303
|
if (settings.kind === "invalid") {
|
|
289
304
|
return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
|
|
290
305
|
}
|