@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 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
- Compatibility: older versions used `pi-chrome-devtools-settings.json`. During the migration
175
- window, a legacy-only file is automatically migrated to `pi-chrome-devtools.json` with a warning.
176
- If both files exist, `pi-chrome-devtools.json` wins and the legacy file is ignored. The legacy
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-chrome-devtools",
3
- "version": "0.31.0",
3
+ "version": "0.35.0",
4
4
  "description": "Pi extension that exposes Chrome DevTools Protocol tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- await handleChromeDevtoolsCommand(pi, args, ctx);
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
- await shutdownManagedBrowser(undefined, { cancelLaunch: true });
81
+ const browserShutdown = shutdownManagedBrowser(undefined, { cancelLaunch: true });
82
+ await waitForChromeDevtoolsSettings();
83
+ await browserShutdown;
77
84
  });
78
85
  }
79
86
 
80
- async function handleChromeDevtoolsCommand(pi: ExtensionAPI, args: string, ctx: CommandContext) {
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
- ctx.ui.notify(await buildToolStatusMessage(pi), "info");
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
- ctx.ui.notify(
117
- `${buildCommandGuide()}
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
- ctx.ui.notify(await buildToolStatusMessage(pi), "info");
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 { installSettingsFileExclusively, normalizeChromeDevtoolsSettings } from "./settings.js";
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, link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
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
- const migration = await migrateLegacySettings(legacyPath, legacySettings.settings);
42
- if (migration.kind === "failed") {
43
- const settingsCreatedDuringMigration = await readSettingsFile(newPath);
44
- if (settingsCreatedDuringMigration.kind !== "missing") {
45
- return withLegacyIgnoredNotice(settingsCreatedDuringMigration);
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
- return { ...legacySettings, notice: migration.notice };
53
+ interface SettingsDocumentResult {
54
+ result: SettingsLoadResult;
55
+ document?: Record<string, unknown>;
50
56
  }
51
57
 
52
- async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
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) return { kind: "loaded", settings };
70
+ if (settings) {
71
+ return {
72
+ result: { kind: "loaded", settings },
73
+ document: { ...(parsed as Record<string, unknown>) },
74
+ };
75
+ }
65
76
  return {
66
- kind: "invalid",
67
- reason: `${filePath}: expected tools to be an array of Chrome DevTools tool names`,
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
- export async function saveSettings(settings: ChromeDevToolsSettings) {
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}.${Date.now()}.tmp`;
174
+ const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
161
175
  try {
162
- await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
163
- await rename(tempFile, filePath);
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 process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
201
+ return getAgentDir();
180
202
  }
181
203
 
182
204
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
@@ -39,26 +39,32 @@ interface ToolStatusSummary {
39
39
  }
40
40
 
41
41
  export async function showToolSelector(pi: ExtensionAPI, ctx: CommandContext) {
42
- if (!ctx.hasUI) {
43
- ctx.ui.notify(
44
- `Chrome DevTools tool selection needs an interactive UI.
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
- applyChromeDevtoolsTools(pi, nextSelectedTools);
57
- persistQueue = persistQueue.then(() => persistSettings(ctx, nextSelectedTools));
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
- ctx.ui.notify(await buildToolStatusMessage(pi), "info");
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
- await setSelectedChromeDevtoolsTools(pi, ctx, orderedChromeDevtoolsTools(selectedTools));
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
- ctx.ui.notify(await buildToolStatusMessage(pi), "info");
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
- await setSelectedChromeDevtoolsTools(pi, ctx, selectedTools);
186
- ctx.ui.notify(
187
- `Chrome DevTools tools ${action}.
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
- ${await buildToolStatusMessage(pi)}`,
190
- "info",
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
- export async function setSelectedChromeDevtoolsTools(
243
+ async function transactSelectedToolsNow(
195
244
  pi: ExtensionAPI,
196
245
  ctx: CommandContext,
197
246
  selectedTools: readonly ChromeDevToolsToolName[],
198
- ) {
199
- applyChromeDevtoolsTools(pi, selectedTools);
200
- await persistSettings(ctx, selectedTools);
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
- ctx: CommandContext,
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
  }