@btraut/browser-bridge 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -418,8 +418,19 @@ var DEBUGGER_PROTOCOL_VERSION = "1.3";
418
418
  var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
419
419
  var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
420
420
  var DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS = 1e4;
421
+ var DEFAULT_SEND_TO_TAB_TIMEOUT_MS = 1e4;
422
+ var HISTORY_DISPATCH_TIMEOUT_MS = 2e3;
423
+ var HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS = 8e3;
424
+ var HISTORY_POST_NAV_DOM_GRACE_TIMEOUT_MS = 2e3;
421
425
  var AGENT_TAB_ID_KEY = "agentTabId";
422
426
  var AGENT_TAB_GROUP_TITLE = "\u{1F309} Browser Bridge";
427
+ var AGENT_TAB_FAVICON_ASSET_PATH = "assets/icons/icon-32.png";
428
+ var getAgentTabBootstrapUrl = () => {
429
+ const faviconUrl = typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_FAVICON_ASSET_PATH) : AGENT_TAB_FAVICON_ASSET_PATH;
430
+ return `data:text/html;charset=UTF-8,${encodeURIComponent(
431
+ `<!doctype html><html><head><meta charset="utf-8"><title>${AGENT_TAB_GROUP_TITLE}</title><link rel="icon" type="image/png" href="${faviconUrl}"></head><body></body></html>`
432
+ )}`;
433
+ };
423
434
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
424
435
  var makeEventId = /* @__PURE__ */ (() => {
425
436
  let counter = 0;
@@ -471,6 +482,80 @@ var delayMs = async (ms) => {
471
482
  self.setTimeout(resolve, ms);
472
483
  });
473
484
  };
485
+ var CAPTURE_VISIBLE_TAB_MIN_INTERVAL_MS = 400;
486
+ var CAPTURE_VISIBLE_TAB_MAX_RETRIES = 3;
487
+ var CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS = 500;
488
+ var captureVisibleTabQueue = Promise.resolve();
489
+ var captureVisibleTabLastCallAt = 0;
490
+ var isCaptureVisibleTabRateLimitedMessage = (message) => {
491
+ const normalized = message.toLowerCase();
492
+ const hasCaptureSignal = normalized.includes("capturevisibletab") || normalized.includes("max_capture_visible_tab_calls_per_second");
493
+ const hasRateSignal = normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
494
+ return hasCaptureSignal && hasRateSignal;
495
+ };
496
+ var isCaptureVisibleTabRateLimitedError = (error) => {
497
+ return error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
498
+ };
499
+ var randomJitterMs = (maxMs) => {
500
+ return Math.floor(Math.random() * Math.max(1, maxMs));
501
+ };
502
+ var runCaptureVisibleTabOperation = async (operation) => {
503
+ const previous = captureVisibleTabQueue;
504
+ let releaseQueue;
505
+ captureVisibleTabQueue = new Promise((resolve) => {
506
+ releaseQueue = resolve;
507
+ });
508
+ await previous.catch(() => void 0);
509
+ try {
510
+ return await operation();
511
+ } finally {
512
+ releaseQueue?.();
513
+ }
514
+ };
515
+ var captureVisibleTabWithThrottle = async (windowId) => {
516
+ return await runCaptureVisibleTabOperation(async () => {
517
+ for (let attempt = 0; attempt <= CAPTURE_VISIBLE_TAB_MAX_RETRIES; attempt += 1) {
518
+ const waitMs = captureVisibleTabLastCallAt + CAPTURE_VISIBLE_TAB_MIN_INTERVAL_MS - Date.now();
519
+ if (waitMs > 0) {
520
+ await delayMs(waitMs);
521
+ }
522
+ try {
523
+ const dataUrl = await wrapChromeCallback(
524
+ (callback) => chrome.tabs.captureVisibleTab(windowId, { format: "png" }, callback)
525
+ );
526
+ captureVisibleTabLastCallAt = Date.now();
527
+ return dataUrl;
528
+ } catch (error) {
529
+ captureVisibleTabLastCallAt = Date.now();
530
+ if (!isCaptureVisibleTabRateLimitedError(error) || attempt >= CAPTURE_VISIBLE_TAB_MAX_RETRIES) {
531
+ throw error;
532
+ }
533
+ const backoffMs = CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS * (attempt + 1) + randomJitterMs(120);
534
+ await delayMs(backoffMs);
535
+ }
536
+ }
537
+ throw new Error("captureVisibleTab failed unexpectedly.");
538
+ });
539
+ };
540
+ var mapScreenshotCaptureError = (error, fallbackMessage) => {
541
+ const message = error instanceof Error && error.message ? error.message : fallbackMessage;
542
+ if (isCaptureVisibleTabRateLimitedError(error)) {
543
+ return {
544
+ code: "RATE_LIMITED",
545
+ message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
546
+ retryable: true,
547
+ details: {
548
+ reason: "capture_visible_tab_rate_limited",
549
+ original_message: message
550
+ }
551
+ };
552
+ }
553
+ return {
554
+ code: "ARTIFACT_IO_ERROR",
555
+ message,
556
+ retryable: false
557
+ };
558
+ };
474
559
  var parseDataUrl = (dataUrl) => {
475
560
  const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
476
561
  if (!match) {
@@ -722,7 +807,10 @@ var ensureAgentTabGroup = async (tabId, windowId) => {
722
807
  };
723
808
  var createAgentWindow = async () => {
724
809
  const created = await wrapChromeCallback(
725
- (callback) => chrome.windows.create({ url: "about:blank", focused: true }, callback)
810
+ (callback) => chrome.windows.create(
811
+ { url: getAgentTabBootstrapUrl(), focused: true },
812
+ callback
813
+ )
726
814
  );
727
815
  const windowId = created.id;
728
816
  if (typeof windowId !== "number") {
@@ -809,14 +897,42 @@ var getDefaultTabId = async () => {
809
897
  return await getActiveTabId();
810
898
  }
811
899
  };
812
- var sendToTab = async (tabId, action, params) => {
900
+ var sendToTab = async (tabId, action, params, options) => {
901
+ const timeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? Math.max(1, Math.floor(options.timeoutMs)) : DEFAULT_SEND_TO_TAB_TIMEOUT_MS;
813
902
  const attemptSend = async () => {
814
903
  return await new Promise((resolve) => {
815
904
  const message = { action, params };
905
+ let settled = false;
906
+ const finish = (result) => {
907
+ if (settled) {
908
+ return;
909
+ }
910
+ settled = true;
911
+ if (timeout !== void 0) {
912
+ clearTimeout(timeout);
913
+ }
914
+ resolve(result);
915
+ };
916
+ let timeout;
917
+ timeout = self.setTimeout(() => {
918
+ finish({
919
+ ok: false,
920
+ error: {
921
+ code: "TIMEOUT",
922
+ message: `Timed out waiting for content response after ${timeoutMs}ms.`,
923
+ retryable: true,
924
+ details: {
925
+ action,
926
+ tab_id: tabId,
927
+ timeout_ms: timeoutMs
928
+ }
929
+ }
930
+ });
931
+ }, timeoutMs);
816
932
  chrome.tabs.sendMessage(tabId, message, (response) => {
817
933
  const error = chrome.runtime.lastError;
818
934
  if (error) {
819
- resolve({
935
+ finish({
820
936
  ok: false,
821
937
  error: {
822
938
  code: "EVALUATION_FAILED",
@@ -827,7 +943,7 @@ var sendToTab = async (tabId, action, params) => {
827
943
  return;
828
944
  }
829
945
  if (!response || typeof response !== "object") {
830
- resolve({
946
+ finish({
831
947
  ok: false,
832
948
  error: {
833
949
  code: "EVALUATION_FAILED",
@@ -837,7 +953,7 @@ var sendToTab = async (tabId, action, params) => {
837
953
  });
838
954
  return;
839
955
  }
840
- resolve(response);
956
+ finish(response);
841
957
  });
842
958
  });
843
959
  };
@@ -863,6 +979,67 @@ var sendToTab = async (tabId, action, params) => {
863
979
  }
864
980
  };
865
981
  };
982
+ var waitForHistoryNavigationSignal = async (tabId, timeoutMs) => {
983
+ return await new Promise((resolve, reject) => {
984
+ let timeout;
985
+ const cleanup = () => {
986
+ if (timeout !== void 0) {
987
+ clearTimeout(timeout);
988
+ }
989
+ chrome.webNavigation.onCommitted.removeListener(onCommitted);
990
+ chrome.webNavigation.onHistoryStateUpdated.removeListener(
991
+ onHistoryStateUpdated
992
+ );
993
+ chrome.webNavigation.onReferenceFragmentUpdated.removeListener(
994
+ onReferenceFragmentUpdated
995
+ );
996
+ chrome.tabs.onUpdated.removeListener(onTabUpdated);
997
+ };
998
+ const resolveSignal = () => {
999
+ cleanup();
1000
+ resolve();
1001
+ };
1002
+ const onCommitted = (details) => {
1003
+ if (details.tabId !== tabId || details.frameId !== 0) {
1004
+ return;
1005
+ }
1006
+ resolveSignal();
1007
+ };
1008
+ const onHistoryStateUpdated = (details) => {
1009
+ if (details.tabId !== tabId || details.frameId !== 0) {
1010
+ return;
1011
+ }
1012
+ resolveSignal();
1013
+ };
1014
+ const onReferenceFragmentUpdated = (details) => {
1015
+ if (details.tabId !== tabId || details.frameId !== 0) {
1016
+ return;
1017
+ }
1018
+ resolveSignal();
1019
+ };
1020
+ const onTabUpdated = (updatedTabId, changeInfo) => {
1021
+ if (updatedTabId !== tabId) {
1022
+ return;
1023
+ }
1024
+ if (typeof changeInfo.url !== "string" || changeInfo.url.length === 0) {
1025
+ return;
1026
+ }
1027
+ resolveSignal();
1028
+ };
1029
+ chrome.webNavigation.onCommitted.addListener(onCommitted);
1030
+ chrome.webNavigation.onHistoryStateUpdated.addListener(
1031
+ onHistoryStateUpdated
1032
+ );
1033
+ chrome.webNavigation.onReferenceFragmentUpdated.addListener(
1034
+ onReferenceFragmentUpdated
1035
+ );
1036
+ chrome.tabs.onUpdated.addListener(onTabUpdated);
1037
+ timeout = self.setTimeout(() => {
1038
+ cleanup();
1039
+ reject(new Error("Timed out waiting for history navigation signal."));
1040
+ }, timeoutMs);
1041
+ });
1042
+ };
866
1043
  var waitForDomContentLoaded = async (tabId, timeoutMs) => {
867
1044
  return await new Promise((resolve, reject) => {
868
1045
  let timeout;
@@ -1313,21 +1490,37 @@ var DriveSocket = class {
1313
1490
  if (tabId === void 0) {
1314
1491
  tabId = await getDefaultTabId();
1315
1492
  }
1316
- const result = await sendToTab(tabId, message.action);
1317
- if (!result.ok) {
1493
+ const navigationSignal = waitForHistoryNavigationSignal(
1494
+ tabId,
1495
+ HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS
1496
+ );
1497
+ const result = await sendToTab(
1498
+ tabId,
1499
+ message.action,
1500
+ void 0,
1501
+ {
1502
+ timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
1503
+ }
1504
+ );
1505
+ if (!result.ok && result.error.code !== "TIMEOUT") {
1318
1506
  respondError(result.error);
1319
1507
  return;
1320
1508
  }
1321
1509
  markTabActive(tabId);
1322
1510
  try {
1323
- await waitForDomContentLoaded(tabId, 3e4);
1324
- } catch (error) {
1325
- respondError({
1326
- code: "TIMEOUT",
1327
- message: error instanceof Error ? error.message : "Timed out waiting.",
1328
- retryable: true
1329
- });
1330
- return;
1511
+ await navigationSignal;
1512
+ try {
1513
+ await waitForDomContentLoaded(
1514
+ tabId,
1515
+ HISTORY_POST_NAV_DOM_GRACE_TIMEOUT_MS
1516
+ );
1517
+ } catch {
1518
+ }
1519
+ } catch {
1520
+ if (!result.ok) {
1521
+ respondError(result.error);
1522
+ return;
1523
+ }
1331
1524
  }
1332
1525
  respondOk({ ok: true });
1333
1526
  return;
@@ -1901,10 +2094,14 @@ var DriveSocket = class {
1901
2094
  if (tabId === void 0) {
1902
2095
  tabId = await getDefaultTabId();
1903
2096
  }
2097
+ const timeoutMs = message.action === "drive.wait_for" && typeof params.timeout_ms === "number" && Number.isFinite(params.timeout_ms) ? Math.max(1, Math.floor(params.timeout_ms) + 1e3) : void 0;
1904
2098
  const result = await sendToTab(
1905
2099
  tabId,
1906
2100
  message.action,
1907
- params
2101
+ params,
2102
+ {
2103
+ timeoutMs
2104
+ }
1908
2105
  );
1909
2106
  if (result.ok) {
1910
2107
  respondOk(result.result ?? { ok: true });
@@ -1959,13 +2156,7 @@ var DriveSocket = class {
1959
2156
  return;
1960
2157
  }
1961
2158
  const captureVisible = async () => {
1962
- return await wrapChromeCallback(
1963
- (callback) => chrome.tabs.captureVisibleTab(
1964
- windowId,
1965
- { format: "png" },
1966
- callback
1967
- )
1968
- );
2159
+ return await captureVisibleTabWithThrottle(windowId);
1969
2160
  };
1970
2161
  const scrollTo = async (top, left) => {
1971
2162
  const result = await sendToTab(tabId, "drive.scroll", {
@@ -2111,11 +2302,12 @@ var DriveSocket = class {
2111
2302
  );
2112
2303
  respondOk(rendered);
2113
2304
  } catch (error) {
2114
- respondError({
2115
- code: "ARTIFACT_IO_ERROR",
2116
- message: error instanceof Error ? error.message : "Failed to capture screenshot.",
2117
- retryable: false
2118
- });
2305
+ respondError(
2306
+ mapScreenshotCaptureError(
2307
+ error,
2308
+ "Failed to capture screenshot."
2309
+ )
2310
+ );
2119
2311
  }
2120
2312
  return;
2121
2313
  }
@@ -2247,11 +2439,12 @@ var DriveSocket = class {
2247
2439
  );
2248
2440
  respondOk(await canvasToResult(cropCanvas));
2249
2441
  } catch (error) {
2250
- respondError({
2251
- code: "ARTIFACT_IO_ERROR",
2252
- message: error instanceof Error ? error.message : "Failed to capture element screenshot.",
2253
- retryable: false
2254
- });
2442
+ respondError(
2443
+ mapScreenshotCaptureError(
2444
+ error,
2445
+ "Failed to capture element screenshot."
2446
+ )
2447
+ );
2255
2448
  } finally {
2256
2449
  try {
2257
2450
  await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
@@ -2265,11 +2458,12 @@ var DriveSocket = class {
2265
2458
  const canvas = await captureFullPageCanvas(metaInfo);
2266
2459
  respondOk(await canvasToResult(canvas));
2267
2460
  } catch (error) {
2268
- respondError({
2269
- code: "ARTIFACT_IO_ERROR",
2270
- message: error instanceof Error ? error.message : "Failed to capture full page screenshot.",
2271
- retryable: false
2272
- });
2461
+ respondError(
2462
+ mapScreenshotCaptureError(
2463
+ error,
2464
+ "Failed to capture full page screenshot."
2465
+ )
2466
+ );
2273
2467
  }
2274
2468
  return;
2275
2469
  }