@narumitw/pi-chrome-devtools 0.49.4 → 0.50.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 +115 -38
- package/package.json +1 -1
- 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/chrome-devtools.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { shutdownManagedBrowser } from "./browser-manager.js";
|
|
3
|
-
import { state } from "./runtime.js";
|
|
3
|
+
import { applyRuntimeBrowserSettings, state } from "./runtime.js";
|
|
4
4
|
import { loadSettings } from "./settings.js";
|
|
5
5
|
import {
|
|
6
6
|
allChromeDevtoolsTools,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
buildCommandGuide,
|
|
9
9
|
buildQuickstartMessage,
|
|
10
10
|
buildToolStatusMessage,
|
|
11
|
-
|
|
11
|
+
sanitizeChromeDevtoolsDisplay,
|
|
12
12
|
updateChromeDevtoolsTools,
|
|
13
13
|
waitForChromeDevtoolsSettings,
|
|
14
14
|
} from "./tool-selector.js";
|
|
@@ -28,19 +28,13 @@ const COMMAND_COMPLETIONS = [
|
|
|
28
28
|
{ value: "quickstart", label: "quickstart", description: "Show endpoint and launch help" },
|
|
29
29
|
{ value: "status", label: "status", description: "Show tool and settings status" },
|
|
30
30
|
{ value: "tools", label: "tools", description: "Select Chrome DevTools tools" },
|
|
31
|
-
{ value: "toggle", label: "toggle", description: "
|
|
31
|
+
{ value: "toggle", label: "toggle", description: "Alias for tools" },
|
|
32
|
+
{ value: "select", label: "select", description: "Compatibility alias for tools" },
|
|
32
33
|
{ value: "enable", label: "enable", description: "Enable all Chrome DevTools tools" },
|
|
34
|
+
{ value: "on", label: "on", description: "Compatibility alias for enable" },
|
|
33
35
|
{ value: "disable", label: "disable", description: "Disable all Chrome DevTools tools" },
|
|
36
|
+
{ value: "off", label: "off", description: "Compatibility alias for disable" },
|
|
34
37
|
];
|
|
35
|
-
const MENU_OPTIONS = {
|
|
36
|
-
quickstart: "Quick start / endpoint help",
|
|
37
|
-
help: "Command usage guide",
|
|
38
|
-
status: "Show tool status",
|
|
39
|
-
tools: "Select Chrome DevTools tools",
|
|
40
|
-
enable: "Enable all Chrome DevTools tools",
|
|
41
|
-
disable: "Disable all Chrome DevTools tools",
|
|
42
|
-
} as const;
|
|
43
|
-
|
|
44
38
|
export default function chromeDevtools(pi: ExtensionAPI) {
|
|
45
39
|
pi.registerTool(listPagesTool);
|
|
46
40
|
pi.registerTool(selectPageTool);
|
|
@@ -63,16 +57,20 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
63
57
|
state.shuttingDown = false;
|
|
64
58
|
state.settingsNotice = undefined;
|
|
65
59
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
66
|
-
|
|
60
|
+
await shutdownManagedBrowser();
|
|
61
|
+
if (generation !== state.sessionGeneration) return;
|
|
62
|
+
state.activePageId = undefined;
|
|
63
|
+
state.lastLaunchAttempt = undefined;
|
|
64
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
65
|
+
const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
67
66
|
if (generation !== state.sessionGeneration) return;
|
|
67
|
+
applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
|
|
68
68
|
state.settingsNotice = settings.notice;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
applyChromeDevtoolsTools(pi, settings.settings.tools);
|
|
72
|
-
return;
|
|
69
|
+
for (const warning of settings.warnings) {
|
|
70
|
+
ctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), "warning");
|
|
73
71
|
}
|
|
74
|
-
if (settings.kind === "
|
|
75
|
-
|
|
72
|
+
if (settings.kind === "loaded" && settings.settings.tools) {
|
|
73
|
+
applyChromeDevtoolsTools(pi, settings.settings.tools);
|
|
76
74
|
}
|
|
77
75
|
});
|
|
78
76
|
|
|
@@ -98,20 +96,29 @@ async function handleChromeDevtoolsCommand(
|
|
|
98
96
|
await showMenu(pi, ctx, generation);
|
|
99
97
|
return;
|
|
100
98
|
case "help":
|
|
99
|
+
requireObservableUi(ctx, "help");
|
|
101
100
|
ctx.ui.notify(buildCommandGuide(), "info");
|
|
102
101
|
return;
|
|
103
102
|
case "quickstart":
|
|
103
|
+
requireObservableUi(ctx, "quickstart");
|
|
104
104
|
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
105
105
|
return;
|
|
106
106
|
case "status": {
|
|
107
|
+
requireObservableUi(ctx, "status");
|
|
107
108
|
const status = await buildToolStatusMessage(pi);
|
|
108
109
|
if (generation !== state.sessionGeneration) return;
|
|
109
110
|
ctx.ui.notify(status, "info");
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
112
|
-
case "tools":
|
|
113
|
-
|
|
113
|
+
case "tools": {
|
|
114
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
115
|
+
throw new Error("/chrome-devtools tools requires TUI or RPC mode");
|
|
116
|
+
}
|
|
117
|
+
const { showChromeDevtoolsToolWorkflow } = await import("./menu.js");
|
|
118
|
+
if (generation !== state.sessionGeneration) return;
|
|
119
|
+
await showChromeDevtoolsToolWorkflow(pi, ctx, generation);
|
|
114
120
|
return;
|
|
121
|
+
}
|
|
115
122
|
case "enable":
|
|
116
123
|
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "enabled all");
|
|
117
124
|
return;
|
|
@@ -120,6 +127,9 @@ async function handleChromeDevtoolsCommand(
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
131
|
+
throw new Error(`Unknown /chrome-devtools command: ${args.trim()}`);
|
|
132
|
+
}
|
|
123
133
|
ctx.ui.notify(
|
|
124
134
|
`Unknown /chrome-devtools command: ${args.trim()}
|
|
125
135
|
|
|
@@ -128,67 +138,19 @@ ${buildCommandGuide()}`,
|
|
|
128
138
|
);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
if (!ctx.hasUI) {
|
|
133
|
-
|
|
134
|
-
if (generation !== state.sessionGeneration) return;
|
|
135
|
-
ctx.ui.notify(`${buildCommandGuide()}\n\n${status}`, "info");
|
|
136
|
-
return;
|
|
141
|
+
function requireObservableUi(ctx: CommandContext, route: string) {
|
|
142
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
143
|
+
throw new Error(`/chrome-devtools ${route} requires TUI or RPC mode`);
|
|
137
144
|
}
|
|
138
|
-
|
|
139
|
-
const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
|
|
140
|
-
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
141
|
-
if (!isCurrent()) return;
|
|
145
|
+
}
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
title: "Chrome DevTools",
|
|
151
|
-
items: Object.entries(MENU_OPTIONS).map(([id, label]) => ({
|
|
152
|
-
id,
|
|
153
|
-
label,
|
|
154
|
-
action: id as Action,
|
|
155
|
-
})),
|
|
156
|
-
hint: "close",
|
|
157
|
-
}),
|
|
158
|
-
},
|
|
159
|
-
actions: {
|
|
160
|
-
quickstart: async () => {
|
|
161
|
-
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
162
|
-
return { kind: "close" };
|
|
163
|
-
},
|
|
164
|
-
help: async () => {
|
|
165
|
-
ctx.ui.notify(buildCommandGuide(), "info");
|
|
166
|
-
return { kind: "close" };
|
|
167
|
-
},
|
|
168
|
-
status: async () => {
|
|
169
|
-
const status = await buildToolStatusMessage(pi);
|
|
170
|
-
if (generation === state.sessionGeneration) ctx.ui.notify(status, "info");
|
|
171
|
-
return { kind: "close" };
|
|
172
|
-
},
|
|
173
|
-
tools: async () => {
|
|
174
|
-
await showToolSelector(pi, ctx);
|
|
175
|
-
return { kind: "close" };
|
|
176
|
-
},
|
|
177
|
-
enable: async () => {
|
|
178
|
-
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "enabled all");
|
|
179
|
-
return { kind: "close" };
|
|
180
|
-
},
|
|
181
|
-
disable: async () => {
|
|
182
|
-
await updateChromeDevtoolsTools(pi, ctx, [], "disabled all");
|
|
183
|
-
return { kind: "close" };
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
await runMenu(ctx, menu, {
|
|
188
|
-
getState: () => undefined,
|
|
189
|
-
signal: menuSignal,
|
|
190
|
-
isCurrent,
|
|
191
|
-
});
|
|
147
|
+
async function showMenu(pi: ExtensionAPI, ctx: CommandContext, generation: number) {
|
|
148
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
149
|
+
throw new Error("/chrome-devtools menu requires TUI or RPC mode; use a direct subcommand");
|
|
150
|
+
}
|
|
151
|
+
const { showChromeDevtoolsMenu } = await import("./menu.js");
|
|
152
|
+
if (generation !== state.sessionGeneration) return;
|
|
153
|
+
await showChromeDevtoolsMenu(pi, ctx, generation);
|
|
192
154
|
}
|
|
193
155
|
|
|
194
156
|
function replaceSessionController(reason: string) {
|
|
@@ -231,4 +193,7 @@ export {
|
|
|
231
193
|
selectAllowedRoot,
|
|
232
194
|
} from "./screenshot.js";
|
|
233
195
|
export { normalizeChromeDevtoolsSettings } from "./settings.js";
|
|
234
|
-
export {
|
|
196
|
+
export {
|
|
197
|
+
orderedChromeDevtoolsTools,
|
|
198
|
+
sanitizeChromeDevtoolsDisplay,
|
|
199
|
+
} from "./tool-selector.js";
|
package/src/menu.ts
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu, runTask } from "@narumitw/pi-tui-kit";
|
|
3
|
+
import { browserLifecycleState, devToolsEndpoint, launchModeLabel } from "./browser-manager.js";
|
|
4
|
+
import { state } from "./runtime.js";
|
|
5
|
+
import { loadSettings, type SettingsLoadResult } from "./settings.js";
|
|
6
|
+
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
7
|
+
import {
|
|
8
|
+
buildBrowserStatusMessage,
|
|
9
|
+
buildCommandGuide,
|
|
10
|
+
buildSettingsSetupMessage,
|
|
11
|
+
sanitizeChromeDevtoolsDisplay,
|
|
12
|
+
setSelectedChromeDevtoolsTools,
|
|
13
|
+
} from "./tool-selector.js";
|
|
14
|
+
|
|
15
|
+
type CommandContext = ExtensionCommandContext;
|
|
16
|
+
type MainScreen = "main" | "browserStatus" | "settings" | "help";
|
|
17
|
+
type MainAction = "tools" | "bulk";
|
|
18
|
+
type ToolScreen = "tools" | "review";
|
|
19
|
+
type ToolAction = "toggle" | "selectAll" | "selectNone" | "review" | "apply" | "cancel";
|
|
20
|
+
|
|
21
|
+
interface MenuSnapshot {
|
|
22
|
+
activeTools: ChromeDevToolsToolName[];
|
|
23
|
+
persistenceLabel: string;
|
|
24
|
+
mutationBlockedReason?: string;
|
|
25
|
+
settingsWarning?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ToolWorkflowState {
|
|
29
|
+
accepted: Set<ChromeDevToolsToolName>;
|
|
30
|
+
draft: Set<ChromeDevToolsToolName>;
|
|
31
|
+
mutationBlockedReason?: string;
|
|
32
|
+
generation: number;
|
|
33
|
+
applied: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TOOL_PRESENTATION: Record<ChromeDevToolsToolName, { label: string; description: string }> = {
|
|
37
|
+
chrome_devtools_list_pages: {
|
|
38
|
+
label: "List open pages",
|
|
39
|
+
description: "List inspectable Chrome tabs and pages (chrome_devtools_list_pages).",
|
|
40
|
+
},
|
|
41
|
+
chrome_devtools_select_page: {
|
|
42
|
+
label: "Select the active page",
|
|
43
|
+
description: "Choose the page later browser actions use (chrome_devtools_select_page).",
|
|
44
|
+
},
|
|
45
|
+
chrome_devtools_navigate: {
|
|
46
|
+
label: "Navigate a page",
|
|
47
|
+
description: "Open a URL, creating a page when needed (chrome_devtools_navigate).",
|
|
48
|
+
},
|
|
49
|
+
chrome_devtools_evaluate: {
|
|
50
|
+
label: "Run JavaScript",
|
|
51
|
+
description: "Evaluate JavaScript in the selected page (chrome_devtools_evaluate).",
|
|
52
|
+
},
|
|
53
|
+
chrome_devtools_screenshot: {
|
|
54
|
+
label: "Capture a screenshot",
|
|
55
|
+
description: "Save a PNG of the selected page (chrome_devtools_screenshot).",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export async function showChromeDevtoolsMenu(
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
ctx: CommandContext,
|
|
62
|
+
generation: number,
|
|
63
|
+
) {
|
|
64
|
+
const snapshot = await loadSnapshotWithFeedback(pi, ctx, generation);
|
|
65
|
+
if (!snapshot) return;
|
|
66
|
+
const menuSignal = state.sessionController.signal;
|
|
67
|
+
const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
|
|
68
|
+
const menu = defineMenu<MenuSnapshot, MainScreen, MainAction>({
|
|
69
|
+
start: "main",
|
|
70
|
+
screens: {
|
|
71
|
+
main: ({ state: current }) => ({
|
|
72
|
+
kind: "actions",
|
|
73
|
+
title: "Chrome DevTools",
|
|
74
|
+
lines: mainStateLines(current),
|
|
75
|
+
items: [
|
|
76
|
+
{
|
|
77
|
+
id: "tools",
|
|
78
|
+
label: "Choose browser tools…",
|
|
79
|
+
description: "Select what Pi may do in Chrome.",
|
|
80
|
+
disabled: Boolean(current.mutationBlockedReason),
|
|
81
|
+
disabledReason: current.mutationBlockedReason,
|
|
82
|
+
action: "tools",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "bulk",
|
|
86
|
+
label:
|
|
87
|
+
current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
88
|
+
? "Disable all browser tools…"
|
|
89
|
+
: "Enable all browser tools…",
|
|
90
|
+
description:
|
|
91
|
+
current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
92
|
+
? "Preview 0 of 5; other active tools stay enabled."
|
|
93
|
+
: "Preview 5 of 5; other active tools stay enabled.",
|
|
94
|
+
disabled: Boolean(current.mutationBlockedReason),
|
|
95
|
+
disabledReason: current.mutationBlockedReason,
|
|
96
|
+
action: "bulk",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "browser-status",
|
|
100
|
+
label: "Browser status",
|
|
101
|
+
description: "Runtime, endpoint, and last launch attempt.",
|
|
102
|
+
to: "browserStatus",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "settings",
|
|
106
|
+
label: "Settings & setup",
|
|
107
|
+
description: "Files, sources, trust, and reload steps.",
|
|
108
|
+
to: "settings",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "help",
|
|
112
|
+
label: "Help",
|
|
113
|
+
description: "Commands and usage.",
|
|
114
|
+
to: "help",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
hint: "close",
|
|
118
|
+
}),
|
|
119
|
+
browserStatus: () => ({
|
|
120
|
+
kind: "review",
|
|
121
|
+
title: "Browser status",
|
|
122
|
+
content: buildBrowserStatusMessage(),
|
|
123
|
+
format: { kind: "text" },
|
|
124
|
+
viewportSize: "adaptive",
|
|
125
|
+
hint: "back",
|
|
126
|
+
}),
|
|
127
|
+
settings: () => ({
|
|
128
|
+
kind: "review",
|
|
129
|
+
title: "Settings & setup",
|
|
130
|
+
content: buildSettingsSetupMessage(),
|
|
131
|
+
format: { kind: "text" },
|
|
132
|
+
viewportSize: "adaptive",
|
|
133
|
+
hint: "back",
|
|
134
|
+
}),
|
|
135
|
+
help: () => ({
|
|
136
|
+
kind: "review",
|
|
137
|
+
title: "Chrome DevTools help",
|
|
138
|
+
content: buildCommandGuide(),
|
|
139
|
+
format: { kind: "text" },
|
|
140
|
+
viewportSize: "adaptive",
|
|
141
|
+
hint: "back",
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
actions: {
|
|
145
|
+
tools: async ({ state: current }) => {
|
|
146
|
+
const result = await showChromeDevtoolsToolWorkflow(pi, ctx, generation, {
|
|
147
|
+
snapshot: current,
|
|
148
|
+
});
|
|
149
|
+
if (result?.applied && isCurrent()) updateSnapshotAfterApply(current, result.selectedTools);
|
|
150
|
+
return result?.closeParent ? { kind: "close" } : { kind: "stay" };
|
|
151
|
+
},
|
|
152
|
+
bulk: async ({ state: current }) => {
|
|
153
|
+
const selectedTools =
|
|
154
|
+
current.activeTools.length === CHROME_DEVTOOLS_TOOL_NAMES.length
|
|
155
|
+
? []
|
|
156
|
+
: [...CHROME_DEVTOOLS_TOOL_NAMES];
|
|
157
|
+
const result = await showChromeDevtoolsToolWorkflow(pi, ctx, generation, {
|
|
158
|
+
snapshot: current,
|
|
159
|
+
initialDraft: selectedTools,
|
|
160
|
+
startAtReview: true,
|
|
161
|
+
});
|
|
162
|
+
if (result?.applied && isCurrent()) updateSnapshotAfterApply(current, result.selectedTools);
|
|
163
|
+
return result?.closeParent ? { kind: "close" } : { kind: "stay" };
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
await runMenu(ctx, menu, {
|
|
168
|
+
getState: () => snapshot,
|
|
169
|
+
signal: menuSignal,
|
|
170
|
+
isCurrent,
|
|
171
|
+
onError: (currentCtx, error) =>
|
|
172
|
+
currentCtx.ui.notify(
|
|
173
|
+
sanitizeChromeDevtoolsDisplay(`Chrome DevTools menu failed: ${formatError(error)}`),
|
|
174
|
+
"error",
|
|
175
|
+
),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function showChromeDevtoolsToolWorkflow(
|
|
180
|
+
pi: ExtensionAPI,
|
|
181
|
+
ctx: CommandContext,
|
|
182
|
+
generation: number,
|
|
183
|
+
options: {
|
|
184
|
+
snapshot?: MenuSnapshot;
|
|
185
|
+
initialDraft?: readonly ChromeDevToolsToolName[];
|
|
186
|
+
startAtReview?: boolean;
|
|
187
|
+
} = {},
|
|
188
|
+
): Promise<
|
|
189
|
+
{ applied: boolean; closeParent: boolean; selectedTools: ChromeDevToolsToolName[] } | undefined
|
|
190
|
+
> {
|
|
191
|
+
const snapshot = options.snapshot ?? (await loadSnapshotWithFeedback(pi, ctx, generation));
|
|
192
|
+
if (!snapshot) return undefined;
|
|
193
|
+
const menuSignal = state.sessionController.signal;
|
|
194
|
+
const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
|
|
195
|
+
const accepted = new Set(snapshot.activeTools);
|
|
196
|
+
const workflow: ToolWorkflowState = {
|
|
197
|
+
accepted,
|
|
198
|
+
draft: new Set(options.initialDraft ?? snapshot.activeTools),
|
|
199
|
+
mutationBlockedReason: snapshot.mutationBlockedReason,
|
|
200
|
+
generation,
|
|
201
|
+
applied: false,
|
|
202
|
+
};
|
|
203
|
+
const menu = defineMenu<ToolWorkflowState, ToolScreen, ToolAction>({
|
|
204
|
+
start: options.startAtReview ? "review" : "tools",
|
|
205
|
+
screens: {
|
|
206
|
+
tools: ({ state: current }) => ({
|
|
207
|
+
kind: "multiSelect",
|
|
208
|
+
title: `Browser tools (${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length})`,
|
|
209
|
+
lines: toolDraftLines(current),
|
|
210
|
+
items: CHROME_DEVTOOLS_TOOL_NAMES.map((toolName) => ({
|
|
211
|
+
id: toolName,
|
|
212
|
+
label: TOOL_PRESENTATION[toolName].label,
|
|
213
|
+
description: TOOL_PRESENTATION[toolName].description,
|
|
214
|
+
searchText: `${toolName} ${TOOL_PRESENTATION[toolName].description}`,
|
|
215
|
+
selected: current.draft.has(toolName),
|
|
216
|
+
disabled: Boolean(current.mutationBlockedReason),
|
|
217
|
+
disabledReason: current.mutationBlockedReason,
|
|
218
|
+
})),
|
|
219
|
+
action: "toggle",
|
|
220
|
+
actions: [
|
|
221
|
+
{
|
|
222
|
+
id: "select-all",
|
|
223
|
+
label: "Select all",
|
|
224
|
+
disabled: Boolean(current.mutationBlockedReason),
|
|
225
|
+
disabledReason: current.mutationBlockedReason,
|
|
226
|
+
action: "selectAll",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "select-none",
|
|
230
|
+
label: "Select none",
|
|
231
|
+
disabled: Boolean(current.mutationBlockedReason),
|
|
232
|
+
disabledReason: current.mutationBlockedReason,
|
|
233
|
+
action: "selectNone",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "review",
|
|
237
|
+
label: "Review changes…",
|
|
238
|
+
disabled:
|
|
239
|
+
Boolean(current.mutationBlockedReason) || setsEqual(current.accepted, current.draft),
|
|
240
|
+
disabledReason:
|
|
241
|
+
current.mutationBlockedReason ??
|
|
242
|
+
(setsEqual(current.accepted, current.draft) ? "No unapplied changes" : undefined),
|
|
243
|
+
to: "review",
|
|
244
|
+
},
|
|
245
|
+
{ id: "cancel", label: "Cancel", action: "cancel" },
|
|
246
|
+
],
|
|
247
|
+
hint: "back",
|
|
248
|
+
}),
|
|
249
|
+
review: ({ state: current }) => ({
|
|
250
|
+
kind: "review",
|
|
251
|
+
title: "Review tool changes",
|
|
252
|
+
lines: [
|
|
253
|
+
`Current: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
254
|
+
`Proposed: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
255
|
+
],
|
|
256
|
+
content: buildToolReview(current),
|
|
257
|
+
format: { kind: "text" },
|
|
258
|
+
viewportSize: "adaptive",
|
|
259
|
+
...(current.mutationBlockedReason || setsEqual(current.accepted, current.draft)
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
confirm: {
|
|
263
|
+
id: "apply",
|
|
264
|
+
label: "Apply tool changes",
|
|
265
|
+
action: "apply" as const,
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
hint: "back",
|
|
269
|
+
}),
|
|
270
|
+
},
|
|
271
|
+
actions: {
|
|
272
|
+
toggle: ({ state: current, itemId, selected }) => {
|
|
273
|
+
if (!isChromeDevtoolsToolName(itemId) || selected === undefined)
|
|
274
|
+
return { kind: "rejected" };
|
|
275
|
+
if (selected) current.draft.add(itemId);
|
|
276
|
+
else current.draft.delete(itemId);
|
|
277
|
+
return { kind: "stay" };
|
|
278
|
+
},
|
|
279
|
+
selectAll: ({ state: current }) => {
|
|
280
|
+
current.draft = new Set(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
281
|
+
return { kind: "stay" };
|
|
282
|
+
},
|
|
283
|
+
selectNone: ({ state: current }) => {
|
|
284
|
+
current.draft = new Set();
|
|
285
|
+
return { kind: "stay" };
|
|
286
|
+
},
|
|
287
|
+
review: () => ({ kind: "to", screen: "review" }),
|
|
288
|
+
cancel: () => ({ kind: "back" }),
|
|
289
|
+
apply: async ({ state: current, signal }) => {
|
|
290
|
+
await ctx.waitForIdle();
|
|
291
|
+
if (signal.aborted || !isCurrent() || current.generation !== state.sessionGeneration) {
|
|
292
|
+
return { kind: "rejected" };
|
|
293
|
+
}
|
|
294
|
+
const selectedTools = orderedTools(current.draft);
|
|
295
|
+
const saveResult = await setSelectedChromeDevtoolsTools(
|
|
296
|
+
pi,
|
|
297
|
+
ctx,
|
|
298
|
+
selectedTools,
|
|
299
|
+
orderedTools(current.accepted),
|
|
300
|
+
);
|
|
301
|
+
if (!isCurrent()) return { kind: "rejected" };
|
|
302
|
+
if (saveResult === "active-tools-changed") {
|
|
303
|
+
current.accepted = new Set(activeChromeTools(pi));
|
|
304
|
+
}
|
|
305
|
+
if (saveResult !== "saved") return { kind: "rejected" };
|
|
306
|
+
current.accepted = new Set(selectedTools);
|
|
307
|
+
current.draft = new Set(selectedTools);
|
|
308
|
+
current.applied = true;
|
|
309
|
+
ctx.ui.notify(
|
|
310
|
+
`Saved: ${selectedTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} browser tools enabled.`,
|
|
311
|
+
"info",
|
|
312
|
+
);
|
|
313
|
+
return { kind: "close" };
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const result = await runMenu(ctx, menu, {
|
|
318
|
+
getState: () => workflow,
|
|
319
|
+
signal: menuSignal,
|
|
320
|
+
isCurrent,
|
|
321
|
+
onError: (currentCtx, error) =>
|
|
322
|
+
currentCtx.ui.notify(
|
|
323
|
+
sanitizeChromeDevtoolsDisplay(
|
|
324
|
+
`Chrome DevTools tool selection failed: ${formatError(error)}`,
|
|
325
|
+
),
|
|
326
|
+
"error",
|
|
327
|
+
),
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
applied: workflow.applied,
|
|
331
|
+
closeParent: !workflow.applied && result.kind === "closed" && result.reason === "close",
|
|
332
|
+
selectedTools: orderedTools(workflow.accepted),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function loadSnapshotWithFeedback(pi: ExtensionAPI, ctx: CommandContext, generation: number) {
|
|
337
|
+
const menuSignal = state.sessionController.signal;
|
|
338
|
+
const isCurrent = () => generation === state.sessionGeneration && !menuSignal.aborted;
|
|
339
|
+
const result = await runTask(ctx, {
|
|
340
|
+
label: "Loading Chrome DevTools settings…",
|
|
341
|
+
signal: menuSignal,
|
|
342
|
+
isCurrent,
|
|
343
|
+
task: async ({ signal }) => {
|
|
344
|
+
signal.throwIfAborted();
|
|
345
|
+
const settings = await loadSettings();
|
|
346
|
+
signal.throwIfAborted();
|
|
347
|
+
return buildMenuSnapshot(pi, settings);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
return result.kind === "completed" ? result.value : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildMenuSnapshot(pi: ExtensionAPI, settings: SettingsLoadResult): MenuSnapshot {
|
|
354
|
+
const activeTools = activeChromeTools(pi);
|
|
355
|
+
const persistedTools =
|
|
356
|
+
settings.kind === "loaded" && settings.settings.tools
|
|
357
|
+
? orderedTools(new Set(settings.settings.tools))
|
|
358
|
+
: undefined;
|
|
359
|
+
const persistenceLabel =
|
|
360
|
+
settings.kind === "invalid"
|
|
361
|
+
? "settings need repair"
|
|
362
|
+
: persistedTools === undefined
|
|
363
|
+
? "not saved"
|
|
364
|
+
: arraysEqual(activeTools, persistedTools)
|
|
365
|
+
? "saved"
|
|
366
|
+
: "runtime differs from saved selection";
|
|
367
|
+
const settingsWarning = [
|
|
368
|
+
...new Set([settings.notice, state.settingsNotice].filter(Boolean)),
|
|
369
|
+
].join("\n");
|
|
370
|
+
return {
|
|
371
|
+
activeTools,
|
|
372
|
+
persistenceLabel,
|
|
373
|
+
...(settings.kind === "invalid"
|
|
374
|
+
? { mutationBlockedReason: `Repair ${settings.reason} before saving` }
|
|
375
|
+
: {}),
|
|
376
|
+
...(settingsWarning ? { settingsWarning } : {}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function mainStateLines(snapshot: MenuSnapshot) {
|
|
381
|
+
return sanitizeLines([
|
|
382
|
+
`Tools: ${snapshot.activeTools.length} of ${CHROME_DEVTOOLS_TOOL_NAMES.length} enabled · ${snapshot.persistenceLabel}`,
|
|
383
|
+
`Browser: ${browserLifecycleSummary()}`,
|
|
384
|
+
`Endpoint: ${devToolsEndpoint()}`,
|
|
385
|
+
...(snapshot.settingsWarning ? [`Settings warning: ${snapshot.settingsWarning}`] : []),
|
|
386
|
+
...(state.lastLaunchAttempt?.lastError
|
|
387
|
+
? [`Launch warning: ${state.lastLaunchAttempt.lastError}`]
|
|
388
|
+
: []),
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function browserLifecycleSummary() {
|
|
393
|
+
const lifecycle = browserLifecycleState();
|
|
394
|
+
if (lifecycle === "starting") return "starting managed browser";
|
|
395
|
+
if (lifecycle === "running") return "managed browser running";
|
|
396
|
+
if (lifecycle === "exited") return "managed browser exited · open Browser status to recover";
|
|
397
|
+
if (lifecycle === "failed") return "last launch failed · open Browser status to recover";
|
|
398
|
+
const launchMode = launchModeLabel();
|
|
399
|
+
return launchMode.startsWith("attach first")
|
|
400
|
+
? "not started · attaches or launches on first use"
|
|
401
|
+
: `not started · ${launchMode}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function toolDraftLines(current: ToolWorkflowState) {
|
|
405
|
+
if (current.mutationBlockedReason)
|
|
406
|
+
return sanitizeLines([`Unavailable: ${current.mutationBlockedReason}`]);
|
|
407
|
+
const changes = symmetricDifferenceSize(current.accepted, current.draft);
|
|
408
|
+
return [
|
|
409
|
+
`Current accepted: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
410
|
+
changes === 0
|
|
411
|
+
? "No unapplied changes · Escape cancels"
|
|
412
|
+
: `${changes} unapplied ${changes === 1 ? "change" : "changes"} · Escape cancels`,
|
|
413
|
+
"Changes are not applied until Review changes and Apply tool changes.",
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
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));
|
|
420
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
421
|
+
[
|
|
422
|
+
`Current browser tools: ${current.accepted.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
423
|
+
`Proposed browser tools: ${current.draft.size}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
424
|
+
"",
|
|
425
|
+
"Enabled after apply:",
|
|
426
|
+
...(enabled.length > 0
|
|
427
|
+
? enabled.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
|
|
428
|
+
: [" - none"]),
|
|
429
|
+
"",
|
|
430
|
+
"Disabled after apply:",
|
|
431
|
+
...(disabled.length > 0
|
|
432
|
+
? disabled.map((name) => ` - ${TOOL_PRESENTATION[name].label} (${name})`)
|
|
433
|
+
: [" - none"]),
|
|
434
|
+
"",
|
|
435
|
+
"Other active Pi tools remain unchanged.",
|
|
436
|
+
"The accepted selection is saved for future sessions.",
|
|
437
|
+
].join("\n"),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function updateSnapshotAfterApply(
|
|
442
|
+
snapshot: MenuSnapshot,
|
|
443
|
+
selectedTools: readonly ChromeDevToolsToolName[],
|
|
444
|
+
) {
|
|
445
|
+
snapshot.activeTools = [...selectedTools];
|
|
446
|
+
snapshot.persistenceLabel = "saved";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function activeChromeTools(pi: ExtensionAPI) {
|
|
450
|
+
const active = new Set(pi.getActiveTools());
|
|
451
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => active.has(toolName));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function orderedTools(tools: ReadonlySet<ChromeDevToolsToolName>) {
|
|
455
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => tools.has(toolName));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isChromeDevtoolsToolName(value: string): value is ChromeDevToolsToolName {
|
|
459
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.includes(value as ChromeDevToolsToolName);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function setsEqual<T>(left: ReadonlySet<T>, right: ReadonlySet<T>) {
|
|
463
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function symmetricDifferenceSize<T>(left: ReadonlySet<T>, right: ReadonlySet<T>) {
|
|
467
|
+
let changes = 0;
|
|
468
|
+
for (const value of left) if (!right.has(value)) changes += 1;
|
|
469
|
+
for (const value of right) if (!left.has(value)) changes += 1;
|
|
470
|
+
return changes;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function arraysEqual<T>(left: readonly T[], right: readonly T[]) {
|
|
474
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function sanitizeLines(lines: readonly string[]) {
|
|
478
|
+
return sanitizeChromeDevtoolsDisplay(lines.join("\n")).split("\n");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function formatError(error: unknown) {
|
|
482
|
+
return error instanceof Error ? error.message : String(error);
|
|
483
|
+
}
|