pi-chrome 0.15.19 → 0.15.22
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
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.22 — 2026-05-16
|
|
6
|
+
|
|
7
|
+
- **Earlier page-load capture.** Companion extension now injects console/network instrumentation at `document_start`, so initial React render errors and early API calls show up in `chrome_list_console_messages` / `chrome_list_network_requests`.
|
|
8
|
+
- **Quieter locked state.** Startup no longer shows a persistent Chrome bridge notification/status item before authorization; status bar appears only when Chrome control is authorized.
|
|
9
|
+
- **Lazy tool registration.** `chrome_*` tools and primer are registered only after `/chrome authorize`, reducing prompt/tool overhead while Chrome control is locked.
|
|
10
|
+
|
|
11
|
+
## 0.15.21 — 2026-05-16
|
|
12
|
+
|
|
13
|
+
### Reverted 0.16.x and 0.17.x lines
|
|
14
|
+
|
|
15
|
+
- Versions 0.16.0 through 0.17.2 were published to npm and subsequently unpublished. 0.17.3 was prepared locally but never published. The work introduced in those versions — mandatory pairing, signed-envelope auth, standalone bridge daemon, idempotent onboard, etc. — is reachable only via git tags (`v0.16.0` … `v0.17.3`) and is not in the current main branch.
|
|
16
|
+
- This release is **tree-equivalent to 0.15.20** with a version-only bump so future patch releases can ship cleanly.
|
|
17
|
+
|
|
18
|
+
## 0.15.20 — 2026-05-15
|
|
19
|
+
|
|
20
|
+
- **Interruptible `chrome_*` tools.** All `chrome_*` tools now honor the agent harness `AbortSignal`, so pressing Esc aborts in-flight bridge calls (including the long-polling `chrome_wait_for`) immediately instead of waiting out the full `timeoutMs`.
|
|
21
|
+
|
|
5
22
|
## 0.15.19 — 2026-05-14
|
|
6
23
|
|
|
7
24
|
- **Simpler package description.** README hero and npm/pi.dev description now use the same concise authorization-focused sentence.
|
|
@@ -966,6 +966,24 @@ if (chrome.webNavigation && chrome.webNavigation.onCommitted) {
|
|
|
966
966
|
});
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
// Always inject early console/network capture at document_start on every navigation.
|
|
970
|
+
// Catches console messages, errors, and network requests that fire during page load,
|
|
971
|
+
// before chrome_snapshot or chrome_evaluate install the instrumentation normally.
|
|
972
|
+
// The function installEarlyCapture sets __piChromeWrapped flags so the post-hoc
|
|
973
|
+
// installPiChromeInstrumentation() call is idempotent.
|
|
974
|
+
if (chrome.webNavigation && chrome.webNavigation.onCommitted) {
|
|
975
|
+
chrome.webNavigation.onCommitted.addListener((details) => {
|
|
976
|
+
if (details.frameId !== 0) return;
|
|
977
|
+
chrome.scripting.executeScript({
|
|
978
|
+
target: { tabId: details.tabId, frameIds: [0] },
|
|
979
|
+
world: "MAIN",
|
|
980
|
+
injectImmediately: true,
|
|
981
|
+
func: installEarlyCapture,
|
|
982
|
+
args: [],
|
|
983
|
+
}).catch(() => undefined);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
969
987
|
async function bringToFront(tab) {
|
|
970
988
|
await chrome.windows.update(tab.windowId, { focused: true });
|
|
971
989
|
await chrome.tabs.update(tab.id, { active: true });
|
|
@@ -1310,6 +1328,141 @@ function installPiChromeInstrumentation() {
|
|
|
1310
1328
|
}
|
|
1311
1329
|
}
|
|
1312
1330
|
|
|
1331
|
+
// Early-capture version of installPiChromeInstrumentation, designed to be injected
|
|
1332
|
+
// at document_start via webNavigation.onCommitted. Wraps console, fetch, and XHR
|
|
1333
|
+
// before the page's own JavaScript runs, so page-load errors are captured.
|
|
1334
|
+
// Sets __piChromeWrapped flags so the post-hoc installPiChromeInstrumentation()
|
|
1335
|
+
// sees them and skips (idempotent).
|
|
1336
|
+
// NOTE: This function is self-contained — it does NOT close over any outer scope
|
|
1337
|
+
// because it gets serialized by chrome.scripting.executeScript({func: ...}).
|
|
1338
|
+
function installEarlyCapture() {
|
|
1339
|
+
if (window.__piChromeEarlyCaptureInstalled) return;
|
|
1340
|
+
window.__piChromeEarlyCaptureInstalled = true;
|
|
1341
|
+
var state = window.__PI_CHROME_STATE__;
|
|
1342
|
+
if (!state) {
|
|
1343
|
+
state = {
|
|
1344
|
+
nextElementUid: 1,
|
|
1345
|
+
elements: {},
|
|
1346
|
+
console: [],
|
|
1347
|
+
network: [],
|
|
1348
|
+
nextRequestId: 1,
|
|
1349
|
+
instrumentationInstalled: false,
|
|
1350
|
+
};
|
|
1351
|
+
window.__PI_CHROME_STATE__ = state;
|
|
1352
|
+
}
|
|
1353
|
+
function pushConsole(level, args) {
|
|
1354
|
+
state.console.push({
|
|
1355
|
+
id: state.console.length + 1,
|
|
1356
|
+
level: level,
|
|
1357
|
+
timestamp: Date.now(),
|
|
1358
|
+
url: location.href,
|
|
1359
|
+
args: Array.from(args).map(function(arg) {
|
|
1360
|
+
try {
|
|
1361
|
+
if (typeof arg === "string") return arg;
|
|
1362
|
+
if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack };
|
|
1363
|
+
return JSON.parse(JSON.stringify(arg));
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
return String(arg);
|
|
1366
|
+
}
|
|
1367
|
+
}),
|
|
1368
|
+
});
|
|
1369
|
+
if (state.console.length > 500) state.console.splice(0, state.console.length - 500);
|
|
1370
|
+
}
|
|
1371
|
+
for (var i = 0; i < 5; i++) {
|
|
1372
|
+
var levels = ["debug", "log", "info", "warn", "error"];
|
|
1373
|
+
var level = levels[i];
|
|
1374
|
+
var original = console[level];
|
|
1375
|
+
if (typeof original !== "function" || original.__piChromeWrapped) continue;
|
|
1376
|
+
var wrapped = function(lvl, orig) {
|
|
1377
|
+
return function() {
|
|
1378
|
+
pushConsole(lvl, arguments);
|
|
1379
|
+
return orig.apply(this, arguments);
|
|
1380
|
+
};
|
|
1381
|
+
}(level, original);
|
|
1382
|
+
wrapped.__piChromeWrapped = true;
|
|
1383
|
+
console[level] = wrapped;
|
|
1384
|
+
}
|
|
1385
|
+
window.addEventListener("error", function(event) {
|
|
1386
|
+
pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno]);
|
|
1387
|
+
});
|
|
1388
|
+
window.addEventListener("unhandledrejection", function(event) {
|
|
1389
|
+
pushConsole("unhandledrejection", [event.reason]);
|
|
1390
|
+
});
|
|
1391
|
+
var trimBody = function(text) {
|
|
1392
|
+
return typeof text === "string" && text.length > 200000 ? text.slice(0, 200000) + "\n[truncated " + (text.length - 200000) + " chars]" : text;
|
|
1393
|
+
};
|
|
1394
|
+
var record = function(entry) {
|
|
1395
|
+
state.network.push(entry);
|
|
1396
|
+
if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000);
|
|
1397
|
+
return entry;
|
|
1398
|
+
};
|
|
1399
|
+
if (window.fetch && !window.fetch.__piChromeWrapped) {
|
|
1400
|
+
var originalFetch = window.fetch.bind(window);
|
|
1401
|
+
var wrappedFetch = async function() {
|
|
1402
|
+
var args = [];
|
|
1403
|
+
for (var k = 0; k < arguments.length; k++) args.push(arguments[k]);
|
|
1404
|
+
var id = "req-" + state.nextRequestId++;
|
|
1405
|
+
var startedAt = Date.now();
|
|
1406
|
+
var input = args[0];
|
|
1407
|
+
var init = args[1] || {};
|
|
1408
|
+
var url = typeof input === "string" ? input : (input ? input.url : "");
|
|
1409
|
+
var method = (init.method || (input ? input.method : null) || "GET").toUpperCase();
|
|
1410
|
+
var entry = record({ id: id, type: "fetch", method: method, url: String(url || ""), startedAt: startedAt, pageUrl: location.href, status: "pending" });
|
|
1411
|
+
try {
|
|
1412
|
+
var response = await originalFetch.apply(window, args);
|
|
1413
|
+
entry.status = response.status;
|
|
1414
|
+
entry.statusText = response.statusText;
|
|
1415
|
+
entry.ok = response.ok;
|
|
1416
|
+
entry.responseUrl = response.url;
|
|
1417
|
+
entry.durationMs = Date.now() - startedAt;
|
|
1418
|
+
entry.responseHeaders = Array.from(response.headers.entries());
|
|
1419
|
+
response.clone().text().then(function(text) {
|
|
1420
|
+
entry.responseBody = trimBody(text);
|
|
1421
|
+
entry.responseBodyTruncated = typeof text === "string" && text.length > 200000;
|
|
1422
|
+
}).catch(function(error) { entry.responseBodyError = error ? error.message : String(error); });
|
|
1423
|
+
return response;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
entry.error = error ? error.message : String(error);
|
|
1426
|
+
entry.durationMs = Date.now() - startedAt;
|
|
1427
|
+
throw error;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
wrappedFetch.__piChromeWrapped = true;
|
|
1431
|
+
window.fetch = wrappedFetch;
|
|
1432
|
+
}
|
|
1433
|
+
if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) {
|
|
1434
|
+
var originalOpen = XMLHttpRequest.prototype.open;
|
|
1435
|
+
var originalSend = XMLHttpRequest.prototype.send;
|
|
1436
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
1437
|
+
this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") };
|
|
1438
|
+
return originalOpen.apply(this, arguments);
|
|
1439
|
+
};
|
|
1440
|
+
XMLHttpRequest.prototype.open.__piChromeWrapped = true;
|
|
1441
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
1442
|
+
var id = "req-" + state.nextRequestId++;
|
|
1443
|
+
var startedAt = Date.now();
|
|
1444
|
+
var info = this.__piChromeRequest || {};
|
|
1445
|
+
var entry = record({ id: id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt: startedAt, pageUrl: location.href, status: "pending" });
|
|
1446
|
+
this.addEventListener("loadend", function() {
|
|
1447
|
+
entry.status = this.status;
|
|
1448
|
+
entry.statusText = this.statusText;
|
|
1449
|
+
entry.responseUrl = this.responseURL;
|
|
1450
|
+
entry.durationMs = Date.now() - startedAt;
|
|
1451
|
+
try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch (e) {}
|
|
1452
|
+
try {
|
|
1453
|
+
if (typeof this.responseText === "string") {
|
|
1454
|
+
entry.responseBody = trimBody(this.responseText);
|
|
1455
|
+
entry.responseBodyTruncated = this.responseText.length > 200000;
|
|
1456
|
+
}
|
|
1457
|
+
} catch (error) { entry.responseBodyError = error ? error.message : String(error); }
|
|
1458
|
+
});
|
|
1459
|
+
this.addEventListener("error", function() { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; });
|
|
1460
|
+
return originalSend.apply(this, arguments);
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
state.instrumentationInstalled = true;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1313
1466
|
function snapshotPage(maxElements, containingText, roleFilter, nearUid) {
|
|
1314
1467
|
installPiChromeInstrumentation();
|
|
1315
1468
|
const unique = (selector) => {
|
|
@@ -237,32 +237,58 @@ class ChromeProfileBridge {
|
|
|
237
237
|
this.mode = undefined;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
send(action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<unknown> {
|
|
241
|
-
if (this.mode === "client") return this.sendViaOwner(action, params, timeoutMs);
|
|
242
|
-
return this.sendLocal(action, params, timeoutMs);
|
|
240
|
+
send(action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> {
|
|
241
|
+
if (this.mode === "client") return this.sendViaOwner(action, params, timeoutMs, signal);
|
|
242
|
+
return this.sendLocal(action, params, timeoutMs, signal);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
private sendLocal(action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<unknown> {
|
|
245
|
+
private sendLocal(action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> {
|
|
246
246
|
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
247
247
|
const command = { id, action, params };
|
|
248
248
|
return new Promise((resolveCommand, rejectCommand) => {
|
|
249
|
+
if (signal?.aborted) {
|
|
250
|
+
rejectCommand(new Error("Chrome command aborted"));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const cleanupAbort = () => {
|
|
254
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
255
|
+
};
|
|
256
|
+
const onAbort = () => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
this.pending.delete(id);
|
|
259
|
+
this.queue = this.queue.filter((queued) => queued.id !== id);
|
|
260
|
+
cleanupAbort();
|
|
261
|
+
rejectCommand(new Error("Chrome command aborted"));
|
|
262
|
+
};
|
|
249
263
|
const timer = setTimeout(() => {
|
|
250
264
|
this.pending.delete(id);
|
|
251
265
|
this.queue = this.queue.filter((queued) => queued.id !== id);
|
|
266
|
+
cleanupAbort();
|
|
252
267
|
rejectCommand(
|
|
253
268
|
new Error(
|
|
254
269
|
`Timed out waiting for Chrome extension after ${timeoutMs}ms. Run /chrome onboard, then load the bundled browser-extension folder in your normal Chrome profile.`,
|
|
255
270
|
),
|
|
256
271
|
);
|
|
257
272
|
}, timeoutMs);
|
|
258
|
-
this.pending.set(id, {
|
|
273
|
+
this.pending.set(id, {
|
|
274
|
+
command,
|
|
275
|
+
resolve: (value) => { cleanupAbort(); resolveCommand(value); },
|
|
276
|
+
reject: (err) => { cleanupAbort(); rejectCommand(err); },
|
|
277
|
+
timer,
|
|
278
|
+
});
|
|
279
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
259
280
|
this.enqueue(command);
|
|
260
281
|
});
|
|
261
282
|
}
|
|
262
283
|
|
|
263
|
-
private async sendViaOwner(action: string, params: Record<string, unknown>, timeoutMs: number): Promise<unknown> {
|
|
284
|
+
private async sendViaOwner(action: string, params: Record<string, unknown>, timeoutMs: number, signal?: AbortSignal): Promise<unknown> {
|
|
264
285
|
const controller = new AbortController();
|
|
265
286
|
const timer = setTimeout(() => controller.abort(), timeoutMs + 2_000);
|
|
287
|
+
const forwardAbort = () => controller.abort();
|
|
288
|
+
if (signal) {
|
|
289
|
+
if (signal.aborted) controller.abort();
|
|
290
|
+
else signal.addEventListener("abort", forwardAbort, { once: true });
|
|
291
|
+
}
|
|
266
292
|
try {
|
|
267
293
|
const response = await fetch(`${this.url}/command`, {
|
|
268
294
|
method: "POST",
|
|
@@ -280,11 +306,13 @@ class ChromeProfileBridge {
|
|
|
280
306
|
return payload.result;
|
|
281
307
|
} catch (error) {
|
|
282
308
|
if ((error as Error).name === "AbortError") {
|
|
309
|
+
if (signal?.aborted) throw new Error("Chrome command aborted");
|
|
283
310
|
throw new Error(`Timed out waiting for shared Chrome bridge owner after ${timeoutMs}ms`);
|
|
284
311
|
}
|
|
285
312
|
throw error;
|
|
286
313
|
} finally {
|
|
287
314
|
clearTimeout(timer);
|
|
315
|
+
if (signal) signal.removeEventListener("abort", forwardAbort);
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
|
|
@@ -419,8 +447,9 @@ function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
|
|
|
419
447
|
}
|
|
420
448
|
|
|
421
449
|
export default function (pi: ExtensionAPI): void {
|
|
450
|
+
const instanceToken = Symbol("pi-chrome-instance");
|
|
422
451
|
const globalState = globalThis as typeof globalThis & {
|
|
423
|
-
[PI_CHROME_GLOBAL_KEY]?: { version: string; root: string };
|
|
452
|
+
[PI_CHROME_GLOBAL_KEY]?: { version: string; root: string; token: symbol };
|
|
424
453
|
};
|
|
425
454
|
const alreadyLoaded = globalState[PI_CHROME_GLOBAL_KEY];
|
|
426
455
|
if (alreadyLoaded) {
|
|
@@ -429,11 +458,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
429
458
|
);
|
|
430
459
|
return;
|
|
431
460
|
}
|
|
432
|
-
globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: extensionRoot() };
|
|
461
|
+
globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: extensionRoot(), token: instanceToken };
|
|
433
462
|
|
|
434
463
|
const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
|
|
435
464
|
let backgroundDefault = false;
|
|
436
465
|
let chromeAuthorizedUntil: number | "indefinite" | undefined;
|
|
466
|
+
let chromeToolsRegistered = false;
|
|
437
467
|
|
|
438
468
|
const authSummary = (): string => {
|
|
439
469
|
if (chromeAuthorizedUntil === "indefinite") return "authorized indefinitely";
|
|
@@ -457,9 +487,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
457
487
|
}
|
|
458
488
|
};
|
|
459
489
|
|
|
460
|
-
const
|
|
490
|
+
const updateChromeStatus = (ctx: ExtensionContext): void => {
|
|
491
|
+
if (chromeControlAuthorized()) {
|
|
492
|
+
ctx.ui.setStatus("chrome", ctx.ui.theme.fg("success", "●") + " Chrome Bridge");
|
|
493
|
+
} else {
|
|
494
|
+
ctx.ui.setStatus("chrome", undefined);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
|
|
461
499
|
requireChromeControlAuthorized();
|
|
462
|
-
return bridge.send(action, params, timeoutMs);
|
|
500
|
+
return bridge.send(action, params, timeoutMs, signal);
|
|
463
501
|
};
|
|
464
502
|
|
|
465
503
|
// Translate the public `background` parameter (default false = visible/foreground) into the
|
|
@@ -478,21 +516,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
478
516
|
|
|
479
517
|
pi.on("session_start", async (_event, ctx) => {
|
|
480
518
|
await bridge.start();
|
|
481
|
-
|
|
482
|
-
ctx.ui.setStatus("chrome", `Chrome bridge :${DEFAULT_PORT}`);
|
|
483
|
-
ctx.ui.notify(
|
|
484
|
-
status.mode === "client"
|
|
485
|
-
? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened). Run /chrome authorize before using chrome_* tools.`
|
|
486
|
-
: `pi-chrome is ready and waiting for the Chrome companion to connect. Run /chrome onboard to install it, then /chrome authorize to allow chrome_* tools.`,
|
|
487
|
-
"info",
|
|
488
|
-
);
|
|
519
|
+
updateChromeStatus(ctx);
|
|
489
520
|
});
|
|
490
521
|
|
|
491
522
|
pi.on("session_shutdown", () => {
|
|
492
523
|
bridge.stop();
|
|
524
|
+
if (globalState[PI_CHROME_GLOBAL_KEY]?.token === instanceToken) {
|
|
525
|
+
delete globalState[PI_CHROME_GLOBAL_KEY];
|
|
526
|
+
}
|
|
493
527
|
});
|
|
494
528
|
|
|
495
529
|
pi.on("before_agent_start", (event) => {
|
|
530
|
+
if (!chromeToolsRegistered || !chromeControlAuthorized()) {
|
|
531
|
+
return { systemPrompt: event.systemPrompt };
|
|
532
|
+
}
|
|
496
533
|
const primer = `
|
|
497
534
|
<chrome-profile-bridge>
|
|
498
535
|
Chrome control is available through the chrome_* tools via a companion Chrome extension installed in the user's normal Chrome profile. Tools target the existing signed-in profile: no remote-debug port, no throwaway profile.
|
|
@@ -616,8 +653,10 @@ Usage rules:
|
|
|
616
653
|
ctx.ui.notify("Chrome control remains locked.", "info");
|
|
617
654
|
return;
|
|
618
655
|
}
|
|
656
|
+
registerChromeTools(pi);
|
|
619
657
|
chromeAuthorizedUntil = until;
|
|
620
658
|
ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
|
|
659
|
+
updateChromeStatus(ctx);
|
|
621
660
|
};
|
|
622
661
|
|
|
623
662
|
const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
|
|
@@ -640,6 +679,7 @@ Usage rules:
|
|
|
640
679
|
const revokeHandler = (ctx: ExtensionContext) => {
|
|
641
680
|
chromeAuthorizedUntil = undefined;
|
|
642
681
|
ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info");
|
|
682
|
+
updateChromeStatus(ctx);
|
|
643
683
|
};
|
|
644
684
|
|
|
645
685
|
const onboardHandler = async (ctx: ExtensionContext) => {
|
|
@@ -816,6 +856,10 @@ Usage rules:
|
|
|
816
856
|
},
|
|
817
857
|
});
|
|
818
858
|
|
|
859
|
+
function registerChromeTools(pi: ExtensionAPI): void {
|
|
860
|
+
if (chromeToolsRegistered) return;
|
|
861
|
+
chromeToolsRegistered = true;
|
|
862
|
+
|
|
819
863
|
pi.registerTool({
|
|
820
864
|
name: "chrome_launch",
|
|
821
865
|
label: "Chrome Bridge Setup",
|
|
@@ -829,9 +873,9 @@ Usage rules:
|
|
|
829
873
|
useDefaultProfile: Type.Optional(Type.Boolean({ description: "Ignored; existing-profile access comes from the companion Chrome extension." })),
|
|
830
874
|
headless: Type.Optional(Type.Boolean({ description: "Ignored." })),
|
|
831
875
|
}),
|
|
832
|
-
async execute(_id, params,
|
|
876
|
+
async execute(_id, params, signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
833
877
|
if (params.url && bridge.connected) {
|
|
834
|
-
const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS);
|
|
878
|
+
const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS, signal);
|
|
835
879
|
return { content: [{ type: "text", text: `Chrome bridge connected; opened ${params.url}` }], details: { status: bridge.status(), result } };
|
|
836
880
|
}
|
|
837
881
|
return {
|
|
@@ -865,8 +909,8 @@ Usage rules:
|
|
|
865
909
|
host: Type.Optional(Type.String()),
|
|
866
910
|
port: Type.Optional(Type.Number()),
|
|
867
911
|
}),
|
|
868
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
869
|
-
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS);
|
|
912
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
913
|
+
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS, signal);
|
|
870
914
|
if (params.action === "list") {
|
|
871
915
|
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number }>;
|
|
872
916
|
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
|
@@ -896,11 +940,12 @@ Usage rules:
|
|
|
896
940
|
host: Type.Optional(Type.String()),
|
|
897
941
|
port: Type.Optional(Type.Number()),
|
|
898
942
|
}),
|
|
899
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
943
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
900
944
|
const snapshot = await authorizedBridgeSend(
|
|
901
945
|
"page.snapshot",
|
|
902
946
|
withBackground({ ...params, maxElements: params.maxElements ?? MAX_ELEMENTS }),
|
|
903
947
|
DEFAULT_TIMEOUT_MS,
|
|
948
|
+
signal,
|
|
904
949
|
);
|
|
905
950
|
return { content: [{ type: "text", text: truncateText(safeJson(snapshot)) }], details: { snapshot } };
|
|
906
951
|
},
|
|
@@ -926,8 +971,8 @@ Usage rules:
|
|
|
926
971
|
host: Type.Optional(Type.String()),
|
|
927
972
|
port: Type.Optional(Type.Number()),
|
|
928
973
|
}),
|
|
929
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
930
|
-
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000);
|
|
974
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
975
|
+
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000, signal);
|
|
931
976
|
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
|
|
932
977
|
},
|
|
933
978
|
});
|
|
@@ -950,8 +995,8 @@ Usage rules:
|
|
|
950
995
|
host: Type.Optional(Type.String()),
|
|
951
996
|
port: Type.Optional(Type.Number()),
|
|
952
997
|
}),
|
|
953
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
954
|
-
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
998
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
999
|
+
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
955
1000
|
const text = value === undefined
|
|
956
1001
|
? "undefined"
|
|
957
1002
|
: typeof value === "string"
|
|
@@ -983,8 +1028,8 @@ Usage rules:
|
|
|
983
1028
|
host: Type.Optional(Type.String()),
|
|
984
1029
|
port: Type.Optional(Type.Number()),
|
|
985
1030
|
}),
|
|
986
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
987
|
-
const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1031
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1032
|
+
const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
988
1033
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
989
1034
|
const summary = summarizeActionResult(result);
|
|
990
1035
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
@@ -1015,8 +1060,8 @@ Usage rules:
|
|
|
1015
1060
|
host: Type.Optional(Type.String()),
|
|
1016
1061
|
port: Type.Optional(Type.Number()),
|
|
1017
1062
|
}),
|
|
1018
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1019
|
-
const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1063
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1064
|
+
const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1020
1065
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1021
1066
|
const summary = summarizeActionResult(result);
|
|
1022
1067
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -1047,8 +1092,8 @@ Usage rules:
|
|
|
1047
1092
|
host: Type.Optional(Type.String()),
|
|
1048
1093
|
port: Type.Optional(Type.Number()),
|
|
1049
1094
|
}),
|
|
1050
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1051
|
-
const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1095
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1096
|
+
const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1052
1097
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1053
1098
|
const summary = summarizeActionResult(result);
|
|
1054
1099
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -1082,8 +1127,8 @@ Usage rules:
|
|
|
1082
1127
|
host: Type.Optional(Type.String()),
|
|
1083
1128
|
port: Type.Optional(Type.Number()),
|
|
1084
1129
|
}),
|
|
1085
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1086
|
-
const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1130
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1131
|
+
const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1087
1132
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
1088
1133
|
const summary = summarizeActionResult(result);
|
|
1089
1134
|
const base = `Pressed ${params.key}.`;
|
|
@@ -1107,8 +1152,8 @@ Usage rules:
|
|
|
1107
1152
|
host: Type.Optional(Type.String()),
|
|
1108
1153
|
port: Type.Optional(Type.Number()),
|
|
1109
1154
|
}),
|
|
1110
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1111
|
-
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000);
|
|
1155
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1156
|
+
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000, signal);
|
|
1112
1157
|
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
|
|
1113
1158
|
},
|
|
1114
1159
|
});
|
|
@@ -1128,8 +1173,8 @@ Usage rules:
|
|
|
1128
1173
|
host: Type.Optional(Type.String()),
|
|
1129
1174
|
port: Type.Optional(Type.Number()),
|
|
1130
1175
|
}),
|
|
1131
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1132
|
-
const result = await authorizedBridgeSend("page.console.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1176
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1177
|
+
const result = await authorizedBridgeSend("page.console.list", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1133
1178
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1134
1179
|
},
|
|
1135
1180
|
});
|
|
@@ -1150,8 +1195,8 @@ Usage rules:
|
|
|
1150
1195
|
host: Type.Optional(Type.String()),
|
|
1151
1196
|
port: Type.Optional(Type.Number()),
|
|
1152
1197
|
}),
|
|
1153
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1154
|
-
const result = await authorizedBridgeSend("page.network.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1198
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1199
|
+
const result = await authorizedBridgeSend("page.network.list", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1155
1200
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1156
1201
|
},
|
|
1157
1202
|
});
|
|
@@ -1170,8 +1215,8 @@ Usage rules:
|
|
|
1170
1215
|
host: Type.Optional(Type.String()),
|
|
1171
1216
|
port: Type.Optional(Type.Number()),
|
|
1172
1217
|
}),
|
|
1173
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1174
|
-
const result = await authorizedBridgeSend("page.network.get", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1218
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1219
|
+
const result = await authorizedBridgeSend("page.network.get", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1175
1220
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1176
1221
|
},
|
|
1177
1222
|
});
|
|
@@ -1196,12 +1241,12 @@ Usage rules:
|
|
|
1196
1241
|
host: Type.Optional(Type.String()),
|
|
1197
1242
|
port: Type.Optional(Type.Number()),
|
|
1198
1243
|
}),
|
|
1199
|
-
async execute(_id, params,
|
|
1244
|
+
async execute(_id, params, signal, _onUpdate, ctx: ExtensionContext): Promise<ToolTextResult> {
|
|
1200
1245
|
const format = params.format ?? "png";
|
|
1201
1246
|
const cwd = workspaceCwd(ctx);
|
|
1202
1247
|
const defaultPath = join(cwd, ".pi", "chrome-screenshots", `${new Date().toISOString().replace(/[:.]/g, "-")}.${format}`);
|
|
1203
1248
|
const outputPath = params.path ? resolve(cwd, params.path) : defaultPath;
|
|
1204
|
-
const result = (await authorizedBridgeSend("page.screenshot", withBackground(params), params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS)) as {
|
|
1249
|
+
const result = (await authorizedBridgeSend("page.screenshot", withBackground(params), params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS, signal)) as {
|
|
1205
1250
|
dataUrl?: string;
|
|
1206
1251
|
tab?: unknown;
|
|
1207
1252
|
fullPage?: boolean;
|
|
@@ -1250,8 +1295,8 @@ Usage rules:
|
|
|
1250
1295
|
titleIncludes: Type.Optional(Type.String()),
|
|
1251
1296
|
background: Type.Optional(Type.Boolean()),
|
|
1252
1297
|
}),
|
|
1253
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1254
|
-
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1298
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1299
|
+
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1255
1300
|
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
|
|
1256
1301
|
},
|
|
1257
1302
|
});
|
|
@@ -1276,8 +1321,8 @@ Usage rules:
|
|
|
1276
1321
|
titleIncludes: Type.Optional(Type.String()),
|
|
1277
1322
|
background: Type.Optional(Type.Boolean()),
|
|
1278
1323
|
}),
|
|
1279
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1280
|
-
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1324
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1325
|
+
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1281
1326
|
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
|
|
1282
1327
|
},
|
|
1283
1328
|
});
|
|
@@ -1298,8 +1343,8 @@ Usage rules:
|
|
|
1298
1343
|
titleIncludes: Type.Optional(Type.String()),
|
|
1299
1344
|
background: Type.Optional(Type.Boolean()),
|
|
1300
1345
|
}),
|
|
1301
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1302
|
-
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1346
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1347
|
+
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1303
1348
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
1304
1349
|
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
|
|
1305
1350
|
},
|
|
@@ -1321,8 +1366,8 @@ Usage rules:
|
|
|
1321
1366
|
titleIncludes: Type.Optional(Type.String()),
|
|
1322
1367
|
background: Type.Optional(Type.Boolean()),
|
|
1323
1368
|
}),
|
|
1324
|
-
async execute(_id, params): Promise<ToolTextResult> {
|
|
1325
|
-
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1369
|
+
async execute(_id, params, signal): Promise<ToolTextResult> {
|
|
1370
|
+
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
|
1326
1371
|
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
|
|
1327
1372
|
},
|
|
1328
1373
|
});
|
|
@@ -1341,11 +1386,13 @@ Usage rules:
|
|
|
1341
1386
|
titleIncludes: Type.Optional(Type.String()),
|
|
1342
1387
|
background: Type.Optional(Type.Boolean()),
|
|
1343
1388
|
}),
|
|
1344
|
-
async execute(_id, params,
|
|
1389
|
+
async execute(_id, params, signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
1345
1390
|
const cwd = workspaceCwd(ctx);
|
|
1346
1391
|
const paths = params.paths.map((p) => resolve(cwd, p));
|
|
1347
|
-
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS);
|
|
1392
|
+
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS, signal);
|
|
1348
1393
|
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
|
|
1349
1394
|
},
|
|
1350
1395
|
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1351
1398
|
}
|