rerender-lens 0.3.0 → 0.5.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 (66) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +192 -353
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BHGUUo-p.d.ts → devtools-BYWat7nQ.d.ts} +17 -3
  10. package/dist/{devtools-DmhWiWcN.d.cts → devtools-DJi_6AfJ.d.cts} +17 -3
  11. package/dist/index.cjs +635 -109
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +620 -110
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +235 -30
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +8 -6
  34. package/dist/playwright.d.ts +8 -6
  35. package/dist/playwright.js +235 -31
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +1100 -337
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +308 -95
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +308 -95
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +452 -87
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +452 -87
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +515 -123
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +516 -124
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +41 -5
  65. package/panel/panel.css +1 -34
  66. package/panel/panel.js +865 -713
package/panel/panel.js CHANGED
@@ -1,188 +1,288 @@
1
1
  /* Built from extension/src/panel.ts by `npm run build`; do not edit by hand. */
2
2
  "use strict";
3
3
  (() => {
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
- var PROTOCOL = 2;
12
- var KIND_LABEL = {
13
- "deep-equal": "equal by value",
14
- function: "new function",
15
- element: "equal element",
16
- different: "changed",
17
- added: "added",
18
- removed: "removed"
19
- };
20
- var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
21
- var FN_PREFIX = "\u0192 ";
22
- var MAX_REPORTS = 2e3;
23
- var MAX_PER_NODE = 200;
24
- var MAX_COMMITS = 500;
25
- var ROW_H = 22;
26
- var ITEM_H = 20;
27
- var OVERSCAN = 8;
28
- var FALLBACK_VIEWPORT = 800;
29
- function el(tag, attrs, children) {
30
- const node = document.createElement(tag);
31
- if (attrs) {
32
- for (const k of Object.keys(attrs)) {
33
- const v = attrs[k];
34
- if (k === "class") node.className = String(v);
35
- else if (k === "text") node.textContent = String(v);
36
- else if (k.startsWith("on")) {
37
- if (typeof v === "function") node.addEventListener(k.slice(2), v);
38
- } else if (v === true) node.setAttribute(k, "");
39
- else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
40
- }
41
- }
42
- if (children) {
43
- for (const c of [].concat(children)) if (c != null) node.append(c);
44
- }
45
- return node;
4
+ // src/diff.ts
5
+ var REACT_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
6
+ var REACT_TRANSITIONAL_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
7
+ function isReactElement(v) {
8
+ if (typeof v !== "object" || v === null) return false;
9
+ const t = v.$$typeof;
10
+ return t === REACT_ELEMENT || t === REACT_TRANSITIONAL_ELEMENT;
46
11
  }
47
- function fmtTime(ms) {
48
- const d = new Date(ms);
49
- const p = (n, w) => String(n).padStart(w, "0");
50
- return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
51
- }
52
- var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
53
- var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
54
- var componentList = (m) => [...m].map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
55
- function changesOf(report) {
56
- return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
12
+ function isPlainObject(v) {
13
+ if (typeof v !== "object" || v === null) return false;
14
+ const proto = Object.getPrototypeOf(v);
15
+ return proto === Object.prototype || proto === null;
57
16
  }
58
- function summarize(report) {
59
- const counts = /* @__PURE__ */ new Map();
60
- for (const c of changesOf(report)) counts.set(c.kind, (counts.get(c.kind) || 0) + 1);
61
- if (counts.size === 0) return "no changes";
62
- return [...counts].map(([k, n]) => `${n} ${KIND_LABEL[k] || k}`).join(", ");
17
+ function remember(seen, a, b) {
18
+ let set = seen.get(a);
19
+ if (!set) {
20
+ set = /* @__PURE__ */ new Set();
21
+ seen.set(a, set);
22
+ }
23
+ if (set.has(b)) return true;
24
+ set.add(b);
25
+ return false;
63
26
  }
64
- var isRecord = (v) => typeof v === "object" && v !== null;
65
- function normalizeReport(p) {
66
- if (!isRecord(p) || typeof p.component !== "string") return null;
67
- const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
68
- const props = isRecord(p.props) ? p.props : {};
69
- const parent = isRecord(p.parent) && typeof p.parent.name === "string" ? { name: p.parent.name, trigger: typeof p.parent.trigger === "string" ? p.parent.trigger : "parent" } : null;
70
- const r = {
71
- component: p.component,
72
- instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
73
- commitId: typeof p.commitId === "number" ? p.commitId : 0,
74
- renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
75
- trigger: typeof p.trigger === "string" ? p.trigger : "parent",
76
- avoidable: !!p.avoidable,
77
- props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
78
- propChanges: arr(p.propChanges),
79
- stateChanges: arr(p.stateChanges),
80
- hookChanges: arr(p.hookChanges),
81
- parent,
82
- owner: typeof p.owner === "string" ? p.owner : null,
83
- path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
84
- reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
85
- time: typeof p.time === "number" ? p.time : 0,
86
- receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
27
+ var scope = null;
28
+ var DEFAULT_DIFF_BUDGET = 5e4;
29
+ function beginDiffScope(budget = 2e5) {
30
+ const previous = scope;
31
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
32
+ return () => {
33
+ scope = previous;
87
34
  };
88
- if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
89
- if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
90
- if (typeof p.memoized === "boolean") r.memoized = p.memoized;
91
- if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
92
- if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
93
- if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
94
- if (isRecord(p.state)) r.state = p.state;
95
- if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
96
- if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
97
- if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
98
- if (typeof p.key === "string") r.key = p.key;
99
- if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
100
- return r;
101
35
  }
102
- function valueNode(v, depth = 0) {
103
- if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
104
- const t = typeof v;
105
- if (typeof v === "string") {
106
- if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
107
- if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
108
- return el("span", { class: "v str", text: JSON.stringify(v) });
36
+ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
37
+ if (Object.is(a, b)) return true;
38
+ if (typeof a !== typeof b) return false;
39
+ if (typeof a !== "object" || a === null || b === null) return false;
40
+ if (!scope) {
41
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
42
+ try {
43
+ return deepEqual(a, b, seen);
44
+ } finally {
45
+ end();
46
+ }
47
+ }
48
+ const sc = scope;
49
+ const objA = a;
50
+ const objB = b;
51
+ const hit = sc.cache.get(objA)?.get(objB);
52
+ if (hit !== void 0) return hit;
53
+ if (--sc.remaining < 0) {
54
+ sc.exhausted = true;
55
+ return false;
109
56
  }
110
- if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
111
- if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
112
- if (Array.isArray(v)) {
113
- const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
114
- if (short) {
115
- const s = el("span", { class: "v" }, "[");
116
- v.forEach((x, i) => {
117
- if (i) s.append(", ");
118
- s.append(valueNode(x, depth + 1));
119
- });
120
- s.append("]");
121
- return s;
57
+ const result = deepEqualObjects(objA, objB, seen);
58
+ let m = sc.cache.get(objA);
59
+ if (!m) {
60
+ m = /* @__PURE__ */ new WeakMap();
61
+ sc.cache.set(objA, m);
62
+ }
63
+ m.set(objB, result);
64
+ return result;
65
+ }
66
+ function deepEqualObjects(a, b, seen) {
67
+ const objA = a;
68
+ const objB = b;
69
+ if (remember(seen, objA, objB)) return true;
70
+ if (a instanceof Date) return b instanceof Date && a.getTime() === b.getTime();
71
+ if (a instanceof RegExp) return b instanceof RegExp && a.source === b.source && a.flags === b.flags;
72
+ if (Array.isArray(a)) {
73
+ if (!Array.isArray(b) || a.length !== b.length) return false;
74
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i], seen)) return false;
75
+ return true;
76
+ }
77
+ if (Array.isArray(b)) return false;
78
+ if (ArrayBuffer.isView(a)) {
79
+ if (!ArrayBuffer.isView(b) || a.constructor !== b.constructor) return false;
80
+ const ta = a;
81
+ const tb = b;
82
+ if (ta.length !== tb.length) return false;
83
+ for (let i = 0; i < ta.length; i++) if (ta[i] !== tb[i]) return false;
84
+ return true;
85
+ }
86
+ if (a instanceof Map) {
87
+ if (!(b instanceof Map) || a.size !== b.size) return false;
88
+ for (const [k, v] of a) {
89
+ if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false;
122
90
  }
123
- return objectDetails(`Array(${v.length})`, v);
91
+ return true;
124
92
  }
125
- const o = v;
126
- if (o.$type === "element") {
127
- const props = isRecord(o.props) ? o.props : {};
128
- const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
129
- return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
93
+ if (a instanceof Set) {
94
+ if (!(b instanceof Set) || a.size !== b.size) return false;
95
+ if (a.size > 50) {
96
+ for (const v of a) if (!b.has(v)) return false;
97
+ return true;
98
+ }
99
+ outer: for (const v of a) {
100
+ if (b.has(v)) continue;
101
+ for (const w of b) if (deepEqual(v, w, seen)) continue outer;
102
+ return false;
103
+ }
104
+ return true;
130
105
  }
131
- if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
132
- if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
133
- if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
134
- if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
135
- const keys = Object.keys(o).filter((k) => k !== "$type");
136
- const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
137
- return objectDetails(label, o);
106
+ if (isReactElement(a)) {
107
+ if (!isReactElement(b)) return false;
108
+ return a.type === b.type && a.key === b.key && deepEqual(a.props, b.props, seen);
109
+ }
110
+ if (isPlainObject(a) && isPlainObject(b)) {
111
+ const ka = Object.keys(a);
112
+ const kb = Object.keys(b);
113
+ if (ka.length !== kb.length) return false;
114
+ const rb = b;
115
+ for (const k of ka) {
116
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
117
+ if (!deepEqual(a[k], rb[k], seen)) return false;
118
+ }
119
+ return true;
120
+ }
121
+ return false;
138
122
  }
139
- function objectDetails(label, obj) {
140
- const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
141
- d.addEventListener(
142
- "toggle",
143
- () => {
144
- if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
145
- },
146
- { once: true }
147
- );
148
- return d;
123
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
124
+ function joinPath(base, key) {
125
+ if (typeof key === "number") return `${base}[${key}]`;
126
+ if (base === "") return key;
127
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
149
128
  }
150
- function firstDifferentPath(a, b, base = "") {
151
- if (a === b) return null;
152
- if (!isRecord(a) || !isRecord(b)) return base || "(value)";
153
- if (Array.isArray(a) !== Array.isArray(b)) return base || "(value)";
129
+ function firstDifferentPath(a, b, path = "", depth = 0) {
130
+ if (depth > 8) return path || "(value)";
154
131
  if (Array.isArray(a) && Array.isArray(b)) {
155
- if (a.length !== b.length) return base ? `${base}.length` : "length";
132
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
156
133
  for (let i = 0; i < a.length; i++) {
157
- const p = firstDifferentPath(a[i], b[i], `${base}[${i}]`);
158
- if (p) return p;
134
+ if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
159
135
  }
160
136
  return null;
161
137
  }
162
- const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
163
- for (const k of keys) {
164
- const p = firstDifferentPath(a[k], b[k], base ? `${base}.${k}` : k);
165
- if (p) return p;
138
+ if (isPlainObject(a) && isPlainObject(b)) {
139
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
140
+ for (const k of keys) {
141
+ if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
142
+ }
143
+ return null;
166
144
  }
167
- return null;
145
+ return deepEqual(a, b) ? null : path || "(value)";
168
146
  }
169
- function diffLeaves(a, b, limit = 20, base = "", out = []) {
170
- if (out.length >= limit || a === b) return out;
171
- if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b)) {
172
- out.push({ path: base || "(value)", prev: a, next: b });
173
- return out;
174
- }
147
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
148
+ if (out.length >= limit || Object.is(a, b)) return out;
175
149
  if (Array.isArray(a) && Array.isArray(b)) {
176
150
  const n = Math.max(a.length, b.length);
177
- for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, `${base}[${i}]`, out);
151
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
178
152
  return out;
179
153
  }
180
- for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
181
- if (out.length >= limit) break;
182
- diffLeaves(a[k], b[k], limit, base ? `${base}.${k}` : k, out);
154
+ if (isPlainObject(a) && isPlainObject(b)) {
155
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
156
+ if (out.length >= limit) break;
157
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
158
+ }
159
+ return out;
183
160
  }
161
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
184
162
  return out;
185
163
  }
164
+
165
+ // src/causes.ts
166
+ 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);
167
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
168
+ function indexByComponent(reports) {
169
+ const index = /* @__PURE__ */ new Map();
170
+ for (const r of reports) {
171
+ const list = index.get(r.component);
172
+ if (list) list.push(r);
173
+ else index.set(r.component, [r]);
174
+ }
175
+ return index;
176
+ }
177
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
178
+ let cur = r;
179
+ const seen = /* @__PURE__ */ new Set([r]);
180
+ while (cur.trigger === "parent" && cur.parent) {
181
+ const parent = cur.parent;
182
+ const candidates = index.get(parent.name);
183
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
184
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
185
+ seen.add(p);
186
+ cur = p;
187
+ }
188
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
189
+ }
190
+ function rootCausesOf(reports) {
191
+ const roots = /* @__PURE__ */ new Map();
192
+ const rootByReport = /* @__PURE__ */ new Map();
193
+ const index = indexByComponent(reports);
194
+ let avoidable = 0;
195
+ let wasted = 0;
196
+ for (const r of reports) {
197
+ if (!r.avoidable) continue;
198
+ avoidable++;
199
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
200
+ const root = rootCauseOf(r, reports, index);
201
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
202
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
203
+ rootByReport.set(r, name);
204
+ let agg = roots.get(name);
205
+ if (!agg) {
206
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
207
+ roots.set(name, agg);
208
+ }
209
+ agg.count++;
210
+ bump(agg.components, r.component);
211
+ }
212
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
213
+ }
214
+ function analyzeCommit(reports) {
215
+ const first = reports[0];
216
+ return {
217
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
218
+ total: reports.length,
219
+ ...rootCausesOf(reports),
220
+ contexts: contextAttribution(reports),
221
+ reports
222
+ };
223
+ }
224
+ function contextAttribution(reports) {
225
+ const byCtx = /* @__PURE__ */ new Map();
226
+ for (const r of reports) {
227
+ for (const c of r.hookChanges || []) {
228
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
229
+ const m = /useContext\((.*)\)/.exec(c.path);
230
+ const name = m && m[1] ? m[1] : c.path;
231
+ let agg = byCtx.get(name);
232
+ if (!agg) {
233
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
234
+ byCtx.set(name, agg);
235
+ }
236
+ agg.consumers++;
237
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
238
+ bump(agg.components, r.component);
239
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
240
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
241
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
242
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
243
+ }
244
+ }
245
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
246
+ }
247
+ function cascadeTree(reports) {
248
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
249
+ for (const r of reports) {
250
+ let node = root;
251
+ for (const seg of r.path.concat([r.component])) {
252
+ let next = node.children.get(seg);
253
+ if (!next) {
254
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
255
+ node.children.set(seg, next);
256
+ }
257
+ node = next;
258
+ }
259
+ if (!node.report || r.avoidable) node.report = r;
260
+ node.count = (node.count || 0) + 1;
261
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
262
+ }
263
+ return root;
264
+ }
265
+ function rootCauseSummary(name, commits, analyze) {
266
+ const analyzeFn = analyze || ((_, reports) => analyzeCommit(reports));
267
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
268
+ for (const [key, reports] of commits) {
269
+ const analysis = analyzeFn(key, reports);
270
+ const root = analysis.roots.find((x) => x.name === name);
271
+ if (!root) continue;
272
+ out.trigger = root.trigger;
273
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
274
+ out.total += root.count;
275
+ for (const [c, n] of root.components) bump(out.components, c, n);
276
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
277
+ }
278
+ out.commits.reverse();
279
+ return out;
280
+ }
281
+
282
+ // src/fixes.ts
283
+ var MAX_FIX_REPORTS = 50;
284
+ var rootOf = (path) => path.split(/[.[]/)[0] || path;
285
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
186
286
  function shortValue(v, max = 60) {
187
287
  let s;
188
288
  try {
@@ -193,12 +293,11 @@
193
293
  if (s === void 0) s = String(v);
194
294
  return s.length > max ? s.slice(0, max - 3) + "..." : s;
195
295
  }
196
- var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
197
296
  function fixesFor(r) {
198
297
  const out = [];
199
298
  const ownerName = r.owner || r.parent && r.parent.name || null;
200
- const changes = changesOf(r);
201
- const avoidableProps = (r.propChanges || []).filter((c) => AVOIDABLE_KINDS.has(c.kind));
299
+ const propChanges = r.propChanges || [];
300
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
202
301
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
203
302
  const identical = changes.length === 0;
204
303
  out.push({
@@ -217,9 +316,10 @@ export const ${r.component} = memo(function ${r.component}(props) {
217
316
  // class components: extend PureComponent instead`
218
317
  });
219
318
  }
220
- for (const c of avoidableProps) {
319
+ for (const c of propChanges) {
320
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
221
321
  const owner = ownerName || "?";
222
- const root = c.path.split(/[.[]/)[0] || c.path;
322
+ const root = rootOf(c.path);
223
323
  const id = identifier(root);
224
324
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
225
325
  out.push({
@@ -241,9 +341,7 @@ const children = useMemo(() => (
241
341
  // static children: hoist them to module scope
242
342
  const STATIC = <em>hi</em>;`
243
343
  });
244
- continue;
245
- }
246
- if (c.kind === "function") {
344
+ } else if (c.kind === "function") {
247
345
  out.push({
248
346
  kind: "useCallback",
249
347
  owner,
@@ -293,7 +391,7 @@ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
293
391
  });
294
392
  }
295
393
  }
296
- for (const c of [].concat(r.stateChanges || [], r.hookChanges || [])) {
394
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
297
395
  const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
298
396
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
299
397
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
@@ -317,17 +415,16 @@ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedK
317
415
  }
318
416
  if (!AVOIDABLE_KINDS.has(c.kind)) continue;
319
417
  if (isContext) {
320
- const name = ctxName;
321
418
  out.push({
322
419
  kind: "contextValue",
323
- owner: providerOwner || `${name}.Provider`,
420
+ owner: providerOwner || `${ctxName}.Provider`,
324
421
  target: r.component,
325
- prop: name,
326
- label: `memoize the ${name} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
327
- detail: `<${r.component}> re-rendered because ${name} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
328
- snippet: `// ${providerOwner || `where <${name}.Provider> is rendered`}
422
+ prop: ctxName,
423
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
424
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
425
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
329
426
  const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
330
- <${name}.Provider value={value}>`
427
+ <${ctxName}.Provider value={value}>`
331
428
  });
332
429
  } else if (c.hook === "useSyncExternalStore") {
333
430
  const chain = c.custom || [];
@@ -372,127 +469,41 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
372
469
  for (const r of reports) {
373
470
  if (!r.avoidable) continue;
374
471
  for (const f of fixesFor(r)) {
375
- const k = fixKey(f);
376
- let agg = byKey.get(k);
472
+ const key = fixKey(f);
473
+ let agg = byKey.get(key);
377
474
  if (!agg) {
378
- agg = { ...f, key: k, count: 0, components: /* @__PURE__ */ new Map(), reports: [] };
379
- byKey.set(k, agg);
475
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
476
+ byKey.set(key, agg);
380
477
  }
381
478
  agg.count++;
382
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
383
- if (agg.reports.length < 50) agg.reports.push(r);
479
+ agg.components[r.component] = (agg.components[r.component] || 0) + 1;
480
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
384
481
  }
385
482
  }
386
483
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
387
484
  }
388
- 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) {
390
- let cur = r;
391
- const seen = /* @__PURE__ */ new Set([r]);
392
- while (cur.trigger === "parent" && cur.parent) {
393
- const parent = cur.parent;
394
- const p = commitReports.find((x) => x.component === parent.name && isAncestorReport(x, cur));
395
- if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
396
- seen.add(p);
397
- cur = p;
398
- }
399
- return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
400
- }
401
- function analyzeCommit(reports) {
402
- const roots = /* @__PURE__ */ new Map();
403
- let avoidable = 0;
404
- let wasted = 0;
405
- for (const r of reports) {
406
- if (!r.avoidable) continue;
407
- avoidable++;
408
- if (typeof r.selfDuration === "number") wasted += r.selfDuration;
409
- const root = rootCauseOf(r, reports);
410
- const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
411
- const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
412
- let agg = roots.get(name);
413
- if (!agg) {
414
- agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
415
- roots.set(name, agg);
416
- }
417
- agg.count++;
418
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
419
- }
420
- const first = reports[0];
421
- return {
422
- id: first ? first.commitId : 0,
423
- receivedAt: first ? first.receivedAt : 0,
424
- total: reports.length,
425
- avoidable,
426
- wasted,
427
- roots: [...roots.values()].sort((a, b) => b.count - a.count),
428
- contexts: contextAttribution(reports),
429
- fixes: rankFixes(reports),
430
- reports
431
- };
432
- }
433
- function contextAttribution(reports) {
434
- const byCtx = /* @__PURE__ */ new Map();
435
- for (const r of reports) {
436
- for (const c of r.hookChanges || []) {
437
- if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
438
- const m = /useContext\((.*)\)/.exec(c.path);
439
- const name = m && m[1] ? m[1] : c.path;
440
- let agg = byCtx.get(name);
441
- if (!agg) {
442
- agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
443
- byCtx.set(name, agg);
444
- }
445
- agg.consumers++;
446
- if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
447
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
448
- agg.commits.add(r.commitId);
449
- if (c.provider && c.provider.component) agg.providers.set(c.provider.component, (agg.providers.get(c.provider.component) || 0) + 1);
450
- if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
451
- if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
452
- }
453
- }
454
- return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
455
- }
456
- function cascadeTree(reports) {
457
- const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
458
- for (const r of reports) {
459
- let node = root;
460
- for (const seg of r.path.concat([r.component])) {
461
- let next = node.children.get(seg);
462
- if (!next) {
463
- next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
464
- node.children.set(seg, next);
465
- }
466
- node = next;
467
- }
468
- if (!node.report || r.avoidable) node.report = r;
469
- node.count = (node.count || 0) + 1;
470
- if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
471
- }
472
- return root;
473
- }
474
- function rootCauseSummary(name, commits) {
475
- const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
476
- const affected = [];
477
- for (const [key, reports] of commits) {
478
- const analysis = analyzeCommit(reports);
479
- const root = analysis.roots.find((x) => x.name === name);
480
- if (!root) continue;
481
- out.trigger = root.trigger;
482
- out.commits.push({ key, analysis, count: root.count, components: root.components });
483
- out.total += root.count;
484
- 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
- }
485
+
486
+ // src/report.ts
487
+ var KIND_LABEL = {
488
+ "deep-equal": "equal by value",
489
+ function: "new function",
490
+ element: "equal element",
491
+ different: "changed",
492
+ added: "added",
493
+ removed: "removed"
494
+ };
495
+ function summarize(report) {
496
+ const counts = /* @__PURE__ */ new Map();
497
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
498
+ counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
490
499
  }
491
- out.commits.reverse();
492
- out.fixes = rankFixes(affected);
493
- return out;
500
+ const parts = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
501
+ if (parts.length === 0) parts.push("no changes");
502
+ return parts.join(", ");
494
503
  }
495
- function summarizeSession(session, reports) {
504
+
505
+ // src/sessions.ts
506
+ function summarizeReports(reports, meta = {}) {
496
507
  var _a;
497
508
  const byComponent = {};
498
509
  let avoidable = 0;
@@ -510,10 +521,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
510
521
  }
511
522
  }
512
523
  return {
513
- id: session.id,
514
- name: session.name,
515
- startedAt: session.startedAt,
516
- endedAt: session.endedAt,
524
+ id: meta.id ?? `s${Date.now().toString(36)}`,
525
+ name: meta.name ?? "session",
526
+ startedAt: meta.startedAt ?? (reports[0]?.time ?? 0),
527
+ endedAt: meta.endedAt ?? (reports[reports.length - 1]?.time ?? null),
517
528
  total: reports.length,
518
529
  avoidable,
519
530
  wasted,
@@ -521,7 +532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
521
532
  fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
522
533
  };
523
534
  }
524
- function compareSessions(before, after) {
535
+ function compareSummaries(before, after) {
525
536
  const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
526
537
  const rows = [];
527
538
  for (const component of names) {
@@ -540,69 +551,157 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
540
551
  avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
541
552
  wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
542
553
  resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
543
- newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key))
554
+ newFixes: after.fixes.filter((f) => !beforeKeys.has(f.key)),
555
+ regressions: rows.filter((r) => r.delta > 0)
556
+ };
557
+ }
558
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
559
+ var compareSessions = compareSummaries;
560
+
561
+ // extension/src/panel.ts
562
+ var PROTOCOL = 2;
563
+ var FN_PREFIX = "\u0192 ";
564
+ var MAX_REPORTS = 2e3;
565
+ var MAX_PER_NODE = 200;
566
+ var MAX_COMMITS = 500;
567
+ var MAX_PER_COMMIT = 500;
568
+ var THROTTLE_MIN_REPORTS = 200;
569
+ var BEST_FIX_INTERVAL = 500;
570
+ var LEFT_RENDER_INTERVAL = 250;
571
+ var SYNC_RETRY_MIN = 500;
572
+ var SYNC_RETRY_MAX = 5e3;
573
+ var SYNC_RETRIES = 20;
574
+ var NAVIGATION_SETTLE = 1200;
575
+ var INFO_REFRESH_MS = 3e3;
576
+ var ROW_H = 22;
577
+ var ITEM_H = 20;
578
+ var OVERSCAN = 8;
579
+ var FALLBACK_VIEWPORT = 800;
580
+ function el(tag, attrs, children) {
581
+ const node = document.createElement(tag);
582
+ if (attrs) {
583
+ for (const k of Object.keys(attrs)) {
584
+ const v = attrs[k];
585
+ if (k === "class") node.className = String(v);
586
+ else if (k === "text") node.textContent = String(v);
587
+ else if (k.startsWith("on")) {
588
+ if (typeof v === "function") node.addEventListener(k.slice(2), v);
589
+ } else if (v === true) node.setAttribute(k, "");
590
+ else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
591
+ }
592
+ }
593
+ if (children) {
594
+ for (const c of [].concat(children)) if (c != null) node.append(c);
595
+ }
596
+ return node;
597
+ }
598
+ function fmtTime(ms) {
599
+ const d = new Date(ms);
600
+ const p = (n, w) => String(n).padStart(w, "0");
601
+ return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
602
+ }
603
+ var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
604
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
605
+ var componentList = (m) => (m instanceof Map ? [...m] : Object.entries(m)).map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
606
+ function changesOf(report) {
607
+ return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
608
+ }
609
+ var isRecord = (v) => typeof v === "object" && v !== null;
610
+ function normalizeReport(p) {
611
+ if (!isRecord(p) || typeof p.component !== "string") return null;
612
+ const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
613
+ const props = isRecord(p.props) ? p.props : {};
614
+ const parent = isRecord(p.parent) && typeof p.parent.name === "string" ? { name: p.parent.name, trigger: typeof p.parent.trigger === "string" ? p.parent.trigger : "parent" } : null;
615
+ const r = {
616
+ component: p.component,
617
+ instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
618
+ commitId: typeof p.commitId === "number" ? p.commitId : 0,
619
+ renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
620
+ trigger: typeof p.trigger === "string" ? p.trigger : "parent",
621
+ avoidable: !!p.avoidable,
622
+ props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
623
+ propChanges: arr(p.propChanges),
624
+ stateChanges: arr(p.stateChanges),
625
+ hookChanges: arr(p.hookChanges),
626
+ parent,
627
+ owner: typeof p.owner === "string" ? p.owner : null,
628
+ path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
629
+ reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
630
+ time: typeof p.time === "number" ? p.time : 0,
631
+ receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
544
632
  };
633
+ if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
634
+ if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
635
+ if (typeof p.memoized === "boolean") r.memoized = p.memoized;
636
+ if (p.compiled === true) r.compiled = true;
637
+ if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
638
+ if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
639
+ if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
640
+ if (isRecord(p.state)) r.state = p.state;
641
+ if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
642
+ if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
643
+ if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
644
+ if (typeof p.key === "string") r.key = p.key;
645
+ if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
646
+ return r;
545
647
  }
546
- 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
- }
648
+ function valueNode(v, depth = 0) {
649
+ if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
650
+ const t = typeof v;
651
+ if (typeof v === "string") {
652
+ if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
653
+ if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
654
+ return el("span", { class: "v str", text: JSON.stringify(v) });
585
655
  }
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;
656
+ if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
657
+ if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
658
+ if (Array.isArray(v)) {
659
+ const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
660
+ if (short) {
661
+ const s = el("span", { class: "v" }, "[");
662
+ v.forEach((x, i) => {
663
+ if (i) s.append(", ");
664
+ s.append(valueNode(x, depth + 1));
665
+ });
666
+ s.append("]");
667
+ return s;
598
668
  }
669
+ return objectDetails(`Array(${v.length})`, v);
599
670
  }
600
- try {
601
- return normalizeReport(JSON.parse(new TextDecoder().decode(bytes)));
602
- } catch {
603
- return null;
671
+ const o = v;
672
+ if (o.$type === "element") {
673
+ const props = isRecord(o.props) ? o.props : {};
674
+ const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
675
+ return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
604
676
  }
677
+ if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
678
+ if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
679
+ if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
680
+ if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
681
+ const keys = Object.keys(o).filter((k) => k !== "$type");
682
+ const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
683
+ return objectDetails(label, o);
684
+ }
685
+ function objectDetails(label, obj) {
686
+ const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
687
+ d.addEventListener(
688
+ "toggle",
689
+ () => {
690
+ if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
691
+ },
692
+ { once: true }
693
+ );
694
+ return d;
695
+ }
696
+ function analyzeCommit2(reports) {
697
+ const first = reports[0];
698
+ return { ...analyzeCommit(reports), receivedAt: first ? first.receivedAt : 0, fixes: rankFixes(reports) };
699
+ }
700
+ function rootCauseSummary2(name, commits, analyze = (_, reports) => analyzeCommit2(reports)) {
701
+ const s = rootCauseSummary(name, commits, analyze);
702
+ return { ...s, fixes: rankFixes(s.affected) };
605
703
  }
704
+ var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
606
705
  function sourceContext(text, line, around = 3) {
607
706
  const lines = text.split("\n");
608
707
  const from = Math.max(1, line - around);
@@ -725,10 +824,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
725
824
  const copy = actions.copy;
726
825
  bar.append(el("button", { onclick: () => copy(reportToMarkdown(r)) }, "\u2398 Copy as Markdown"));
727
826
  }
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
827
  if (bar.children.length) head.append(bar);
733
828
  const loc = r.source;
734
829
  if (loc && loc.lineNumber && actions.readSource) {
@@ -761,7 +856,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
761
856
  if (r.parent) by.append(el("div", { text: `Triggered by <${r.parent.name}> (${r.parent.trigger})` }));
762
857
  else by.append(el("div", { text: "Update started in this component" }));
763
858
  if (r.owner) by.append(el("div", { class: "meta", text: `Created by <${r.owner}>` }));
764
- if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
859
+ if (r.compiled) by.append(el("div", { class: "meta", text: "Compiled by React Compiler (output memoized per input; identical inputs make the render cheap)" }));
860
+ else if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
765
861
  else if (r.memoized === true) by.append(el("div", { class: "meta", text: "Memoized (React.memo / PureComponent)" }));
766
862
  if (r.updaters && r.updaters.length) by.append(el("div", { text: `Update scheduled by ${r.updaters.map((u) => `<${u}>`).join(", ")}` }));
767
863
  if (r.commitCause === "effect-after-commit") by.append(el("div", { class: "cause effect", text: `Effect loop: state set right after commit #${r.afterCommit ?? "?"}` }));
@@ -922,16 +1018,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
922
1018
  flashOn: false,
923
1019
  settingsOpen: false,
924
1020
  origin: null,
925
- legacyCommit: 0,
926
1021
  tabLabel: transport.tabLabel ?? null,
927
1022
  compact: false,
928
1023
  sessions: [],
929
1024
  recording: null,
930
1025
  selectedSession: null,
931
1026
  compareWith: null,
932
- byInstance: false,
933
- columns: [],
934
- notes: {}
1027
+ byInstance: false
935
1028
  };
936
1029
  let persistTimer = null;
937
1030
  let queue = [];
@@ -1091,6 +1184,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1091
1184
  const measure = () => setCompact(root.clientWidth > 0 && root.clientWidth < 720);
1092
1185
  if (typeof ResizeObserver === "function") new ResizeObserver(measure).observe(root);
1093
1186
  else window.addEventListener("resize", measure);
1187
+ const totals = { avoidable: 0, wasted: 0, perComponent: /* @__PURE__ */ new Map() };
1188
+ let bestFix;
1189
+ let bestFixAt = 0;
1190
+ let bestFixGen = -1;
1191
+ let bestFixTimer = null;
1192
+ const throttled = () => state.reports.length >= THROTTLE_MIN_REPORTS;
1193
+ function refreshBestFix() {
1194
+ if (!totals.avoidable) {
1195
+ bestFix = void 0;
1196
+ bestFixGen = dataGen;
1197
+ return;
1198
+ }
1199
+ if (bestFixGen === dataGen) return;
1200
+ const now = Date.now();
1201
+ if (throttled() && now - bestFixAt < BEST_FIX_INTERVAL) {
1202
+ if (!bestFixTimer) {
1203
+ bestFixTimer = setTimeout(() => {
1204
+ bestFixTimer = null;
1205
+ renderSummary();
1206
+ }, BEST_FIX_INTERVAL - (now - bestFixAt));
1207
+ }
1208
+ return;
1209
+ }
1210
+ bestFixAt = now;
1211
+ bestFixGen = dataGen;
1212
+ bestFix = rankFixes(state.reports)[0];
1213
+ }
1094
1214
  function renderSummary() {
1095
1215
  summary.textContent = "";
1096
1216
  const total = state.reports.length;
@@ -1099,17 +1219,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1099
1219
  return;
1100
1220
  }
1101
1221
  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;
1222
+ const { avoidable, wasted } = totals;
1223
+ let top;
1224
+ for (const entry of totals.perComponent) if (!top || entry[1] > top[1]) top = entry;
1225
+ refreshBestFix();
1226
+ const fix = bestFix;
1113
1227
  const stat = (value, label, cls = "") => el("span", { class: "stat " + cls }, [el("b", { text: value }), el("span", { class: "label", text: label })]);
1114
1228
  summary.append(stat(String(total), plural(total, "render").replace(/^\d+ /, "")), stat(String(avoidable), "avoidable", avoidable ? "bad" : "good"));
1115
1229
  if (wasted) summary.append(stat(fmtMs(wasted), "wasted", "bad"));
@@ -1177,8 +1291,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1177
1291
  collapsed: [...state.collapsed],
1178
1292
  treeWidth: state.treeWidth,
1179
1293
  flashOn: state.flashOn,
1180
- byInstance: state.byInstance,
1181
- columns: state.columns
1294
+ byInstance: state.byInstance
1182
1295
  };
1183
1296
  transport.storage.set("panel", saved);
1184
1297
  }, 150);
@@ -1230,7 +1343,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1230
1343
  instancesBtn.classList.toggle("active", state.byInstance);
1231
1344
  rebuildTree();
1232
1345
  }
1233
- if (Array.isArray(saved.columns)) state.columns = saved.columns.filter((c) => OPTIONAL_COLUMNS.some((o) => o.key === c));
1234
1346
  if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
1235
1347
  if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
1236
1348
  for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
@@ -1266,7 +1378,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1266
1378
  for (const r of state.reports) {
1267
1379
  const node = nodeOfReport(r);
1268
1380
  node.reports.push(r);
1269
- if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1270
1381
  node.total++;
1271
1382
  if (r.avoidable) {
1272
1383
  node.avoidable++;
@@ -1274,49 +1385,93 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1274
1385
  }
1275
1386
  node.lastReport = r;
1276
1387
  }
1388
+ for (const node of state.nodesByKey.values()) trimNode(node);
1277
1389
  renderLeft();
1278
1390
  renderDetails();
1279
1391
  }
1280
- function commitKeyFor(report) {
1281
- if (report.commitId > 0) return report.commitId;
1282
- return -state.legacyCommit;
1392
+ function trimNode(node) {
1393
+ const excess = node.reports.length - MAX_PER_NODE;
1394
+ if (excess > 0) node.reports.splice(0, excess);
1395
+ }
1396
+ const commitAnalyses = /* @__PURE__ */ new Map();
1397
+ function analysisFor(key, reports) {
1398
+ const cached = commitAnalyses.get(key);
1399
+ if (cached && cached.len === reports.length) return cached.analysis;
1400
+ const analysis = analyzeCommit2(reports);
1401
+ commitAnalyses.set(key, { len: reports.length, analysis });
1402
+ return analysis;
1403
+ }
1404
+ const commitMember = /* @__PURE__ */ new WeakSet();
1405
+ function forgetCommit(key) {
1406
+ state.commits.delete(key);
1407
+ commitAnalyses.delete(key);
1283
1408
  }
1284
1409
  function ingest(report) {
1285
1410
  if (!report.receivedAt) report.receivedAt = Date.now();
1286
1411
  state.reports.push(report);
1287
- if (state.reports.length > MAX_REPORTS) state.reports.shift();
1288
1412
  const node = nodeOfReport(report);
1289
1413
  node.reports.push(report);
1290
- if (node.reports.length > MAX_PER_NODE) node.reports.shift();
1291
1414
  node.total++;
1292
1415
  if (report.avoidable) {
1293
1416
  node.avoidable++;
1294
- if (typeof report.selfDuration === "number") node.wasted += report.selfDuration;
1417
+ totals.avoidable++;
1418
+ totals.perComponent.set(report.component, (totals.perComponent.get(report.component) || 0) + 1);
1419
+ if (typeof report.selfDuration === "number") {
1420
+ node.wasted += report.selfDuration;
1421
+ totals.wasted += report.selfDuration;
1422
+ }
1295
1423
  }
1296
1424
  node.lastReport = report;
1297
1425
  node.flash = true;
1298
1426
  node.flashAt = Date.now();
1299
- const ck = commitKeyFor(report);
1427
+ const ck = report.commitId;
1300
1428
  let list = state.commits.get(ck);
1301
1429
  if (!list) {
1302
1430
  list = [];
1303
1431
  state.commits.set(ck, list);
1304
1432
  state.commitOrder.push(ck);
1305
- if (state.commitOrder.length > MAX_COMMITS) state.commits.delete(state.commitOrder.shift());
1433
+ if (state.commitOrder.length > MAX_COMMITS) forgetCommit(state.commitOrder.shift());
1434
+ }
1435
+ if (list.length < MAX_PER_COMMIT) {
1436
+ list.push(report);
1437
+ commitMember.add(report);
1306
1438
  }
1307
- list.push(report);
1308
1439
  return node;
1309
1440
  }
1441
+ function evict() {
1442
+ const excess = state.reports.length - MAX_REPORTS;
1443
+ if (excess <= 0) return;
1444
+ for (const r of state.reports.splice(0, excess)) {
1445
+ if (r.avoidable) {
1446
+ totals.avoidable--;
1447
+ const n = (totals.perComponent.get(r.component) || 0) - 1;
1448
+ if (n > 0) totals.perComponent.set(r.component, n);
1449
+ else totals.perComponent.delete(r.component);
1450
+ if (typeof r.selfDuration === "number") totals.wasted -= r.selfDuration;
1451
+ }
1452
+ if (!commitMember.has(r)) continue;
1453
+ const list = state.commits.get(r.commitId);
1454
+ if (!list || list[0] !== r) continue;
1455
+ list.shift();
1456
+ if (!list.length) {
1457
+ forgetCommit(r.commitId);
1458
+ const i = state.commitOrder.indexOf(r.commitId);
1459
+ if (i >= 0) state.commitOrder.splice(i, 1);
1460
+ }
1461
+ }
1462
+ if (totals.wasted < 0) totals.wasted = 0;
1463
+ }
1310
1464
  function flush() {
1311
1465
  flushScheduled = false;
1312
1466
  if (!queue.length) return;
1313
1467
  const batch = queue;
1314
1468
  queue = [];
1315
- state.legacyCommit++;
1316
1469
  let touchedSelected = false;
1317
1470
  let avoidableCount = 0;
1471
+ const touched = /* @__PURE__ */ new Set();
1318
1472
  for (const r of batch) {
1319
1473
  const node = ingest(r);
1474
+ touched.add(node);
1320
1475
  if (state.recording && state.recording.reports.length < MAX_REPORTS) state.recording.reports.push(r);
1321
1476
  if (r.avoidable) avoidableCount++;
1322
1477
  if (state.selectedKey === node.key) {
@@ -1324,11 +1479,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1324
1479
  if (state.tab === "latest") state.selectedReport = r;
1325
1480
  }
1326
1481
  }
1327
- renderLeft();
1482
+ for (const node of touched) trimNode(node);
1483
+ evict();
1484
+ dataGen++;
1328
1485
  renderStream(batch);
1329
1486
  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);
1487
+ const heavy = state.view === "commits" || state.view === "fixes" || state.tab === "root" || !!(state.recording && state.tab === "session");
1488
+ if (state.view === "tree") {
1489
+ renderTree();
1490
+ if (touchedSelected && !heavy) renderDetails();
1491
+ }
1492
+ scheduleHeavy(heavy || touchedSelected && state.view !== "tree");
1493
+ if (state.polling && avoidableCount) transport.badge?.(totals.avoidable);
1494
+ }
1495
+ let heavyAt = 0;
1496
+ let heavyTimer = null;
1497
+ let heavyDetailsPending = false;
1498
+ function renderHeavy(details2) {
1499
+ heavyAt = Date.now();
1500
+ if (state.view !== "tree") renderLeft();
1501
+ if (details2) renderDetails();
1502
+ }
1503
+ function scheduleHeavy(details2) {
1504
+ const wait = throttled() ? LEFT_RENDER_INTERVAL - (Date.now() - heavyAt) : 0;
1505
+ if (wait <= 0) {
1506
+ if (heavyTimer) clearTimeout(heavyTimer);
1507
+ heavyTimer = null;
1508
+ heavyDetailsPending = false;
1509
+ renderHeavy(details2);
1510
+ return;
1511
+ }
1512
+ heavyDetailsPending = heavyDetailsPending || details2;
1513
+ if (!heavyTimer) {
1514
+ heavyTimer = setTimeout(() => {
1515
+ heavyTimer = null;
1516
+ const d = heavyDetailsPending;
1517
+ heavyDetailsPending = false;
1518
+ renderHeavy(d);
1519
+ }, wait);
1520
+ }
1332
1521
  }
1333
1522
  function enqueue(report) {
1334
1523
  queue.push(report);
@@ -1343,6 +1532,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1343
1532
  state.reports = [];
1344
1533
  state.commits.clear();
1345
1534
  state.commitOrder = [];
1535
+ commitAnalyses.clear();
1536
+ totals.avoidable = 0;
1537
+ totals.wasted = 0;
1538
+ totals.perComponent.clear();
1539
+ bestFix = void 0;
1540
+ bestFixAt = 0;
1541
+ dataGen++;
1346
1542
  state.selectedKey = null;
1347
1543
  state.selectedReport = null;
1348
1544
  state.selectedCommit = null;
@@ -1355,18 +1551,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1355
1551
  renderStream();
1356
1552
  renderSummary();
1357
1553
  }
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;
1554
+ let filterCache = { filter: "", value: null, regex: null, text: "" };
1555
+ function parsedFilter() {
1556
+ if (filterCache.filter === state.filter) return filterCache;
1361
1557
  const f = state.filter.trim();
1362
- const m = /^\/(.+)\/([a-z]*)$/.exec(f);
1558
+ const value = f.startsWith("~") ? f.slice(1).toLowerCase() : null;
1559
+ let regex = null;
1560
+ const m = value === null ? /^\/(.+)\/([a-z]*)$/.exec(f) : null;
1363
1561
  if (m && m[1] !== void 0) {
1364
1562
  try {
1365
- return new RegExp(m[1], m[2]).test(name);
1563
+ regex = new RegExp(m[1], (m[2] || "").replace(/[gy]/g, ""));
1366
1564
  } catch {
1367
1565
  }
1368
1566
  }
1369
- return name.toLowerCase().includes(f.toLowerCase());
1567
+ filterCache = { filter: state.filter, value, regex, text: f.toLowerCase() };
1568
+ return filterCache;
1569
+ }
1570
+ const valueQuery = () => parsedFilter().value;
1571
+ function matchesFilter(name) {
1572
+ const f = parsedFilter();
1573
+ if (!f.filter || f.value !== null) return true;
1574
+ if (f.regex) return f.regex.test(name);
1575
+ return name.toLowerCase().includes(f.text);
1370
1576
  }
1371
1577
  const valueCache = /* @__PURE__ */ new WeakMap();
1372
1578
  function matchesValues(r) {
@@ -1390,10 +1596,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1390
1596
  for (const c of node.children.values()) if (visible(c)) return true;
1391
1597
  return false;
1392
1598
  }
1393
- const isMuted = (component) => !!state.notes[component]?.muted;
1394
1599
  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);
1600
+ let dataGen = 0;
1601
+ let filtered = { key: "", reports: [], fixes: null };
1602
+ function filteredReports() {
1603
+ const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
1604
+ if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
1605
+ return filtered.reports;
1606
+ }
1607
+ function filteredFixes() {
1608
+ const reports = filteredReports();
1609
+ if (!filtered.fixes) filtered.fixes = rankFixes(reports);
1610
+ return filtered.fixes;
1611
+ }
1397
1612
  function setView(view) {
1398
1613
  state.view = view;
1399
1614
  for (const [id, b] of viewButtons) b.classList.toggle("active", id === view);
@@ -1472,7 +1687,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1472
1687
  row.classList.toggle("selected", state.selectedKey === node.key);
1473
1688
  row.setAttribute("aria-selected", state.selectedKey === node.key ? "true" : "false");
1474
1689
  row.setAttribute("aria-level", String(depth + 1));
1475
- row.classList.toggle("muted", isMuted(node.lastReport ? node.lastReport.component : node.name));
1476
1690
  const indent = row.querySelector(".indent");
1477
1691
  if (indent.childElementCount !== depth) {
1478
1692
  indent.textContent = "";
@@ -1574,7 +1788,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1574
1788
  for (const r of filteredReports()) {
1575
1789
  let o = byName.get(r.component);
1576
1790
  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) };
1791
+ o = { component: r.component, total: 0, avoidable: 0, wasted: 0, paths: /* @__PURE__ */ new Set(), reports: [], fix: "" };
1578
1792
  byName.set(r.component, o);
1579
1793
  }
1580
1794
  o.total++;
@@ -1583,12 +1797,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1583
1797
  if (typeof r.selfDuration === "number") o.wasted += r.selfDuration;
1584
1798
  }
1585
1799
  o.paths.add(keyOf(r.path));
1586
- o.lastSeen = Math.max(o.lastSeen, r.receivedAt);
1587
1800
  o.reports.push(r);
1588
1801
  }
1589
1802
  const rows = [...byName.values()];
1590
1803
  for (const o of rows) {
1591
- o.places = o.paths.size;
1592
1804
  const fixes = o.avoidable ? rankFixes(o.reports) : [];
1593
1805
  o.fix = fixes.length ? fixes[0].label : "";
1594
1806
  }
@@ -1601,62 +1813,25 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1601
1813
  });
1602
1814
  return rows;
1603
1815
  }
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" })];
1816
+ function offenderRow(o) {
1817
+ return el(
1818
+ "tr",
1819
+ {
1820
+ class: o.avoidable ? "has-avoid" : "",
1821
+ onclick: () => {
1822
+ const last = o.reports[o.reports.length - 1];
1823
+ state.tab = "fix";
1824
+ select(nodeOfReport(last), last);
1825
+ }
1826
+ },
1827
+ [
1828
+ 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]),
1829
+ el("td", { class: "num" }, o.avoidable ? el("span", { class: "badge avoid", text: String(o.avoidable) }) : "0"),
1830
+ el("td", { class: "num", text: String(o.total) }),
1831
+ el("td", { class: "num", text: o.wasted ? fmtMs(o.wasted) : "" }),
1832
+ el("td", { class: "fix", text: o.fix })
1833
+ ]
1834
+ );
1660
1835
  }
1661
1836
  function renderOffenders() {
1662
1837
  table.textContent = "";
@@ -1665,28 +1840,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1665
1840
  table.append(el("div", { class: "empty", text: "No re-renders reported yet." }));
1666
1841
  return;
1667
1842
  }
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
1843
  const t = el("table", { class: "grid" });
1684
- const headRow = el("tr", null, offendersHeader());
1685
- t.append(el("thead", null, headRow));
1844
+ 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
1845
  const body = el("tbody");
1687
- for (const o of rows) body.append(offenderRow(o, "tr"));
1846
+ for (const o of rows) body.append(offenderRow(o));
1688
1847
  t.append(body);
1689
- table.append(el("div", { class: "table-tools" }, columnsMenu()), t);
1848
+ table.append(t);
1690
1849
  }
1691
1850
  function commitSummaries() {
1692
1851
  const out = [];
@@ -1694,7 +1853,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1694
1853
  const key = state.commitOrder[i];
1695
1854
  const reports = state.commits.get(key);
1696
1855
  if (!reports || !reports.some(passes)) continue;
1697
- out.push({ key, analysis: analyzeCommit(reports) });
1856
+ out.push({ key, analysis: analysisFor(key, reports) });
1698
1857
  }
1699
1858
  return out;
1700
1859
  }
@@ -1721,7 +1880,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1721
1880
  const root2 = analysis.roots[0];
1722
1881
  list.append(
1723
1882
  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" }),
1883
+ el("span", { class: "id", text: `#${key}` }),
1725
1884
  el("span", { class: "t", text: fmtTime(analysis.receivedAt) }),
1726
1885
  el("span", { class: "n", text: plural(analysis.total, "render") }),
1727
1886
  analysis.avoidable ? el("span", { class: "badge avoid", text: `${analysis.avoidable} avoidable` }) : el("span", { class: "badge", text: "ok" }),
@@ -1736,13 +1895,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1736
1895
  }
1737
1896
  table.append(list);
1738
1897
  }
1739
- function fixItem(f, tag) {
1898
+ function fixItem(f) {
1899
+ const selected = state.selectedFix === f.key && state.tab === "fixlist";
1740
1900
  return el(
1741
- tag,
1901
+ "li",
1742
1902
  {
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,
1903
+ class: selected ? "selected" : "",
1904
+ "aria-selected": selected ? "true" : null,
1746
1905
  onclick: () => {
1747
1906
  state.selectedFix = f.key;
1748
1907
  state.tab = "fixlist";
@@ -1757,27 +1916,19 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1757
1916
  ]
1758
1917
  );
1759
1918
  }
1760
- const fixesList = virtualList(table, GRID_ROW_H, (f) => fixItem(f, "div"), { attach: false, headerHeight: () => table.querySelector(".section-title")?.offsetHeight || 0 });
1761
1919
  function renderFixes() {
1762
1920
  table.textContent = "";
1763
- const reports = ranked();
1764
- const fixes = rankFixes(reports);
1921
+ const reports = filteredReports();
1922
+ const fixes = filteredFixes();
1765
1923
  const contexts = contextAttribution(reports);
1766
- const mutedCount = Object.values(state.notes).filter((n) => n.muted).length;
1767
1924
  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." }));
1925
+ table.append(el("div", { class: "empty", text: "No avoidable re-renders, nothing to fix." }));
1769
1926
  return;
1770
1927
  }
1771
1928
  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
- }
1929
+ const list = el("ol", { class: "fixes", role: "list" });
1930
+ for (const f of fixes) list.append(fixItem(f));
1931
+ table.append(el("div", { class: "section-title", text: "Ranked by avoidable re-renders removed" }), list);
1781
1932
  }
1782
1933
  if (contexts.length) {
1783
1934
  const list = el("ul", { class: "contexts" });
@@ -1816,9 +1967,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1816
1967
  openSource: transport.openResource ? (src) => transport.openResource(src.fileName, src.lineNumber, src.columnNumber) : null,
1817
1968
  highlight: transport.highlight ? (id) => transport.highlight(id) : null,
1818
1969
  copy: copyText,
1819
- share: transport.panelUrl ? (r) => {
1820
- void encodeShare(r).then((code) => copyText(`${transport.panelUrl}?report=${code}`));
1821
- } : null,
1822
1970
  readSource: transport.readSource ? (url) => transport.readSource(url) : null
1823
1971
  });
1824
1972
  const tabButton = (id, label, onclick) => el("button", { class: state.tab === id ? "active" : "", onclick }, label);
@@ -1827,15 +1975,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1827
1975
  details.append(el("div", { class: "empty", text: "Select a component to see why it re-rendered." }));
1828
1976
  return;
1829
1977
  }
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
1978
  const header = el("div", { class: "details-header" }, [
1835
1979
  el("span", { class: "title" }, [el("span", { class: "bracket", text: "<" }), el("span", { class: "name", text: node.name }), el("span", { class: "bracket", text: ">" })]),
1836
1980
  el("span", { class: "meta", text: `${plural(node.total, "re-render")}, ${node.avoidable} avoidable${node.wasted ? ", " + fmtMs(node.wasted) + " wasted" : ""}` }),
1837
- noteBtn,
1838
- muteBtn,
1839
1981
  el("span", { class: "tabs" }, [
1840
1982
  tabButton("latest", "Report", () => {
1841
1983
  state.tab = "latest";
@@ -1858,21 +2000,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1858
2000
  details.append(header);
1859
2001
  const body = el("div", { class: "details-body" });
1860
2002
  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
2003
  if (state.tab === "history") {
1877
2004
  const list = el("ul", { class: "history" });
1878
2005
  for (const r2 of [...node.reports].reverse()) {
@@ -1927,10 +2054,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1927
2054
  details.append(el("div", { class: "empty", text: "This commit is no longer buffered." }));
1928
2055
  return;
1929
2056
  }
1930
- const a = analyzeCommit(reports);
2057
+ const a = analysisFor(key, reports);
1931
2058
  details.append(
1932
2059
  el("div", { class: "details-header" }, [
1933
- el("span", { class: "title", text: key > 0 ? `Commit #${key}` : "Commit" }),
2060
+ el("span", { class: "title", text: `Commit #${key}` }),
1934
2061
  el("span", { class: "meta", text: `${plural(a.total, "render")}, ${a.avoidable} avoidable${a.wasted ? ", " + fmtMs(a.wasted) + " wasted" : ""} \xB7 ${fmtTime(a.receivedAt)}` })
1935
2062
  ])
1936
2063
  );
@@ -1988,7 +2115,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1988
2115
  return list;
1989
2116
  }
1990
2117
  function renderFixDetails() {
1991
- const fix = rankFixes(filteredReports()).find((f) => f.key === state.selectedFix);
2118
+ const fix = filteredFixes().find((f) => f.key === state.selectedFix);
1992
2119
  if (!fix) {
1993
2120
  details.append(el("div", { class: "empty", text: "Select a fix." }));
1994
2121
  return;
@@ -2001,7 +2128,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2001
2128
  }
2002
2129
  function renderRootDetails() {
2003
2130
  const name = state.selectedRoot;
2004
- const s = rootCauseSummary(name, state.commits);
2131
+ const s = rootCauseSummary2(name, state.commits, analysisFor);
2005
2132
  details.append(
2006
2133
  el("div", { class: "details-header" }, [
2007
2134
  el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
@@ -2016,7 +2143,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2016
2143
  for (const c of s.commits) {
2017
2144
  list.append(
2018
2145
  el("li", { onclick: () => showCommit(c.key) }, [
2019
- el("span", { class: "id", text: c.key > 0 ? `#${c.key}` : "\u2014" }),
2146
+ el("span", { class: "id", text: `#${c.key}` }),
2020
2147
  el("span", { class: "t", text: fmtTime(c.analysis.receivedAt) }),
2021
2148
  el("span", { class: "badge avoid", text: `${c.count} avoidable` }),
2022
2149
  el("span", { class: "root", text: componentList(c.components) })
@@ -2029,37 +2156,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2029
2156
  body.append(fixView(s.fixes, { copy: copyText }));
2030
2157
  }
2031
2158
  }
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
2159
  const sessionSummary = (s) => summarizeSession(s, s.reports);
2064
2160
  function persistSessions() {
2065
2161
  if (!transport.storage) return;
@@ -2278,10 +2374,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2278
2374
  };
2279
2375
  const text = JSON.stringify(data, null, 2);
2280
2376
  const name = `rerender-lens-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
2281
- if (transport.download) {
2282
- transport.download(name, text);
2283
- return;
2284
- }
2285
2377
  try {
2286
2378
  const blob = new Blob([text], { type: "application/json" });
2287
2379
  const url = URL.createObjectURL(blob);
@@ -2333,6 +2425,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2333
2425
  cls = "connected";
2334
2426
  title = state.relay ? "live via content script" : "polling the page";
2335
2427
  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`;
2428
+ if (lib.truncated) title += ` \xB7 ${plural(lib.truncated, "report")} skipped by the per-commit cap`;
2336
2429
  } else if (state.relay || state.polling) {
2337
2430
  text = "no library in page";
2338
2431
  cls = "partial";
@@ -2347,15 +2440,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2347
2440
  tabChip.textContent = state.tabLabel || "";
2348
2441
  banner.textContent = "";
2349
2442
  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
- }
2443
+ 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
2444
  if (lib && lib.production) warnings.push("Production React build detected: component names may be minified and hooks are unlabeled. Use a development build.");
2356
2445
  if (lib && lib.injected && lib.source === "page")
2357
2446
  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
2447
  if (lib && lib.enabled === false) warnings.push("rerender-lens is present but disabled in this page.");
2448
+ 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
2449
  banner.hidden = warnings.length === 0;
2360
2450
  for (const w of warnings) banner.append(el("div", { text: w }));
2361
2451
  }
@@ -2460,10 +2550,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2460
2550
  sec.append(el("div", { class: "meta", text: "Connect to a page running rerender-lens to change its options." }));
2461
2551
  return;
2462
2552
  }
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
2553
  const current = Object.assign({}, lib.options || {});
2468
2554
  const applyOptions = async (patch) => {
2469
2555
  Object.assign(current, patch);
@@ -2495,6 +2581,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2495
2581
  const max = el("input", { type: "number", min: "0", value: String(current.maxReportsPerComponent || 0) });
2496
2582
  max.addEventListener("change", () => void applyOptions({ maxReportsPerComponent: Math.max(0, Number(max.value) || 0) }));
2497
2583
  sec.append(el("label", { class: "opt col" }, ["Stop printing a component after N reports (0 = never)", max]));
2584
+ const help = el("div", { class: "section" }, [el("h3", { text: "Help" })]);
2585
+ help.append(
2586
+ el("div", { class: "meta", text: "Opens a GitHub issue prefilled with versions, options and counts. No report contents are included; attach an Export if it helps." }),
2587
+ el("a", { class: "report-link", href: issueUrl(), target: "_blank", rel: "noreferrer", text: "Report a problem \u2197" })
2588
+ );
2589
+ body.append(help);
2590
+ }
2591
+ function issueUrl() {
2592
+ const lib = state.library;
2593
+ const ext = typeof chrome !== "undefined" && chrome.runtime && typeof chrome.runtime.getManifest === "function" ? chrome.runtime.getManifest().version : "n/a";
2594
+ const react = lib?.react?.map((r) => `${r.version || "?"}${r.bundleType === 0 ? " (prod)" : ""}`).join(", ") || "not detected";
2595
+ const transportLabel = state.tabLabel || (state.relay ? "content script" : state.polling ? "polling" : "none");
2596
+ const lines = [
2597
+ "### What happened",
2598
+ "",
2599
+ "(what you did, what you expected, what the panel showed)",
2600
+ "",
2601
+ "### Environment",
2602
+ `- extension ${ext}, panel protocol ${PROTOCOL}, transport: ${transportLabel}`,
2603
+ `- library ${lib?.library || "not detected"} (protocol ${lib?.protocol ?? "?"}), React ${react}${lib?.production ? ", production build" : ""}`,
2604
+ `- options: ${JSON.stringify(lib?.options || {})}`,
2605
+ `- overhead: ${lib?.overhead ? `${lib.overhead.totalMs.toFixed(1)} ms over ${lib.commits ?? 0} commits, worst ${lib.overhead.maxCommitMs.toFixed(1)} ms` : "n/a"}; reports skipped by the cap: ${lib?.truncated ?? 0}`,
2606
+ `- buffered: ${state.reports.length} reports, ${totals.avoidable} avoidable; top fix: ${bestFix ? bestFix.label : "none"}`,
2607
+ `- browser: ${typeof navigator !== "undefined" ? navigator.userAgent : "n/a"}`
2608
+ ];
2609
+ const params2 = new URLSearchParams({ title: "Panel: ", body: lines.join("\n"), labels: "bug" });
2610
+ return `https://github.com/NexaLeaf/rerender-lens/issues/new?${params2.toString()}`;
2498
2611
  }
2499
2612
  function handle(message) {
2500
2613
  if (!isRecord(message)) return;
@@ -2522,7 +2635,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2522
2635
  renderStatus();
2523
2636
  break;
2524
2637
  case "hello":
2525
- setLibrary(isRecord(message.payload) ? Object.assign({ protocol: message.version || 1 }, message.payload) : { protocol: message.version || 1 });
2638
+ if (isRecord(message.payload)) setLibrary(message.payload);
2526
2639
  break;
2527
2640
  case "clear":
2528
2641
  case "navigated":
@@ -2538,6 +2651,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2538
2651
  if (r) enqueue(r);
2539
2652
  break;
2540
2653
  }
2654
+ case "batch":
2655
+ if (Array.isArray(message.items)) for (const item of message.items) handle(item);
2656
+ break;
2541
2657
  }
2542
2658
  }
2543
2659
  const panelApi = {
@@ -2552,8 +2668,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2552
2668
  setView,
2553
2669
  openSettings: () => toggleSettings(true),
2554
2670
  startRecording,
2555
- stopRecording,
2556
- setNote
2671
+ stopRecording
2557
2672
  };
2558
2673
  if (options.theme === "dark") document.documentElement.classList.add("theme-dark");
2559
2674
  state.origin = transport.origin || null;
@@ -2568,12 +2683,60 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2568
2683
  });
2569
2684
  Promise.resolve(transport.storage.get("sessions")).then(restoreSessions, () => {
2570
2685
  });
2571
- Promise.resolve(transport.storage.get("notes")).then(restoreNotes, () => {
2572
- });
2573
2686
  }
2574
2687
  transport.subscribe(handle);
2575
2688
  return panelApi;
2576
2689
  }
2690
+ var systemTheme = () => typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
2691
+ var fetchSource = (url) => fetch(url).then((res) => res.ok ? res.text() : null).catch(() => null);
2692
+ function localStorageAdapter(prefix) {
2693
+ const mem = (key) => `${prefix}:${key}`;
2694
+ return {
2695
+ get: (key) => {
2696
+ try {
2697
+ const raw = localStorage.getItem(mem(key));
2698
+ return raw ? JSON.parse(raw) : void 0;
2699
+ } catch {
2700
+ return void 0;
2701
+ }
2702
+ },
2703
+ set: (key, value) => {
2704
+ try {
2705
+ localStorage.setItem(mem(key), JSON.stringify(value));
2706
+ } catch {
2707
+ }
2708
+ }
2709
+ };
2710
+ }
2711
+ function commandBridge(send, timeoutMs) {
2712
+ const pending = /* @__PURE__ */ new Map();
2713
+ const bridge = (cmd, arg) => new Promise((resolve, reject) => {
2714
+ const id = Math.random().toString(36).slice(2);
2715
+ const fail = () => {
2716
+ clearTimeout(timer);
2717
+ pending.delete(id);
2718
+ resolve(null);
2719
+ };
2720
+ const timer = setTimeout(fail, timeoutMs);
2721
+ pending.set(id, { resolve: (v) => v instanceof Error ? reject(v) : resolve(v), timer });
2722
+ try {
2723
+ void Promise.resolve(send({ __rerenderLensCmd: true, id, cmd, arg })).catch(fail);
2724
+ } catch {
2725
+ fail();
2726
+ }
2727
+ });
2728
+ const reply = (m) => {
2729
+ if (m.__rerenderLensReply !== true || typeof m.id !== "string") return false;
2730
+ const p = pending.get(m.id);
2731
+ if (p) {
2732
+ pending.delete(m.id);
2733
+ clearTimeout(p.timer);
2734
+ p.resolve(typeof m.error === "string" ? new Error(m.error) : m.result);
2735
+ }
2736
+ return true;
2737
+ };
2738
+ return { bridge, reply };
2739
+ }
2577
2740
  function createRelayTransport(io) {
2578
2741
  let listener = null;
2579
2742
  let relayConnected = false;
@@ -2600,18 +2763,72 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2600
2763
  try {
2601
2764
  const info = await io.bridge("info");
2602
2765
  if (info) {
2603
- emit({ type: "hello", version: info.protocol || 1, payload: info });
2766
+ emit({ type: "hello", version: info.protocol, payload: info });
2767
+ startInfoRefresh();
2604
2768
  return true;
2605
2769
  }
2606
2770
  } catch {
2607
2771
  }
2608
2772
  return false;
2609
2773
  }
2774
+ let infoTimer = null;
2775
+ function startInfoRefresh() {
2776
+ if (infoTimer) return;
2777
+ infoTimer = setInterval(() => {
2778
+ if (disposed) return stopInfoRefresh();
2779
+ io.bridge("info").then((info) => {
2780
+ if (info) emit({ type: "hello", version: info.protocol, payload: info });
2781
+ else stopInfoRefresh();
2782
+ }).catch(() => stopInfoRefresh());
2783
+ }, INFO_REFRESH_MS);
2784
+ }
2785
+ function stopInfoRefresh() {
2786
+ if (infoTimer) clearInterval(infoTimer);
2787
+ infoTimer = null;
2788
+ }
2789
+ let syncGen = 0;
2790
+ let syncTimer = null;
2791
+ let disposed = false;
2792
+ function cancelSync() {
2793
+ syncGen++;
2794
+ if (syncTimer) clearTimeout(syncTimer);
2795
+ syncTimer = null;
2796
+ stopInfoRefresh();
2797
+ }
2798
+ function syncWithRetry(onReady, initialDelay = 0) {
2799
+ cancelSync();
2800
+ const gen = syncGen;
2801
+ let delay = SYNC_RETRY_MIN;
2802
+ let tries = 0;
2803
+ const attempt = async () => {
2804
+ syncTimer = null;
2805
+ if (gen !== syncGen || disposed) return;
2806
+ const ok = await syncWithPage();
2807
+ if (gen !== syncGen || disposed) return;
2808
+ if (ok) {
2809
+ onReady();
2810
+ return;
2811
+ }
2812
+ if (++tries >= SYNC_RETRIES) return;
2813
+ syncTimer = setTimeout(() => void attempt(), delay);
2814
+ delay = Math.min(delay * 2, SYNC_RETRY_MAX);
2815
+ };
2816
+ if (initialDelay > 0) syncTimer = setTimeout(() => void attempt(), initialDelay);
2817
+ else void attempt();
2818
+ }
2819
+ let droppedSeen = false;
2820
+ function resetSince() {
2821
+ since = 0;
2822
+ droppedSeen = false;
2823
+ }
2610
2824
  async function pollOnce() {
2611
2825
  try {
2612
2826
  const res = await io.bridge("pull", since);
2613
2827
  if (!res) return;
2614
- if (res.dropped) emit({ type: "clear" });
2828
+ if (res.dropped && !droppedSeen) {
2829
+ droppedSeen = true;
2830
+ emit({ type: "clear" });
2831
+ }
2615
2832
  for (const p of res.reports) emit({ type: "report", payload: p });
2616
2833
  since = res.seq;
2617
2834
  } catch {
@@ -2627,19 +2844,23 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2627
2844
  emit({ type: "polling", on: false });
2628
2845
  }
2629
2846
  }
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(() => {
2847
+ function attachToPage(initialDelay = 0) {
2848
+ syncWithRetry(() => {
2849
+ if (relayConnected) io.bridge("replay").catch(() => {
2640
2850
  });
2641
- setPolling(true);
2642
- }
2851
+ else {
2852
+ const gen = syncGen;
2853
+ void io.bridge("pull", 0).catch(() => null).then((res) => {
2854
+ if (gen !== syncGen || disposed) return;
2855
+ if (res) {
2856
+ for (const p of res.reports) emit({ type: "report", payload: p });
2857
+ since = res.seq;
2858
+ } else io.bridge("replay").catch(() => {
2859
+ });
2860
+ setPolling(true);
2861
+ });
2862
+ }
2863
+ }, initialDelay);
2643
2864
  }
2644
2865
  function connect() {
2645
2866
  if (panelPort) {
@@ -2660,16 +2881,20 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2660
2881
  setPolling(false);
2661
2882
  } else if (m.type === "disconnected") {
2662
2883
  relayConnected = false;
2663
- void syncWithPage().then((ok) => ok && setPolling(true));
2884
+ syncWithRetry(() => setPolling(true));
2664
2885
  }
2665
2886
  emit(m);
2666
2887
  });
2667
2888
  port.onDisconnect.addListener(() => {
2668
2889
  if (panelPort !== port) return;
2669
2890
  panelPort = null;
2670
- setTimeout(connect, 1e3);
2891
+ reconnectTimer = setTimeout(() => {
2892
+ reconnectTimer = null;
2893
+ if (!disposed) connect();
2894
+ }, 1e3);
2671
2895
  });
2672
2896
  }
2897
+ let reconnectTimer = null;
2673
2898
  const transport = {
2674
2899
  origin,
2675
2900
  tabLabel: io.tabLabel ?? null,
@@ -2677,36 +2902,44 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2677
2902
  listener = fn;
2678
2903
  connect();
2679
2904
  io.onNavigated(() => {
2680
- since = 0;
2905
+ if (disposed) return;
2906
+ resetSince();
2907
+ setPolling(false);
2908
+ cancelSync();
2681
2909
  fn({ type: "navigated" });
2682
2910
  void resolveOrigin().then(() => {
2683
- setTimeout(() => void attachToPage(), 1200);
2911
+ if (!disposed) attachToPage(NAVIGATION_SETTLE);
2684
2912
  });
2685
2913
  });
2686
2914
  io.onTabChange?.((label) => {
2915
+ if (disposed) return;
2687
2916
  transport.tabLabel = label;
2688
2917
  const next = io.tabId();
2689
2918
  if (next === currentTab) {
2690
2919
  fn({ type: "tab-label", payload: label });
2691
2920
  return;
2692
2921
  }
2693
- since = 0;
2922
+ resetSince();
2694
2923
  relayConnected = false;
2695
2924
  setPolling(false);
2925
+ cancelSync();
2696
2926
  currentTab = next;
2697
2927
  void resolveOrigin().then(() => {
2928
+ if (disposed) return;
2698
2929
  fn({ type: "tab", payload: label });
2699
2930
  connect();
2700
- void attachToPage();
2931
+ attachToPage();
2701
2932
  });
2702
2933
  });
2703
- void resolveOrigin().then(() => attachToPage());
2934
+ void resolveOrigin().then(() => {
2935
+ if (!disposed) attachToPage();
2936
+ });
2704
2937
  },
2705
2938
  replay() {
2706
2939
  if (relayConnected) io.bridge("replay").catch(() => {
2707
2940
  });
2708
2941
  else {
2709
- since = 0;
2942
+ resetSince();
2710
2943
  emit({ type: "clear" });
2711
2944
  void syncWithPage().then(() => pollOnce());
2712
2945
  }
@@ -2714,7 +2947,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2714
2947
  clear() {
2715
2948
  io.bridge("clear").catch(() => {
2716
2949
  });
2717
- since = 0;
2950
+ resetSince();
2951
+ },
2952
+ dispose() {
2953
+ disposed = true;
2954
+ cancelSync();
2955
+ setPolling(false);
2956
+ if (reconnectTimer) clearTimeout(reconnectTimer);
2957
+ reconnectTimer = null;
2958
+ if (panelPort) {
2959
+ try {
2960
+ panelPort.disconnect();
2961
+ } catch {
2962
+ }
2963
+ panelPort = null;
2964
+ }
2965
+ listener = null;
2718
2966
  },
2719
2967
  configure: (options) => io.bridge("configure", options).then((r) => r ?? void 0),
2720
2968
  highlight: (id) => io.bridge("highlight", id).catch(() => {
@@ -2737,7 +2985,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2737
2985
  if (io.openResource) transport.openResource = io.openResource;
2738
2986
  if (io.undock) transport.undock = io.undock;
2739
2987
  if (io.readSource) transport.readSource = io.readSource;
2740
- transport.panelUrl = chrome.runtime.getURL("panel.html");
2741
2988
  return transport;
2742
2989
  }
2743
2990
  function devtoolsIO() {
@@ -2749,7 +2996,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2749
2996
  })
2750
2997
  );
2751
2998
  const expressions = {
2752
- info: () => "b.info?b.info():{count:b.size,protocol:b.version}",
2999
+ info: () => "b.info()",
2753
3000
  pull: (since) => `b.pull?b.pull(${Number(since) || 0}):null`,
2754
3001
  replay: () => "b.replay()",
2755
3002
  clear: () => "b.clear()",
@@ -2786,7 +3033,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2786
3033
  try {
2787
3034
  switch (cmd) {
2788
3035
  case "info":
2789
- return b.info ? b.info() : { count: b.size, protocol: b.version };
3036
+ return b.info();
2790
3037
  case "pull":
2791
3038
  return b.pull ? b.pull(arg) : null;
2792
3039
  case "replay":
@@ -2893,19 +3140,17 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2893
3140
  },
2894
3141
  openResource: (url) => void chrome.tabs.create({ url }),
2895
3142
  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)
3143
+ // Host permission for the origin is required to fetch the module source (and present when the panel works at all).
3144
+ readSource: fetchSource
2898
3145
  };
2899
3146
  return io;
2900
3147
  }
2901
3148
  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";
3149
+ const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : systemTheme();
2904
3150
  createPanel(document.getElementById("root"), createRelayTransport(devtoolsIO()), { theme });
2905
3151
  }
2906
3152
  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" });
3153
+ return createPanel(document.getElementById("root"), createRelayTransport(standaloneIO(opts)), { theme: systemTheme() });
2909
3154
  }
2910
3155
  function sampleReports() {
2911
3156
  const fn = (name) => FN_PREFIX + name;
@@ -3036,7 +3281,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3036
3281
  function createBroadcastTransport(name) {
3037
3282
  let listener = null;
3038
3283
  let channel = null;
3039
- const pending = /* @__PURE__ */ new Map();
3040
3284
  const emit = (m) => {
3041
3285
  if (listener) listener(m);
3042
3286
  };
@@ -3045,36 +3289,15 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3045
3289
  channel = new BroadcastChannel(name);
3046
3290
  channel.onmessage = (event) => {
3047
3291
  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
- }
3292
+ if (!data || reply(data)) return;
3057
3293
  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
3294
  };
3059
3295
  return channel;
3060
3296
  };
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}`;
3297
+ const { bridge, reply } = commandBridge((message) => open().postMessage(message), 1500);
3074
3298
  const transport = {
3075
3299
  origin: location.origin,
3076
3300
  tabLabel: `channel "${name}"`,
3077
- panelUrl: location.origin + location.pathname,
3078
3301
  subscribe(fn) {
3079
3302
  listener = fn;
3080
3303
  open();
@@ -3084,7 +3307,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3084
3307
  fn({ type: "disconnected" });
3085
3308
  return;
3086
3309
  }
3087
- fn({ type: "hello", version: info.protocol || 1, payload: info });
3310
+ fn({ type: "hello", version: info.protocol, payload: info });
3088
3311
  const res = await bridge("pull", 0);
3089
3312
  if (res) for (const p of res.reports) fn({ type: "report", payload: p });
3090
3313
  });
@@ -3096,23 +3319,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3096
3319
  }),
3097
3320
  flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3098
3321
  }),
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),
3322
+ storage: localStorageAdapter(`rerender-lens:${name}`),
3323
+ readSource: fetchSource,
3116
3324
  copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3117
3325
  })
3118
3326
  };
@@ -3123,24 +3331,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3123
3331
  let listener = null;
3124
3332
  let stream = null;
3125
3333
  let appsOnline = null;
3126
- const pending = /* @__PURE__ */ new Map();
3127
3334
  const emit = (m) => {
3128
3335
  if (listener) listener(m);
3129
3336
  };
3130
3337
  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
- });
3338
+ const { bridge, reply } = commandBridge(post, 2e3);
3144
3339
  const handleData = (data) => {
3145
3340
  let parsed;
3146
3341
  try {
@@ -3149,18 +3344,12 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3149
3344
  return;
3150
3345
  }
3151
3346
  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") {
3347
+ if (!isRecord(m) || reply(m)) continue;
3348
+ if (m.__rerenderLens === true && m.type === "relay") {
3160
3349
  const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
3161
3350
  const wasOnline = appsOnline;
3162
3351
  appsOnline = apps;
3163
- if (apps > 0 && wasOnline !== null && wasOnline === 0) void attach();
3352
+ if (apps > 0 && (wasOnline === null || wasOnline === 0)) void attach();
3164
3353
  else if (apps === 0) emit({ type: "disconnected" });
3165
3354
  } else if (m.__rerenderLens === true && typeof m.type === "string") emit({ type: m.type, version: typeof m.version === "number" ? m.version : void 0, payload: m.payload });
3166
3355
  }
@@ -3172,15 +3361,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3172
3361
  return;
3173
3362
  }
3174
3363
  emit({ type: "connected" });
3175
- emit({ type: "hello", version: info.protocol || 1, payload: info });
3364
+ emit({ type: "hello", version: info.protocol, payload: info });
3176
3365
  const res = await bridge("pull", 0);
3177
3366
  if (res) for (const p of res.reports) emit({ type: "report", payload: p });
3178
3367
  }
3179
- const mem = (key) => `rerender-lens:relay:${base}:${key}`;
3180
3368
  const transport = {
3181
3369
  origin: base,
3182
3370
  tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
3183
- panelUrl: location.origin + location.pathname,
3184
3371
  subscribe(fn) {
3185
3372
  listener = fn;
3186
3373
  const open = () => {
@@ -3191,7 +3378,6 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3191
3378
  };
3192
3379
  };
3193
3380
  open();
3194
- void attach();
3195
3381
  },
3196
3382
  replay: () => void bridge("replay"),
3197
3383
  clear: () => void bridge("clear"),
@@ -3200,47 +3386,18 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3200
3386
  }),
3201
3387
  flashAvoidable: (on) => bridge("flash", !!on).catch(() => {
3202
3388
  }),
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),
3389
+ storage: localStorageAdapter(`rerender-lens:relay:${base}`),
3390
+ readSource: fetchSource,
3220
3391
  copy: (text) => navigator.clipboard.writeText(text).catch(() => {
3221
3392
  })
3222
3393
  };
3223
3394
  return transport;
3224
3395
  }
3225
3396
  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" });
3397
+ return createPanel(document.getElementById("root"), createRelayClientTransport(relayUrl), { theme: systemTheme() });
3228
3398
  }
3229
3399
  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);
3400
+ return createPanel(document.getElementById("root"), createBroadcastTransport(name), { theme: systemTheme() });
3244
3401
  }
3245
3402
  function bootDemo() {
3246
3403
  const params2 = new URLSearchParams(location.search);
@@ -3253,7 +3410,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3253
3410
  origin: "http://localhost:5199",
3254
3411
  subscribe(fn) {
3255
3412
  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 } });
3413
+ 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
3414
  if (flood > 0) {
3258
3415
  const t0 = performance.now();
3259
3416
  for (const r of floodReports(flood)) fn({ type: "report", payload: r });
@@ -3283,11 +3440,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3283
3440
  originStatus: () => Promise.resolve({ origin: "http://localhost:5199", builtIn: true, permitted: true, enabled: true, inject: false, deferHook: false }),
3284
3441
  setOrigin: () => Promise.resolve(),
3285
3442
  storage: { get: (k) => Promise.resolve(mem[k]), set: (k, v) => Promise.resolve(mem[k] = v) },
3286
- panelUrl: location.origin + location.pathname,
3287
3443
  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
3444
  };
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" });
3445
+ createPanel(document.getElementById("root"), transport, { theme: /theme=dark/.test(location.search) ? "dark" : systemTheme() });
3291
3446
  }
3292
3447
  var api = {
3293
3448
  PROTOCOL,
@@ -3306,18 +3461,15 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3306
3461
  createRelayTransport,
3307
3462
  createBroadcastTransport,
3308
3463
  createRelayClientTransport,
3309
- encodeShare,
3310
- decodeShare,
3311
3464
  sourceContext,
3312
- analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
3465
+ analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit: analyzeCommit2, contextAttribution, cascadeTree, rootCauseSummary: rootCauseSummary2, summarizeSession, compareSessions }
3313
3466
  };
3314
3467
  window.RerenderLensPanel = api;
3315
3468
  var hasChrome = typeof chrome !== "undefined" && !!chrome && !!chrome.runtime && !!chrome.runtime.id;
3316
3469
  var hasDevtools = hasChrome && !!chrome.devtools && !!chrome.devtools.inspectedWindow;
3317
3470
  var params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
3318
3471
  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);
3472
+ if (params.has("relay") && typeof EventSource === "function") bootRelay(params.get("relay") || location.origin);
3321
3473
  else if (params.has("channel") && typeof BroadcastChannel === "function") bootBroadcast(params.get("channel") || "rerender-lens");
3322
3474
  else if (hasDevtools && /panel\.html/.test(pathname) && !params.has("tabId")) bootExtension();
3323
3475
  else if (hasChrome && (/sidepanel\.html/.test(pathname) || params.has("tabId"))) bootStandalone({ tabId: params.has("tabId") ? Number(params.get("tabId")) : null });