@narumitw/pi-chrome-devtools 0.53.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunks/{browser-settings-menu-N5O65I7S.js → browser-settings-menu-REIU7C3Q.js} +5 -3
- package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +7 -0
- package/dist/chunks/{chunk-RTECHAEZ.js → chunk-GTTYS5GT.js} +20 -9
- package/dist/chunks/chunk-GTTYS5GT.js.map +7 -0
- package/dist/chunks/{chunk-GVBXT3FB.js → chunk-SRT3UUIP.js} +117 -73
- package/dist/chunks/chunk-SRT3UUIP.js.map +7 -0
- package/dist/chunks/{chunk-B7B63PWB.js → chunk-YJVYJ2L2.js} +19 -15
- package/dist/chunks/chunk-YJVYJ2L2.js.map +7 -0
- package/dist/chunks/{menu-UBHW6UP2.js → menu-H5HMVGHI.js} +4 -4
- package/dist/chunks/{tools-V7OR5KJ3.js → tools-6P7EUX37.js} +49 -30
- package/dist/chunks/tools-6P7EUX37.js.map +7 -0
- package/dist/index.ts +16 -12
- package/dist/index.ts.map +2 -2
- package/package.json +1 -1
- package/src/browser-manager.ts +138 -72
- package/src/browser-settings-menu.ts +2 -1
- package/src/cdp-client.ts +18 -7
- package/src/chrome-devtools.ts +8 -1
- package/src/tool-selector.ts +18 -14
- package/src/tools.ts +5 -5
- package/src/webmcp/policy.ts +14 -7
- package/src/webmcp/protocol.ts +33 -21
- package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +0 -7
- package/dist/chunks/chunk-B7B63PWB.js.map +0 -7
- package/dist/chunks/chunk-GVBXT3FB.js.map +0 -7
- package/dist/chunks/chunk-RTECHAEZ.js.map +0 -7
- package/dist/chunks/tools-V7OR5KJ3.js.map +0 -7
- /package/dist/chunks/{menu-UBHW6UP2.js.map → menu-H5HMVGHI.js.map} +0 -0
package/src/browser-manager.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type ManagedBrowser,
|
|
20
20
|
state,
|
|
21
21
|
} from "./runtime.js";
|
|
22
|
+
import type { EffectiveBrowserSettings } from "./settings.js";
|
|
22
23
|
|
|
23
24
|
// Endpoint attachment, managed launch, dynamic-port publication, and shutdown intentionally remain
|
|
24
25
|
// together because they form one ownership state machine behind the testable operations boundary.
|
|
@@ -54,6 +55,7 @@ const DEFAULT_BROWSER_MANAGER_OPERATIONS: BrowserManagerOperations = {
|
|
|
54
55
|
let browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
|
|
55
56
|
|
|
56
57
|
interface ManagedBrowserScope {
|
|
58
|
+
browser: EffectiveBrowserSettings;
|
|
57
59
|
controller: AbortController;
|
|
58
60
|
generation: number;
|
|
59
61
|
launchPromise?: Promise<void>;
|
|
@@ -64,13 +66,36 @@ interface ManagedBrowserScope {
|
|
|
64
66
|
|
|
65
67
|
const managedBrowserScopes = new WeakMap<object, ManagedBrowserScope>();
|
|
66
68
|
|
|
69
|
+
function cloneBrowserSettings(browser: EffectiveBrowserSettings): EffectiveBrowserSettings {
|
|
70
|
+
return { ...browser, extensionPaths: [...browser.extensionPaths] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runtimeBrowserSettings(): EffectiveBrowserSettings {
|
|
74
|
+
return {
|
|
75
|
+
endpoint: `http://${formatHostForUrl(state.host)}:${state.configuredPort}`,
|
|
76
|
+
host: state.host,
|
|
77
|
+
port: state.configuredPort,
|
|
78
|
+
hostConfigured: state.hostConfigured,
|
|
79
|
+
portConfigured: state.portConfigured,
|
|
80
|
+
autoLaunchEnabled: state.autoLaunchEnabled,
|
|
81
|
+
...(state.browserExecutable ? { executablePath: state.browserExecutable } : {}),
|
|
82
|
+
extensionPaths: [...state.extensionPaths],
|
|
83
|
+
endpointSource: state.endpointSource,
|
|
84
|
+
autoLaunchSource: state.autoLaunchSource,
|
|
85
|
+
executablePathSource: state.browserExecutableSource,
|
|
86
|
+
extensionPathsSource: state.extensionPathsSource,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
function managedBrowserScope(owner: object) {
|
|
68
91
|
const existing = managedBrowserScopes.get(owner);
|
|
69
92
|
if (existing) return existing;
|
|
93
|
+
const browser = runtimeBrowserSettings();
|
|
70
94
|
const created: ManagedBrowserScope = {
|
|
95
|
+
browser,
|
|
71
96
|
controller: new AbortController(),
|
|
72
97
|
generation: 0,
|
|
73
|
-
port:
|
|
98
|
+
port: browser.port,
|
|
74
99
|
};
|
|
75
100
|
managedBrowserScopes.set(owner, created);
|
|
76
101
|
return created;
|
|
@@ -81,10 +106,27 @@ export function startManagedBrowserSession(owner: object) {
|
|
|
81
106
|
scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
|
|
82
107
|
scope.controller = new AbortController();
|
|
83
108
|
scope.generation += 1;
|
|
84
|
-
scope.port = state.configuredPort;
|
|
85
109
|
scope.lastLaunchAttempt = undefined;
|
|
86
110
|
}
|
|
87
111
|
|
|
112
|
+
export function syncManagedBrowserSettings(owner: object, browser: EffectiveBrowserSettings) {
|
|
113
|
+
const scope = managedBrowserScope(owner);
|
|
114
|
+
scope.browser = cloneBrowserSettings(browser);
|
|
115
|
+
scope.port = browser.port;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function browserSettingsForOwner(owner?: object) {
|
|
119
|
+
return owner ? managedBrowserScope(owner).browser : runtimeBrowserSettings();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function managedBrowserExtensionPaths(owner?: object) {
|
|
123
|
+
return browserSettingsForOwner(owner).extensionPaths;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function managedBrowserExtensionPathsSource(owner?: object) {
|
|
127
|
+
return browserSettingsForOwner(owner).extensionPathsSource;
|
|
128
|
+
}
|
|
129
|
+
|
|
88
130
|
export function managedBrowserForOwner(owner?: object) {
|
|
89
131
|
if (!owner) return state.managedBrowser;
|
|
90
132
|
const scopedBrowser = managedBrowserScope(owner).managedBrowser;
|
|
@@ -110,6 +152,14 @@ function managedBrowserPort(owner?: object) {
|
|
|
110
152
|
return owner ? managedBrowserScope(owner).port : state.port;
|
|
111
153
|
}
|
|
112
154
|
|
|
155
|
+
function managedBrowserConfiguredPort(owner?: object) {
|
|
156
|
+
return browserSettingsForOwner(owner).port;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function managedBrowserPortConfigured(owner?: object) {
|
|
160
|
+
return browserSettingsForOwner(owner).portConfigured;
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
function setManagedBrowserPort(owner: object | undefined, port: number) {
|
|
114
164
|
if (owner) managedBrowserScope(owner).port = port;
|
|
115
165
|
else state.port = port;
|
|
@@ -153,12 +203,12 @@ export async function ensureDevToolsEndpoint(
|
|
|
153
203
|
owner?: object,
|
|
154
204
|
) {
|
|
155
205
|
callerSignal?.throwIfAborted();
|
|
156
|
-
if (extensionsConfigured()) {
|
|
157
|
-
validateExtensionLaunchMode();
|
|
206
|
+
if (extensionsConfigured(owner)) {
|
|
207
|
+
validateExtensionLaunchMode(owner);
|
|
158
208
|
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
159
209
|
return;
|
|
160
210
|
}
|
|
161
|
-
if (canAutoLaunchBrowser()) {
|
|
211
|
+
if (canAutoLaunchBrowser(owner)) {
|
|
162
212
|
try {
|
|
163
213
|
await withEndpointRetry(
|
|
164
214
|
() => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
|
|
@@ -167,7 +217,7 @@ export async function ensureDevToolsEndpoint(
|
|
|
167
217
|
);
|
|
168
218
|
return;
|
|
169
219
|
} catch (error) {
|
|
170
|
-
if (shouldAutoLaunchAfterEndpointError(error)) {
|
|
220
|
+
if (shouldAutoLaunchAfterEndpointError(error, owner)) {
|
|
171
221
|
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
172
222
|
return;
|
|
173
223
|
}
|
|
@@ -183,18 +233,19 @@ export async function ensureDevToolsEndpoint(
|
|
|
183
233
|
}
|
|
184
234
|
}
|
|
185
235
|
|
|
186
|
-
function validateExtensionLaunchMode() {
|
|
187
|
-
|
|
236
|
+
function validateExtensionLaunchMode(owner?: object) {
|
|
237
|
+
const browser = browserSettingsForOwner(owner);
|
|
238
|
+
if (!isLocalDevToolsHost(browser.host)) {
|
|
188
239
|
throw new DevToolsEndpointError(
|
|
189
240
|
"Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified.",
|
|
190
241
|
);
|
|
191
242
|
}
|
|
192
|
-
if (!
|
|
243
|
+
if (!browser.autoLaunchEnabled) {
|
|
193
244
|
throw new DevToolsEndpointError(
|
|
194
245
|
"Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser.",
|
|
195
246
|
);
|
|
196
247
|
}
|
|
197
|
-
if (!
|
|
248
|
+
if (!browser.executablePath) {
|
|
198
249
|
throw new DevToolsEndpointError(
|
|
199
250
|
"Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json.",
|
|
200
251
|
);
|
|
@@ -258,9 +309,9 @@ async function prepareManagedBrowserLaunch(
|
|
|
258
309
|
await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
|
|
259
310
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
260
311
|
}
|
|
261
|
-
if (
|
|
312
|
+
if (managedBrowserPortConfigured(owner)) {
|
|
262
313
|
const available = await browserManagerOperations.isPortAvailable(
|
|
263
|
-
|
|
314
|
+
browserSettingsForOwner(owner).host,
|
|
264
315
|
managedBrowserPort(owner),
|
|
265
316
|
);
|
|
266
317
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
@@ -280,7 +331,7 @@ async function launchManagedBrowser(
|
|
|
280
331
|
owner?: object,
|
|
281
332
|
) {
|
|
282
333
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
283
|
-
const candidateDefinitions = browserCandidateDefinitions();
|
|
334
|
+
const candidateDefinitions = browserCandidateDefinitions(owner);
|
|
284
335
|
const candidates = await resolveBrowserCandidates(
|
|
285
336
|
candidateDefinitions,
|
|
286
337
|
generation,
|
|
@@ -290,25 +341,25 @@ async function launchManagedBrowser(
|
|
|
290
341
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
291
342
|
setLastLaunchAttempt(owner, {
|
|
292
343
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
293
|
-
mode:
|
|
344
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
|
|
294
345
|
});
|
|
295
346
|
|
|
296
347
|
if (candidates.length === 0) {
|
|
297
|
-
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
|
|
348
|
+
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
|
|
298
349
|
}
|
|
299
350
|
|
|
300
351
|
let lastError: unknown;
|
|
301
352
|
for (const candidate of candidates) {
|
|
302
353
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
303
354
|
try {
|
|
304
|
-
if (extensionsConfigured())
|
|
355
|
+
if (extensionsConfigured(owner))
|
|
305
356
|
await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
|
|
306
357
|
await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
|
|
307
358
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
308
359
|
setLastLaunchAttempt(owner, {
|
|
309
360
|
...lastLaunchAttempt(owner),
|
|
310
361
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
311
|
-
mode:
|
|
362
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
|
|
312
363
|
selectedCandidate: formatBrowserCandidate(candidate),
|
|
313
364
|
userDataDir: managedBrowserForOwner(owner)?.userDataDir,
|
|
314
365
|
});
|
|
@@ -320,7 +371,7 @@ async function launchManagedBrowser(
|
|
|
320
371
|
setLastLaunchAttempt(owner, {
|
|
321
372
|
...(lastLaunchAttempt(owner) ?? {
|
|
322
373
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
323
|
-
mode:
|
|
374
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
|
|
324
375
|
}),
|
|
325
376
|
lastError: formatError(cancellation),
|
|
326
377
|
});
|
|
@@ -330,20 +381,20 @@ async function launchManagedBrowser(
|
|
|
330
381
|
setLastLaunchAttempt(owner, {
|
|
331
382
|
...(lastLaunchAttempt(owner) ?? {
|
|
332
383
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
333
|
-
mode:
|
|
384
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
|
|
334
385
|
}),
|
|
335
386
|
lastError: formatError(error),
|
|
336
387
|
});
|
|
337
388
|
}
|
|
338
389
|
}
|
|
339
390
|
|
|
340
|
-
if (lastError instanceof DevToolsEndpointError && extensionsConfigured()) throw lastError;
|
|
391
|
+
if (lastError instanceof DevToolsEndpointError && extensionsConfigured(owner)) throw lastError;
|
|
341
392
|
throw new DevToolsEndpointError(
|
|
342
393
|
[
|
|
343
394
|
"Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
|
|
344
395
|
`Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
|
|
345
396
|
lastError ? `Last error: ${formatError(lastError)}` : undefined,
|
|
346
|
-
launchHint(),
|
|
397
|
+
launchHint(owner),
|
|
347
398
|
endpointConfigHint(),
|
|
348
399
|
]
|
|
349
400
|
.filter(Boolean)
|
|
@@ -403,11 +454,13 @@ async function launchBrowserCandidate(
|
|
|
403
454
|
let managedBrowser: ManagedBrowser | undefined;
|
|
404
455
|
try {
|
|
405
456
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
406
|
-
const portArgument =
|
|
457
|
+
const portArgument = managedBrowserPortConfigured(owner)
|
|
458
|
+
? String(managedBrowserPort(owner))
|
|
459
|
+
: "0";
|
|
407
460
|
const args = buildManagedBrowserLaunchArguments(
|
|
408
461
|
userDataDir,
|
|
409
462
|
portArgument,
|
|
410
|
-
|
|
463
|
+
managedBrowserExtensionPaths(owner),
|
|
411
464
|
);
|
|
412
465
|
const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
|
|
413
466
|
shell: false,
|
|
@@ -430,14 +483,17 @@ async function launchBrowserCandidate(
|
|
|
430
483
|
}
|
|
431
484
|
launchedBrowser.exited = true;
|
|
432
485
|
launchedBrowser.ready = false;
|
|
433
|
-
if (
|
|
434
|
-
|
|
486
|
+
if (
|
|
487
|
+
!managedBrowserPortConfigured(owner) &&
|
|
488
|
+
launchedBrowser.port === managedBrowserPort(owner)
|
|
489
|
+
) {
|
|
490
|
+
setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
|
|
435
491
|
}
|
|
436
492
|
});
|
|
437
493
|
|
|
438
494
|
await waitForBrowserSpawn(child);
|
|
439
495
|
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
440
|
-
if (
|
|
496
|
+
if (managedBrowserPortConfigured(owner)) {
|
|
441
497
|
launchedBrowser.port = managedBrowserPort(owner);
|
|
442
498
|
} else {
|
|
443
499
|
launchedBrowser.port = await readManagedBrowserPort(
|
|
@@ -533,7 +589,7 @@ async function readManagedBrowserPort(
|
|
|
533
589
|
[
|
|
534
590
|
"Timed out waiting for auto-launched browser DevToolsActivePort.",
|
|
535
591
|
`Expected file: ${activePortFile}`,
|
|
536
|
-
launchHint(),
|
|
592
|
+
launchHint(owner),
|
|
537
593
|
].join("\n"),
|
|
538
594
|
);
|
|
539
595
|
}
|
|
@@ -565,7 +621,7 @@ async function waitForDevToolsEndpoint(
|
|
|
565
621
|
throw new DevToolsEndpointError(
|
|
566
622
|
[
|
|
567
623
|
`Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
|
|
568
|
-
launchHint(),
|
|
624
|
+
launchHint(owner),
|
|
569
625
|
].join("\n"),
|
|
570
626
|
);
|
|
571
627
|
}
|
|
@@ -636,8 +692,8 @@ async function cleanupManagedBrowser(managedBrowser: ManagedBrowser, owner?: obj
|
|
|
636
692
|
await browserManagerOperations
|
|
637
693
|
.rm(managedBrowser.userDataDir, { recursive: true, force: true })
|
|
638
694
|
.catch(() => undefined);
|
|
639
|
-
if (!
|
|
640
|
-
setManagedBrowserPort(owner,
|
|
695
|
+
if (!managedBrowserPortConfigured(owner) && managedBrowser.port === managedBrowserPort(owner)) {
|
|
696
|
+
setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
|
|
641
697
|
}
|
|
642
698
|
}
|
|
643
699
|
|
|
@@ -743,28 +799,29 @@ function isLaunchableEndpointError(error: unknown) {
|
|
|
743
799
|
return error instanceof DevToolsEndpointError && error.launchable;
|
|
744
800
|
}
|
|
745
801
|
|
|
746
|
-
function shouldAutoLaunchAfterEndpointError(error: unknown) {
|
|
747
|
-
if (!canAutoLaunchBrowser()) return false;
|
|
802
|
+
function shouldAutoLaunchAfterEndpointError(error: unknown, owner?: object) {
|
|
803
|
+
if (!canAutoLaunchBrowser(owner)) return false;
|
|
748
804
|
if (isLaunchableEndpointError(error)) return true;
|
|
749
805
|
|
|
750
806
|
// After the attach-first attempt (including its retry window, when applicable) fails, treat any
|
|
751
807
|
// DevTools endpoint error on an unpinned port as a conflict we can avoid with a dynamic port.
|
|
752
|
-
return !
|
|
808
|
+
return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
|
|
753
809
|
}
|
|
754
810
|
|
|
755
|
-
function canAutoLaunchBrowser() {
|
|
756
|
-
|
|
811
|
+
function canAutoLaunchBrowser(owner?: object) {
|
|
812
|
+
const browser = browserSettingsForOwner(owner);
|
|
813
|
+
return browser.autoLaunchEnabled && isLocalDevToolsHost(browser.host);
|
|
757
814
|
}
|
|
758
815
|
|
|
759
|
-
function extensionsConfigured() {
|
|
760
|
-
return
|
|
816
|
+
function extensionsConfigured(owner?: object) {
|
|
817
|
+
return managedBrowserExtensionPaths(owner).length > 0;
|
|
761
818
|
}
|
|
762
819
|
|
|
763
820
|
function endpointConnectionErrorMessage(error: unknown, owner?: object) {
|
|
764
821
|
const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
|
|
765
822
|
return [
|
|
766
823
|
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
|
|
767
|
-
launchHint(),
|
|
824
|
+
launchHint(owner),
|
|
768
825
|
endpointConfigHint(),
|
|
769
826
|
].join("\n");
|
|
770
827
|
}
|
|
@@ -774,7 +831,7 @@ function isTimeoutError(error: unknown) {
|
|
|
774
831
|
}
|
|
775
832
|
|
|
776
833
|
export function devToolsEndpoint(owner?: object) {
|
|
777
|
-
return `http://${formatHostForUrl(
|
|
834
|
+
return `http://${formatHostForUrl(browserSettingsForOwner(owner).host)}:${managedBrowserPort(owner)}`;
|
|
778
835
|
}
|
|
779
836
|
|
|
780
837
|
export function formatHostForUrl(host: string) {
|
|
@@ -782,8 +839,8 @@ export function formatHostForUrl(host: string) {
|
|
|
782
839
|
return host.includes(":") ? `[${host}]` : host;
|
|
783
840
|
}
|
|
784
841
|
|
|
785
|
-
export function endpointSourceLabel() {
|
|
786
|
-
return
|
|
842
|
+
export function endpointSourceLabel(owner?: object) {
|
|
843
|
+
return browserSettingsForOwner(owner).endpointSource;
|
|
787
844
|
}
|
|
788
845
|
|
|
789
846
|
export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
|
|
@@ -797,27 +854,28 @@ export function browserLifecycleState(owner?: object): BrowserLifecycleState {
|
|
|
797
854
|
}
|
|
798
855
|
|
|
799
856
|
export function launchModeLabel(owner?: object) {
|
|
857
|
+
const browser = browserSettingsForOwner(owner);
|
|
800
858
|
const managedBrowser = managedBrowserForOwner(owner);
|
|
801
|
-
if (extensionsConfigured()) {
|
|
802
|
-
if (!isLocalDevToolsHost(
|
|
803
|
-
if (!
|
|
859
|
+
if (extensionsConfigured(owner)) {
|
|
860
|
+
if (!isLocalDevToolsHost(browser.host)) return "invalid; extensions require a local endpoint";
|
|
861
|
+
if (!browser.autoLaunchEnabled) return "invalid; extensions require auto-launch";
|
|
804
862
|
if (managedBrowser && !managedBrowser.exited) {
|
|
805
|
-
return
|
|
863
|
+
return managedBrowserPortConfigured(owner)
|
|
806
864
|
? "extension-owned browser on explicit port"
|
|
807
865
|
: "extension-owned browser on dynamic port";
|
|
808
866
|
}
|
|
809
|
-
return
|
|
867
|
+
return managedBrowserPortConfigured(owner)
|
|
810
868
|
? "force extension-owned launch on explicit port"
|
|
811
869
|
: "force extension-owned launch on dynamic port";
|
|
812
870
|
}
|
|
813
|
-
if (!isLocalDevToolsHost(
|
|
814
|
-
if (!
|
|
871
|
+
if (!isLocalDevToolsHost(browser.host)) return "manual remote endpoint";
|
|
872
|
+
if (!browser.autoLaunchEnabled) return "manual; auto-launch disabled";
|
|
815
873
|
if (managedBrowser && !managedBrowser.exited) {
|
|
816
|
-
return
|
|
874
|
+
return managedBrowserPortConfigured(owner)
|
|
817
875
|
? "auto-launched on explicit port"
|
|
818
876
|
: "auto-launched on dynamic port";
|
|
819
877
|
}
|
|
820
|
-
return
|
|
878
|
+
return managedBrowserPortConfigured(owner)
|
|
821
879
|
? "attach first; auto-launch explicit port"
|
|
822
880
|
: "attach first; auto-launch dynamic port";
|
|
823
881
|
}
|
|
@@ -837,34 +895,38 @@ export function launchAttemptLines(owner?: object) {
|
|
|
837
895
|
return lines;
|
|
838
896
|
}
|
|
839
897
|
|
|
840
|
-
export function launchHint() {
|
|
841
|
-
|
|
898
|
+
export function launchHint(owner?: object) {
|
|
899
|
+
const browser = browserSettingsForOwner(owner);
|
|
900
|
+
if (extensionsConfigured(owner)) {
|
|
842
901
|
return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
|
|
843
902
|
}
|
|
844
|
-
if (!isLocalDevToolsHost(
|
|
845
|
-
return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint()}.`;
|
|
903
|
+
if (!isLocalDevToolsHost(browser.host)) {
|
|
904
|
+
return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint(owner)}.`;
|
|
846
905
|
}
|
|
847
|
-
if (!
|
|
848
|
-
return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand()}`;
|
|
906
|
+
if (!browser.autoLaunchEnabled) {
|
|
907
|
+
return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
|
|
849
908
|
}
|
|
850
|
-
const managedMode =
|
|
851
|
-
|
|
909
|
+
const managedMode = managedBrowserPortConfigured(owner)
|
|
910
|
+
? `port ${managedBrowserPort(owner)}`
|
|
911
|
+
: "a dynamic DevTools port";
|
|
912
|
+
return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand(owner)}`;
|
|
852
913
|
}
|
|
853
914
|
|
|
854
|
-
export function browserCandidateHint() {
|
|
855
|
-
if (extensionsConfigured()) {
|
|
915
|
+
export function browserCandidateHint(owner?: object) {
|
|
916
|
+
if (extensionsConfigured(owner)) {
|
|
856
917
|
return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
|
|
857
918
|
}
|
|
858
|
-
return `Browser candidates: ${browserCandidateDefinitions()
|
|
919
|
+
return `Browser candidates: ${browserCandidateDefinitions(owner)
|
|
859
920
|
.map((candidate) => candidate.label)
|
|
860
921
|
.join(", ")}`;
|
|
861
922
|
}
|
|
862
923
|
|
|
863
|
-
export function chromeLaunchCommand() {
|
|
864
|
-
const executable =
|
|
924
|
+
export function chromeLaunchCommand(owner?: object) {
|
|
925
|
+
const executable =
|
|
926
|
+
browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
|
|
865
927
|
const dataDir =
|
|
866
928
|
process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
|
|
867
|
-
return `${quoteCommandPart(executable)} --remote-debugging-port=${
|
|
929
|
+
return `${quoteCommandPart(executable)} --remote-debugging-port=${managedBrowserPort(owner)} --user-data-dir=${dataDir}`;
|
|
868
930
|
}
|
|
869
931
|
|
|
870
932
|
function defaultManualBrowserExecutable() {
|
|
@@ -888,22 +950,23 @@ export function isLocalDevToolsHost(host: string) {
|
|
|
888
950
|
return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
|
|
889
951
|
}
|
|
890
952
|
|
|
891
|
-
function browserCandidateDefinitions(): BrowserCandidateDefinition[] {
|
|
892
|
-
const explicitCandidate = explicitBrowserCandidateDefinition();
|
|
953
|
+
function browserCandidateDefinitions(owner?: object): BrowserCandidateDefinition[] {
|
|
954
|
+
const explicitCandidate = explicitBrowserCandidateDefinition(owner);
|
|
893
955
|
if (explicitCandidate.length > 0) return explicitCandidate;
|
|
894
956
|
|
|
895
957
|
return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
|
|
896
958
|
}
|
|
897
959
|
|
|
898
|
-
function explicitBrowserCandidateDefinition(): BrowserCandidateDefinition[] {
|
|
899
|
-
|
|
960
|
+
function explicitBrowserCandidateDefinition(owner?: object): BrowserCandidateDefinition[] {
|
|
961
|
+
const browser = browserSettingsForOwner(owner);
|
|
962
|
+
if (!browser.executablePath) return [];
|
|
900
963
|
return [
|
|
901
964
|
{
|
|
902
965
|
label:
|
|
903
|
-
|
|
966
|
+
browser.executablePathSource === "environment"
|
|
904
967
|
? "PI_CHROME_DEVTOOLS_BROWSER"
|
|
905
968
|
: "browser.executablePath",
|
|
906
|
-
executable:
|
|
969
|
+
executable: browser.executablePath,
|
|
907
970
|
source: "env",
|
|
908
971
|
},
|
|
909
972
|
];
|
|
@@ -1070,8 +1133,11 @@ function formatBrowserCandidateDefinition(candidate: BrowserCandidateDefinition)
|
|
|
1070
1133
|
return `${candidate.label} (${candidate.executable})`;
|
|
1071
1134
|
}
|
|
1072
1135
|
|
|
1073
|
-
function noBrowserCandidateMessage(
|
|
1074
|
-
|
|
1136
|
+
function noBrowserCandidateMessage(
|
|
1137
|
+
candidateDefinitions: BrowserCandidateDefinition[],
|
|
1138
|
+
owner?: object,
|
|
1139
|
+
) {
|
|
1140
|
+
if (extensionsConfigured(owner)) {
|
|
1075
1141
|
return [
|
|
1076
1142
|
"Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
|
|
1077
1143
|
`Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
4
|
-
import { shutdownManagedBrowser } from "./browser-manager.js";
|
|
4
|
+
import { shutdownManagedBrowser, syncManagedBrowserSettings } from "./browser-manager.js";
|
|
5
5
|
import {
|
|
6
6
|
applyAvailableChromeDevtoolsTools,
|
|
7
7
|
configuredChromeDevtoolsTools,
|
|
@@ -276,6 +276,7 @@ async function saveAndApplyBrowserPatch(
|
|
|
276
276
|
const loaded = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
277
277
|
if (!isCurrent(generation, ownerSignal, actionSignal)) return false;
|
|
278
278
|
applyRuntimeBrowserSettings(loaded.effectiveBrowser, loaded.paths, projectTrusted);
|
|
279
|
+
syncManagedBrowserSettings(ctx.sessionManager, loaded.effectiveBrowser);
|
|
279
280
|
state.settingsNotice = loaded.notice;
|
|
280
281
|
return loaded;
|
|
281
282
|
} catch (error) {
|
package/src/cdp-client.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface CdpConnectOptions extends CdpOperationOptions {
|
|
|
51
51
|
|
|
52
52
|
const MAX_BUFFERED_EVENTS_PER_METHOD = 32;
|
|
53
53
|
const MAX_CDP_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
54
|
+
const activePageIds = new WeakMap<object, string>();
|
|
54
55
|
|
|
55
56
|
interface PageOperationOptions {
|
|
56
57
|
sessionOwner?: object;
|
|
@@ -84,13 +85,13 @@ export async function resolvePage(pageId?: string, options: PageOperationOptions
|
|
|
84
85
|
const pages = await listPages(options);
|
|
85
86
|
if (pageId) return requirePage(pageId, pages);
|
|
86
87
|
|
|
87
|
-
const page = resolveDefaultPage(pages);
|
|
88
|
+
const page = resolveDefaultPage(pages, options.sessionOwner);
|
|
88
89
|
if (!page) {
|
|
89
90
|
throw new Error(
|
|
90
91
|
[
|
|
91
92
|
`No Chrome pages found at ${devToolsEndpoint(options.sessionOwner)}.`,
|
|
92
93
|
"Use chrome_devtools_navigate with a URL to create a page, or open a Chrome tab manually.",
|
|
93
|
-
launchHint(),
|
|
94
|
+
launchHint(options.sessionOwner),
|
|
94
95
|
].join("\n"),
|
|
95
96
|
);
|
|
96
97
|
}
|
|
@@ -105,22 +106,32 @@ export async function resolvePageForNavigation(
|
|
|
105
106
|
const pages = await listPages(options);
|
|
106
107
|
if (pageId) return { created: false, page: requirePage(pageId, pages) };
|
|
107
108
|
|
|
108
|
-
const page = resolveDefaultPage(pages);
|
|
109
|
+
const page = resolveDefaultPage(pages, options.sessionOwner);
|
|
109
110
|
if (page) return { created: false, page };
|
|
110
111
|
|
|
111
112
|
return { created: true, page: await createPage("about:blank", options) };
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
function resolveDefaultPage(pages: DevToolsPage[]) {
|
|
115
|
-
|
|
115
|
+
function resolveDefaultPage(pages: DevToolsPage[], owner?: object) {
|
|
116
|
+
const activePageId = owner ? activePageIds.get(owner) : state.activePageId;
|
|
117
|
+
if (!activePageId) return pages[0];
|
|
116
118
|
|
|
117
|
-
const activePage = pages.find((candidate) => candidate.id ===
|
|
119
|
+
const activePage = pages.find((candidate) => candidate.id === activePageId);
|
|
118
120
|
if (activePage) return activePage;
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
setActivePageId(owner, undefined);
|
|
121
123
|
return pages[0];
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
export function setActivePageId(owner: object | undefined, pageId: string | undefined) {
|
|
127
|
+
if (!owner) {
|
|
128
|
+
state.activePageId = pageId;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (pageId) activePageIds.set(owner, pageId);
|
|
132
|
+
else activePageIds.delete(owner);
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
function requirePage(pageId: string, pages: DevToolsPage[]) {
|
|
125
136
|
const page = pages.find((candidate) => candidate.id === pageId);
|
|
126
137
|
if (page) return page;
|
package/src/chrome-devtools.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
shutdownManagedBrowser,
|
|
4
|
+
startManagedBrowserSession,
|
|
5
|
+
syncManagedBrowserSettings,
|
|
6
|
+
} from "./browser-manager.js";
|
|
7
|
+
import { setActivePageId } from "./cdp-client.js";
|
|
3
8
|
import {
|
|
4
9
|
availableChromeDevtoolsTools,
|
|
5
10
|
configureChromeDevtoolsToolExposure,
|
|
@@ -98,12 +103,14 @@ export default function chromeDevtools(pi: ExtensionAPI) {
|
|
|
98
103
|
});
|
|
99
104
|
startManagedBrowserSession(ctx.sessionManager);
|
|
100
105
|
if (generation !== state.sessionGeneration) return;
|
|
106
|
+
setActivePageId(ctx.sessionManager, undefined);
|
|
101
107
|
state.activePageId = undefined;
|
|
102
108
|
state.lastLaunchAttempt = undefined;
|
|
103
109
|
const projectTrusted = ctx.isProjectTrusted();
|
|
104
110
|
const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
105
111
|
if (generation !== state.sessionGeneration) return;
|
|
106
112
|
applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
|
|
113
|
+
syncManagedBrowserSettings(ctx.sessionManager, settings.effectiveBrowser);
|
|
107
114
|
applyRuntimeWebMcpSetting(settings.effectiveWebMcpEnabled, ctx.sessionManager);
|
|
108
115
|
state.settingsNotice = settings.notice;
|
|
109
116
|
for (const warning of settings.warnings) {
|