rerender-lens 0.3.0 → 0.4.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.
package/panel/panel.js CHANGED
@@ -2,12 +2,6 @@
2
2
  "use strict";
3
3
  (() => {
4
4
  // extension/src/panel.ts
5
- var OPTIONAL_COLUMNS = [
6
- { key: "places", label: "Places" },
7
- { key: "lastSeen", label: "Last seen" }
8
- ];
9
- var VIRTUAL_THRESHOLD = 200;
10
- var GRID_ROW_H = 26;
11
5
  var PROTOCOL = 2;
12
6
  var KIND_LABEL = {
13
7
  "deep-equal": "equal by value",
@@ -22,6 +16,14 @@
22
16
  var MAX_REPORTS = 2e3;
23
17
  var MAX_PER_NODE = 200;
24
18
  var MAX_COMMITS = 500;
19
+ var MAX_PER_COMMIT = 500;
20
+ var THROTTLE_MIN_REPORTS = 200;
21
+ var BEST_FIX_INTERVAL = 500;
22
+ var LEFT_RENDER_INTERVAL = 250;
23
+ var SYNC_RETRY_MIN = 500;
24
+ var SYNC_RETRY_MAX = 5e3;
25
+ var SYNC_RETRIES = 20;
26
+ var NAVIGATION_SETTLE = 1200;
25
27
  var ROW_H = 22;
26
28
  var ITEM_H = 20;
27
29
  var OVERSCAN = 8;
@@ -386,12 +388,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
386
388
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
387
389
  }
388
390
  var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
389
- function rootCauseOf(r, commitReports) {
391
+ function indexByComponent(reports) {
392
+ const index = /* @__PURE__ */ new Map();
393
+ for (const r of reports) {
394
+ const list = index.get(r.component);
395
+ if (list) list.push(r);
396
+ else index.set(r.component, [r]);
397
+ }
398
+ return index;
399
+ }
400
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
390
401
  let cur = r;
391
402
  const seen = /* @__PURE__ */ new Set([r]);
392
403
  while (cur.trigger === "parent" && cur.parent) {
393
404
  const parent = cur.parent;
394
- const p = commitReports.find((x) => x.component === parent.name && isAncestorReport(x, cur));
405
+ const candidates = index.get(parent.name);
406
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
395
407
  if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
396
408
  seen.add(p);
397
409
  cur = p;
@@ -400,15 +412,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
400
412
  }
401
413
  function analyzeCommit(reports) {
402
414
  const roots = /* @__PURE__ */ new Map();
415
+ const rootByReport = /* @__PURE__ */ new Map();
416
+ const index = indexByComponent(reports);
403
417
  let avoidable = 0;
404
418
  let wasted = 0;
405
419
  for (const r of reports) {
406
420
  if (!r.avoidable) continue;
407
421
  avoidable++;
408
422
  if (typeof r.selfDuration === "number") wasted += r.selfDuration;
409
- const root = rootCauseOf(r, reports);
423
+ const root = rootCauseOf(r, reports, index);
410
424
  const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
411
425
  const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
426
+ rootByReport.set(r, name);
412
427
  let agg = roots.get(name);
413
428
  if (!agg) {
414
429
  agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
@@ -425,6 +440,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
425
440
  avoidable,
426
441
  wasted,
427
442
  roots: [...roots.values()].sort((a, b) => b.count - a.count),
443
+ rootByReport,
428
444
  contexts: contextAttribution(reports),
429
445
  fixes: rankFixes(reports),
430
446
  reports
@@ -471,22 +487,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
471
487
  }
472
488
  return root;
473
489
  }
474
- function rootCauseSummary(name, commits) {
490
+ function rootCauseSummary(name, commits, analyze = (_, reports) => analyzeCommit(reports)) {
475
491
  const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
476
492
  const affected = [];
477
493
  for (const [key, reports] of commits) {
478
- const analysis = analyzeCommit(reports);
494
+ const analysis = analyze(key, reports);
479
495
  const root = analysis.roots.find((x) => x.name === name);
480
496
  if (!root) continue;
481
497
  out.trigger = root.trigger;
482
498
  out.commits.push({ key, analysis, count: root.count, components: root.components });
483
499
  out.total += root.count;
484
500
  for (const [c, n] of root.components) out.components.set(c, (out.components.get(c) || 0) + n);
485
- for (const r of reports) {
486
- if (!r.avoidable) continue;
487
- const rc = rootCauseOf(r, reports);
488
- if ((rc ? rc.name : r.parent && r.parent.name) === name) affected.push(r);
489
- }
501
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) affected.push(r);
490
502
  }
491
503
  out.commits.reverse();
492
504
  out.fixes = rankFixes(affected);
@@ -544,65 +556,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
544
556
  };
545
557
  }
546
558
  var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
547
- var b64url = (bytes) => {
548
- let s = "";
549
- for (const b of bytes) s += String.fromCharCode(b);
550
- return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
551
- };
552
- var unb64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
553
- async function transformBytes(bytes, transform) {
554
- const source = new ReadableStream({
555
- start(controller) {
556
- controller.enqueue(bytes);
557
- controller.close();
558
- }
559
- });
560
- const reader = source.pipeThrough(transform).getReader();
561
- const chunks = [];
562
- let total = 0;
563
- for (; ; ) {
564
- const { value, done } = await reader.read();
565
- if (done) break;
566
- chunks.push(value);
567
- total += value.length;
568
- }
569
- const out = new Uint8Array(total);
570
- let offset = 0;
571
- for (const c of chunks) {
572
- out.set(c, offset);
573
- offset += c.length;
574
- }
575
- return out;
576
- }
577
- async function encodeShare(report) {
578
- const json = JSON.stringify(report);
579
- const bytes = new TextEncoder().encode(json);
580
- if (typeof CompressionStream === "function") {
581
- try {
582
- return "d." + b64url(await transformBytes(bytes, new CompressionStream("deflate-raw")));
583
- } catch {
584
- }
585
- }
586
- return "j." + b64url(bytes);
587
- }
588
- async function decodeShare(value) {
589
- const [kind, data] = value.split(".", 2);
590
- if (!data) return null;
591
- let bytes = unb64url(data);
592
- if (kind === "d") {
593
- if (typeof DecompressionStream !== "function") return null;
594
- try {
595
- bytes = await transformBytes(bytes, new DecompressionStream("deflate-raw"));
596
- } catch {
597
- return null;
598
- }
599
- }
600
- try {
601
- return normalizeReport(JSON.parse(new TextDecoder().decode(bytes)));
602
- } catch {
603
- return null;
604
- }
605
- }
606
559
  function sourceContext(text, line, around = 3) {
607
560
  const lines = text.split("\n");
608
561
  const from = Math.max(1, line - around);
@@ -725,10 +678,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
725
678
  const copy = actions.copy;
726
679
  bar.append(el("button", { onclick: () => copy(reportToMarkdown(r)) }, "\u2398 Copy as Markdown"));
727
680
  }
728
- if (actions.share) {
729
- const share = actions.share;
730
- bar.append(el("button", { title: "Copy a link that opens this report in the panel", onclick: () => share(r) }, "\u{1F517} Copy link"));
731
- }
732
681
  if (bar.children.length) head.append(bar);
733
682
  const loc = r.source;
734
683
  if (loc && loc.lineNumber && actions.readSource) {
@@ -922,16 +871,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
922
871
  flashOn: false,
923
872
  settingsOpen: false,
924
873
  origin: null,
925
- legacyCommit: 0,
926
874
  tabLabel: transport.tabLabel ?? null,
927
875
  compact: false,
928
876
  sessions: [],
929
877
  recording: null,
930
878
  selectedSession: null,
931
879
  compareWith: null,
932
- byInstance: false,
933
- columns: [],
934
- notes: {}
880
+ byInstance: false
935
881
  };
936
882
  let persistTimer = null;
937
883
  let queue = [];
@@ -1091,6 +1037,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1091
1037
  const measure = () => setCompact(root.clientWidth > 0 && root.clientWidth < 720);
1092
1038
  if (typeof ResizeObserver === "function") new ResizeObserver(measure).observe(root);
1093
1039
  else window.addEventListener("resize", measure);
1040
+ const totals = { avoidable: 0, wasted: 0, perComponent: /* @__PURE__ */ new Map() };
1041
+ let bestFix;
1042
+ let bestFixAt = 0;
1043
+ let bestFixGen = -1;
1044
+ let bestFixTimer = null;
1045
+ const throttled = () => state.reports.length >= THROTTLE_MIN_REPORTS;
1046
+ function refreshBestFix() {
1047
+ if (!totals.avoidable) {
1048
+ bestFix = void 0;
1049
+ bestFixGen = dataGen;
1050
+ return;
1051
+ }
1052
+ if (bestFixGen === dataGen) return;
1053
+ const now = Date.now();
1054
+ if (throttled() && now - bestFixAt < BEST_FIX_INTERVAL) {
1055
+ if (!bestFixTimer) {
1056
+ bestFixTimer = setTimeout(() => {
1057
+ bestFixTimer = null;
1058
+ renderSummary();
1059
+ }, BEST_FIX_INTERVAL - (now - bestFixAt));
1060
+ }
1061
+ return;
1062
+ }
1063
+ bestFixAt = now;
1064
+ bestFixGen = dataGen;
1065
+ bestFix = rankFixes(state.reports)[0];
1066
+ }
1094
1067
  function renderSummary() {
1095
1068
  summary.textContent = "";
1096
1069
  const total = state.reports.length;
@@ -1099,17 +1072,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1099
1072
  return;
1100
1073
  }
1101
1074
  summary.hidden = false;
1102
- let avoidable = 0;
1103
- let wasted = 0;
1104
- const perComponent = /* @__PURE__ */ new Map();
1105
- for (const r of state.reports) {
1106
- if (!r.avoidable) continue;
1107
- avoidable++;
1108
- if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1109
- if (!isMuted(r.component)) perComponent.set(r.component, (perComponent.get(r.component) || 0) + 1);
1110
- }
1111
- const top = [...perComponent].sort((a, b) => b[1] - a[1])[0];
1112
- const fix = avoidable ? rankFixes(state.reports.filter((r) => !isMuted(r.component)))[0] : void 0;
1075
+ const { avoidable, wasted } = totals;
1076
+ let top;
1077
+ for (const entry of totals.perComponent) if (!top || entry[1] > top[1]) top = entry;
1078
+ refreshBestFix();
1079
+ const fix = bestFix;
1113
1080
  const stat = (value, label, cls = "") => el("span", { class: "stat " + cls }, [el("b", { text: value }), el("span", { class: "label", text: label })]);
1114
1081
  summary.append(stat(String(total), plural(total, "render").replace(/^\d+ /, "")), stat(String(avoidable), "avoidable", avoidable ? "bad" : "good"));
1115
1082
  if (wasted) summary.append(stat(fmtMs(wasted), "wasted", "bad"));
@@ -1177,8 +1144,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1177
1144
  collapsed: [...state.collapsed],
1178
1145
  treeWidth: state.treeWidth,
1179
1146
  flashOn: state.flashOn,
1180
- byInstance: state.byInstance,
1181
- columns: state.columns
1147
+ byInstance: state.byInstance
1182
1148
  };
1183
1149
  transport.storage.set("panel", saved);
1184
1150
  }, 150);
@@ -1230,7 +1196,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1230
1196
  instancesBtn.classList.toggle("active", state.byInstance);
1231
1197
  rebuildTree();
1232
1198
  }
1233
- if (Array.isArray(saved.columns)) state.columns = saved.columns.filter((c) => OPTIONAL_COLUMNS.some((o) => o.key === c));
1234
1199
  if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
1235
1200
  if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
1236
1201
  for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
@@ -1266,7 +1231,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1266
1231
  for (const r of state.reports) {
1267
1232
  const node = nodeOfReport(r);
1268
1233
  node.reports.push(r);
1269
- if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1270
1234
  node.total++;
1271
1235
  if (r.avoidable) {
1272
1236
  node.avoidable++;
@@ -1274,49 +1238,93 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1274
1238
  }
1275
1239
  node.lastReport = r;
1276
1240
  }
1241
+ for (const node of state.nodesByKey.values()) trimNode(node);
1277
1242
  renderLeft();
1278
1243
  renderDetails();
1279
1244
  }
1280
- function commitKeyFor(report) {
1281
- if (report.commitId > 0) return report.commitId;
1282
- return -state.legacyCommit;
1245
+ function trimNode(node) {
1246
+ const excess = node.reports.length - MAX_PER_NODE;
1247
+ if (excess > 0) node.reports.splice(0, excess);
1248
+ }
1249
+ const commitAnalyses = /* @__PURE__ */ new Map();
1250
+ function analysisFor(key, reports) {
1251
+ const cached = commitAnalyses.get(key);
1252
+ if (cached && cached.len === reports.length) return cached.analysis;
1253
+ const analysis = analyzeCommit(reports);
1254
+ commitAnalyses.set(key, { len: reports.length, analysis });
1255
+ return analysis;
1256
+ }
1257
+ const commitMember = /* @__PURE__ */ new WeakSet();
1258
+ function forgetCommit(key) {
1259
+ state.commits.delete(key);
1260
+ commitAnalyses.delete(key);
1283
1261
  }
1284
1262
  function ingest(report) {
1285
1263
  if (!report.receivedAt) report.receivedAt = Date.now();
1286
1264
  state.reports.push(report);
1287
- if (state.reports.length > MAX_REPORTS) state.reports.shift();
1288
1265
  const node = nodeOfReport(report);
1289
1266
  node.reports.push(report);
1290
- if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1291
1267
  node.total++;
1292
1268
  if (report.avoidable) {
1293
1269
  node.avoidable++;
1294
- if (typeof report.selfDuration === "number") node.wasted += report.selfDuration;
1270
+ totals.avoidable++;
1271
+ totals.perComponent.set(report.component, (totals.perComponent.get(report.component) || 0) + 1);
1272
+ if (typeof report.selfDuration === "number") {
1273
+ node.wasted += report.selfDuration;
1274
+ totals.wasted += report.selfDuration;
1275
+ }
1295
1276
  }
1296
1277
  node.lastReport = report;
1297
1278
  node.flash = true;
1298
1279
  node.flashAt = Date.now();
1299
- const ck = commitKeyFor(report);
1280
+ const ck = report.commitId;
1300
1281
  let list = state.commits.get(ck);
1301
1282
  if (!list) {
1302
1283
  list = [];
1303
1284
  state.commits.set(ck, list);
1304
1285
  state.commitOrder.push(ck);
1305
- if (state.commitOrder.length > MAX_COMMITS) state.commits.delete(state.commitOrder.shift());
1286
+ if (state.commitOrder.length > MAX_COMMITS) forgetCommit(state.commitOrder.shift());
1287
+ }
1288
+ if (list.length < MAX_PER_COMMIT) {
1289
+ list.push(report);
1290
+ commitMember.add(report);
1306
1291
  }
1307
- list.push(report);
1308
1292
  return node;
1309
1293
  }
1294
+ function evict() {
1295
+ const excess = state.reports.length - MAX_REPORTS;
1296
+ if (excess <= 0) return;
1297
+ for (const r of state.reports.splice(0, excess)) {
1298
+ if (r.avoidable) {
1299
+ totals.avoidable--;
1300
+ const n = (totals.perComponent.get(r.component) || 0) - 1;
1301
+ if (n > 0) totals.perComponent.set(r.component, n);
1302
+ else totals.perComponent.delete(r.component);
1303
+ if (typeof r.selfDuration === "number") totals.wasted -= r.selfDuration;
1304
+ }
1305
+ if (!commitMember.has(r)) continue;
1306
+ const list = state.commits.get(r.commitId);
1307
+ if (!list || list[0] !== r) continue;
1308
+ list.shift();
1309
+ if (!list.length) {
1310
+ forgetCommit(r.commitId);
1311
+ const i = state.commitOrder.indexOf(r.commitId);
1312
+ if (i >= 0) state.commitOrder.splice(i, 1);
1313
+ }
1314
+ }
1315
+ if (totals.wasted < 0) totals.wasted = 0;
1316
+ }
1310
1317
  function flush() {
1311
1318
  flushScheduled = false;
1312
1319
  if (!queue.length) return;
1313
1320
  const batch = queue;
1314
1321
  queue = [];
1315
- state.legacyCommit++;
1316
1322
  let touchedSelected = false;
1317
1323
  let avoidableCount = 0;
1324
+ const touched = /* @__PURE__ */ new Set();
1318
1325
  for (const r of batch) {
1319
1326
  const node = ingest(r);
1327
+ touched.add(node);
1320
1328
  if (state.recording && state.recording.reports.length < MAX_REPORTS) state.recording.reports.push(r);
1321
1329
  if (r.avoidable) avoidableCount++;
1322
1330
  if (state.selectedKey === node.key) {
@@ -1324,11 +1332,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1324
1332
  if (state.tab === "latest") state.selectedReport = r;
1325
1333
  }
1326
1334
  }
1327
- renderLeft();
1335
+ for (const node of touched) trimNode(node);
1336
+ evict();
1337
+ dataGen++;
1328
1338
  renderStream(batch);
1329
1339
  renderSummary();
1330
- if (touchedSelected || state.view === "commits" || state.view === "fixes" || state.tab === "root" || state.recording && state.tab === "session") renderDetails();
1331
- if (state.polling && avoidableCount) transport.badge?.(state.reports.filter((r) => r.avoidable).length);
1340
+ const heavy = state.view === "commits" || state.view === "fixes" || state.tab === "root" || !!(state.recording && state.tab === "session");
1341
+ if (state.view === "tree") {
1342
+ renderTree();
1343
+ if (touchedSelected && !heavy) renderDetails();
1344
+ }
1345
+ scheduleHeavy(heavy || touchedSelected && state.view !== "tree");
1346
+ if (state.polling && avoidableCount) transport.badge?.(totals.avoidable);
1347
+ }
1348
+ let heavyAt = 0;
1349
+ let heavyTimer = null;
1350
+ let heavyDetailsPending = false;
1351
+ function renderHeavy(details2) {
1352
+ heavyAt = Date.now();
1353
+ if (state.view !== "tree") renderLeft();
1354
+ if (details2) renderDetails();
1355
+ }
1356
+ function scheduleHeavy(details2) {
1357
+ const wait = throttled() ? LEFT_RENDER_INTERVAL - (Date.now() - heavyAt) : 0;
1358
+ if (wait <= 0) {
1359
+ if (heavyTimer) clearTimeout(heavyTimer);
1360
+ heavyTimer = null;
1361
+ heavyDetailsPending = false;
1362
+ renderHeavy(details2);
1363
+ return;
1364
+ }
1365
+ heavyDetailsPending = heavyDetailsPending || details2;
1366
+ if (!heavyTimer) {
1367
+ heavyTimer = setTimeout(() => {
1368
+ heavyTimer = null;
1369
+ const d = heavyDetailsPending;
1370
+ heavyDetailsPending = false;
1371
+ renderHeavy(d);
1372
+ }, wait);
1373
+ }
1332
1374
  }
1333
1375
  function enqueue(report) {
1334
1376
  queue.push(report);
@@ -1343,6 +1385,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1343
1385
  state.reports = [];
1344
1386
  state.commits.clear();
1345
1387
  state.commitOrder = [];
1388
+ commitAnalyses.clear();
1389
+ totals.avoidable = 0;
1390
+ totals.wasted = 0;
1391
+ totals.perComponent.clear();
1392
+ bestFix = void 0;
1393
+ bestFixAt = 0;
1394
+ dataGen++;
1346
1395
  state.selectedKey = null;
1347
1396
  state.selectedReport = null;
1348
1397
  state.selectedCommit = null;
@@ -1355,18 +1404,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1355
1404
  renderStream();
1356
1405
  renderSummary();
1357
1406
  }
1358
- const valueQuery = () => state.filter.trim().startsWith("~") ? state.filter.trim().slice(1).toLowerCase() : null;
1359
- function matchesFilter(name) {
1360
- if (!state.filter || valueQuery() !== null) return true;
1407
+ let filterCache = { filter: "", value: null, regex: null, text: "" };
1408
+ function parsedFilter() {
1409
+ if (filterCache.filter === state.filter) return filterCache;
1361
1410
  const f = state.filter.trim();
1362
- const m = /^\/(.+)\/([a-z]*)$/.exec(f);
1411
+ const value = f.startsWith("~") ? f.slice(1).toLowerCase() : null;
1412
+ let regex = null;
1413
+ const m = value === null ? /^\/(.+)\/([a-z]*)$/.exec(f) : null;
1363
1414
  if (m && m[1] !== void 0) {
1364
1415
  try {
1365
- return new RegExp(m[1], m[2]).test(name);
1416
+ regex = new RegExp(m[1], (m[2] || "").replace(/[gy]/g, ""));
1366
1417
  } catch {
1367
1418
  }
1368
1419
  }
1369
- return name.toLowerCase().includes(f.toLowerCase());
1420
+ filterCache = { filter: state.filter, value, regex, text: f.toLowerCase() };
1421
+ return filterCache;
1422
+ }
1423
+ const valueQuery = () => parsedFilter().value;
1424
+ function matchesFilter(name) {
1425
+ const f = parsedFilter();
1426
+ if (!f.filter || f.value !== null) return true;
1427
+ if (f.regex) return f.regex.test(name);
1428
+ return name.toLowerCase().includes(f.text);
1370
1429
  }
1371
1430
  const valueCache = /* @__PURE__ */ new WeakMap();
1372
1431
  function matchesValues(r) {
@@ -1390,10 +1449,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1390
1449
  for (const c of node.children.values()) if (visible(c)) return true;
1391
1450
  return false;
1392
1451
  }
1393
- const isMuted = (component) => !!state.notes[component]?.muted;
1394
1452
  const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
1395
- const ranked = () => state.reports.filter((r) => passes(r) && !isMuted(r.component));
1396
- const filteredReports = () => state.reports.filter(passes);
1453
+ let dataGen = 0;
1454
+ let filtered = { key: "", reports: [], fixes: null };
1455
+ function filteredReports() {
1456
+ const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
1457
+ if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
1458
+ return filtered.reports;
1459
+ }
1460
+ function filteredFixes() {
1461
+ const reports = filteredReports();
1462
+ if (!filtered.fixes) filtered.fixes = rankFixes(reports);
1463
+ return filtered.fixes;
1464
+ }
1397
1465
  function setView(view) {
1398
1466
  state.view = view;
1399
1467
  for (const [id, b] of viewButtons) b.classList.toggle("active", id === view);
@@ -1472,7 +1540,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1472
1540
  row.classList.toggle("selected", state.selectedKey === node.key);
1473
1541
  row.setAttribute("aria-selected", state.selectedKey === node.key ? "true" : "false");
1474
1542
  row.setAttribute("aria-level", String(depth + 1));
1475
- row.classList.toggle("muted", isMuted(node.lastReport ? node.lastReport.component : node.name));
1476
1543
  const indent = row.querySelector(".indent");
1477
1544
  if (indent.childElementCount !== depth) {
1478
1545
  indent.textContent = "";
@@ -1574,7 +1641,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1574
1641
  for (const r of filteredReports()) {
1575
1642
  let o = byName.get(r.component);
1576
1643
  if (!o) {
1577
- o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(), places: 0, lastSeen: 0, reports: [], fix: "", muted: isMuted(r.component) };
1644
+ o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(), reports: [], fix: "" };
1578
1645
  byName.set(r.component, o);
1579
1646
  }
1580
1647
  o.total++;
@@ -1583,12 +1650,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1583
1650
  if (typeof r.selfDuration === "number") o.wasted += r.selfDuration;
1584
1651
  }
1585
1652
  o.paths.add(keyOf(r.path));
1586
- o.lastSeen = Math.max(o.lastSeen, r.receivedAt);
1587
1653
  o.reports.push(r);
1588
1654
  }
1589
1655
  const rows = [...byName.values()];
1590
1656
  for (const o of rows) {
1591
- o.places = o.paths.size;
1592
1657
  const fixes = o.avoidable ? rankFixes(o.reports) : [];
1593
1658
  o.fix = fixes.length ? fixes[0].label : "";
1594
1659
  }
@@ -1601,62 +1666,25 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1601
1666
  });
1602
1667
  return rows;
1603
1668
  }
1604
- function columnsMenu() {
1605
- const menu = el("details", { class: "columns-menu" }, [el("summary", { title: "Choose columns", text: "\u2699 Columns" })]);
1606
- const box = el("div", { class: "menu" });
1607
- for (const col of OPTIONAL_COLUMNS) {
1608
- const input = el("input", { type: "checkbox" });
1609
- input.checked = state.columns.includes(col.key);
1610
- input.addEventListener("change", () => {
1611
- state.columns = OPTIONAL_COLUMNS.map((c) => c.key).filter((k) => k === col.key ? input.checked : state.columns.includes(k));
1612
- renderOffenders();
1613
- persist();
1614
- });
1615
- box.append(el("label", { class: "opt" }, [input, col.label]));
1616
- }
1617
- menu.append(box);
1618
- return menu;
1619
- }
1620
- const offenderCell = (o, key) => {
1621
- switch (key) {
1622
- case "places":
1623
- return el("td", { class: "num", text: String(o.places) });
1624
- case "lastSeen":
1625
- return el("td", { class: "num mono", text: o.lastSeen ? fmtTime(o.lastSeen) : "" });
1626
- default:
1627
- return el("td");
1628
- }
1629
- };
1630
- function offenderRow(o, tag) {
1631
- const cells = [
1632
- el("td", { class: "c" }, [el("span", { class: "name", text: o.component }), o.muted ? el("span", { class: "badge", text: "muted" }) : null, o.paths.size > 1 && !state.columns.includes("places") ? el("span", { class: "meta", text: ` \xD7${o.paths.size} places` }) : null]),
1633
- el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
1634
- el("td", { class: "num", text: String(o.total) }),
1635
- el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
1636
- ...state.columns.map((k) => offenderCell(o, k)),
1637
- el("td", { class: "fix", text: o.fix })
1638
- ];
1639
- const attrs = {
1640
- class: (o.avoidable ? "has-avoid" : "") + (o.muted ? " muted" : ""),
1641
- role: tag === "div" ? "listitem" : null,
1642
- onclick: () => {
1643
- const last = o.reports[o.reports.length - 1];
1644
- state.tab = "fix";
1645
- select(nodeOfReport(last), last);
1646
- }
1647
- };
1648
- if (tag === "tr") return el("tr", attrs, cells);
1649
- const row = el("div", { ...attrs, class: "vrow " + attrs.class });
1650
- for (const c of cells) {
1651
- const span = el("span", { class: c.className });
1652
- span.append(...c.childNodes);
1653
- row.append(span);
1654
- }
1655
- return row;
1656
- }
1657
- const offendersList = virtualList(table, GRID_ROW_H, (o) => offenderRow(o, "div"), { attach: false, headerHeight: () => table.querySelector(".vhead")?.offsetHeight || 0 });
1658
- function offendersHeader() {
1659
- return [sortableHeader("Component", "component"), sortableHeader("Avoidable", "avoidable", true), sortableHeader("Total", "total", true), sortableHeader("Wasted", "wasted", true), ...state.columns.map((k) => sortableHeader(OPTIONAL_COLUMNS.find((c) => c.key === k).label, k, true)), el("th", { text: "Top fix" })];
1669
+ function offenderRow(o) {
1670
+ return el(
1671
+ "tr",
1672
+ {
1673
+ class: o.avoidable ? "has-avoid" : "",
1674
+ onclick: () => {
1675
+ const last = o.reports[o.reports.length - 1];
1676
+ state.tab = "fix";
1677
+ select(nodeOfReport(last), last);
1678
+ }
1679
+ },
1680
+ [
1681
+ el("td", { class: "c" }, [el("span", { class: "name", text: o.component }), o.paths.size > 1 ? el("span", { class: "meta", text: ` \xD7${o.paths.size} places` }) : null]),
1682
+ el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
1683
+ el("td", { class: "num", text: String(o.total) }),
1684
+ el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
1685
+ el("td", { class: "fix", text: o.fix })
1686
+ ]
1687
+ );
1660
1688
  }
1661
1689
  function renderOffenders() {
1662
1690
  table.textContent = "";
@@ -1665,28 +1693,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1665
1693
  table.append(el("div", { class: "empty", text: "No re-renders reported yet." }));
1666
1694
  return;
1667
1695
  }
1668
- const gridCols = `minmax(140px, 2fr) 70px 60px 80px ${state.columns.map(() => "90px").join(" ")} minmax(120px, 2fr)`;
1669
- if (rows.length > VIRTUAL_THRESHOLD) {
1670
- const head = el("div", { class: "vhead", style: `grid-template-columns:${gridCols}` });
1671
- for (const th of offendersHeader()) {
1672
- const cell = el("span", { class: th.className, onclick: null });
1673
- cell.append(...th.childNodes);
1674
- cell.addEventListener("click", () => th.click());
1675
- head.append(cell);
1676
- }
1677
- head.append(columnsMenu());
1678
- table.append(head, offendersList.inner);
1679
- offendersList.inner.style.setProperty("--grid-cols", gridCols);
1680
- offendersList.setItems(rows);
1681
- return;
1682
- }
1683
1696
  const t = el("table", { class: "grid" });
1684
- const headRow = el("tr", null, offendersHeader());
1685
- t.append(el("thead", null, headRow));
1697
+ t.append(el("thead", null, el("tr", null, [sortableHeader("Component", "component"), sortableHeader("Avoidable", "avoidable", true), sortableHeader("Total", "total", true), sortableHeader("Wasted", "wasted", true), el("th", { text: "Top fix" })])));
1686
1698
  const body = el("tbody");
1687
- for (const o of rows) body.append(offenderRow(o, "tr"));
1699
+ for (const o of rows) body.append(offenderRow(o));
1688
1700
  t.append(body);
1689
- table.append(el("div", { class: "table-tools" }, columnsMenu()), t);
1701
+ table.append(t);
1690
1702
  }
1691
1703
  function commitSummaries() {
1692
1704
  const out = [];
@@ -1694,7 +1706,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1694
1706
  const key = state.commitOrder[i];
1695
1707
  const reports = state.commits.get(key);
1696
1708
  if (!reports || !reports.some(passes)) continue;
1697
- out.push({ key, analysis: analyzeCommit(reports) });
1709
+ out.push({ key, analysis: analysisFor(key, reports) });
1698
1710
  }
1699
1711
  return out;
1700
1712
  }
@@ -1721,7 +1733,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1721
1733
  const root2 = analysis.roots[0];
1722
1734
  list.append(
1723
1735
  el("li", { class: (state.selectedCommit === key && state.tab === "commit" ? "selected " : "") + (analysis.avoidable ? "has-avoid" : ""), onclick: () => showCommit(key) }, [
1724
- el("span", { class: "id", text: key > 0 ? `#${key}` : "\u2014" }),
1736
+ el("span", { class: "id", text: `#${key}` }),
1725
1737
  el("span", { class: "t", text: fmtTime(analysis.receivedAt) }),
1726
1738
  el("span", { class: "n", text: plural(analysis.total, "render") }),
1727
1739
  analysis.avoidable ? el("span", { class: "badge avoid", text: `${analysis.avoidable} avoidable` }) : el("span", { class: "badge", text: "ok" }),
@@ -1736,13 +1748,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1736
1748
  }
1737
1749
  table.append(list);
1738
1750
  }
1739
- function fixItem(f, tag) {
1751
+ function fixItem(f) {
1752
+ const selected = state.selectedFix === f.key && state.tab === "fixlist";
1740
1753
  return el(
1741
- tag,
1754
+ "li",
1742
1755
  {
1743
- class: (tag === "div" ? "vrow fix-row " : "") + (state.selectedFix === f.key && state.tab === "fixlist" ? "selected" : ""),
1744
- role: tag === "div" ? "listitem" : null,
1745
- "aria-selected": state.selectedFix === f.key && state.tab === "fixlist" ? "true" : null,
1756
+ class: selected ? "selected" : "",
1757
+ "aria-selected": selected ? "true" : null,
1746
1758
  onclick: () => {
1747
1759
  state.selectedFix = f.key;
1748
1760
  state.tab = "fixlist";
@@ -1757,27 +1769,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1757
1769
  ]
1758
1770
  );
1759
1771
  }
1760
- const fixesList = virtualList(table, GRID_ROW_H, (f) => fixItem(f, "div"), { attach: false, headerHeight: () => table.querySelector(".section-title")?.offsetHeight || 0 });
1761
1772
  function renderFixes() {
1762
1773
  table.textContent = "";
1763
- const reports = ranked();
1764
- const fixes = rankFixes(reports);
1774
+ const reports = filteredReports();
1775
+ const fixes = filteredFixes();
1765
1776
  const contexts = contextAttribution(reports);
1766
- const mutedCount = Object.values(state.notes).filter((n) => n.muted).length;
1767
1777
  if (!fixes.length && !contexts.length) {
1768
- table.append(el("div", { class: "empty", text: mutedCount ? `No avoidable re-renders outside the ${plural(mutedCount, "muted component")}.` : "No avoidable re-renders, nothing to fix." }));
1778
+ table.append(el("div", { class: "empty", text: "No avoidable re-renders, nothing to fix." }));
1769
1779
  return;
1770
1780
  }
1771
1781
  if (fixes.length) {
1772
- const title = el("div", { class: "section-title", text: `Ranked by avoidable re-renders removed${mutedCount ? ` (${plural(mutedCount, "muted component")} hidden)` : ""}` });
1773
- if (fixes.length > VIRTUAL_THRESHOLD) {
1774
- table.append(title, fixesList.inner);
1775
- fixesList.setItems(fixes);
1776
- } else {
1777
- const list = el("ol", { class: "fixes", role: "list" });
1778
- for (const f of fixes) list.append(fixItem(f, "li"));
1779
- table.append(title, list);
1780
- }
1782
+ const list = el("ol", { class: "fixes", role: "list" });
1783
+ for (const f of fixes) list.append(fixItem(f));
1784
+ table.append(el("div", { class: "section-title", text: "Ranked by avoidable re-renders removed" }), list);
1781
1785
  }
1782
1786
  if (contexts.length) {
1783
1787
  const list = el("ul", { class: "contexts" });
@@ -1816,9 +1820,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1816
1820
  openSource: transport.openResource ? (src) => transport.openResource(src.fileName, src.lineNumber, src.columnNumber) : null,
1817
1821
  highlight: transport.highlight ? (id) => transport.highlight(id) : null,
1818
1822
  copy: copyText,
1819
- share: transport.panelUrl ? (r) => {
1820
- void encodeShare(r).then((code) => copyText(`${transport.panelUrl}?report=${code}`));
1821
- } : null,
1822
1823
  readSource: transport.readSource ? (url) => transport.readSource(url) : null
1823
1824
  });
1824
1825
  const tabButton = (id, label, onclick) => el("button", { class: state.tab === id ? "active" : "", onclick }, label);
@@ -1827,15 +1828,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1827
1828
  details.append(el("div", { class: "empty", text: "Select a component to see why it re-rendered." }));
1828
1829
  return;
1829
1830
  }
1830
- const componentName = node.lastReport ? node.lastReport.component : node.name;
1831
- const note = state.notes[componentName] || {};
1832
- const noteBtn = el("button", { class: "ib small" + (note.note ? " active" : ""), title: note.note ? note.note : "Add a note for this component", "aria-label": "Note", onclick: () => toggleNoteEditor() }, [el("span", { class: "glyph", text: "\u270E" })]);
1833
- const muteBtn = el("button", { class: "ib small" + (note.muted ? " active" : ""), title: note.muted ? "Muted: hidden from Fixes and the summary. Click to unmute." : "Mute: hide this component from Fixes and the summary", "aria-label": note.muted ? "Unmute" : "Mute", onclick: () => setNote(componentName, { muted: !note.muted }) }, [el("span", { class: "glyph", text: note.muted ? "\u{1F515}" : "\u{1F514}" })]);
1834
1831
  const header = el("div", { class: "details-header" }, [
1835
1832
  el("span", { class: "title" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: node.name }), el("span", { class: "bracket", text: ">" })]),
1836
1833
  el("span", { class: "meta", text: `${plural(node.total, "re-render")}, ${node.avoidable} avoidable${node.wasted ? ", " + fmtMs(node.wasted) + " wasted" : ""}` }),
1837
- noteBtn,
1838
- muteBtn,
1839
1834
  el("span", { class: "tabs" }, [
1840
1835
  tabButton("latest", "Report", () => {
1841
1836
  state.tab = "latest";
@@ -1858,21 +1853,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1858
1853
  details.append(header);
1859
1854
  const body = el("div", { class: "details-body" });
1860
1855
  details.append(body);
1861
- if (note.note || noteEditorOpen === componentName) {
1862
- const input = el("input", { type: "text", class: "note-input", placeholder: 'Note for this component (e.g. "known, ticket #123")', value: note.note || "" });
1863
- input.addEventListener("change", () => {
1864
- setNote(componentName, { note: input.value.trim() || void 0 });
1865
- noteEditorOpen = null;
1866
- });
1867
- input.addEventListener("keydown", (e) => {
1868
- if (e.key === "Escape") {
1869
- noteEditorOpen = null;
1870
- renderDetails();
1871
- }
1872
- });
1873
- body.append(el("div", { class: "note-box" + (note.muted ? " muted" : "") }, [el("span", { class: "glyph", text: "\u270E" }), input, note.muted ? el("span", { class: "badge", text: "muted" }) : null]));
1874
- if (noteEditorOpen === componentName) setTimeout(() => input.focus(), 0);
1875
- }
1876
1856
  if (state.tab === "history") {
1877
1857
  const list = el("ul", { class: "history" });
1878
1858
  for (const r2 of [...node.reports].reverse()) {
@@ -1927,10 +1907,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1927
1907
  details.append(el("div", { class: "empty", text: "This commit is no longer buffered." }));
1928
1908
  return;
1929
1909
  }
1930
- const a = analyzeCommit(reports);
1910
+ const a = analysisFor(key, reports);
1931
1911
  details.append(
1932
1912
  el("div", { class: "details-header" }, [
1933
- el("span", { class: "title", text: key > 0 ? `Commit #${key}` : "Commit" }),
1913
+ el("span", { class: "title", text: `Commit #${key}` }),
1934
1914
  el("span", { class: "meta", text: `${plural(a.total, "render")}, ${a.avoidable} avoidable${a.wasted ? ", " + fmtMs(a.wasted) + " wasted" : ""} \xB7 ${fmtTime(a.receivedAt)}` })
1935
1915
  ])
1936
1916
  );
@@ -1988,7 +1968,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1988
1968
  return list;
1989
1969
  }
1990
1970
  function renderFixDetails() {
1991
- const fix = rankFixes(filteredReports()).find((f) => f.key === state.selectedFix);
1971
+ const fix = filteredFixes().find((f) => f.key === state.selectedFix);
1992
1972
  if (!fix) {
1993
1973
  details.append(el("div", { class: "empty", text: "Select a fix." }));
1994
1974
  return;
@@ -2001,7 +1981,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2001
1981
  }
2002
1982
  function renderRootDetails() {
2003
1983
  const name = state.selectedRoot;
2004
- const s = rootCauseSummary(name, state.commits);
1984
+ const s = rootCauseSummary(name, state.commits, analysisFor);
2005
1985
  details.append(
2006
1986
  el("div", { class: "details-header" }, [
2007
1987
  el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
@@ -2016,7 +1996,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2016
1996
  for (const c of s.commits) {
2017
1997
  list.append(
2018
1998
  el("li", { onclick: () => showCommit(c.key) }, [
2019
- el("span", { class: "id", text: c.key > 0 ? `#${c.key}` : "\u2014" }),
1999
+ el("span", { class: "id", text: `#${c.key}` }),
2020
2000
  el("span", { class: "t", text: fmtTime(c.analysis.receivedAt) }),
2021
2001
  el("span", { class: "badge avoid", text: `${c.count} avoidable` }),
2022
2002
  el("span", { class: "root", text: componentList(c.components) })
@@ -2029,37 +2009,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2029
2009
  body.append(fixView(s.fixes, { copy: copyText }));
2030
2010
  }
2031
2011
  }
2032
- let noteEditorOpen = null;
2033
- function toggleNoteEditor() {
2034
- const node = state.selectedKey ? state.nodesByKey.get(state.selectedKey) : null;
2035
- const name = node ? node.lastReport ? node.lastReport.component : node.name : null;
2036
- noteEditorOpen = noteEditorOpen === name ? null : name;
2037
- renderDetails();
2038
- }
2039
- function setNote(component, patch) {
2040
- const next = { ...state.notes[component] || {}, ...patch };
2041
- if (!next.note && !next.muted) delete state.notes[component];
2042
- else state.notes[component] = next;
2043
- if (transport.storage) transport.storage.set("notes", state.notes);
2044
- renderSummary();
2045
- renderLeft();
2046
- renderDetails();
2047
- }
2048
- function restoreNotes(raw) {
2049
- if (!isRecord(raw)) return;
2050
- const notes = {};
2051
- for (const [k, v] of Object.entries(raw)) {
2052
- if (!isRecord(v)) continue;
2053
- const n = {};
2054
- if (typeof v.note === "string" && v.note) n.note = v.note;
2055
- if (v.muted === true) n.muted = true;
2056
- if (n.note || n.muted) notes[k] = n;
2057
- }
2058
- state.notes = notes;
2059
- renderSummary();
2060
- renderLeft();
2061
- renderDetails();
2062
- }
2063
2012
  const sessionSummary = (s) => summarizeSession(s, s.reports);
2064
2013
  function persistSessions() {
2065
2014
  if (!transport.storage) return;
@@ -2278,10 +2227,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2278
2227
  };
2279
2228
  const text = JSON.stringify(data, null, 2);
2280
2229
  const name = `rerender-lens-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2281
- if (transport.download) {
2282
- transport.download(name, text);
2283
- return;
2284
- }
2285
2230
  try {
2286
2231
  const blob = new Blob([text], { type: "application/json" });
2287
2232
  const url = URL.createObjectURL(blob);
@@ -2333,6 +2278,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2333
2278
  cls = "connected";
2334
2279
  title = state.relay ? "live via content script" : "polling the page";
2335
2280
  if (lib.overhead) title += ` \xB7 library overhead ${lib.overhead.totalMs.toFixed(1)} ms over ${plural(lib.commits ?? 0, "commit")}, worst ${lib.overhead.maxCommitMs.toFixed(1)} ms`;
2281
+ if (lib.truncated) title += ` \xB7 ${plural(lib.truncated, "report")} skipped by the per-commit cap`;
2336
2282
  } else if (state.relay || state.polling) {
2337
2283
  text = "no library in page";
2338
2284
  cls = "partial";
@@ -2347,15 +2293,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2347
2293
  tabChip.textContent = state.tabLabel || "";
2348
2294
  banner.textContent = "";
2349
2295
  const warnings = [];
2350
- if (lib && typeof lib.protocol === "number" && lib.protocol !== PROTOCOL) {
2351
- warnings.push(
2352
- lib.protocol < PROTOCOL ? `The page runs rerender-lens ${lib.library || ""} (protocol ${lib.protocol}); this panel expects protocol ${PROTOCOL}. Update the rerender-lens package for commit grouping, source links and settings.` : `The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`
2353
- );
2354
- }
2296
+ if (lib && typeof lib.protocol === "number" && lib.protocol > PROTOCOL) warnings.push(`The page runs a newer rerender-lens (protocol ${lib.protocol}) than this panel (${PROTOCOL}). Update the extension.`);
2355
2297
  if (lib && lib.production) warnings.push("Production React build detected: component names may be minified and hooks are unlabeled. Use a development build.");
2356
2298
  if (lib && lib.injected && lib.source === "page")
2357
2299
  warnings.push(`The page runs its own rerender-lens ${lib.library || ""}; the copy injected by the extension stepped aside. Turn injection off for this origin in Settings to avoid loading the library twice.`);
2358
2300
  if (lib && lib.enabled === false) warnings.push("rerender-lens is present but disabled in this page.");
2301
+ if (lib && lib.truncated) warnings.push(`${plural(lib.truncated, "report")} skipped: a commit re-rendered more tracked components than the per-commit cap (200) or took over its time budget. Narrow "include" in Settings, or fix the top offenders first.`);
2359
2302
  banner.hidden = warnings.length === 0;
2360
2303
  for (const w of warnings) banner.append(el("div", { text: w }));
2361
2304
  }
@@ -2460,10 +2403,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2460
2403
  sec.append(el("div", { class: "meta", text: "Connect to a page running rerender-lens to change its options." }));
2461
2404
  return;
2462
2405
  }
2463
- if (lib.protocol < PROTOCOL) {
2464
- sec.append(el("div", { class: "meta", text: "The page library is too old to be configured from here." }));
2465
- return;
2466
- }
2467
2406
  const current = Object.assign({}, lib.options || {});
2468
2407
  const applyOptions = async (patch) => {
2469
2408
  Object.assign(current, patch);
@@ -2522,7 +2461,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2522
2461
  renderStatus();
2523
2462
  break;
2524
2463
  case "hello":
2525
- setLibrary(isRecord(message.payload) ? Object.assign({ protocol: message.version || 1 }, message.payload) : { protocol: message.version || 1 });
2464
+ if (isRecord(message.payload)) setLibrary(message.payload);
2526
2465
  break;
2527
2466
  case "clear":
2528
2467
  case "navigated":
@@ -2538,6 +2477,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2538
2477
  if (r) enqueue(r);
2539
2478
  break;
2540
2479
  }
2480
+ case "batch":
2481
+ if (Array.isArray(message.items)) for (const item of message.items) handle(item);
2482
+ break;
2541
2483
  }
2542
2484
  }
2543
2485
  const panelApi = {
@@ -2552,8 +2494,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2552
2494
  setView,
2553
2495
  openSettings: () => toggleSettings(true),
2554
2496
  startRecording,
2555
- stopRecording,
2556
- setNote
2497
+ stopRecording
2557
2498
  };
2558
2499
  if (options.theme === "dark") document.documentElement.classList.add("theme-dark");
2559
2500
  state.origin = transport.origin || null;
@@ -2568,12 +2509,60 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2568
2509
  });
2569
2510
  Promise.resolve(transport.storage.get("sessions")).then(restoreSessions, () => {
2570
2511
  });
2571
- Promise.resolve(transport.storage.get("notes")).then(restoreNotes, () => {
2572
- });
2573
2512
  }
2574
2513
  transport.subscribe(handle);
2575
2514
  return panelApi;
2576
2515
  }
2516
+ var systemTheme = () => typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
2517
+ var fetchSource = (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null);
2518
+ function localStorageAdapter(prefix) {
2519
+ const mem = (key) => `${prefix}:${key}`;
2520
+ return {
2521
+ get: (key) => {
2522
+ try {
2523
+ const raw = localStorage.getItem(mem(key));
2524
+ return raw ? JSON.parse(raw) : void 0;
2525
+ } catch {
2526
+ return void 0;
2527
+ }
2528
+ },
2529
+ set: (key, value) => {
2530
+ try {
2531
+ localStorage.setItem(mem(key), JSON.stringify(value));
2532
+ } catch {
2533
+ }
2534
+ }
2535
+ };
2536
+ }
2537
+ function commandBridge(send, timeoutMs) {
2538
+ const pending = /* @__PURE__ */ new Map();
2539
+ const bridge = (cmd, arg) => new Promise((resolve, reject) => {
2540
+ const id = Math.random().toString(36).slice(2);
2541
+ const fail = () => {
2542
+ clearTimeout(timer);
2543
+ pending.delete(id);
2544
+ resolve(null);
2545
+ };
2546
+ const timer = setTimeout(fail, timeoutMs);
2547
+ pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
2548
+ try {
2549
+ void Promise.resolve(send({ __rerenderLensCmd: true, id, cmd, arg })).catch(fail);
2550
+ } catch {
2551
+ fail();
2552
+ }
2553
+ });
2554
+ const reply = (m) => {
2555
+ if (m.__rerenderLensReply !== true || typeof m.id !== "string") return false;
2556
+ const p = pending.get(m.id);
2557
+ if (p) {
2558
+ pending.delete(m.id);
2559
+ clearTimeout(p.timer);
2560
+ p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
2561
+ }
2562
+ return true;
2563
+ };
2564
+ return { bridge, reply };
2565
+ }
2577
2566
  function createRelayTransport(io) {
2578
2567
  let listener = null;
2579
2568
  let relayConnected = false;
@@ -2600,18 +2589,55 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2600
2589
  try {
2601
2590
  const info = await io.bridge("info");
2602
2591
  if (info) {
2603
- emit({ type: "hello", version: info.protocol || 1, payload: info });
2592
+ emit({ type: "hello", version: info.protocol, payload: info });
2604
2593
  return true;
2605
2594
  }
2606
2595
  } catch {
2607
2596
  }
2608
2597
  return false;
2609
2598
  }
2599
+ let syncGen = 0;
2600
+ let syncTimer = null;
2601
+ let disposed = false;
2602
+ function cancelSync() {
2603
+ syncGen++;
2604
+ if (syncTimer) clearTimeout(syncTimer);
2605
+ syncTimer = null;
2606
+ }
2607
+ function syncWithRetry(onReady, initialDelay = 0) {
2608
+ cancelSync();
2609
+ const gen = syncGen;
2610
+ let delay = SYNC_RETRY_MIN;
2611
+ let tries = 0;
2612
+ const attempt = async () => {
2613
+ syncTimer = null;
2614
+ if (gen !== syncGen || disposed) return;
2615
+ const ok = await syncWithPage();
2616
+ if (gen !== syncGen || disposed) return;
2617
+ if (ok) {
2618
+ onReady();
2619
+ return;
2620
+ }
2621
+ if (++tries >= SYNC_RETRIES) return;
2622
+ syncTimer = setTimeout(() => void attempt(), delay);
2623
+ delay = Math.min(delay * 2, SYNC_RETRY_MAX);
2624
+ };
2625
+ if (initialDelay > 0) syncTimer = setTimeout(() => void attempt(), initialDelay);
2626
+ else void attempt();
2627
+ }
2628
+ let droppedSeen = false;
2629
+ function resetSince() {
2630
+ since = 0;
2631
+ droppedSeen = false;
2632
+ }
2610
2633
  async function pollOnce() {
2611
2634
  try {
2612
2635
  const res = await io.bridge("pull", since);
2613
2636
  if (!res) return;
2614
- if (res.dropped) emit({ type: "clear" });
2637
+ if (res.dropped && !droppedSeen) {
2638
+ droppedSeen = true;
2639
+ emit({ type: "clear" });
2640
+ }
2615
2641
  for (const p of res.reports) emit({ type: "report", payload: p });
2616
2642
  since = res.seq;
2617
2643
  } catch {
@@ -2627,19 +2653,23 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2627
2653
  emit({ type: "polling", on: false });
2628
2654
  }
2629
2655
  }
2630
- async function attachToPage() {
2631
- if (!await syncWithPage()) return;
2632
- if (relayConnected) io.bridge("replay").catch(() => {
2633
- });
2634
- else {
2635
- const res = await io.bridge("pull", 0).catch(() => null);
2636
- if (res) {
2637
- for (const p of res.reports) emit({ type: "report", payload: p });
2638
- since = res.seq;
2639
- } else io.bridge("replay").catch(() => {
2656
+ function attachToPage(initialDelay = 0) {
2657
+ syncWithRetry(() => {
2658
+ if (relayConnected) io.bridge("replay").catch(() => {
2640
2659
  });
2641
- setPolling(true);
2642
- }
2660
+ else {
2661
+ const gen = syncGen;
2662
+ void io.bridge("pull", 0).catch(() => null).then((res) => {
2663
+ if (gen !== syncGen || disposed) return;
2664
+ if (res) {
2665
+ for (const p of res.reports) emit({ type: "report", payload: p });
2666
+ since = res.seq;
2667
+ } else io.bridge("replay").catch(() => {
2668
+ });
2669
+ setPolling(true);
2670
+ });
2671
+ }
2672
+ }, initialDelay);
2643
2673
  }
2644
2674
  function connect() {
2645
2675
  if (panelPort) {
@@ -2660,16 +2690,20 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2660
2690
  setPolling(false);
2661
2691
  } else if (m.type === "disconnected") {
2662
2692
  relayConnected = false;
2663
- void syncWithPage().then((ok) => ok && setPolling(true));
2693
+ syncWithRetry(() => setPolling(true));
2664
2694
  }
2665
2695
  emit(m);
2666
2696
  });
2667
2697
  port.onDisconnect.addListener(() => {
2668
2698
  if (panelPort !== port) return;
2669
2699
  panelPort = null;
2670
- setTimeout(connect, 1e3);
2700
+ reconnectTimer = setTimeout(() => {
2701
+ reconnectTimer = null;
2702
+ if (!disposed) connect();
2703
+ }, 1e3);
2671
2704
  });
2672
2705
  }
2706
+ let reconnectTimer = null;
2673
2707
  const transport = {
2674
2708
  origin,
2675
2709
  tabLabel: io.tabLabel ?? null,
@@ -2677,36 +2711,44 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2677
2711
  listener = fn;
2678
2712
  connect();
2679
2713
  io.onNavigated(() => {
2680
- since = 0;
2714
+ if (disposed) return;
2715
+ resetSince();
2716
+ setPolling(false);
2717
+ cancelSync();
2681
2718
  fn({ type: "navigated" });
2682
2719
  void resolveOrigin().then(() => {
2683
- setTimeout(() => void attachToPage(), 1200);
2720
+ if (!disposed) attachToPage(NAVIGATION_SETTLE);
2684
2721
  });
2685
2722
  });
2686
2723
  io.onTabChange?.((label) => {
2724
+ if (disposed) return;
2687
2725
  transport.tabLabel = label;
2688
2726
  const next = io.tabId();
2689
2727
  if (next === currentTab) {
2690
2728
  fn({ type: "tab-label", payload: label });
2691
2729
  return;
2692
2730
  }
2693
- since = 0;
2731
+ resetSince();
2694
2732
  relayConnected = false;
2695
2733
  setPolling(false);
2734
+ cancelSync();
2696
2735
  currentTab = next;
2697
2736
  void resolveOrigin().then(() => {
2737
+ if (disposed) return;
2698
2738
  fn({ type: "tab", payload: label });
2699
2739
  connect();
2700
- void attachToPage();
2740
+ attachToPage();
2701
2741
  });
2702
2742
  });
2703
- void resolveOrigin().then(() => attachToPage());
2743
+ void resolveOrigin().then(() => {
2744
+ if (!disposed) attachToPage();
2745
+ });
2704
2746
  },
2705
2747
  replay() {
2706
2748
  if (relayConnected) io.bridge("replay").catch(() => {
2707
2749
  });
2708
2750
  else {
2709
- since = 0;
2751
+ resetSince();
2710
2752
  emit({ type: "clear" });
2711
2753
  void syncWithPage().then(() => pollOnce());
2712
2754
  }
@@ -2714,7 +2756,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2714
2756
  clear() {
2715
2757
  io.bridge("clear").catch(() => {
2716
2758
  });
2717
- since = 0;
2759
+ resetSince();
2760
+ },
2761
+ dispose() {
2762
+ disposed = true;
2763
+ cancelSync();
2764
+ setPolling(false);
2765
+ if (reconnectTimer) clearTimeout(reconnectTimer);
2766
+ reconnectTimer = null;
2767
+ if (panelPort) {
2768
+ try {
2769
+ panelPort.disconnect();
2770
+ } catch {
2771
+ }
2772
+ panelPort = null;
2773
+ }
2774
+ listener = null;
2718
2775
  },
2719
2776
  configure: (options) => io.bridge("configure", options).then((r) => r ?? void 0),
2720
2777
  highlight: (id) => io.bridge("highlight", id).catch(() => {
@@ -2737,7 +2794,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2737
2794
  if (io.openResource) transport.openResource = io.openResource;
2738
2795
  if (io.undock) transport.undock = io.undock;
2739
2796
  if (io.readSource) transport.readSource = io.readSource;
2740
- transport.panelUrl = chrome.runtime.getURL("panel.html");
2741
2797
  return transport;
2742
2798
  }
2743
2799
  function devtoolsIO() {
@@ -2749,7 +2805,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2749
2805
  })
2750
2806
  );
2751
2807
  const expressions = {
2752
- info: () => "b.info?b.info():{count:b.size,protocol:b.version}",
2808
+ info: () => "b.info()",
2753
2809
  pull: (since) => `b.pull?b.pull(${Number(since) || 0}):null`,
2754
2810
  replay: () => "b.replay()",
2755
2811
  clear: () => "b.clear()",
@@ -2786,7 +2842,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2786
2842
  try {
2787
2843
  switch (cmd) {
2788
2844
  case "info":
2789
- return b.info ? b.info() : { count: b.size, protocol: b.version };
2845
+ return b.info();
2790
2846
  case "pull":
2791
2847
  return b.pull ? b.pull(arg) : null;
2792
2848
  case "replay":
@@ -2893,19 +2949,17 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2893
2949
  },
2894
2950
  openResource: (url) => void chrome.tabs.create({ url }),
2895
2951
  undock: (mode) => tabId === null ? Promise.reject(new Error("no tab")) : openOutside(mode, tabId),
2896
- // Dev servers serve the module source; host permission for the origin is required (and present when the panel works at all).
2897
- readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null)
2952
+ // Host permission for the origin is required to fetch the module source (and present when the panel works at all).
2953
+ readSource: fetchSource
2898
2954
  };
2899
2955
  return io;
2900
2956
  }
2901
2957
  function bootExtension() {
2902
- const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
2903
- const theme = chrome.devtools.panels.themeName === "dark" || prefersDark ? "dark" : "light";
2958
+ const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : systemTheme();
2904
2959
  createPanel(document.getElementById("root"), createRelayTransport(devtoolsIO()), { theme });
2905
2960
  }
2906
2961
  function bootStandalone(opts = {}) {
2907
- const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
2908
- return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: prefersDark ? "dark" : "light" });
2962
+ return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: systemTheme() });
2909
2963
  }
2910
2964
  function sampleReports() {
2911
2965
  const fn = (name) => FN_PREFIX + name;
@@ -3036,7 +3090,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3036
3090
  function createBroadcastTransport(name) {
3037
3091
  let listener = null;
3038
3092
  let channel = null;
3039
- const pending = /* @__PURE__ */ new Map();
3040
3093
  const emit = (m) => {
3041
3094
  if (listener) listener(m);
3042
3095
  };
@@ -3045,36 +3098,15 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3045
3098
  channel = new BroadcastChannel(name);
3046
3099
  channel.onmessage = (event) => {
3047
3100
  const data = event.data;
3048
- if (!data) return;
3049
- if (data.__rerenderLensReply === true && typeof data.id === "string") {
3050
- const p = pending.get(data.id);
3051
- if (!p) return;
3052
- pending.delete(data.id);
3053
- clearTimeout(p.timer);
3054
- p.resolve(typeof data.error === "string" ? new Error(data.error) : data.result);
3055
- return;
3056
- }
3101
+ if (!data || reply(data)) return;
3057
3102
  if (data.__rerenderLens === true && typeof data.type === "string") emit({ type: data.type, version: typeof data.version === "number" ? data.version : void 0, payload: data.payload });
3058
3103
  };
3059
3104
  return channel;
3060
3105
  };
3061
- const bridge = (cmd, arg) => new Promise((resolve, reject) => {
3062
- const id = Math.random().toString(36).slice(2);
3063
- const timer = setTimeout(() => {
3064
- pending.delete(id);
3065
- resolve(null);
3066
- }, 1500);
3067
- pending.set(id, {
3068
- resolve: (v) => v instanceof Error ? reject(v) : resolve(v),
3069
- timer
3070
- });
3071
- open().postMessage({ __rerenderLensCmd: true, id, cmd, arg });
3072
- });
3073
- const mem = (key) => `rerender-lens:${name}:${key}`;
3106
+ const { bridge, reply } = commandBridge((message) => open().postMessage(message), 1500);
3074
3107
  const transport = {
3075
3108
  origin: location.origin,
3076
3109
  tabLabel: `channel "${name}"`,
3077
- panelUrl: location.origin + location.pathname,
3078
3110
  subscribe(fn) {
3079
3111
  listener = fn;
3080
3112
  open();
@@ -3084,7 +3116,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3084
3116
  fn({ type: "disconnected" });
3085
3117
  return;
3086
3118
  }
3087
- fn({ type: "hello", version: info.protocol || 1, payload: info });
3119
+ fn({ type: "hello", version: info.protocol, payload: info });
3088
3120
  const res = await bridge("pull", 0);
3089
3121
  if (res) for (const p of res.reports) fn({ type: "report", payload: p });
3090
3122
  });
@@ -3096,23 +3128,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3096
3128
  }),
3097
3129
  flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3098
3130
  }),
3099
- storage: {
3100
- get: (key) => {
3101
- try {
3102
- const raw = localStorage.getItem(mem(key));
3103
- return raw ? JSON.parse(raw) : void 0;
3104
- } catch {
3105
- return void 0;
3106
- }
3107
- },
3108
- set: (key, value) => {
3109
- try {
3110
- localStorage.setItem(mem(key), JSON.stringify(value));
3111
- } catch {
3112
- }
3113
- }
3114
- },
3115
- readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
3131
+ storage: localStorageAdapter(`rerender-lens:${name}`),
3132
+ readSource: fetchSource,
3116
3133
  copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3117
3134
  })
3118
3135
  };
@@ -3123,24 +3140,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3123
3140
  let listener = null;
3124
3141
  let stream = null;
3125
3142
  let appsOnline = null;
3126
- const pending = /* @__PURE__ */ new Map();
3127
3143
  const emit = (m) => {
3128
3144
  if (listener) listener(m);
3129
3145
  };
3130
3146
  const post = (message) => fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }).then(() => void 0);
3131
- const bridge = (cmd, arg) => new Promise((resolve, reject) => {
3132
- const id = Math.random().toString(36).slice(2);
3133
- const timer = setTimeout(() => {
3134
- pending.delete(id);
3135
- resolve(null);
3136
- }, 2e3);
3137
- pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
3138
- post({ __rerenderLensCmd: true, id, cmd, arg }).catch(() => {
3139
- clearTimeout(timer);
3140
- pending.delete(id);
3141
- resolve(null);
3142
- });
3143
- });
3147
+ const { bridge, reply } = commandBridge(post, 2e3);
3144
3148
  const handleData = (data) => {
3145
3149
  let parsed;
3146
3150
  try {
@@ -3149,14 +3153,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3149
3153
  return;
3150
3154
  }
3151
3155
  for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
3152
- if (!isRecord(m)) continue;
3153
- if (m.__rerenderLensReply === true && typeof m.id === "string") {
3154
- const p = pending.get(m.id);
3155
- if (!p) continue;
3156
- pending.delete(m.id);
3157
- clearTimeout(p.timer);
3158
- p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
3159
- } else if (m.__rerenderLens === true && m.type === "relay") {
3156
+ if (!isRecord(m) || reply(m)) continue;
3157
+ if (m.__rerenderLens === true && m.type === "relay") {
3160
3158
  const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
3161
3159
  const wasOnline = appsOnline;
3162
3160
  appsOnline = apps;
@@ -3172,15 +3170,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3172
3170
  return;
3173
3171
  }
3174
3172
  emit({ type: "connected" });
3175
- emit({ type: "hello", version: info.protocol || 1, payload: info });
3173
+ emit({ type: "hello", version: info.protocol, payload: info });
3176
3174
  const res = await bridge("pull", 0);
3177
3175
  if (res) for (const p of res.reports) emit({ type: "report", payload: p });
3178
3176
  }
3179
- const mem = (key) => `rerender-lens:relay:${base}:${key}`;
3180
3177
  const transport = {
3181
3178
  origin: base,
3182
3179
  tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
3183
- panelUrl: location.origin + location.pathname,
3184
3180
  subscribe(fn) {
3185
3181
  listener = fn;
3186
3182
  const open = () => {
@@ -3200,47 +3196,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3200
3196
  }),
3201
3197
  flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3202
3198
  }),
3203
- storage: {
3204
- get: (key) => {
3205
- try {
3206
- const raw = localStorage.getItem(mem(key));
3207
- return raw ? JSON.parse(raw) : void 0;
3208
- } catch {
3209
- return void 0;
3210
- }
3211
- },
3212
- set: (key, value) => {
3213
- try {
3214
- localStorage.setItem(mem(key), JSON.stringify(value));
3215
- } catch {
3216
- }
3217
- }
3218
- },
3219
- readSource: (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null),
3199
+ storage: localStorageAdapter(`rerender-lens:relay:${base}`),
3200
+ readSource: fetchSource,
3220
3201
  copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3221
3202
  })
3222
3203
  };
3223
3204
  return transport;
3224
3205
  }
3225
3206
  function bootRelay(relayUrl) {
3226
- const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3227
- return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: prefersDark ? "dark" : "light" });
3207
+ return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: systemTheme() });
3228
3208
  }
3229
3209
  function bootBroadcast(name) {
3230
- const prefersDark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3231
- return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: prefersDark ? "dark" : "light" });
3232
- }
3233
- async function bootShared(code) {
3234
- const report = await decodeShare(code);
3235
- const transport = { subscribe() {
3236
- }, panelUrl: location.origin + location.pathname };
3237
- const panel = createPanel(document.getElementById("root"), transport, { theme: typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" });
3238
- if (!report) {
3239
- panel.handle({ type: "hello", version: PROTOCOL, payload: { protocol: PROTOCOL, library: "shared link", react: [], enabled: false } });
3240
- return;
3241
- }
3242
- panel.importData([report]);
3243
- panel.select(report.component);
3210
+ return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: systemTheme() });
3244
3211
  }
3245
3212
  function bootDemo() {
3246
3213
  const params2 = new URLSearchParams(location.search);
@@ -3253,7 +3220,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3253
3220
  origin: "http://localhost:5199",
3254
3221
  subscribe(fn) {
3255
3222
  fn({ type: "connected" });
3256
- fn({ type: "hello", version: PROTOCOL, payload: { count: 0, library: "demo", protocol: PROTOCOL, react: [{ version: "19.2.0", bundleType: 1 }], production: false, enabled: true, options: { trackAllMemoized: true, silent: true }, source: "page", injected: false } });
3223
+ fn({ type: "hello", version: PROTOCOL, payload: { library: "demo", protocol: PROTOCOL, react: [{ version: "19.2.0", bundleType: 1 }], production: false, enabled: true, options: { trackAllMemoized: true, silent: true }, source: "page", injected: false } });
3257
3224
  if (flood > 0) {
3258
3225
  const t0 = performance.now();
3259
3226
  for (const r of floodReports(flood)) fn({ type: "report", payload: r });
@@ -3283,11 +3250,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3283
3250
  originStatus: () => Promise.resolve({ origin: "http://localhost:5199", builtIn: true, permitted: true, enabled: true, inject: false, deferHook: false }),
3284
3251
  setOrigin: () => Promise.resolve(),
3285
3252
  storage: { get: (k) => Promise.resolve(mem[k]), set: (k, v) => Promise.resolve(mem[k] = v) },
3286
- panelUrl: location.origin + location.pathname,
3287
3253
  readSource: () => Promise.resolve("import { memo } from 'react';\n\nexport const ProductList = memo(function ProductList(props) {\n const [selected, setSelected] = useState(null);\n return (\n <ul>\n {props.products.map((p) => (\n <ProductRow key={p.id} product={p} style={{ color: 'red' }} onSelect={(id) => props.onSelect(id)} />\n ))}\n </ul>\n );\n});\n")
3288
3254
  };
3289
- const dark = /theme=dark/.test(location.search) || typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
3290
- createPanel(document.getElementById("root"), transport, { theme: dark ? "dark" : "light" });
3255
+ createPanel(document.getElementById("root"), transport, { theme: /theme=dark/.test(location.search) ? "dark" : systemTheme() });
3291
3256
  }
3292
3257
  var api = {
3293
3258
  PROTOCOL,
@@ -3306,8 +3271,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3306
3271
  createRelayTransport,
3307
3272
  createBroadcastTransport,
3308
3273
  createRelayClientTransport,
3309
- encodeShare,
3310
- decodeShare,
3311
3274
  sourceContext,
3312
3275
  analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
3313
3276
  };
@@ -3316,8 +3279,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3316
3279
  var hasDevtools = hasChrome && !!chrome.devtools && !!chrome.devtools.inspectedWindow;
3317
3280
  var params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
3318
3281
  var pathname = typeof location !== "undefined" ? String(location.pathname) : "";
3319
- if (params.has("report")) void bootShared(params.get("report") || "");
3320
- else if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
3282
+ if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
3321
3283
  else if (params.has("channel") && typeof BroadcastChannel === "function") bootBroadcast(params.get("channel") || "rerender-lens");
3322
3284
  else if (hasDevtools && /panel\.html/.test(pathname) && !params.has("tabId")) bootExtension();
3323
3285
  else if (hasChrome && (/sidepanel\.html/.test(pathname) || params.has("tabId"))) bootStandalone({ tabId: params.has("tabId") ? Number(params.get("tabId")) : null });