@narumitw/pi-chrome-devtools 0.31.0 → 0.35.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 +8 -5
- package/package.json +1 -1
- package/src/chrome-devtools.ts +32 -16
- package/src/runtime.ts +2 -0
- package/src/settings.ts +89 -67
- package/src/tool-selector.ts +99 -33
package/README.md
CHANGED
|
@@ -169,11 +169,14 @@ ${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-chrome-devtools.json
|
|
|
169
169
|
|
|
170
170
|
When the file is missing or invalid, the extension preserves Pi's current active-tool policy
|
|
171
171
|
instead of enabling tools by itself. A valid saved selection is restored on Pi startup and
|
|
172
|
-
`/reload`.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
`/reload`. A missing file is created by the first successful selection change. Within one Pi process, selection saves run in invocation order, reread the latest valid document, and preserve unknown fields. Malformed JSON
|
|
173
|
+
or invalid recognized fields block the save without replacement; a failed save restores the prior
|
|
174
|
+
Chrome DevTools tool selection while preserving other extensions' current tools.
|
|
175
|
+
|
|
176
|
+
Compatibility: older versions used `pi-chrome-devtools-settings.json`. A legacy-only file remains
|
|
177
|
+
readable with a warning and is never modified automatically; rename it to
|
|
178
|
+
`pi-chrome-devtools.json`. The first subsequent settings save writes the canonical file. If both
|
|
179
|
+
files exist, `pi-chrome-devtools.json` wins and the legacy file is ignored. The legacy
|
|
177
180
|
filename is deprecated and will be removed in a future major release.
|
|
178
181
|
|
|
179
182
|
## 🧠 Use cases
|
package/package.json
CHANGED
package/src/chrome-devtools.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildToolStatusMessage,
|
|
11
11
|
showToolSelector,
|
|
12
12
|
updateChromeDevtoolsTools,
|
|
13
|
+
waitForChromeDevtoolsSettings,
|
|
13
14
|
} from "./tool-selector.js";
|
|
14
15
|
import {
|
|
15
16
|
evaluateTool,
|
|
@@ -51,15 +52,18 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
51
52
|
description: "Open Chrome DevTools help and tool controls",
|
|
52
53
|
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
53
54
|
handler: async (args, ctx) => {
|
|
54
|
-
|
|
55
|
+
const generation = state.sessionGeneration;
|
|
56
|
+
await handleChromeDevtoolsCommand(pi, args, ctx, generation);
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
pi.on("session_start", async (_event, ctx) => {
|
|
61
|
+
const generation = ++state.sessionGeneration;
|
|
59
62
|
state.shuttingDown = false;
|
|
60
63
|
state.settingsNotice = undefined;
|
|
61
64
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
62
65
|
const settings = await loadSettings();
|
|
66
|
+
if (generation !== state.sessionGeneration) return;
|
|
63
67
|
state.settingsNotice = settings.notice;
|
|
64
68
|
if (settings.notice) ctx.ui.notify(settings.notice, "warning");
|
|
65
69
|
if (settings.kind === "loaded") {
|
|
@@ -72,16 +76,24 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
79
|
+
state.sessionGeneration += 1;
|
|
75
80
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
76
|
-
|
|
81
|
+
const browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });
|
|
82
|
+
await waitForChromeDevtoolsSettings();
|
|
83
|
+
await browserShutdown;
|
|
77
84
|
});
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
async function handleChromeDevtoolsCommand(
|
|
87
|
+
async function handleChromeDevtoolsCommand(
|
|
88
|
+
pi: ExtensionAPI,
|
|
89
|
+
args: string,
|
|
90
|
+
ctx: CommandContext,
|
|
91
|
+
generation: number,
|
|
92
|
+
) {
|
|
81
93
|
const command = parseCommand(args);
|
|
82
94
|
switch (command) {
|
|
83
95
|
case "menu":
|
|
84
|
-
await showMenu(pi, ctx);
|
|
96
|
+
await showMenu(pi, ctx, generation);
|
|
85
97
|
return;
|
|
86
98
|
case "help":
|
|
87
99
|
ctx.ui.notify(buildCommandGuide(), "info");
|
|
@@ -89,9 +101,12 @@ async function handleChromeDevtoolsCommand(pi: ExtensionAPI, args: string, ctx:
|
|
|
89
101
|
case "quickstart":
|
|
90
102
|
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
91
103
|
return;
|
|
92
|
-
case "status":
|
|
93
|
-
|
|
104
|
+
case "status": {
|
|
105
|
+
const status = await buildToolStatusMessage(pi);
|
|
106
|
+
if (generation !== state.sessionGeneration) return;
|
|
107
|
+
ctx.ui.notify(status, "info");
|
|
94
108
|
return;
|
|
109
|
+
}
|
|
95
110
|
case "tools":
|
|
96
111
|
await showToolSelector(pi, ctx);
|
|
97
112
|
return;
|
|
@@ -111,18 +126,16 @@ ${buildCommandGuide()}`,
|
|
|
111
126
|
);
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
async function showMenu(pi: ExtensionAPI, ctx: CommandContext) {
|
|
129
|
+
async function showMenu(pi: ExtensionAPI, ctx: CommandContext, generation: number) {
|
|
115
130
|
if (!ctx.hasUI) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
${await buildToolStatusMessage(pi)}`,
|
|
120
|
-
"info",
|
|
121
|
-
);
|
|
131
|
+
const status = await buildToolStatusMessage(pi);
|
|
132
|
+
if (generation !== state.sessionGeneration) return;
|
|
133
|
+
ctx.ui.notify(`${buildCommandGuide()}\n\n${status}`, "info");
|
|
122
134
|
return;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
const choice = await ctx.ui.select("Chrome DevTools", Object.values(MENU_OPTIONS));
|
|
138
|
+
if (generation !== state.sessionGeneration) return;
|
|
126
139
|
switch (choice) {
|
|
127
140
|
case MENU_OPTIONS.quickstart:
|
|
128
141
|
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
@@ -130,9 +143,12 @@ ${await buildToolStatusMessage(pi)}`,
|
|
|
130
143
|
case MENU_OPTIONS.help:
|
|
131
144
|
ctx.ui.notify(buildCommandGuide(), "info");
|
|
132
145
|
return;
|
|
133
|
-
case MENU_OPTIONS.status:
|
|
134
|
-
|
|
146
|
+
case MENU_OPTIONS.status: {
|
|
147
|
+
const status = await buildToolStatusMessage(pi);
|
|
148
|
+
if (generation !== state.sessionGeneration) return;
|
|
149
|
+
ctx.ui.notify(status, "info");
|
|
135
150
|
return;
|
|
151
|
+
}
|
|
136
152
|
case MENU_OPTIONS.tools:
|
|
137
153
|
await showToolSelector(pi, ctx);
|
|
138
154
|
return;
|
|
@@ -179,5 +195,5 @@ export {
|
|
|
179
195
|
resolveScreenshotPath,
|
|
180
196
|
selectAllowedRoot,
|
|
181
197
|
} from "./screenshot.js";
|
|
182
|
-
export {
|
|
198
|
+
export { normalizeChromeDevtoolsSettings } from "./settings.js";
|
|
183
199
|
export { orderedChromeDevtoolsTools } from "./tool-selector.js";
|
package/src/runtime.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface ChromeDevToolsState {
|
|
|
31
31
|
launchPromise?: Promise<void>;
|
|
32
32
|
lastLaunchAttempt?: BrowserLaunchAttempt;
|
|
33
33
|
shuttingDown: boolean;
|
|
34
|
+
sessionGeneration: number;
|
|
34
35
|
settingsNotice?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -82,4 +83,5 @@ export const state: ChromeDevToolsState = {
|
|
|
82
83
|
autoLaunchEnabled: process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH !== "0",
|
|
83
84
|
browserExecutable: process.env.PI_CHROME_DEVTOOLS_BROWSER,
|
|
84
85
|
shuttingDown: false,
|
|
86
|
+
sessionGeneration: 0,
|
|
85
87
|
};
|
package/src/settings.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { access,
|
|
4
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { access, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
7
7
|
|
|
8
8
|
const NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
|
|
@@ -12,17 +12,23 @@ export interface ChromeDevToolsSettings {
|
|
|
12
12
|
updatedAt: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface SettingsFileOperations {
|
|
16
|
+
write(path: string, data: string): Promise<void>;
|
|
17
|
+
rename(source: string, destination: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_FILE_OPERATIONS: SettingsFileOperations = {
|
|
21
|
+
write: (path, data) => writeFile(path, data, "utf8").then(() => undefined),
|
|
22
|
+
rename,
|
|
23
|
+
};
|
|
24
|
+
|
|
15
25
|
export type SettingsLoadResult =
|
|
16
26
|
| { kind: "missing"; notice?: string }
|
|
17
27
|
| { kind: "invalid"; reason: string; notice?: string }
|
|
18
28
|
| { kind: "loaded"; settings: ChromeDevToolsSettings; notice?: string };
|
|
19
29
|
|
|
20
|
-
type SettingsMigrationResult = {
|
|
21
|
-
kind: "migrated" | "failed";
|
|
22
|
-
notice: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
30
|
export async function loadSettings(): Promise<SettingsLoadResult> {
|
|
31
|
+
await settingsSaveQueue;
|
|
26
32
|
const newPath = settingsFilePath();
|
|
27
33
|
const newSettings = await readSettingsFile(newPath);
|
|
28
34
|
if (newSettings.kind !== "missing") {
|
|
@@ -38,39 +44,50 @@ export async function loadSettings(): Promise<SettingsLoadResult> {
|
|
|
38
44
|
if (legacySettings.kind === "missing") return { kind: "missing" };
|
|
39
45
|
if (legacySettings.kind === "invalid") return legacySettings;
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
}
|
|
47
|
+
return {
|
|
48
|
+
...legacySettings,
|
|
49
|
+
notice: `Using legacy ${LEGACY_SETTINGS_FILE_NAME}; rename it to ${NEW_SETTINGS_FILE_NAME}. Future saves write ${NEW_SETTINGS_FILE_NAME} without modifying the legacy file.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
interface SettingsDocumentResult {
|
|
54
|
+
result: SettingsLoadResult;
|
|
55
|
+
document?: Record<string, unknown>;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
async function
|
|
58
|
+
async function readSettingsDocument(filePath: string): Promise<SettingsDocumentResult> {
|
|
53
59
|
let text: string;
|
|
54
60
|
try {
|
|
55
61
|
text = await readFile(filePath, "utf8");
|
|
56
62
|
} catch (error) {
|
|
57
|
-
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
58
|
-
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
63
|
+
if (isNodeError(error) && error.code === "ENOENT") return { result: { kind: "missing" } };
|
|
64
|
+
return { result: { kind: "invalid", reason: `${filePath}: ${formatError(error)}` } };
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
try {
|
|
62
68
|
const parsed = JSON.parse(text) as unknown;
|
|
63
69
|
const settings = normalizeChromeDevtoolsSettings(parsed);
|
|
64
|
-
if (settings)
|
|
70
|
+
if (settings) {
|
|
71
|
+
return {
|
|
72
|
+
result: { kind: "loaded", settings },
|
|
73
|
+
document: { ...(parsed as Record<string, unknown>) },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
65
76
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
result: {
|
|
78
|
+
kind: "invalid",
|
|
79
|
+
reason: `${filePath}: expected tools to be an array of Chrome DevTools tool names`,
|
|
80
|
+
},
|
|
68
81
|
};
|
|
69
82
|
} catch (error) {
|
|
70
|
-
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
83
|
+
return { result: { kind: "invalid", reason: `${filePath}: ${formatError(error)}` } };
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
|
|
88
|
+
return (await readSettingsDocument(filePath)).result;
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<SettingsLoadResult> {
|
|
75
92
|
if (!(await fileExists(legacySettingsFilePath()))) return settings;
|
|
76
93
|
return {
|
|
@@ -79,46 +96,6 @@ async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<Se
|
|
|
79
96
|
};
|
|
80
97
|
}
|
|
81
98
|
|
|
82
|
-
export async function installSettingsFileExclusively(filePath: string, contents: string) {
|
|
83
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
84
|
-
const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
85
|
-
try {
|
|
86
|
-
await writeFile(tempFile, contents, { encoding: "utf8", flag: "wx" });
|
|
87
|
-
await link(tempFile, filePath);
|
|
88
|
-
} finally {
|
|
89
|
-
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function migrateLegacySettings(
|
|
94
|
-
legacyPath: string,
|
|
95
|
-
settings: ChromeDevToolsSettings,
|
|
96
|
-
): Promise<SettingsMigrationResult> {
|
|
97
|
-
const newPath = settingsFilePath();
|
|
98
|
-
try {
|
|
99
|
-
await installSettingsFileExclusively(newPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
return {
|
|
102
|
-
kind: "failed",
|
|
103
|
-
notice: `Chrome DevTools legacy settings migration failed: could not migrate ${legacyPath} to ${newPath}: ${formatError(error)}. The legacy file was used for this session; future saves will write ${NEW_SETTINGS_FILE_NAME}.`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
await rm(legacyPath, { force: true });
|
|
109
|
-
} catch (error) {
|
|
110
|
-
return {
|
|
111
|
-
kind: "migrated",
|
|
112
|
-
notice: `Chrome DevTools settings migrated from ${legacyPath} to ${newPath}, but the legacy file could not be removed: ${formatError(error)}. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
kind: "migrated",
|
|
118
|
-
notice: `Chrome DevTools settings migrated from ${legacyPath} to ${newPath}. ${LEGACY_SETTINGS_FILE_NAME} is deprecated and will be removed in a future major release.`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
99
|
async function fileExists(filePath: string) {
|
|
123
100
|
try {
|
|
124
101
|
await access(filePath, constants.F_OK);
|
|
@@ -128,6 +105,16 @@ async function fileExists(filePath: string) {
|
|
|
128
105
|
}
|
|
129
106
|
}
|
|
130
107
|
|
|
108
|
+
async function pathEntryExists(filePath: string) {
|
|
109
|
+
try {
|
|
110
|
+
await lstat(filePath);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
131
118
|
export function normalizeChromeDevtoolsSettings(
|
|
132
119
|
value: unknown,
|
|
133
120
|
): ChromeDevToolsSettings | undefined {
|
|
@@ -154,13 +141,48 @@ function orderedUniqueChromeDevtoolsTools(tools: readonly ChromeDevToolsToolName
|
|
|
154
141
|
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
|
|
155
142
|
}
|
|
156
143
|
|
|
157
|
-
|
|
144
|
+
let settingsSaveQueue = Promise.resolve();
|
|
145
|
+
|
|
146
|
+
export function saveSettings(
|
|
147
|
+
settings: ChromeDevToolsSettings,
|
|
148
|
+
operations: Partial<SettingsFileOperations> = {},
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const operation = settingsSaveQueue.then(() => saveSettingsNow(settings, operations));
|
|
151
|
+
settingsSaveQueue = operation.catch(() => undefined);
|
|
152
|
+
return operation;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function saveSettingsNow(
|
|
156
|
+
settings: ChromeDevToolsSettings,
|
|
157
|
+
operations: Partial<SettingsFileOperations>,
|
|
158
|
+
): Promise<void> {
|
|
158
159
|
const filePath = settingsFilePath();
|
|
160
|
+
let current = await readSettingsDocument(filePath);
|
|
161
|
+
const replaceCanonical = current.result.kind !== "missing";
|
|
162
|
+
if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath());
|
|
163
|
+
if (current.result.kind === "invalid") {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Cannot save Chrome DevTools settings until you repair ${current.result.reason}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const nextDocument = {
|
|
169
|
+
...(current.document ?? {}),
|
|
170
|
+
tools: [...settings.tools],
|
|
171
|
+
updatedAt: settings.updatedAt,
|
|
172
|
+
};
|
|
159
173
|
await mkdir(dirname(filePath), { recursive: true });
|
|
160
|
-
const tempFile = `${filePath}.${process.pid}.${
|
|
174
|
+
const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
161
175
|
try {
|
|
162
|
-
await
|
|
163
|
-
|
|
176
|
+
await (operations.write ?? DEFAULT_FILE_OPERATIONS.write)(
|
|
177
|
+
tempFile,
|
|
178
|
+
`${JSON.stringify(nextDocument, null, 2)}\n`,
|
|
179
|
+
);
|
|
180
|
+
if (!replaceCanonical && (await pathEntryExists(filePath))) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`${NEW_SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
await (operations.rename ?? DEFAULT_FILE_OPERATIONS.rename)(tempFile, filePath);
|
|
164
186
|
} catch (error) {
|
|
165
187
|
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
166
188
|
throw error;
|
|
@@ -176,7 +198,7 @@ function legacySettingsFilePath() {
|
|
|
176
198
|
}
|
|
177
199
|
|
|
178
200
|
function agentDir() {
|
|
179
|
-
return
|
|
201
|
+
return getAgentDir();
|
|
180
202
|
}
|
|
181
203
|
|
|
182
204
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
package/src/tool-selector.ts
CHANGED
|
@@ -39,26 +39,32 @@ interface ToolStatusSummary {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export async function showToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
${await buildToolStatusMessage(pi)}`,
|
|
47
|
-
"info",
|
|
48
|
-
);
|
|
42
|
+
const generation = state.sessionGeneration;
|
|
43
|
+
if (!ctx.hasUI) return;
|
|
44
|
+
if (ctx.mode !== "tui") {
|
|
45
|
+
await showDialogToolSelector(pi, ctx, generation);
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
let selectedTools = new Set<ChromeDevToolsToolName>(getActiveChromeDevtoolsTools(pi));
|
|
53
50
|
let persistQueue = Promise.resolve();
|
|
51
|
+
let requestedRevision = 0;
|
|
52
|
+
let requestRender: () => void = () => undefined;
|
|
54
53
|
const commitSelectedTools = () => {
|
|
55
54
|
const nextSelectedTools = orderedChromeDevtoolsTools(selectedTools);
|
|
56
|
-
|
|
57
|
-
persistQueue = persistQueue.then(() =>
|
|
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
|
+
});
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
const customResult = await ctx.ui.custom<"closed" | undefined>(
|
|
61
66
|
(tui, theme, keybindings, done) => {
|
|
67
|
+
requestRender = () => tui.requestRender();
|
|
62
68
|
const rows = chromeDevtoolsToolSelectorRows();
|
|
63
69
|
let selectedIndex = 0;
|
|
64
70
|
const moveSelection = (delta: number) => {
|
|
@@ -140,22 +146,26 @@ ${await buildToolStatusMessage(pi)}`,
|
|
|
140
146
|
},
|
|
141
147
|
);
|
|
142
148
|
|
|
149
|
+
if (generation !== state.sessionGeneration) return;
|
|
143
150
|
if (customResult !== "closed") {
|
|
144
|
-
await showDialogToolSelector(pi, ctx);
|
|
151
|
+
await showDialogToolSelector(pi, ctx, generation);
|
|
145
152
|
return;
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
await persistQueue;
|
|
149
|
-
|
|
156
|
+
if (generation !== state.sessionGeneration) return;
|
|
157
|
+
const status = await buildToolStatusMessage(pi);
|
|
158
|
+
if (generation !== state.sessionGeneration) return;
|
|
159
|
+
ctx.ui.notify(status, "info");
|
|
150
160
|
}
|
|
151
161
|
|
|
152
|
-
async function showDialogToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
162
|
+
async function showDialogToolSelector(pi: ExtensionAPI, ctx: CommandContext, generation: number) {
|
|
153
163
|
let selectedTools = new Set<ChromeDevToolsToolName>(getActiveChromeDevtoolsTools(pi));
|
|
154
164
|
while (true) {
|
|
155
165
|
const rows = chromeDevtoolsToolSelectorRows();
|
|
156
166
|
const choices = rows.map((row) => formatToolSelectorRow(row, selectedTools));
|
|
157
167
|
const choice = await ctx.ui.select(toolSelectorTitle(selectedTools), choices);
|
|
158
|
-
if (!choice) break;
|
|
168
|
+
if (generation !== state.sessionGeneration || !choice) break;
|
|
159
169
|
|
|
160
170
|
const row = rows[choices.indexOf(choice)];
|
|
161
171
|
if (!row) continue;
|
|
@@ -170,10 +180,20 @@ async function showDialogToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
|
|
|
170
180
|
selectedTools = new Set();
|
|
171
181
|
}
|
|
172
182
|
|
|
173
|
-
|
|
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));
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
|
|
193
|
+
if (generation !== state.sessionGeneration) return;
|
|
194
|
+
const status = await buildToolStatusMessage(pi);
|
|
195
|
+
if (generation !== state.sessionGeneration) return;
|
|
196
|
+
ctx.ui.notify(status, "info");
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
export async function updateChromeDevtoolsTools(
|
|
@@ -182,22 +202,75 @@ export async function updateChromeDevtoolsTools(
|
|
|
182
202
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
183
203
|
action: string,
|
|
184
204
|
) {
|
|
185
|
-
|
|
186
|
-
ctx
|
|
187
|
-
|
|
205
|
+
const generation = state.sessionGeneration;
|
|
206
|
+
const saved = await transactSelectedTools(pi, ctx, selectedTools, generation);
|
|
207
|
+
if (!saved || generation !== state.sessionGeneration) return;
|
|
208
|
+
const status = await buildToolStatusMessage(pi);
|
|
209
|
+
if (generation !== state.sessionGeneration) return;
|
|
210
|
+
ctx.ui.notify(`Chrome DevTools tools ${action}.\n\n${status}`, "info");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function setSelectedChromeDevtoolsTools(
|
|
214
|
+
pi: ExtensionAPI,
|
|
215
|
+
ctx: CommandContext,
|
|
216
|
+
selectedTools: readonly ChromeDevToolsToolName[],
|
|
217
|
+
): Promise<boolean> {
|
|
218
|
+
return transactSelectedTools(pi, ctx, selectedTools, state.sessionGeneration);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let toolTransactionQueue = Promise.resolve();
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
|
|
223
|
+
export async function waitForChromeDevtoolsSettings(): Promise<void> {
|
|
224
|
+
await toolTransactionQueue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function transactSelectedTools(
|
|
228
|
+
pi: ExtensionAPI,
|
|
229
|
+
ctx: CommandContext,
|
|
230
|
+
selectedTools: readonly ChromeDevToolsToolName[],
|
|
231
|
+
expectedGeneration: number,
|
|
232
|
+
): Promise<boolean> {
|
|
233
|
+
const operation = toolTransactionQueue.then(() =>
|
|
234
|
+
transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration),
|
|
235
|
+
);
|
|
236
|
+
toolTransactionQueue = operation.then(
|
|
237
|
+
() => undefined,
|
|
238
|
+
() => undefined,
|
|
191
239
|
);
|
|
240
|
+
return operation;
|
|
192
241
|
}
|
|
193
242
|
|
|
194
|
-
|
|
243
|
+
async function transactSelectedToolsNow(
|
|
195
244
|
pi: ExtensionAPI,
|
|
196
245
|
ctx: CommandContext,
|
|
197
246
|
selectedTools: readonly ChromeDevToolsToolName[],
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
247
|
+
expectedGeneration: number,
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
if (expectedGeneration !== state.sessionGeneration) return false;
|
|
250
|
+
const previousActiveTools = pi.getActiveTools();
|
|
251
|
+
try {
|
|
252
|
+
applyChromeDevtoolsTools(pi, selectedTools);
|
|
253
|
+
await persistSettings(selectedTools);
|
|
254
|
+
return expectedGeneration === state.sessionGeneration;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
let rollbackError: unknown;
|
|
257
|
+
try {
|
|
258
|
+
const previousChromeTools = previousActiveTools.filter((name) =>
|
|
259
|
+
CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName),
|
|
260
|
+
) as ChromeDevToolsToolName[];
|
|
261
|
+
applyChromeDevtoolsTools(pi, previousChromeTools);
|
|
262
|
+
} catch (caught) {
|
|
263
|
+
rollbackError = caught;
|
|
264
|
+
}
|
|
265
|
+
if (expectedGeneration !== state.sessionGeneration) return false;
|
|
266
|
+
ctx.ui.notify(
|
|
267
|
+
rollbackError
|
|
268
|
+
? `Chrome DevTools settings save failed: ${formatError(error)}; active-tool rollback failed: ${formatError(rollbackError)}`
|
|
269
|
+
: `Chrome DevTools settings save failed; active tools restored: ${formatError(error)}`,
|
|
270
|
+
"warning",
|
|
271
|
+
);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
201
274
|
}
|
|
202
275
|
|
|
203
276
|
export function applyChromeDevtoolsTools(
|
|
@@ -327,13 +400,6 @@ function formatPersistedSelection(tools: readonly ChromeDevToolsToolName[]) {
|
|
|
327
400
|
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
328
401
|
}
|
|
329
402
|
|
|
330
|
-
async function persistSettings(
|
|
331
|
-
|
|
332
|
-
selectedTools: readonly ChromeDevToolsToolName[],
|
|
333
|
-
) {
|
|
334
|
-
try {
|
|
335
|
-
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
336
|
-
} catch (error) {
|
|
337
|
-
ctx.ui.notify(`Chrome DevTools settings save failed: ${formatError(error)}`, "warning");
|
|
338
|
-
}
|
|
403
|
+
async function persistSettings(selectedTools: readonly ChromeDevToolsToolName[]) {
|
|
404
|
+
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
339
405
|
}
|