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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.19",
4
+ "version": "0.15.22",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -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, { command, resolve: resolveCommand, reject: rejectCommand, timer });
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 authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<unknown> => {
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
- const status = bridge.status();
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, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
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, _signal, _onUpdate, ctx: ExtensionContext): Promise<ToolTextResult> {
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, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.19",
3
+ "version": "0.15.22",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"