@btraut/browser-bridge 0.11.0 → 0.12.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.
@@ -36,7 +36,7 @@ body.bb-page {
36
36
  body.bb-page.bb-page--popup {
37
37
  height: auto;
38
38
  padding: 10px 10px 12px;
39
- min-width: 260px;
39
+ min-width: 300px;
40
40
  background: var(--bb-surface);
41
41
  }
42
42
 
@@ -68,6 +68,113 @@ body.bb-page.bb-page--popup {
68
68
  padding: 2px;
69
69
  }
70
70
 
71
+ .bb-health {
72
+ border: 1px solid var(--bb-border-2);
73
+ border-radius: var(--bb-radius-sm);
74
+ background: var(--bb-bg);
75
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
76
+ padding: 10px 12px;
77
+ margin: 0 0 10px;
78
+ }
79
+
80
+ .bb-health-head {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ margin-bottom: 8px;
85
+ }
86
+
87
+ .bb-health-label {
88
+ font-size: 12px;
89
+ font-weight: 700;
90
+ letter-spacing: 0.04em;
91
+ color: var(--bb-ink-2);
92
+ text-transform: uppercase;
93
+ }
94
+
95
+ .bb-health-state {
96
+ border: 1px solid var(--bb-border);
97
+ border-radius: 999px;
98
+ padding: 2px 8px;
99
+ font-size: 11px;
100
+ font-weight: 700;
101
+ text-transform: capitalize;
102
+ color: var(--bb-ink);
103
+ background: rgba(0, 0, 0, 0.04);
104
+ }
105
+
106
+ .bb-health-state[data-state='connected'] {
107
+ color: #1f6f3f;
108
+ border-color: rgba(31, 111, 63, 0.35);
109
+ background: rgba(31, 111, 63, 0.12);
110
+ }
111
+
112
+ .bb-health-state[data-state='connecting'],
113
+ .bb-health-state[data-state='backoff'] {
114
+ color: #8a5200;
115
+ border-color: rgba(138, 82, 0, 0.35);
116
+ background: rgba(138, 82, 0, 0.12);
117
+ }
118
+
119
+ .bb-health-state[data-state='disconnected'] {
120
+ color: #a22b2b;
121
+ border-color: rgba(162, 43, 43, 0.35);
122
+ background: rgba(162, 43, 43, 0.12);
123
+ }
124
+
125
+ .bb-health-grid {
126
+ display: grid;
127
+ grid-template-columns: auto 1fr;
128
+ gap: 6px 8px;
129
+ margin: 0;
130
+ }
131
+
132
+ .bb-health-grid dt {
133
+ margin: 0;
134
+ font-size: 11px;
135
+ color: var(--bb-ink-2);
136
+ }
137
+
138
+ .bb-health-grid dd {
139
+ margin: 0;
140
+ font-size: 12px;
141
+ color: var(--bb-ink);
142
+ overflow-wrap: anywhere;
143
+ }
144
+
145
+ .bb-health-error {
146
+ min-height: 14px;
147
+ margin: 8px 0 0;
148
+ font-size: 11px;
149
+ color: #a22b2b;
150
+ }
151
+
152
+ .bb-health-actions {
153
+ margin-top: 6px;
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 8px;
157
+ }
158
+
159
+ .bb-health-copy {
160
+ border: 1px solid var(--bb-border);
161
+ border-radius: 8px;
162
+ background: var(--bb-bg);
163
+ color: var(--bb-ink);
164
+ padding: 5px 8px;
165
+ font-size: 12px;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .bb-health-copy:hover {
170
+ background: var(--bb-hover);
171
+ }
172
+
173
+ .bb-health-copy-status {
174
+ font-size: 11px;
175
+ color: var(--bb-ink-2);
176
+ }
177
+
71
178
  .bb-popup-head {
72
179
  display: flex;
73
180
  align-items: center;
@@ -410,6 +410,80 @@ var PermissionPromptController = class {
410
410
  }
411
411
  };
412
412
 
413
+ // packages/extension/src/connection-state.ts
414
+ var toIso = (ms) => new Date(ms).toISOString();
415
+ var ConnectionStateTracker = class {
416
+ constructor(now = Date.now, failureLogThrottleMs = 15e3) {
417
+ this.now = now;
418
+ this.failureLogThrottleMs = failureLogThrottleMs;
419
+ this.state = "disconnected";
420
+ this.consecutiveFailures = 0;
421
+ this.lastFailureLogAt = 0;
422
+ this.suppressedFailureLogs = 0;
423
+ }
424
+ setEndpoint(endpoint) {
425
+ this.endpoint = endpoint;
426
+ }
427
+ markConnecting() {
428
+ this.state = "connecting";
429
+ this.reconnectDelayMs = void 0;
430
+ this.retryAt = void 0;
431
+ }
432
+ markConnected() {
433
+ this.state = "connected";
434
+ this.lastConnectedAt = toIso(this.now());
435
+ this.reconnectDelayMs = void 0;
436
+ this.retryAt = void 0;
437
+ this.consecutiveFailures = 0;
438
+ }
439
+ markDisconnected() {
440
+ this.state = "disconnected";
441
+ this.reconnectDelayMs = void 0;
442
+ this.retryAt = void 0;
443
+ this.lastDisconnectedAt = toIso(this.now());
444
+ }
445
+ markBackoff(delayMs2) {
446
+ this.state = "backoff";
447
+ this.reconnectDelayMs = delayMs2;
448
+ this.retryAt = toIso(this.now() + Math.max(0, delayMs2));
449
+ }
450
+ recordFailure(message) {
451
+ this.lastErrorAt = toIso(this.now());
452
+ this.lastErrorMessage = message;
453
+ this.consecutiveFailures += 1;
454
+ }
455
+ consumeFailureLogBudget() {
456
+ const now = this.now();
457
+ if (this.lastFailureLogAt === 0 || now - this.lastFailureLogAt >= this.failureLogThrottleMs) {
458
+ const suppressedCount = this.suppressedFailureLogs;
459
+ this.suppressedFailureLogs = 0;
460
+ this.lastFailureLogAt = now;
461
+ return { shouldLog: true, suppressedCount };
462
+ }
463
+ this.suppressedFailureLogs += 1;
464
+ return { shouldLog: false, suppressedCount: this.suppressedFailureLogs };
465
+ }
466
+ flushSuppressedFailureLogs() {
467
+ const count = this.suppressedFailureLogs;
468
+ this.suppressedFailureLogs = 0;
469
+ return count;
470
+ }
471
+ getStatus() {
472
+ return {
473
+ state: this.state,
474
+ endpoint: this.endpoint,
475
+ ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
476
+ reconnect_delay_ms: this.reconnectDelayMs,
477
+ retry_at: this.retryAt,
478
+ last_connected_at: this.lastConnectedAt,
479
+ last_disconnected_at: this.lastDisconnectedAt,
480
+ last_error_at: this.lastErrorAt,
481
+ last_error_message: this.lastErrorMessage,
482
+ consecutive_failures: this.consecutiveFailures
483
+ };
484
+ }
485
+ };
486
+
413
487
  // packages/extension/src/background.ts
414
488
  var DEFAULT_CORE_PORT = 3210;
415
489
  var CORE_PORT_KEY = "corePort";
@@ -423,13 +497,10 @@ var HISTORY_DISPATCH_TIMEOUT_MS = 2e3;
423
497
  var HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS = 8e3;
424
498
  var HISTORY_POST_NAV_DOM_GRACE_TIMEOUT_MS = 2e3;
425
499
  var AGENT_TAB_ID_KEY = "agentTabId";
426
- var AGENT_TAB_GROUP_TITLE = "\u{1F309} Browser Bridge";
427
- var AGENT_TAB_FAVICON_ASSET_PATH = "assets/icons/icon-32.png";
500
+ var AGENT_TAB_GROUP_TITLE = "Browser Bridge";
501
+ var AGENT_TAB_BOOTSTRAP_PATH = "agent-tab.html";
428
502
  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
- )}`;
503
+ return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_BOOTSTRAP_PATH) : AGENT_TAB_BOOTSTRAP_PATH;
433
504
  };
434
505
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
435
506
  var makeEventId = /* @__PURE__ */ (() => {
@@ -601,24 +672,36 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
601
672
  bitmap.close();
602
673
  }
603
674
  };
604
- var readCorePort = async () => {
675
+ var readCoreEndpointConfig = async () => {
605
676
  return await new Promise((resolve) => {
606
677
  chrome.storage.local.get(
607
678
  [CORE_PORT_KEY],
608
679
  (result) => {
609
680
  const raw = result?.[CORE_PORT_KEY];
610
681
  if (typeof raw === "number" && Number.isFinite(raw)) {
611
- resolve(raw);
682
+ resolve({
683
+ host: "127.0.0.1",
684
+ port: raw,
685
+ portSource: "storage"
686
+ });
612
687
  return;
613
688
  }
614
689
  if (typeof raw === "string") {
615
690
  const parsed = Number(raw);
616
691
  if (Number.isFinite(parsed)) {
617
- resolve(parsed);
692
+ resolve({
693
+ host: "127.0.0.1",
694
+ port: parsed,
695
+ portSource: "storage"
696
+ });
618
697
  return;
619
698
  }
620
699
  }
621
- resolve(DEFAULT_CORE_PORT);
700
+ resolve({
701
+ host: "127.0.0.1",
702
+ port: DEFAULT_CORE_PORT,
703
+ portSource: "default"
704
+ });
622
705
  }
623
706
  );
624
707
  });
@@ -1063,9 +1146,12 @@ var waitForDomContentLoaded = async (tabId, timeoutMs) => {
1063
1146
  }, timeoutMs);
1064
1147
  });
1065
1148
  };
1066
- var getWsUrl = async () => {
1067
- const port = await readCorePort();
1068
- return `ws://127.0.0.1:${port}${CORE_WS_PATH}`;
1149
+ var getWsEndpoint = async () => {
1150
+ const endpoint = await readCoreEndpointConfig();
1151
+ return {
1152
+ endpoint,
1153
+ url: `ws://${endpoint.host}:${endpoint.port}${CORE_WS_PATH}`
1154
+ };
1069
1155
  };
1070
1156
  var DriveSocket = class {
1071
1157
  constructor() {
@@ -1077,10 +1163,12 @@ var DriveSocket = class {
1077
1163
  this.keepAliveIntervalMs = 3e4;
1078
1164
  this.debuggerSessions = /* @__PURE__ */ new Map();
1079
1165
  this.debuggerIdleTimeoutMs = null;
1166
+ this.connection = new ConnectionStateTracker();
1080
1167
  }
1081
1168
  start() {
1169
+ this.connection.markConnecting();
1082
1170
  void this.connect().catch((error) => {
1083
- console.error("DriveSocket connect failed:", error);
1171
+ this.recordConnectionFailure("initial connect", error);
1084
1172
  });
1085
1173
  }
1086
1174
  stop() {
@@ -1093,6 +1181,7 @@ var DriveSocket = class {
1093
1181
  this.socket.close();
1094
1182
  this.socket = null;
1095
1183
  }
1184
+ this.connection.markDisconnected();
1096
1185
  }
1097
1186
  sendTabReport() {
1098
1187
  void this.emitTabReport().catch((error) => {
@@ -1104,10 +1193,12 @@ var DriveSocket = class {
1104
1193
  return;
1105
1194
  }
1106
1195
  const delay2 = this.reconnectDelayMs;
1196
+ this.connection.markBackoff(delay2);
1107
1197
  this.reconnectTimer = self.setTimeout(() => {
1108
1198
  this.reconnectTimer = null;
1199
+ this.connection.markConnecting();
1109
1200
  void this.connect().catch((error) => {
1110
- console.error("DriveSocket reconnect failed:", error);
1201
+ this.recordConnectionFailure("reconnect", error);
1111
1202
  });
1112
1203
  }, delay2);
1113
1204
  this.reconnectDelayMs = Math.min(
@@ -1116,12 +1207,20 @@ var DriveSocket = class {
1116
1207
  );
1117
1208
  }
1118
1209
  async connect() {
1119
- const url = await getWsUrl();
1210
+ const { endpoint, url } = await getWsEndpoint();
1211
+ this.connection.setEndpoint(endpoint);
1120
1212
  try {
1121
1213
  const socket2 = new WebSocket(url);
1122
1214
  this.socket = socket2;
1123
1215
  socket2.addEventListener("open", () => {
1124
1216
  this.reconnectDelayMs = 1e3;
1217
+ this.connection.markConnected();
1218
+ const suppressed = this.connection.flushSuppressedFailureLogs();
1219
+ if (suppressed > 0) {
1220
+ console.info(
1221
+ `DriveSocket reconnected after suppressing ${suppressed} repeated connection failures.`
1222
+ );
1223
+ }
1125
1224
  this.startKeepAlive();
1126
1225
  void this.sendHello().catch((error) => {
1127
1226
  console.error("DriveSocket hello failed:", error);
@@ -1131,22 +1230,49 @@ var DriveSocket = class {
1131
1230
  this.handleMessage(event.data);
1132
1231
  });
1133
1232
  socket2.addEventListener("close", () => {
1134
- this.socket = null;
1135
- this.stopKeepAlive();
1136
- this.scheduleReconnect();
1233
+ this.handleSocketUnavailable(socket2, "socket closed");
1137
1234
  });
1138
1235
  socket2.addEventListener("error", () => {
1139
- this.socket = null;
1140
- this.stopKeepAlive();
1141
- this.scheduleReconnect();
1236
+ this.handleSocketUnavailable(socket2, "socket error");
1142
1237
  });
1143
1238
  } catch (error) {
1144
- console.debug("DriveSocket connect failed, scheduling reconnect.", error);
1239
+ this.recordConnectionFailure("connect", error);
1240
+ this.connection.markDisconnected();
1145
1241
  this.scheduleReconnect();
1146
1242
  }
1147
1243
  }
1244
+ getConnectionStatus() {
1245
+ return this.connection.getStatus();
1246
+ }
1247
+ handleSocketUnavailable(socket2, reason) {
1248
+ if (this.socket !== socket2) {
1249
+ return;
1250
+ }
1251
+ this.socket = null;
1252
+ this.stopKeepAlive();
1253
+ this.connection.markDisconnected();
1254
+ this.recordConnectionFailure(reason, new Error(reason));
1255
+ this.scheduleReconnect();
1256
+ }
1257
+ recordConnectionFailure(context, error) {
1258
+ const detail = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
1259
+ this.connection.recordFailure(`${context}: ${detail}`);
1260
+ const budget = this.connection.consumeFailureLogBudget();
1261
+ if (!budget.shouldLog) {
1262
+ return;
1263
+ }
1264
+ if (budget.suppressedCount > 0) {
1265
+ console.warn(
1266
+ `DriveSocket ${context} failed (${budget.suppressedCount} repeated failures suppressed).`,
1267
+ error
1268
+ );
1269
+ return;
1270
+ }
1271
+ console.warn(`DriveSocket ${context} failed.`, error);
1272
+ }
1148
1273
  async sendHello() {
1149
1274
  const manifest = chrome.runtime.getManifest();
1275
+ const endpoint = await readCoreEndpointConfig();
1150
1276
  let tabs = [];
1151
1277
  try {
1152
1278
  tabs = await queryTabs();
@@ -1156,6 +1282,9 @@ var DriveSocket = class {
1156
1282
  }
1157
1283
  const params = {
1158
1284
  version: manifest.version,
1285
+ core_host: endpoint.host,
1286
+ core_port: endpoint.port,
1287
+ core_port_source: endpoint.portSource,
1159
1288
  tabs
1160
1289
  };
1161
1290
  this.sendEvent("drive.hello", params);
@@ -3191,5 +3320,17 @@ chrome.debugger.onDetach.addListener(
3191
3320
  });
3192
3321
  }
3193
3322
  );
3323
+ chrome.runtime.onMessage.addListener(
3324
+ (message, _sender, sendResponse) => {
3325
+ if (!message || typeof message !== "object" || message.action !== "drive.connection_status") {
3326
+ return void 0;
3327
+ }
3328
+ sendResponse({
3329
+ ok: true,
3330
+ result: socket.getConnectionStatus()
3331
+ });
3332
+ return true;
3333
+ }
3334
+ );
3194
3335
  socket.start();
3195
3336
  //# sourceMappingURL=background.js.map