@narumitw/pi-chrome-devtools 0.53.4 → 0.54.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/CHANGELOG.md +109 -0
- package/README.md +34 -16
- package/dist/chunks/{browser-settings-menu-FVMCCUPM.ts → browser-settings-menu-7M3VM7QN.ts} +25 -3
- package/dist/chunks/browser-settings-menu-7M3VM7QN.ts.map +7 -0
- package/dist/chunks/{chunk-V7N3WI5B.ts → chunk-JKPJYUVG.ts} +340 -108
- package/dist/chunks/chunk-JKPJYUVG.ts.map +7 -0
- package/dist/chunks/{chunk-4B3EQHOH.ts → chunk-JLEJ6WFO.ts} +2 -2
- package/dist/chunks/{chunk-LSJSDY2M.ts → chunk-WTMZID3G.ts} +382 -246
- package/dist/chunks/chunk-WTMZID3G.ts.map +7 -0
- package/dist/chunks/{menu-MTPSLDHZ.ts → menu-HGG26E3X.ts} +7 -7
- package/dist/chunks/{menu-MTPSLDHZ.ts.map → menu-HGG26E3X.ts.map} +1 -1
- package/dist/chunks/{tools-OVNH45IJ.ts → tools-O5N4KIAK.ts} +3 -3
- package/dist/index.ts +45 -21
- package/dist/index.ts.map +2 -2
- package/docs/browser-setup.md +7 -1
- package/package.json +6 -5
- package/src/browser-candidates.ts +184 -0
- package/src/browser-manager.ts +93 -276
- package/src/browser-settings-menu.ts +23 -0
- package/src/browser-startup.ts +122 -0
- package/src/chrome-devtools.ts +48 -15
- package/src/lazy-tools.ts +358 -48
- package/src/menu.ts +3 -3
- package/src/runtime.ts +2 -2
- package/src/settings.ts +65 -1
- package/src/tool-selector.ts +95 -40
- package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +0 -7
- package/dist/chunks/chunk-LSJSDY2M.ts.map +0 -7
- package/dist/chunks/chunk-V7N3WI5B.ts.map +0 -7
- /package/dist/chunks/{chunk-4B3EQHOH.ts.map → chunk-JLEJ6WFO.ts.map} +0 -0
- /package/dist/chunks/{tools-OVNH45IJ.ts.map → tools-O5N4KIAK.ts.map} +0 -0
package/src/lazy-tools.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type ToolDefinition,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
7
|
import { Type } from "typebox";
|
|
8
|
+
import { sanitizeChromeDevtoolsDisplay } from "./display.js";
|
|
3
9
|
import { webMcpEnabled } from "./runtime.js";
|
|
10
|
+
import type { ChromeDevToolsToolMode } from "./settings.js";
|
|
4
11
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName, isWebMcpToolName } from "./tool-names.js";
|
|
5
12
|
|
|
6
13
|
export const CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
|
|
@@ -15,6 +22,208 @@ const availableToolsByApi = existingAvailableToolsStore ?? new WeakMap<Extension
|
|
|
15
22
|
if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
|
|
16
23
|
const lazyExposureByApi = new WeakMap<ExtensionAPI, boolean>();
|
|
17
24
|
const sessionOwnerByApi = new WeakMap<ExtensionAPI, object>();
|
|
25
|
+
// Public sessionManager identity survives factory/API rebuilds during /reload.
|
|
26
|
+
// A redundant host selection of an already-owned active name has no public
|
|
27
|
+
// provenance signal; keep known ownership rather than treating carryover as intent.
|
|
28
|
+
const PROVENANCE_STORE = Symbol.for("@narumitw/pi-chrome-devtools.activation-provenance");
|
|
29
|
+
const PROVENANCE_ENTRY = "chrome-devtools.activation-provenance";
|
|
30
|
+
type ActivationOwnership = {
|
|
31
|
+
explicit: Set<string>;
|
|
32
|
+
owned: Set<string>;
|
|
33
|
+
available?: Set<ChromeDevToolsToolName>;
|
|
34
|
+
mode?: ChromeDevToolsToolMode;
|
|
35
|
+
published?: Set<ChromeDevToolsToolName>;
|
|
36
|
+
serialized?: string;
|
|
37
|
+
};
|
|
38
|
+
const provenanceGlobal = globalThis as typeof globalThis & {
|
|
39
|
+
[PROVENANCE_STORE]?: WeakMap<object, ActivationOwnership>;
|
|
40
|
+
};
|
|
41
|
+
const ownedBySession = provenanceGlobal[PROVENANCE_STORE] ?? new WeakMap<object, ActivationOwnership>();
|
|
42
|
+
provenanceGlobal[PROVENANCE_STORE] = ownedBySession;
|
|
43
|
+
const modeByApi = new WeakMap<ExtensionAPI, ChromeDevToolsToolMode>();
|
|
44
|
+
const loaderRegistered = new WeakSet<ExtensionAPI>();
|
|
45
|
+
|
|
46
|
+
function retainedExplicitTools(pi: ExtensionAPI, before: readonly string[]) {
|
|
47
|
+
const owner = sessionOwnerByApi.get(pi);
|
|
48
|
+
const ownership = owner ? ownedBySession.get(owner) : undefined;
|
|
49
|
+
const observed = new Set(before);
|
|
50
|
+
// Only our own last publication can prove suppression rather than withdrawal.
|
|
51
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter(
|
|
52
|
+
(name) => ownership?.explicit.has(name) && (observed.has(name) || ownership.published?.has(name) === false),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function publishActiveTools(pi: ExtensionAPI, names: string[], before: readonly string[]) {
|
|
57
|
+
pi.setActiveTools(names);
|
|
58
|
+
recordOwnership(pi, before);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function recordOwnership(pi: ExtensionAPI, before: readonly string[]) {
|
|
62
|
+
const names = pi.getActiveTools();
|
|
63
|
+
const owner = sessionOwnerByApi.get(pi);
|
|
64
|
+
if (!owner) return;
|
|
65
|
+
const ownership: ActivationOwnership = ownedBySession.get(owner) ?? {
|
|
66
|
+
explicit: new Set<string>(),
|
|
67
|
+
owned: new Set<string>(),
|
|
68
|
+
};
|
|
69
|
+
const observed = new Set(before);
|
|
70
|
+
for (const name of ownership.explicit) {
|
|
71
|
+
// Absence after our own suppression is not a host withdrawal. Only a
|
|
72
|
+
// previously published activation disappearing is observable withdrawal.
|
|
73
|
+
if (!observed.has(name) && (ownership.published?.has(name as ChromeDevToolsToolName) ?? true))
|
|
74
|
+
ownership.explicit.delete(name);
|
|
75
|
+
}
|
|
76
|
+
for (const name of before)
|
|
77
|
+
if (CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName) && !ownership.owned.has(name))
|
|
78
|
+
ownership.explicit.add(name);
|
|
79
|
+
if (chromeDevtoolsToolMode(pi) !== "codemode") {
|
|
80
|
+
for (const name of names)
|
|
81
|
+
if (CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName) && !ownership.explicit.has(name))
|
|
82
|
+
ownership.owned.add(name);
|
|
83
|
+
}
|
|
84
|
+
if (chromeDevtoolsToolMode(pi) === "codemode") ownership.owned.clear();
|
|
85
|
+
const published = new Set(names);
|
|
86
|
+
ownership.published = new Set(CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => published.has(name)));
|
|
87
|
+
ownedBySession.set(owner, ownership);
|
|
88
|
+
persistOwnership(pi, ownership);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ownershipData(ownership: ActivationOwnership) {
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
explicit: CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => ownership.explicit.has(name)),
|
|
95
|
+
owned: CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => ownership.owned.has(name)),
|
|
96
|
+
available: ownership.available
|
|
97
|
+
? CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => ownership.available?.has(name))
|
|
98
|
+
: undefined,
|
|
99
|
+
mode: ownership.mode,
|
|
100
|
+
published: ownership.published
|
|
101
|
+
? CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => ownership.published?.has(name))
|
|
102
|
+
: undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class ActivationProvenanceError extends Error {
|
|
107
|
+
constructor(cause: unknown) {
|
|
108
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatProvenanceWarning(error: ActivationProvenanceError) {
|
|
113
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
114
|
+
`Activation ownership could not be saved: ${error.message}. Activation is not rolled back; durable ownership may be incomplete until a later loader call or lifecycle update retries successfully.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function publishLifecycleTools(pi: ExtensionAPI, names: string[], before: readonly string[]) {
|
|
119
|
+
try {
|
|
120
|
+
publishActiveTools(pi, names, before);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof ActivationProvenanceError)) throw error;
|
|
123
|
+
// Exposure has already succeeded. Lifecycle events are not rejected saves;
|
|
124
|
+
// keep the accepted policy and let callers warn without stopping setup.
|
|
125
|
+
return formatProvenanceWarning(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function persistOwnership(pi: ExtensionAPI, ownership: ActivationOwnership) {
|
|
130
|
+
ownership.available = new Set(configuredChromeDevtoolsTools(pi));
|
|
131
|
+
ownership.mode = chromeDevtoolsToolMode(pi);
|
|
132
|
+
const data = ownershipData(ownership);
|
|
133
|
+
const serialized = JSON.stringify(data);
|
|
134
|
+
if (ownership.serialized === serialized) return;
|
|
135
|
+
try {
|
|
136
|
+
pi.appendEntry(PROVENANCE_ENTRY, data);
|
|
137
|
+
ownership.serialized = serialized;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Pi can advance its in-memory branch before disk persistence throws. Force
|
|
140
|
+
// recovery to append its policy even when it matches the last saved record.
|
|
141
|
+
ownership.serialized = undefined;
|
|
142
|
+
throw new ActivationProvenanceError(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function restoredOwnership(
|
|
147
|
+
entries: readonly { type: string; customType?: string; data?: unknown }[],
|
|
148
|
+
): ActivationOwnership | undefined {
|
|
149
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
150
|
+
const entry = entries[index];
|
|
151
|
+
if (entry.type !== "custom" || entry.customType !== PROVENANCE_ENTRY) continue;
|
|
152
|
+
const data = entry.data;
|
|
153
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined;
|
|
154
|
+
const record = data as Record<string, unknown>;
|
|
155
|
+
const validNames = (value: unknown): value is ChromeDevToolsToolName[] =>
|
|
156
|
+
Array.isArray(value) &&
|
|
157
|
+
value.length <= CHROME_DEVTOOLS_TOOL_NAMES.length &&
|
|
158
|
+
value.every((name) => CHROME_DEVTOOLS_TOOL_NAMES.includes(name));
|
|
159
|
+
if (
|
|
160
|
+
record.version !== 1 ||
|
|
161
|
+
!validNames(record.explicit) ||
|
|
162
|
+
!validNames(record.owned) ||
|
|
163
|
+
(record.available !== undefined && !validNames(record.available)) ||
|
|
164
|
+
(record.published !== undefined && !validNames(record.published)) ||
|
|
165
|
+
(record.mode !== undefined && record.mode !== "codemode" && record.mode !== "lazy" && record.mode !== "direct") ||
|
|
166
|
+
record.owned.some((name) => (record.explicit as string[]).includes(name))
|
|
167
|
+
)
|
|
168
|
+
return undefined;
|
|
169
|
+
const restored = {
|
|
170
|
+
explicit: new Set<string>(record.explicit),
|
|
171
|
+
owned: new Set<string>(record.owned),
|
|
172
|
+
available: validNames(record.available) ? new Set(record.available) : undefined,
|
|
173
|
+
mode: record.mode as ChromeDevToolsToolMode | undefined,
|
|
174
|
+
published: validNames(record.published) ? new Set(record.published) : undefined,
|
|
175
|
+
};
|
|
176
|
+
return { ...restored, serialized: JSON.stringify(ownershipData(restored)) };
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const definitionsByApi = new WeakMap<ExtensionAPI, ToolDefinition[]>();
|
|
181
|
+
|
|
182
|
+
export function registerChromeDevtoolsCapabilities(pi: ExtensionAPI, tools: ToolDefinition[]) {
|
|
183
|
+
definitionsByApi.set(pi, tools);
|
|
184
|
+
for (const tool of tools) pi.registerTool({ ...tool, exposure: "codemode", defaultActive: false });
|
|
185
|
+
// Keep a carried-over predecessor loader observable until session_start.
|
|
186
|
+
// Pi drops unregistered/hidden active names while rebuilding on /reload.
|
|
187
|
+
pi.registerTool({
|
|
188
|
+
...createChromeDevtoolsLoadTool(pi),
|
|
189
|
+
exposure: "codemode",
|
|
190
|
+
defaultActive: false,
|
|
191
|
+
promptSnippet: undefined,
|
|
192
|
+
promptGuidelines: undefined,
|
|
193
|
+
});
|
|
194
|
+
loaderRegistered.add(pi);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function chromeDevtoolsToolMode(pi: ExtensionAPI): ChromeDevToolsToolMode {
|
|
198
|
+
return modeByApi.get(pi) ?? "codemode";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function refreshExposure(pi: ExtensionAPI) {
|
|
202
|
+
const available = effectiveAvailableTools(pi);
|
|
203
|
+
const mode = chromeDevtoolsToolMode(pi);
|
|
204
|
+
for (const tool of definitionsByApi.get(pi) ?? []) {
|
|
205
|
+
pi.registerTool({
|
|
206
|
+
...tool,
|
|
207
|
+
defaultActive: false,
|
|
208
|
+
exposure: available.has(tool.name as ChromeDevToolsToolName)
|
|
209
|
+
? mode === "codemode"
|
|
210
|
+
? "codemode"
|
|
211
|
+
: "direct"
|
|
212
|
+
: "hidden",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (mode === "lazy") {
|
|
216
|
+
pi.registerTool({ ...createChromeDevtoolsLoadTool(pi), defaultActive: false });
|
|
217
|
+
loaderRegistered.add(pi);
|
|
218
|
+
} else if (loaderRegistered.has(pi)) {
|
|
219
|
+
pi.registerTool({
|
|
220
|
+
...createChromeDevtoolsLoadTool(pi),
|
|
221
|
+
exposure: "hidden",
|
|
222
|
+
promptSnippet: undefined,
|
|
223
|
+
promptGuidelines: undefined,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
18
227
|
|
|
19
228
|
const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
|
|
20
229
|
chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
|
|
@@ -26,80 +235,151 @@ const SEARCH_TEXT: Record<ChromeDevToolsToolName, string> = {
|
|
|
26
235
|
chrome_devtools_webmcp_call_tool: "call invoke page provided website webmcp tool confirmation experimental",
|
|
27
236
|
};
|
|
28
237
|
|
|
29
|
-
export function setChromeDevtoolsSessionOwner(
|
|
238
|
+
export function setChromeDevtoolsSessionOwner(
|
|
239
|
+
pi: ExtensionAPI,
|
|
240
|
+
owner: object,
|
|
241
|
+
entries?: readonly { type: string; customType?: string; data?: unknown }[],
|
|
242
|
+
) {
|
|
30
243
|
sessionOwnerByApi.set(pi, owner);
|
|
244
|
+
if (entries) {
|
|
245
|
+
const ownership = restoredOwnership(entries);
|
|
246
|
+
if (ownership) {
|
|
247
|
+
const pending = ownedBySession.get(owner);
|
|
248
|
+
// An append can advance this branch before persistence fails. Restoring
|
|
249
|
+
// that same validated policy must not erase its pending retry marker.
|
|
250
|
+
if (
|
|
251
|
+
pending &&
|
|
252
|
+
pending.serialized === undefined &&
|
|
253
|
+
ownership.serialized === JSON.stringify(ownershipData(pending))
|
|
254
|
+
)
|
|
255
|
+
ownership.serialized = undefined;
|
|
256
|
+
ownedBySession.set(owner, ownership);
|
|
257
|
+
} else {
|
|
258
|
+
// Tree navigation can reuse a manager while replacing its active branch.
|
|
259
|
+
// Never lend another branch's activation or policy provenance to it.
|
|
260
|
+
ownedBySession.delete(owner);
|
|
261
|
+
availableToolsByApi.delete(pi);
|
|
262
|
+
modeByApi.delete(pi);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const ownership = ownedBySession.get(owner);
|
|
266
|
+
if (ownership?.available) setAvailableTools(pi, [...ownership.available]);
|
|
267
|
+
if (ownership?.mode) modeByApi.set(pi, ownership.mode);
|
|
31
268
|
}
|
|
32
269
|
|
|
33
270
|
export function initializeAvailableChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
34
271
|
if (availableToolsByApi.has(pi)) return;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => activeTools.has(name)),
|
|
39
|
-
);
|
|
272
|
+
// Availability is configuration, not the provider-visible declaration list.
|
|
273
|
+
// In particular, codemode starts with no active capability names.
|
|
274
|
+
setAvailableTools(pi, CHROME_DEVTOOLS_TOOL_NAMES);
|
|
40
275
|
}
|
|
41
276
|
|
|
42
277
|
export function configureChromeDevtoolsToolExposure(
|
|
43
278
|
pi: ExtensionAPI,
|
|
44
279
|
availableTools: readonly ChromeDevToolsToolName[],
|
|
45
280
|
model?: ExtensionContext["model"],
|
|
281
|
+
mode: ChromeDevToolsToolMode = "codemode",
|
|
46
282
|
) {
|
|
283
|
+
modeByApi.set(pi, mode);
|
|
47
284
|
setAvailableTools(pi, availableTools);
|
|
48
285
|
const available = effectiveAvailableTools(pi);
|
|
49
|
-
const lazyExposure = supportsNativeDeferredToolLoading(model);
|
|
286
|
+
const lazyExposure = mode === "lazy" && supportsNativeDeferredToolLoading(model);
|
|
50
287
|
lazyExposureByApi.set(pi, lazyExposure);
|
|
51
|
-
const
|
|
288
|
+
const before = pi.getActiveTools();
|
|
289
|
+
const owner = sessionOwnerByApi.get(pi);
|
|
290
|
+
let ownership = owner ? ownedBySession.get(owner) : undefined;
|
|
291
|
+
if (owner && !ownership && before.includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME)) {
|
|
292
|
+
// The predecessor always activated its loader but had no provenance store.
|
|
293
|
+
// With no public activation-origin API, this loader cohort is migrated as
|
|
294
|
+
// extension-owned; capability-only host selections remain explicit.
|
|
295
|
+
ownership = {
|
|
296
|
+
explicit: new Set(),
|
|
297
|
+
owned: new Set(before.filter((name) => CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName))),
|
|
298
|
+
};
|
|
299
|
+
ownedBySession.set(owner, ownership);
|
|
300
|
+
}
|
|
301
|
+
const owned = ownership?.owned;
|
|
302
|
+
const exposedTools =
|
|
303
|
+
mode === "codemode"
|
|
304
|
+
? unique([...before.filter((name) => !owned?.has(name)), ...retainedExplicitTools(pi, before)]).filter((name) =>
|
|
305
|
+
available.has(name as ChromeDevToolsToolName),
|
|
306
|
+
)
|
|
307
|
+
: lazyExposure
|
|
308
|
+
? []
|
|
309
|
+
: CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
52
310
|
const nonCapabilityTools = pi
|
|
53
311
|
.getActiveTools()
|
|
54
312
|
.filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName));
|
|
55
|
-
pi
|
|
313
|
+
refreshExposure(pi);
|
|
314
|
+
const target = new Set([
|
|
315
|
+
...nonCapabilityTools.filter((name) => name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME),
|
|
316
|
+
...(mode === "lazy" ? [CHROME_DEVTOOLS_LOAD_TOOL_NAME] : []),
|
|
317
|
+
...exposedTools,
|
|
318
|
+
]);
|
|
319
|
+
return publishLifecycleTools(pi, unique([...before.filter((name) => target.has(name)), ...target]), before);
|
|
56
320
|
}
|
|
57
321
|
|
|
58
322
|
export function requireEagerChromeDevtoolsToolExposure(pi: ExtensionAPI) {
|
|
323
|
+
if (chromeDevtoolsToolMode(pi) !== "lazy") return;
|
|
59
324
|
lazyExposureByApi.set(pi, false);
|
|
60
325
|
const active = pi.getActiveTools();
|
|
61
326
|
const available = availableChromeDevtoolsTools(pi);
|
|
62
|
-
pi
|
|
327
|
+
return publishLifecycleTools(pi, unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...available]), active);
|
|
63
328
|
}
|
|
64
329
|
|
|
65
|
-
export function applyAvailableChromeDevtoolsTools(
|
|
330
|
+
export function applyAvailableChromeDevtoolsTools(
|
|
331
|
+
pi: ExtensionAPI,
|
|
332
|
+
availableTools: readonly ChromeDevToolsToolName[],
|
|
333
|
+
restoredActiveTools?: readonly string[],
|
|
334
|
+
) {
|
|
66
335
|
setAvailableTools(pi, availableTools);
|
|
67
336
|
const available = effectiveAvailableTools(pi);
|
|
68
337
|
const lazyExposure = lazyExposureByApi.get(pi) === true;
|
|
69
|
-
const
|
|
70
|
-
.getActiveTools()
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
338
|
+
const before = restoredActiveTools
|
|
339
|
+
? restoreCapabilityPositions(pi.getActiveTools(), restoredActiveTools, available)
|
|
340
|
+
: pi.getActiveTools();
|
|
341
|
+
const active = before.filter(
|
|
342
|
+
(name) =>
|
|
343
|
+
!CHROME_DEVTOOLS_TOOL_NAMES.includes(name as ChromeDevToolsToolName) ||
|
|
344
|
+
(lazyExposure && available.has(name as ChromeDevToolsToolName)),
|
|
345
|
+
);
|
|
346
|
+
const mode = chromeDevtoolsToolMode(pi);
|
|
347
|
+
const eagerTools =
|
|
348
|
+
mode === "codemode"
|
|
349
|
+
? unique([...before, ...retainedExplicitTools(pi, before)]).filter((name) =>
|
|
350
|
+
available.has(name as ChromeDevToolsToolName),
|
|
351
|
+
)
|
|
352
|
+
: lazyExposure
|
|
353
|
+
? []
|
|
354
|
+
: CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
355
|
+
refreshExposure(pi);
|
|
356
|
+
const target = new Set([
|
|
357
|
+
...active.filter((name) => name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME),
|
|
358
|
+
...(mode === "lazy" ? [CHROME_DEVTOOLS_LOAD_TOOL_NAME] : []),
|
|
359
|
+
...eagerTools,
|
|
360
|
+
]);
|
|
361
|
+
publishActiveTools(pi, unique([...before.filter((name) => target.has(name)), ...target]), before);
|
|
78
362
|
}
|
|
79
363
|
|
|
80
364
|
export function chromeDevtoolsToolExposureMode(pi: ExtensionAPI) {
|
|
81
|
-
return
|
|
365
|
+
return chromeDevtoolsToolMode(pi) === "codemode"
|
|
366
|
+
? "codemode"
|
|
367
|
+
: lazyExposureByApi.get(pi) === true
|
|
368
|
+
? "native deferred"
|
|
369
|
+
: "eager";
|
|
82
370
|
}
|
|
83
371
|
|
|
84
372
|
export function supportsNativeDeferredToolLoading(model: ExtensionContext["model"]): boolean {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const major = Number(version[1]);
|
|
96
|
-
const minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;
|
|
97
|
-
return major > 4 || (major === 4 && minor >= 5);
|
|
98
|
-
}
|
|
99
|
-
if (model.api === "openai-completions") {
|
|
100
|
-
return compatString(model.compat, "deferredToolsMode") === "kimi";
|
|
101
|
-
}
|
|
102
|
-
if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
|
|
373
|
+
// Without native system-message updates, Pi folds later tool additions into
|
|
374
|
+
// the leading request and invalidates its cached prefix.
|
|
375
|
+
if (!model || compatBoolean(model.compat, "supportsMidConvoSystemMessages") !== true) return false;
|
|
376
|
+
if (model.api === "anthropic-messages") return compatBoolean(model.compat, "supportsMidConvoToolChanges") === true;
|
|
377
|
+
if (model.api === "openai-completions") return compatBoolean(model.compat, "supportsMidConvoToolAdditions") === true;
|
|
378
|
+
if (
|
|
379
|
+
model.api === "openai-responses" ||
|
|
380
|
+
model.api === "openai-codex-responses" ||
|
|
381
|
+
model.api === "azure-openai-responses"
|
|
382
|
+
) {
|
|
103
383
|
return (
|
|
104
384
|
compatBoolean(model.compat, "supportsAdditionalTools") === true ||
|
|
105
385
|
compatBoolean(model.compat, "supportsToolSearch") === true
|
|
@@ -113,6 +393,14 @@ export function availableChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
|
113
393
|
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
114
394
|
}
|
|
115
395
|
|
|
396
|
+
export function previousChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
397
|
+
if (availableToolsByApi.has(pi)) return configuredChromeDevtoolsTools(pi);
|
|
398
|
+
// An unknown catalog cannot authorize new capabilities. Preserve only names
|
|
399
|
+
// already explicitly active; this fallback is used for invalid settings only.
|
|
400
|
+
const active = new Set(pi.getActiveTools());
|
|
401
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => active.has(name));
|
|
402
|
+
}
|
|
403
|
+
|
|
116
404
|
export function configuredChromeDevtoolsTools(pi: ExtensionAPI) {
|
|
117
405
|
const available = availableToolsByApi.get(pi) ?? new Set();
|
|
118
406
|
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
@@ -149,8 +437,19 @@ export function createChromeDevtoolsLoadTool(pi: ExtensionAPI) {
|
|
|
149
437
|
const active = pi.getActiveTools();
|
|
150
438
|
const activeSet = new Set(active);
|
|
151
439
|
const added = matches.filter((name) => !activeSet.has(name));
|
|
152
|
-
|
|
153
|
-
|
|
440
|
+
let provenanceWarning: string | undefined;
|
|
441
|
+
try {
|
|
442
|
+
if (added.length > 0) {
|
|
443
|
+
publishActiveTools(pi, unique([...active, ...added]), active);
|
|
444
|
+
} else {
|
|
445
|
+
const ownership = ownedBySession.get(ctx.sessionManager);
|
|
446
|
+
if (ownership && ownership.serialized === undefined) recordOwnership(pi, active);
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (!(error instanceof ActivationProvenanceError)) throw error;
|
|
450
|
+
// Native loading must remain additive. Activation succeeded; do not
|
|
451
|
+
// remove tools or claim it failed because its bookkeeping could not save.
|
|
452
|
+
provenanceWarning = formatProvenanceWarning(error);
|
|
154
453
|
}
|
|
155
454
|
|
|
156
455
|
const text =
|
|
@@ -160,8 +459,8 @@ export function createChromeDevtoolsLoadTool(pi: ExtensionAPI) {
|
|
|
160
459
|
? `Loaded Chrome DevTools tools: ${added.join(", ")}`
|
|
161
460
|
: `Matching Chrome DevTools tools are already loaded: ${matches.join(", ")}`;
|
|
162
461
|
return {
|
|
163
|
-
content: [{ type: "text" as const, text }],
|
|
164
|
-
details: { matches, added },
|
|
462
|
+
content: [{ type: "text" as const, text: provenanceWarning ? `${text}\nWarning: ${provenanceWarning}` : text }],
|
|
463
|
+
details: { matches, added, ...(provenanceWarning ? { provenanceWarning } : {}) },
|
|
165
464
|
};
|
|
166
465
|
},
|
|
167
466
|
});
|
|
@@ -206,10 +505,21 @@ function compatBoolean(value: unknown, key: string) {
|
|
|
206
505
|
return typeof record[key] === "boolean" ? record[key] : undefined;
|
|
207
506
|
}
|
|
208
507
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
508
|
+
// Rollback may reinsert capabilities, but must not undo unrelated additions,
|
|
509
|
+
// removals, or ordering changes made while persistence was awaiting I/O.
|
|
510
|
+
function restoreCapabilityPositions(
|
|
511
|
+
current: readonly string[],
|
|
512
|
+
previous: readonly string[],
|
|
513
|
+
available: ReadonlySet<ChromeDevToolsToolName>,
|
|
514
|
+
) {
|
|
515
|
+
const restored = [...current];
|
|
516
|
+
for (const [index, name] of previous.entries()) {
|
|
517
|
+
if (!available.has(name as ChromeDevToolsToolName) || restored.includes(name)) continue;
|
|
518
|
+
const following = previous.slice(index + 1).find((candidate) => restored.includes(candidate));
|
|
519
|
+
const insertion = following === undefined ? restored.length : restored.indexOf(following);
|
|
520
|
+
restored.splice(insertion, 0, name);
|
|
521
|
+
}
|
|
522
|
+
return restored;
|
|
213
523
|
}
|
|
214
524
|
|
|
215
525
|
function unique(values: readonly string[]) {
|
package/src/menu.ts
CHANGED
|
@@ -84,7 +84,7 @@ export async function showChromeDevtoolsMenu(pi: ExtensionAPI, ctx: CommandConte
|
|
|
84
84
|
{
|
|
85
85
|
id: "tools",
|
|
86
86
|
label: "Choose available browser tools…",
|
|
87
|
-
description: "Choose which capabilities
|
|
87
|
+
description: "Choose which browser capabilities are available.",
|
|
88
88
|
disabled: Boolean(current.mutationBlockedReason),
|
|
89
89
|
disabledReason: current.mutationBlockedReason,
|
|
90
90
|
action: "tools",
|
|
@@ -435,8 +435,8 @@ function buildToolReview(current: ToolWorkflowState, owner: object) {
|
|
|
435
435
|
: [" - none"]),
|
|
436
436
|
"",
|
|
437
437
|
"Other active Pi tools remain unchanged.",
|
|
438
|
-
"
|
|
439
|
-
"
|
|
438
|
+
"Enabled capabilities follow the running tool mode; saved mode changes apply on reload.",
|
|
439
|
+
"Disabled capabilities cannot be discovered or called.",
|
|
440
440
|
"The accepted availability policy is saved for future sessions.",
|
|
441
441
|
].join("\n"),
|
|
442
442
|
);
|
package/src/runtime.ts
CHANGED
|
@@ -13,7 +13,6 @@ export const DEFAULT_HTTP_TIMEOUT_MS = 1_000;
|
|
|
13
13
|
export const DEFAULT_ENDPOINT_WAIT_MS = 5_000;
|
|
14
14
|
export const DEFAULT_ENDPOINT_RETRY_MS = 250;
|
|
15
15
|
export const MANAGED_BROWSER_PROFILE_PREFIX = "pi-chrome-devtools-profile-";
|
|
16
|
-
export const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
17
16
|
export const BROWSER_SHUTDOWN_WAIT_MS = 1_500;
|
|
18
17
|
|
|
19
18
|
export interface DevToolsPage {
|
|
@@ -43,6 +42,7 @@ export interface ChromeDevToolsState {
|
|
|
43
42
|
activePageId?: string;
|
|
44
43
|
managedBrowser?: ManagedBrowser;
|
|
45
44
|
launchPromise?: Promise<void>;
|
|
45
|
+
latestLaunchPromise?: Promise<void>;
|
|
46
46
|
lastLaunchAttempt?: BrowserLaunchAttempt;
|
|
47
47
|
shuttingDown: boolean;
|
|
48
48
|
sessionGeneration: number;
|
|
@@ -63,7 +63,7 @@ export interface ManagedBrowser {
|
|
|
63
63
|
|
|
64
64
|
export interface BrowserLaunchAttempt {
|
|
65
65
|
candidateLabels: string[];
|
|
66
|
-
mode: "
|
|
66
|
+
mode: "automatic-port" | "explicit-port";
|
|
67
67
|
selectedCandidate?: string;
|
|
68
68
|
userDataDir?: string;
|
|
69
69
|
lastError?: string;
|
package/src/settings.ts
CHANGED
|
@@ -8,6 +8,12 @@ import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-
|
|
|
8
8
|
const NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
|
|
9
9
|
const LEGACY_SETTINGS_FILE_NAME = "pi-chrome-devtools-settings.json";
|
|
10
10
|
|
|
11
|
+
export type ChromeDevToolsToolMode = "codemode" | "lazy" | "direct";
|
|
12
|
+
|
|
13
|
+
export function isChromeDevtoolsToolMode(value: unknown): value is ChromeDevToolsToolMode {
|
|
14
|
+
return value === "codemode" || value === "lazy" || value === "direct";
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export interface ChromeDevToolsSettings {
|
|
12
18
|
tools: ChromeDevToolsToolName[];
|
|
13
19
|
updatedAt: number;
|
|
@@ -48,6 +54,7 @@ export interface EffectiveBrowserSettings {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export interface ResolvedChromeDevToolsSettings {
|
|
57
|
+
toolMode: ChromeDevToolsToolMode;
|
|
51
58
|
tools?: ChromeDevToolsToolName[];
|
|
52
59
|
updatedAt?: number;
|
|
53
60
|
browser: EffectiveBrowserSettings;
|
|
@@ -81,6 +88,7 @@ export interface SettingsFileOperations {
|
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
interface NormalizedSettingsDocument {
|
|
91
|
+
toolMode?: ChromeDevToolsToolMode;
|
|
84
92
|
tools?: ChromeDevToolsToolName[];
|
|
85
93
|
updatedAt?: number;
|
|
86
94
|
browser?: UserBrowserSettings;
|
|
@@ -154,6 +162,7 @@ export async function loadSettings(options: SettingsLoadOptions = {}): Promise<S
|
|
|
154
162
|
const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
|
|
155
163
|
const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
|
|
156
164
|
const recognized =
|
|
165
|
+
user.normalized?.toolMode !== undefined ||
|
|
157
166
|
settings.tools !== undefined ||
|
|
158
167
|
user.normalized?.browser !== undefined ||
|
|
159
168
|
project.normalized?.browser !== undefined ||
|
|
@@ -191,6 +200,7 @@ function resolveSettings(
|
|
|
191
200
|
browser: EffectiveBrowserSettings,
|
|
192
201
|
): ResolvedChromeDevToolsSettings {
|
|
193
202
|
return {
|
|
203
|
+
toolMode: user?.toolMode ?? "codemode",
|
|
194
204
|
...(user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {}),
|
|
195
205
|
browser,
|
|
196
206
|
webmcp: { enabled: user?.webmcp?.enabled ?? false },
|
|
@@ -311,6 +321,11 @@ async function normalizeSettingsDocument(
|
|
|
311
321
|
cwd?: string,
|
|
312
322
|
): Promise<NormalizedSettingsDocument> {
|
|
313
323
|
const normalized: NormalizedSettingsDocument = {};
|
|
324
|
+
if (scope === "user" && document.toolMode !== undefined) {
|
|
325
|
+
if (!isChromeDevtoolsToolMode(document.toolMode))
|
|
326
|
+
throw new Error("expected toolMode to be codemode, lazy, or direct");
|
|
327
|
+
normalized.toolMode = document.toolMode;
|
|
328
|
+
}
|
|
314
329
|
|
|
315
330
|
if (scope === "user" && (document.tools !== undefined || document.updatedAt !== undefined)) {
|
|
316
331
|
const toolSettings = normalizeChromeDevtoolsSettings(document);
|
|
@@ -494,9 +509,24 @@ function orderedUnique(values: readonly string[]) {
|
|
|
494
509
|
return [...new Set(values)];
|
|
495
510
|
}
|
|
496
511
|
|
|
512
|
+
export class ToolCatalogApplicationError extends Error {
|
|
513
|
+
constructor(
|
|
514
|
+
error: unknown,
|
|
515
|
+
readonly rollbackError?: unknown,
|
|
516
|
+
) {
|
|
517
|
+
super(
|
|
518
|
+
rollbackError
|
|
519
|
+
? `${formatError(error)}; saved catalog rollback failed: ${formatError(rollbackError)}`
|
|
520
|
+
: `${formatError(error)}; previous catalog restored`,
|
|
521
|
+
{ cause: error },
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
497
526
|
export function saveSettings(
|
|
498
527
|
settings: ChromeDevToolsSettings,
|
|
499
528
|
operations: Partial<SettingsFileOperations> = {},
|
|
529
|
+
applyAfterSave?: () => void,
|
|
500
530
|
): Promise<void> {
|
|
501
531
|
return queueSettingsMutation(
|
|
502
532
|
(current) => ({
|
|
@@ -505,9 +535,18 @@ export function saveSettings(
|
|
|
505
535
|
updatedAt: settings.updatedAt,
|
|
506
536
|
}),
|
|
507
537
|
operations,
|
|
538
|
+
applyAfterSave,
|
|
508
539
|
);
|
|
509
540
|
}
|
|
510
541
|
|
|
542
|
+
export function saveToolMode(
|
|
543
|
+
mode: ChromeDevToolsToolMode,
|
|
544
|
+
operations: Partial<SettingsFileOperations> = {},
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
if (!isChromeDevtoolsToolMode(mode)) return Promise.reject(new Error("Invalid tool mode"));
|
|
547
|
+
return queueSettingsMutation((current) => ({ ...current, toolMode: mode }), operations);
|
|
548
|
+
}
|
|
549
|
+
|
|
511
550
|
export function saveWebMcpSettings(enabled: boolean, operations: Partial<SettingsFileOperations> = {}): Promise<void> {
|
|
512
551
|
return queueSettingsMutation(
|
|
513
552
|
(current) => ({
|
|
@@ -541,8 +580,9 @@ export function saveBrowserSettings(
|
|
|
541
580
|
function queueSettingsMutation(
|
|
542
581
|
mutate: (current: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>,
|
|
543
582
|
operations: Partial<SettingsFileOperations>,
|
|
583
|
+
applyToolCatalog?: () => void,
|
|
544
584
|
) {
|
|
545
|
-
const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations));
|
|
585
|
+
const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations, applyToolCatalog));
|
|
546
586
|
settingsSaveQueue = operation.catch(() => undefined);
|
|
547
587
|
return operation;
|
|
548
588
|
}
|
|
@@ -550,6 +590,7 @@ function queueSettingsMutation(
|
|
|
550
590
|
async function saveSettingsMutationNow(
|
|
551
591
|
mutate: (current: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>,
|
|
552
592
|
operations: Partial<SettingsFileOperations>,
|
|
593
|
+
applyToolCatalog?: () => void,
|
|
553
594
|
): Promise<void> {
|
|
554
595
|
const filePath = settingsFilePath();
|
|
555
596
|
let current = await readSettingsDocument(filePath, "user");
|
|
@@ -571,6 +612,29 @@ async function saveSettingsMutationNow(
|
|
|
571
612
|
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
572
613
|
throw error;
|
|
573
614
|
}
|
|
615
|
+
if (!applyToolCatalog) return;
|
|
616
|
+
try {
|
|
617
|
+
// Synchronous runtime publication and its compensating write remain inside
|
|
618
|
+
// this queue, so dependent reads/replacements cannot see a rejected catalog.
|
|
619
|
+
applyToolCatalog();
|
|
620
|
+
} catch (error) {
|
|
621
|
+
let rollbackError: unknown;
|
|
622
|
+
try {
|
|
623
|
+
await saveSettingsMutationNow((latest) => {
|
|
624
|
+
const restored = { ...latest };
|
|
625
|
+
for (const field of ["tools", "updatedAt"] as const) {
|
|
626
|
+
if (JSON.stringify(latest[field]) !== JSON.stringify(nextDocument[field]))
|
|
627
|
+
throw new Error("Tool catalog changed after save; refusing to overwrite newer settings");
|
|
628
|
+
delete restored[field];
|
|
629
|
+
if (current.document?.[field] !== undefined) restored[field] = current.document[field];
|
|
630
|
+
}
|
|
631
|
+
return restored;
|
|
632
|
+
}, operations);
|
|
633
|
+
} catch (caught) {
|
|
634
|
+
rollbackError = caught;
|
|
635
|
+
}
|
|
636
|
+
throw new ToolCatalogApplicationError(error, rollbackError);
|
|
637
|
+
}
|
|
574
638
|
}
|
|
575
639
|
|
|
576
640
|
export function settingsFilePath() {
|