chrome-bridge-mcp 1.23.4 → 1.25.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.
Files changed (45) hide show
  1. package/README.md +17 -11
  2. package/extension/console-capture.js +31 -3
  3. package/extension/lib/capture-pacing.js +7 -3
  4. package/extension/lib/css-cascade.js +26 -1
  5. package/extension/lib/page-fingerprint.js +24 -1
  6. package/extension/manifest.json +1 -1
  7. package/extension/network-hook.js +100 -0
  8. package/extension/service-worker.js +143 -136
  9. package/install.sh +4 -4
  10. package/package.json +2 -2
  11. package/server/cli.js +49 -1
  12. package/server/formatters.js +9 -6
  13. package/server/index.js +6 -0
  14. package/server/protocol.js +1 -1
  15. package/server/sourcemaps.js +39 -3
  16. package/server/tools.js +277 -82
  17. package/skills/chrome-bridge/SKILL.md +62 -270
  18. package/skills/chrome-bridge/recipes/accessibility-keyboard.md +7 -0
  19. package/skills/chrome-bridge/recipes/checkout-test-card.md +11 -0
  20. package/skills/chrome-bridge/recipes/cookies-consent-banner.md +9 -0
  21. package/skills/chrome-bridge/recipes/design-tokens-fonts-colours.md +13 -0
  22. package/skills/chrome-bridge/recipes/email-deliverability.md +4 -0
  23. package/skills/chrome-bridge/recipes/error-states-backend-will-not.md +10 -0
  24. package/skills/chrome-bridge/recipes/evidence-file-incident.md +10 -0
  25. package/skills/chrome-bridge/recipes/export-from-back-office-analyse.md +4 -0
  26. package/skills/chrome-bridge/recipes/finding-setting-unknown-admin.md +7 -0
  27. package/skills/chrome-bridge/recipes/form-end-end-until-email-arrives.md +12 -0
  28. package/skills/chrome-bridge/recipes/gutenberg-block-editors.md +5 -0
  29. package/skills/chrome-bridge/recipes/hand-browser-user.md +9 -0
  30. package/skills/chrome-bridge/recipes/heavy-web-apps.md +14 -0
  31. package/skills/chrome-bridge/recipes/hosting-panel-logs.md +6 -0
  32. package/skills/chrome-bridge/recipes/page-audit-before-release.md +7 -0
  33. package/skills/chrome-bridge/recipes/pagespeed-without-api-key.md +6 -0
  34. package/skills/chrome-bridge/recipes/pixel-ga4-gtm-events.md +8 -0
  35. package/skills/chrome-bridge/recipes/post-deploy-check.md +6 -0
  36. package/skills/chrome-bridge/recipes/redirects-after-migration.md +7 -0
  37. package/skills/chrome-bridge/recipes/second-pair-eyes-before.md +10 -0
  38. package/skills/chrome-bridge/recipes/security-headers-https.md +3 -0
  39. package/skills/chrome-bridge/recipes/seo-links-structured-data.md +5 -0
  40. package/skills/chrome-bridge/recipes/smoke-test-recorded-flow.md +10 -0
  41. package/skills/chrome-bridge/recipes/three-viewports.md +8 -0
  42. package/skills/chrome-bridge/recipes/visual-regression-between-two.md +9 -0
  43. package/skills/chrome-bridge/recipes/watch-how-do-it.md +11 -0
  44. package/skills/chrome-bridge/recipes/watch-page-tell-when.md +10 -0
  45. package/skills/chrome-bridge/recipes/which-plugin-slows-page.md +8 -0
package/README.md CHANGED
@@ -9,15 +9,16 @@ with your extensions and your cookies — works in it, and hands it back. What
9
9
  only a person can do (a login, a 2FA code, a CAPTCHA, a choice) comes back to
10
10
  you through `handoff`: a banner in the page, you act, the agent continues.**
11
11
 
12
- Measured against the official browser extension (paired runs, 27/09/2026, n=5
13
- per task, same model): filling a form, 2.5× fewer turns and 1.9× lower cost;
14
- finding one row in a 1,500-row table, 2.3× lower cost in about the same number
15
- of turns. ~3× the toolset and no paid plan. 60
12
+ Measured against the official browser extension (paired runs, 29/09/2026, n=5
13
+ per task, same model): filling a form, 4.7× fewer turns and 2.8× lower cost;
14
+ finding one row in a 1,500-row table, 2.3× fewer turns and 3.4× lower cost;
15
+ debugging a broken page, 4.7× fewer turns and 3.2× lower cost. 15 correct
16
+ answers out of 15, against 12 ([bench/RESULTS.md](bench/RESULTS.md)). ~3× the toolset and no paid plan. 60
16
17
  web-development tools (navigation, DOM inspection, visual regression, audits,
17
18
  network mocking) over a local WebSocket bridge, plus a headless instance for
18
19
  CI. Self-hosted, local-only. Works on ChromeOS.
19
20
 
20
- ![The same form filled in 6 turns instead of 15 — 2.5× fewer turns on the form, about half the cost on the form and on a 1,500-row table (paired runs, 27/09/2026, n=5)](assets/readme/card1-race.png)
21
+ ![The same form filled in 3 turns instead of 14 — 4.7× fewer turns on the form, 2.8-3.4× lower cost on form, 1,500-row table and debugging (paired runs, 29/09/2026, n=5)](assets/readme/card1-race.png)
21
22
 
22
23
  ## Quickstart
23
24
 
@@ -57,10 +58,13 @@ element refs, `click(ref="n1")` follows with no discovery turn in between.
57
58
  For `execute_js`, enable **Allow user scripts** in `chrome://extensions` →
58
59
  Chrome Bridge → Details (on Chrome 135-137, enable Developer Mode instead).
59
60
 
60
- **As a plugin, without a clone:** in Claude Code,
61
+ **As a plugin, without a clone:** inside a Claude Code session,
61
62
  `/plugin install chrome-bridge --marketplace frsorrentino/chrome-bridge`
62
63
  (one command, Claude Code 2.1.275+; on older versions `/plugin marketplace add
63
- frsorrentino/chrome-bridge` then `/plugin install chrome-bridge@chrome-bridge`)
64
+ frsorrentino/chrome-bridge` then `/plugin install chrome-bridge@chrome-bridge`).
65
+ From a shell, `claude plugin install` has no `--marketplace` option: run
66
+ `claude plugin marketplace add frsorrentino/chrome-bridge`, then
67
+ `claude plugin install chrome-bridge@chrome-bridge`. Either way the plugin
64
68
  registers the MCP server from npm (all capabilities) together with the recipes
65
69
  skill; clients that read [Agent Plugins 1.0](https://agent-plugins.org) get the
66
70
  same from `plugin.json` + `mcp.json`. The extension still comes from the Web
@@ -91,7 +95,7 @@ or let you pick one or more elements on the page.
91
95
  | | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
92
96
  |---|---|---|---|---|
93
97
  | **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
94
- | **Tools** | **60** (39 core) | 22 | 29 default (56 with flags) | 24 core (71 total) |
98
+ | **Tools** | **60** (43 core) | 22 | 29 default (56 with flags) | 24 core (71 total) |
95
99
  | **Requires paid plan** | **No** | Yes (Pro+) | No | No |
96
100
  | **Network mocking** | **Yes** (stub/headers) | No | No | Yes |
97
101
  | **Visual regression** | **Yes** (`screenshot_diff`) | No | No | No |
@@ -160,8 +164,10 @@ user-script toggle isn't available.
160
164
 
161
165
  ## Tools
162
166
 
163
- 60 in total, in seven groups. Only `core` (39 tools) loads by default; the rest
164
- are opt-in via `--caps`.
167
+ 60 in total. `core` (43 tools, every tool used in 101 real sessions) loads by
168
+ default; the other 17 sit in six optional caps that the agent switches on
169
+ mid-session with `get_status({enable: ["visual"]})`, with no restart
170
+ (`--caps` still sets them at startup).
165
171
 
166
172
  ![60 tools in seven groups, from clicking a button to auditing a whole page](assets/readme/card4-toolbox.png)
167
173
 
@@ -199,7 +205,7 @@ Environment variables, each with a matching CLI flag:
199
205
  | `CHROME_BRIDGE_PORT` | `8765` | |
200
206
  | `CHROME_BRIDGE_HOST` / `--host` | `127.0.0.1` | `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and only with a token |
201
207
  | `CHROME_BRIDGE_TOKEN` | unset | Required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind isn't loopback |
202
- | `CHROME_BRIDGE_CAPS` / `--caps` | `core` | `core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, `all`. `install.sh` uses `all` |
208
+ | `CHROME_BRIDGE_CAPS` / `--caps` | `core` | `core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, `all`. Optional caps can also be switched on at runtime with `get_status({enable})` |
203
209
  | `CHROME_BRIDGE_NO_JS` / `--no-js` | unset | No arbitrary JavaScript in the page: `execute_js` and `modify_dom` leave the schema, `wait_for(condition=function)` and `javascript:`/`data:` URLs are refused. `get_status` reports `js_evaluation` |
204
210
  | `CHROME_BRIDGE_READ_ROOT` / `--read-root` | unset | Where `upload_file` may read from. Unset: any file except keys and credentials (`~/.ssh`, `~/.aws`, `~/.gnupg`, `.env*`, `id_*`, `*.pem`, `*.key`, …), symlinks resolved first. Set: only files under this directory, whatever their name — the way to upload a certificate key on purpose |
205
211
  | `CHROME_BRIDGE_BROWSER` | unset | Launch mode only: the browser binary, e.g. `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`. Unset: the first of Chromium, Edge, Brave, Google Chrome found in the standard paths (on Linux Chromium, then Google Chrome). Google Chrome 137+ cannot load the extension |
@@ -12,11 +12,22 @@
12
12
  window.__chromeBridge_consoleHooked = true;
13
13
  window.__chromeBridge_consoleLogs = [];
14
14
  const MAX = 1000;
15
+ // seq: cursore per read_console({since}), che restituisce solo le voci nuove
16
+ // senza cancellare niente.
17
+ let seq = 0;
15
18
  const push = (entry) => {
19
+ entry.seq = ++seq;
16
20
  const buf = window.__chromeBridge_consoleLogs;
17
21
  if (buf.length >= MAX) buf.shift(); // ring buffer: tieni i più recenti, non i primi 1000
18
22
  buf.push(entry);
19
23
  };
24
+ // Un Error passato a console.error diventava "{}" (JSON.stringify non vede
25
+ // message e stack, che non sono enumerabili): lo stack è l'unica cosa che
26
+ // read_console(sourcemap) può riportare ai sorgenti. Duck typing e non
27
+ // instanceof, perché un Error di un iframe viene da un altro realm.
28
+ const errorText = (v) => (v && typeof v === 'object' && typeof v.message === 'string'
29
+ ? (typeof v.stack === 'string' && v.stack ? v.stack : `${v.name || 'Error'}: ${v.message}`)
30
+ : null);
20
31
  for (const method of ['log', 'warn', 'error', 'info', 'debug']) {
21
32
  const orig = console[method].bind(console);
22
33
  console[method] = (...args) => {
@@ -24,7 +35,7 @@
24
35
  push({
25
36
  level: method,
26
37
  args: args.map((a) => {
27
- try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
38
+ try { return errorText(a) ?? (typeof a === 'object' ? JSON.stringify(a) : String(a)); }
28
39
  catch { return String(a); }
29
40
  }),
30
41
  timestamp: Date.now(),
@@ -34,12 +45,29 @@
34
45
  };
35
46
  }
36
47
  window.addEventListener('error', (e) => {
37
- try { push({ level: 'error', args: [`Uncaught ${e.message} at ${e.filename || '?'}:${e.lineno || 0}`], timestamp: Date.now() }); } catch {}
48
+ try {
49
+ // Un'immagine o uno script che non si carica arriva qui in fase di
50
+ // cattura, con target l'elemento e nessun messaggio: prima diventava
51
+ // «Uncaught at ?:0:0». Ora dice quale risorsa.
52
+ const t = e.target;
53
+ if (t && t !== window && t.tagName) {
54
+ const url = t.currentSrc || t.src || t.href || '';
55
+ push({ level: 'error', args: [`Failed to load ${String(t.tagName).toLowerCase()}${url ? ` ${url}` : ''}`], timestamp: Date.now() });
56
+ return;
57
+ }
58
+ // Con la colonna il frame diventa url:riga:colonna, risolvibile dalla source map.
59
+ const stack = errorText(e.error);
60
+ push({
61
+ level: 'error',
62
+ args: [stack ? `Uncaught ${stack}` : `Uncaught ${e.message} at ${e.filename || '?'}:${e.lineno || 0}:${e.colno || 0}`],
63
+ timestamp: Date.now(),
64
+ });
65
+ } catch {}
38
66
  }, true);
39
67
  window.addEventListener('unhandledrejection', (e) => {
40
68
  try {
41
69
  let reason;
42
- try { reason = String(e.reason); } catch { reason = '<unstringifiable>'; }
70
+ try { reason = errorText(e.reason) ?? String(e.reason); } catch { reason = '<unstringifiable>'; }
43
71
  push({ level: 'error', args: [`Unhandled rejection: ${reason}`], timestamp: Date.now() });
44
72
  } catch {}
45
73
  });
@@ -5,11 +5,15 @@
5
5
  * 520 ms dalla precedente. Orologio e sleep iniettabili per i test.
6
6
  */
7
7
  export function createPacer({ minIntervalMs = 520, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
8
- let last = -Infinity;
8
+ // Lo slot si prenota prima di dormire: due catture concorrenti leggevano lo
9
+ // stesso "ultimo" e si svegliavano insieme, e la seconda sforava la quota.
10
+ let next = -Infinity;
9
11
  const pace = async () => {
10
- const wait = Math.max(0, last + minIntervalMs - now());
12
+ const t = now();
13
+ const slot = Math.max(t, next);
14
+ next = slot + minIntervalMs;
15
+ const wait = slot - t;
11
16
  if (wait > 0) await sleep(wait);
12
- last = now();
13
17
  return wait;
14
18
  };
15
19
  pace.minIntervalMs = minIntervalMs;
@@ -290,7 +290,21 @@
290
290
  const out = [];
291
291
  for (let i = 0; i < style.length; i += 1) {
292
292
  const prop = style[i];
293
- out.push({ prop, value: style.getPropertyValue(prop), important: style.getPropertyPriority(prop) === 'important' });
293
+ let value = style.getPropertyValue(prop);
294
+ // `background: var(--x)`: per la specifica CSSOM le proprietà singole
295
+ // valgono "" (sostituzione in sospeso). Il testo vero sta nella
296
+ // abbreviata: background-color → background, border-top-color →
297
+ // border-top → border.
298
+ let shorthand;
299
+ if (value === '' && !prop.startsWith('--')) {
300
+ const parts = prop.split('-');
301
+ for (let k = parts.length - 1; k >= 1 && value === ''; k -= 1) {
302
+ const sh = parts.slice(0, k).join('-');
303
+ const v = style.getPropertyValue(sh);
304
+ if (v) { value = v; shorthand = sh; }
305
+ }
306
+ }
307
+ out.push({ prop, value, ...(shorthand && { shorthand }), important: style.getPropertyPriority(prop) === 'important' });
294
308
  }
295
309
  return out;
296
310
  };
@@ -330,6 +344,7 @@
330
344
  const fmtEntry = (list, computed, inheritedFrom) => {
331
345
  const [winner, ...rest] = list;
332
346
  const entry = { value: winner.value };
347
+ if (winner.shorthand) entry.shorthand = winner.shorthand;
333
348
  if (winner.important) entry.important = true;
334
349
  if (computed != null) entry.computed = computed;
335
350
  if (inheritedFrom) entry.inherited_from = inheritedFrom;
@@ -386,6 +401,16 @@
386
401
  }
387
402
  }
388
403
 
404
+ // var(--x) senza --x definita sull'elemento (né ereditata): la proprietà
405
+ // cade sul valore iniziale. Detto qui, il modello non spende una seconda
406
+ // chiamata a cercare dove --x sia definita (benchmark debug del 28/09).
407
+ for (const entry of Object.values(properties)) {
408
+ const names = [...String(entry.value ?? '').matchAll(/var\(\s*(--[\w-]+)\s*(,)?/g)]
409
+ .filter((m) => !m[2]).map((m) => m[1]);
410
+ const missing = [...new Set(names)].filter((n) => String(computedOf(n) ?? '').trim() === '');
411
+ if (missing.length) entry.undefined_vars = missing;
412
+ }
413
+
389
414
  const sorted = {};
390
415
  for (const k of Object.keys(properties).sort()) sorted[k] = properties[k];
391
416
  const out = { selector: opts.selector ?? null, element: describe(el), matched_rules: matched };
@@ -7,8 +7,31 @@
7
7
  * sorgente e la esegue nella pagina, dove niente di questo modulo esiste.
8
8
  * Prende `doc` per essere testabile su un DOM finto (test/unit).
9
9
  */
10
- export function pageFingerprint(doc) {
10
+ export function pageFingerprint(doc, settle) {
11
11
  const d = doc || document;
12
+ // Con settle l'impronta aspetta che il DOM si fermi: nessuna mutazione per
13
+ // quiet_ms, al più max_ms. Sostituisce la pausa fissa dopo un click (150 ms
14
+ // anche su una pagina già ferma). Su una pagina nascosta i timer sono
15
+ // strozzati a 1 s: niente attesa, e lo dice.
16
+ if (settle && typeof MutationObserver === 'function') {
17
+ const quiet = settle.quiet_ms ?? 50;
18
+ const max = settle.max_ms ?? 250;
19
+ if (d.hidden) return { ...pageFingerprint(d), settled_ms: 0, hidden: true };
20
+ return new Promise((resolve) => {
21
+ const t0 = performance.now();
22
+ let last = t0;
23
+ const mo = new MutationObserver(() => { last = performance.now(); });
24
+ try { mo.observe(d.documentElement, { subtree: true, childList: true, attributes: true, characterData: true }); } catch { /* niente da osservare */ }
25
+ const tick = () => {
26
+ const now = performance.now();
27
+ if (now - last >= quiet || now - t0 >= max) {
28
+ mo.disconnect();
29
+ resolve({ ...pageFingerprint(d), settled_ms: Math.round(now - t0) });
30
+ } else setTimeout(tick, 10);
31
+ };
32
+ setTimeout(tick, quiet);
33
+ });
34
+ }
12
35
  const count = (sel) => { try { return d.querySelectorAll(sel).length; } catch { return 0; } };
13
36
  let checked = 0;
14
37
  try { for (const el of d.querySelectorAll('input')) if (el.checked) checked += 1; } catch { /* nessun input */ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chrome Bridge for Claude Code",
4
- "version": "1.23.4",
4
+ "version": "1.25.0",
5
5
  "description": "Bridge your browser to Claude Code: 60 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
6
6
  "homepage_url": "https://github.com/frsorrentino/chrome-bridge",
7
7
  "minimum_chrome_version": "135",
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Gancio fetch/XHR in MAIN world, a document_start insieme alla cattura della
3
+ * console (vedi _applyInstrumentation nel service worker): le richieste fatte
4
+ * durante il caricamento della pagina entrano nel buffer letto da
5
+ * monitor_network. Prima il gancio arrivava solo alla prima chiamata e la
6
+ * richiesta fallita all'avvio, il caso tipico da debuggare, non si vedeva.
7
+ * Lo stesso file viene iniettato da ensureNetworkHook sulle pagine aperte
8
+ * prima dell'estensione; la guardia lo rende idempotente.
9
+ */
10
+ (() => {
11
+ if (window.__chromeBridge_networkHooked) return;
12
+ window.__chromeBridge_networkHooked = true;
13
+ window.__chromeBridge_networkRequests = [];
14
+ window.__chromeBridge_netSeq = 0;
15
+ window.__chromeBridge_inflight = 0;
16
+ window.__chromeBridge_lastNetActivity = Date.now();
17
+ const MAX = 1000;
18
+
19
+ // --- Patch fetch ---
20
+ const origFetch = window.fetch.bind(window);
21
+ window.fetch = async (...args) => {
22
+ const req = args[0];
23
+ const url = typeof req === 'string' ? req : req?.url || String(req);
24
+ const method = (args[1]?.method || (req?.method) || 'GET').toUpperCase();
25
+ const entry = { type: 'fetch', method, url, startTime: Date.now(), status: null, duration: null, error: null };
26
+ window.__chromeBridge_inflight += 1;
27
+ window.__chromeBridge_lastNetActivity = Date.now();
28
+ try {
29
+ const resp = await origFetch(...args);
30
+ entry.status = resp.status;
31
+ entry.duration = Date.now() - entry.startTime;
32
+ // Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
33
+ // faceva consegnare al modello le richieste dei primi secondi di vita
34
+ // della pagina etichettate come "most recent".
35
+ entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
36
+ if (window.__chromeBridge_networkRequests.length > MAX) {
37
+ window.__chromeBridge_networkRequests.shift();
38
+ }
39
+ window.__chromeBridge_inflight -= 1;
40
+ window.__chromeBridge_lastNetActivity = Date.now();
41
+ return resp;
42
+ } catch (err) {
43
+ entry.error = err.message;
44
+ entry.duration = Date.now() - entry.startTime;
45
+ // Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
46
+ // faceva consegnare al modello le richieste dei primi secondi di vita
47
+ // della pagina etichettate come "most recent".
48
+ entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
49
+ if (window.__chromeBridge_networkRequests.length > MAX) {
50
+ window.__chromeBridge_networkRequests.shift();
51
+ }
52
+ window.__chromeBridge_inflight -= 1;
53
+ window.__chromeBridge_lastNetActivity = Date.now();
54
+ throw err;
55
+ }
56
+ };
57
+
58
+ // --- Patch XMLHttpRequest ---
59
+ const OrigXHR = window.XMLHttpRequest;
60
+ const origOpen = OrigXHR.prototype.open;
61
+ const origSend = OrigXHR.prototype.send;
62
+ OrigXHR.prototype.open = function (method, url, ...rest) {
63
+ this.__cb_method = method;
64
+ this.__cb_url = url;
65
+ return origOpen.call(this, method, url, ...rest);
66
+ };
67
+ OrigXHR.prototype.send = function (...args) {
68
+ const entry = { type: 'xhr', method: (this.__cb_method || 'GET').toUpperCase(), url: this.__cb_url || '', startTime: Date.now(), status: null, duration: null, error: null };
69
+ this.addEventListener('load', () => {
70
+ entry.status = this.status;
71
+ entry.duration = Date.now() - entry.startTime;
72
+ // Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
73
+ // faceva consegnare al modello le richieste dei primi secondi di vita
74
+ // della pagina etichettate come "most recent".
75
+ entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
76
+ if (window.__chromeBridge_networkRequests.length > MAX) {
77
+ window.__chromeBridge_networkRequests.shift();
78
+ }
79
+ });
80
+ this.addEventListener('error', () => {
81
+ entry.error = 'Network error';
82
+ entry.duration = Date.now() - entry.startTime;
83
+ // Ring buffer: scarta le più VECCHIE, non le nuove. Scartare le nuove
84
+ // faceva consegnare al modello le richieste dei primi secondi di vita
85
+ // della pagina etichettate come "most recent".
86
+ entry.seq = ++window.__chromeBridge_netSeq; window.__chromeBridge_networkRequests.push(entry);
87
+ if (window.__chromeBridge_networkRequests.length > MAX) {
88
+ window.__chromeBridge_networkRequests.shift();
89
+ }
90
+ });
91
+ // loadend copre load, error e abort: traccia sempre la fine dell'in-flight
92
+ this.addEventListener('loadend', () => {
93
+ window.__chromeBridge_inflight -= 1;
94
+ window.__chromeBridge_lastNetActivity = Date.now();
95
+ });
96
+ window.__chromeBridge_inflight += 1;
97
+ window.__chromeBridge_lastNetActivity = Date.now();
98
+ return origSend.apply(this, args);
99
+ };
100
+ })();