rerender-lens 0.4.0 → 0.7.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 (68) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +208 -359
  3. package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
  4. package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
  5. package/dist/cli.cjs +322 -53
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +322 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
  10. package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
  11. package/dist/index.cjs +537 -49
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +137 -8
  14. package/dist/index.d.ts +137 -8
  15. package/dist/index.js +521 -50
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1690 -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 +1688 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1777 -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 +1768 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +71 -19
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.d.cts +25 -3
  40. package/dist/relay.d.ts +25 -3
  41. package/dist/relay.js +71 -19
  42. package/dist/relay.js.map +1 -1
  43. package/dist/rerender-lens.iife.js +457 -62
  44. package/dist/runner-report-Bvqoe89M.d.ts +46 -0
  45. package/dist/runner-report-CvpDg2zt.d.cts +46 -0
  46. package/dist/setup.cjs +209 -35
  47. package/dist/setup.cjs.map +1 -1
  48. package/dist/setup.js +209 -35
  49. package/dist/setup.js.map +1 -1
  50. package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
  51. package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
  52. package/dist/vite.d.cts +1 -1
  53. package/dist/vite.d.ts +1 -1
  54. package/dist/vitest-setup.cjs +357 -44
  55. package/dist/vitest-setup.cjs.map +1 -1
  56. package/dist/vitest-setup.d.cts +4 -4
  57. package/dist/vitest-setup.d.ts +4 -4
  58. package/dist/vitest-setup.js +357 -44
  59. package/dist/vitest-setup.js.map +1 -1
  60. package/dist/vitest.cjs +420 -80
  61. package/dist/vitest.cjs.map +1 -1
  62. package/dist/vitest.d.cts +21 -39
  63. package/dist/vitest.d.ts +21 -39
  64. package/dist/vitest.js +421 -81
  65. package/dist/vitest.js.map +1 -1
  66. package/package.json +41 -4
  67. package/panel/panel.css +49 -0
  68. package/panel/panel.js +871 -353
package/panel/panel.js CHANGED
@@ -1,190 +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 PROTOCOL = 2;
6
- var KIND_LABEL = {
7
- "deep-equal": "equal by value",
8
- function: "new function",
9
- element: "equal element",
10
- different: "changed",
11
- added: "added",
12
- removed: "removed"
13
- };
14
- var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
15
- var FN_PREFIX = "\u0192 ";
16
- var MAX_REPORTS = 2e3;
17
- var MAX_PER_NODE = 200;
18
- var MAX_COMMITS = 500;
19
- var MAX_PER_COMMIT = 500;
20
- var THROTTLE_MIN_REPORTS = 200;
21
- var BEST_FIX_INTERVAL = 500;
22
- var LEFT_RENDER_INTERVAL = 250;
23
- var SYNC_RETRY_MIN = 500;
24
- var SYNC_RETRY_MAX = 5e3;
25
- var SYNC_RETRIES = 20;
26
- var NAVIGATION_SETTLE = 1200;
27
- var ROW_H = 22;
28
- var ITEM_H = 20;
29
- var OVERSCAN = 8;
30
- var FALLBACK_VIEWPORT = 800;
31
- function el(tag, attrs, children) {
32
- const node = document.createElement(tag);
33
- if (attrs) {
34
- for (const k of Object.keys(attrs)) {
35
- const v = attrs[k];
36
- if (k === "class") node.className = String(v);
37
- else if (k === "text") node.textContent = String(v);
38
- else if (k.startsWith("on")) {
39
- if (typeof v === "function") node.addEventListener(k.slice(2), v);
40
- } else if (v === true) node.setAttribute(k, "");
41
- else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
42
- }
43
- }
44
- if (children) {
45
- for (const c of [].concat(children)) if (c != null) node.append(c);
46
- }
47
- 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;
48
11
  }
49
- function fmtTime(ms) {
50
- const d = new Date(ms);
51
- const p = (n, w) => String(n).padStart(w, "0");
52
- return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
53
- }
54
- var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
55
- var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
56
- var componentList = (m) => [...m].map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
57
- function changesOf(report) {
58
- 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;
59
16
  }
60
- function summarize(report) {
61
- const counts = /* @__PURE__ */ new Map();
62
- for (const c of changesOf(report)) counts.set(c.kind, (counts.get(c.kind) || 0) + 1);
63
- if (counts.size === 0) return "no changes";
64
- 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;
65
26
  }
66
- var isRecord = (v) => typeof v === "object" && v !== null;
67
- function normalizeReport(p) {
68
- if (!isRecord(p) || typeof p.component !== "string") return null;
69
- const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
70
- const props = isRecord(p.props) ? p.props : {};
71
- 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;
72
- const r = {
73
- component: p.component,
74
- instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
75
- commitId: typeof p.commitId === "number" ? p.commitId : 0,
76
- renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
77
- trigger: typeof p.trigger === "string" ? p.trigger : "parent",
78
- avoidable: !!p.avoidable,
79
- props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
80
- propChanges: arr(p.propChanges),
81
- stateChanges: arr(p.stateChanges),
82
- hookChanges: arr(p.hookChanges),
83
- parent,
84
- owner: typeof p.owner === "string" ? p.owner : null,
85
- path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
86
- reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
87
- time: typeof p.time === "number" ? p.time : 0,
88
- 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;
89
34
  };
90
- if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
91
- if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
92
- if (typeof p.memoized === "boolean") r.memoized = p.memoized;
93
- if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
94
- if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
95
- if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
96
- if (isRecord(p.state)) r.state = p.state;
97
- if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
98
- if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
99
- if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
100
- if (typeof p.key === "string") r.key = p.key;
101
- if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
102
- return r;
103
35
  }
104
- function valueNode(v, depth = 0) {
105
- if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
106
- const t = typeof v;
107
- if (typeof v === "string") {
108
- if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
109
- if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
110
- 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;
111
56
  }
112
- if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
113
- if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
114
- if (Array.isArray(v)) {
115
- const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
116
- if (short) {
117
- const s = el("span", { class: "v" }, "[");
118
- v.forEach((x, i) => {
119
- if (i) s.append(", ");
120
- s.append(valueNode(x, depth + 1));
121
- });
122
- s.append("]");
123
- 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;
124
90
  }
125
- return objectDetails(`Array(${v.length})`, v);
91
+ return true;
126
92
  }
127
- const o = v;
128
- if (o.$type === "element") {
129
- const props = isRecord(o.props) ? o.props : {};
130
- const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
131
- 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;
132
105
  }
133
- if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
134
- if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
135
- if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
136
- if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
137
- const keys = Object.keys(o).filter((k) => k !== "$type");
138
- const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
139
- 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;
140
122
  }
141
- function objectDetails(label, obj) {
142
- const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
143
- d.addEventListener(
144
- "toggle",
145
- () => {
146
- if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
147
- },
148
- { once: true }
149
- );
150
- 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)}]`;
151
128
  }
152
- function firstDifferentPath(a, b, base = "") {
153
- if (a === b) return null;
154
- if (!isRecord(a) || !isRecord(b)) return base || "(value)";
155
- 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)";
156
131
  if (Array.isArray(a) && Array.isArray(b)) {
157
- if (a.length !== b.length) return base ? `${base}.length` : "length";
132
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
158
133
  for (let i = 0; i < a.length; i++) {
159
- const p = firstDifferentPath(a[i], b[i], `${base}[${i}]`);
160
- if (p) return p;
134
+ if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
161
135
  }
162
136
  return null;
163
137
  }
164
- const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
165
- for (const k of keys) {
166
- const p = firstDifferentPath(a[k], b[k], base ? `${base}.${k}` : k);
167
- 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;
168
144
  }
169
- return null;
145
+ return deepEqual(a, b) ? null : path || "(value)";
170
146
  }
171
- function diffLeaves(a, b, limit = 20, base = "", out = []) {
172
- if (out.length >= limit || a === b) return out;
173
- if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b)) {
174
- out.push({ path: base || "(value)", prev: a, next: b });
175
- return out;
176
- }
147
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
148
+ if (out.length >= limit || Object.is(a, b)) return out;
177
149
  if (Array.isArray(a) && Array.isArray(b)) {
178
150
  const n = Math.max(a.length, b.length);
179
- 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);
180
152
  return out;
181
153
  }
182
- for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
183
- if (out.length >= limit) break;
184
- 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;
185
160
  }
161
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
186
162
  return out;
187
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";
188
286
  function shortValue(v, max = 60) {
189
287
  let s;
190
288
  try {
@@ -195,12 +293,11 @@
195
293
  if (s === void 0) s = String(v);
196
294
  return s.length > max ? s.slice(0, max - 3) + "..." : s;
197
295
  }
198
- var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
199
296
  function fixesFor(r) {
200
297
  const out = [];
201
298
  const ownerName = r.owner || r.parent && r.parent.name || null;
202
- const changes = changesOf(r);
203
- const avoidableProps = (r.propChanges || []).filter((c) => AVOIDABLE_KINDS.has(c.kind));
299
+ const propChanges = r.propChanges || [];
300
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
204
301
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
205
302
  const identical = changes.length === 0;
206
303
  out.push({
@@ -219,9 +316,10 @@ export const ${r.component} = memo(function ${r.component}(props) {
219
316
  // class components: extend PureComponent instead`
220
317
  });
221
318
  }
222
- for (const c of avoidableProps) {
319
+ for (const c of propChanges) {
320
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
223
321
  const owner = ownerName || "?";
224
- const root = c.path.split(/[.[]/)[0] || c.path;
322
+ const root = rootOf(c.path);
225
323
  const id = identifier(root);
226
324
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
227
325
  out.push({
@@ -243,9 +341,7 @@ const children = useMemo(() => (
243
341
  // static children: hoist them to module scope
244
342
  const STATIC = <em>hi</em>;`
245
343
  });
246
- continue;
247
- }
248
- if (c.kind === "function") {
344
+ } else if (c.kind === "function") {
249
345
  out.push({
250
346
  kind: "useCallback",
251
347
  owner,
@@ -295,7 +391,7 @@ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
295
391
  });
296
392
  }
297
393
  }
298
- for (const c of [].concat(r.stateChanges || [], r.hookChanges || [])) {
394
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
299
395
  const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
300
396
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
301
397
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
@@ -319,17 +415,16 @@ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedK
319
415
  }
320
416
  if (!AVOIDABLE_KINDS.has(c.kind)) continue;
321
417
  if (isContext) {
322
- const name = ctxName;
323
418
  out.push({
324
419
  kind: "contextValue",
325
- owner: providerOwner || `${name}.Provider`,
420
+ owner: providerOwner || `${ctxName}.Provider`,
326
421
  target: r.component,
327
- prop: name,
328
- label: `memoize the ${name} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
329
- detail: `<${r.component}> re-rendered because ${name} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
330
- 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`}
331
426
  const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
332
- <${name}.Provider value={value}>`
427
+ <${ctxName}.Provider value={value}>`
333
428
  });
334
429
  } else if (c.hook === "useSyncExternalStore") {
335
430
  const chain = c.custom || [];
@@ -348,163 +443,67 @@ import { shallowEqual } from 'react-redux';
348
443
  const slice = useSelector(selectSlice, shallowEqual);
349
444
  // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
350
445
  import { useShallow } from 'zustand/react/shallow';
351
- const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
352
- // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
353
- // getSnapshot must return the same reference while the data is unchanged
354
- const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
355
- });
356
- } else {
357
- out.push({
358
- kind: "bailout",
359
- owner: r.component,
360
- target: r.component,
361
- prop: c.path,
362
- label: `bail out before setting ${c.path} in <${r.component}>`,
363
- detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
364
- snippet: `// ${r.component}
365
- setState((prev) => (deepEqual(prev, next) ? prev : next));`
366
- });
367
- }
368
- }
369
- return out;
370
- }
371
- var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
372
- function rankFixes(reports) {
373
- const byKey = /* @__PURE__ */ new Map();
374
- for (const r of reports) {
375
- if (!r.avoidable) continue;
376
- for (const f of fixesFor(r)) {
377
- const k = fixKey(f);
378
- let agg = byKey.get(k);
379
- if (!agg) {
380
- agg = { ...f, key: k, count: 0, components: /* @__PURE__ */ new Map(), reports: [] };
381
- byKey.set(k, agg);
382
- }
383
- agg.count++;
384
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
385
- if (agg.reports.length < 50) agg.reports.push(r);
386
- }
387
- }
388
- return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
389
- }
390
- var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
391
- function indexByComponent(reports) {
392
- const index = /* @__PURE__ */ new Map();
393
- for (const r of reports) {
394
- const list = index.get(r.component);
395
- if (list) list.push(r);
396
- else index.set(r.component, [r]);
397
- }
398
- return index;
399
- }
400
- function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
401
- let cur = r;
402
- const seen = /* @__PURE__ */ new Set([r]);
403
- while (cur.trigger === "parent" && cur.parent) {
404
- const parent = cur.parent;
405
- const candidates = index.get(parent.name);
406
- const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
407
- if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
408
- seen.add(p);
409
- cur = p;
410
- }
411
- return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
412
- }
413
- function analyzeCommit(reports) {
414
- const roots = /* @__PURE__ */ new Map();
415
- const rootByReport = /* @__PURE__ */ new Map();
416
- const index = indexByComponent(reports);
417
- let avoidable = 0;
418
- let wasted = 0;
419
- for (const r of reports) {
420
- if (!r.avoidable) continue;
421
- avoidable++;
422
- if (typeof r.selfDuration === "number") wasted += r.selfDuration;
423
- const root = rootCauseOf(r, reports, index);
424
- const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
425
- const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
426
- rootByReport.set(r, name);
427
- let agg = roots.get(name);
428
- if (!agg) {
429
- agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
430
- roots.set(name, agg);
431
- }
432
- agg.count++;
433
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
434
- }
435
- const first = reports[0];
436
- return {
437
- id: first ? first.commitId : 0,
438
- receivedAt: first ? first.receivedAt : 0,
439
- total: reports.length,
440
- avoidable,
441
- wasted,
442
- roots: [...roots.values()].sort((a, b) => b.count - a.count),
443
- rootByReport,
444
- contexts: contextAttribution(reports),
445
- fixes: rankFixes(reports),
446
- reports
447
- };
448
- }
449
- function contextAttribution(reports) {
450
- const byCtx = /* @__PURE__ */ new Map();
451
- for (const r of reports) {
452
- for (const c of r.hookChanges || []) {
453
- if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
454
- const m = /useContext\((.*)\)/.exec(c.path);
455
- const name = m && m[1] ? m[1] : c.path;
456
- let agg = byCtx.get(name);
457
- if (!agg) {
458
- agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
459
- byCtx.set(name, agg);
460
- }
461
- agg.consumers++;
462
- if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
463
- agg.components.set(r.component, (agg.components.get(r.component) || 0) + 1);
464
- agg.commits.add(r.commitId);
465
- if (c.provider && c.provider.component) agg.providers.set(c.provider.component, (agg.providers.get(c.provider.component) || 0) + 1);
466
- if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
467
- if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
446
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
447
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
448
+ // getSnapshot must return the same reference while the data is unchanged
449
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
450
+ });
451
+ } else {
452
+ out.push({
453
+ kind: "bailout",
454
+ owner: r.component,
455
+ target: r.component,
456
+ prop: c.path,
457
+ label: `bail out before setting ${c.path} in <${r.component}>`,
458
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
459
+ snippet: `// ${r.component}
460
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
461
+ });
468
462
  }
469
463
  }
470
- return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
464
+ return out;
471
465
  }
472
- function cascadeTree(reports) {
473
- const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
466
+ var fixKey = (f) => `${f.kind}|${f.owner}|${f.prop || f.target}`;
467
+ function rankFixes(reports) {
468
+ const byKey = /* @__PURE__ */ new Map();
474
469
  for (const r of reports) {
475
- let node = root;
476
- for (const seg of r.path.concat([r.component])) {
477
- let next = node.children.get(seg);
478
- if (!next) {
479
- next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
480
- node.children.set(seg, next);
470
+ if (!r.avoidable) continue;
471
+ for (const f of fixesFor(r)) {
472
+ const key = fixKey(f);
473
+ let agg = byKey.get(key);
474
+ if (!agg) {
475
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
476
+ byKey.set(key, agg);
481
477
  }
482
- node = next;
478
+ agg.count++;
479
+ agg.components[r.component] = (agg.components[r.component] || 0) + 1;
480
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
483
481
  }
484
- if (!node.report || r.avoidable) node.report = r;
485
- node.count = (node.count || 0) + 1;
486
- if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
487
482
  }
488
- return root;
483
+ return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
489
484
  }
490
- function rootCauseSummary(name, commits, analyze = (_, reports) => analyzeCommit(reports)) {
491
- const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), fixes: [] };
492
- const affected = [];
493
- for (const [key, reports] of commits) {
494
- const analysis = analyze(key, reports);
495
- const root = analysis.roots.find((x) => x.name === name);
496
- if (!root) continue;
497
- out.trigger = root.trigger;
498
- out.commits.push({ key, analysis, count: root.count, components: root.components });
499
- out.total += root.count;
500
- for (const [c, n] of root.components) out.components.set(c, (out.components.get(c) || 0) + n);
501
- for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) affected.push(r);
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);
502
499
  }
503
- out.commits.reverse();
504
- out.fixes = rankFixes(affected);
505
- 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(", ");
506
503
  }
507
- function summarizeSession(session, reports) {
504
+
505
+ // src/sessions.ts
506
+ function summarizeReports(reports, meta = {}) {
508
507
  var _a;
509
508
  const byComponent = {};
510
509
  let avoidable = 0;
@@ -522,10 +521,10 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
522
521
  }
523
522
  }
524
523
  return {
525
- id: session.id,
526
- name: session.name,
527
- startedAt: session.startedAt,
528
- 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),
529
528
  total: reports.length,
530
529
  avoidable,
531
530
  wasted,
@@ -533,7 +532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
533
532
  fixes: rankFixes(reports).map((f) => ({ key: f.key, label: f.label, count: f.count }))
534
533
  };
535
534
  }
536
- function compareSessions(before, after) {
535
+ function compareSummaries(before, after) {
537
536
  const names = /* @__PURE__ */ new Set([...Object.keys(before.byComponent), ...Object.keys(after.byComponent)]);
538
537
  const rows = [];
539
538
  for (const component of names) {
@@ -552,8 +551,159 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
552
551
  avoidable: { before: before.avoidable, after: after.avoidable, delta: after.avoidable - before.avoidable },
553
552
  wasted: { before: before.wasted, after: after.wasted, delta: after.wasted - before.wasted },
554
553
  resolvedFixes: before.fixes.filter((f) => !afterKeys.has(f.key)),
555
- 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
+ var MAX_STRIP_BARS = 200;
581
+ var MIN_BAR_PCT = 8;
582
+ function el(tag, attrs, children) {
583
+ const node = document.createElement(tag);
584
+ if (attrs) {
585
+ for (const k of Object.keys(attrs)) {
586
+ const v = attrs[k];
587
+ if (k === "class") node.className = String(v);
588
+ else if (k === "text") node.textContent = String(v);
589
+ else if (k.startsWith("on")) {
590
+ if (typeof v === "function") node.addEventListener(k.slice(2), v);
591
+ } else if (v === true) node.setAttribute(k, "");
592
+ else if (v !== void 0 && v !== null && v !== false) node.setAttribute(k, String(v));
593
+ }
594
+ }
595
+ if (children) {
596
+ for (const c of [].concat(children)) if (c != null) node.append(c);
597
+ }
598
+ return node;
599
+ }
600
+ function fmtTime(ms) {
601
+ const d = new Date(ms);
602
+ const p = (n, w) => String(n).padStart(w, "0");
603
+ return `${p(d.getHours(), 2)}:${p(d.getMinutes(), 2)}:${p(d.getSeconds(), 2)}.${p(d.getMilliseconds(), 3)}`;
604
+ }
605
+ var fmtMs = (n) => typeof n === "number" && Number.isFinite(n) ? `${n.toFixed(1)} ms` : "";
606
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
607
+ var fmtSpan = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`;
608
+ var spokenAgo = (ms) => ms < 1e3 ? "just now" : ms < 6e4 ? `${Math.round(ms / 1e3)} seconds ago` : `${Math.round(ms / 6e4)} minutes ago`;
609
+ var componentList = (m) => (m instanceof Map ? [...m] : Object.entries(m)).map(([c, n]) => `<${c}>${n > 1 ? " \xD7" + n : ""}`).join(", ");
610
+ function changesOf(report) {
611
+ return [].concat(report.propChanges || [], report.stateChanges || [], report.hookChanges || []);
612
+ }
613
+ var isRecord = (v) => typeof v === "object" && v !== null;
614
+ function normalizeReport(p) {
615
+ if (!isRecord(p) || typeof p.component !== "string") return null;
616
+ const arr = (x) => Array.isArray(x) ? x.filter((c) => isRecord(c) && typeof c.path === "string") : [];
617
+ const props = isRecord(p.props) ? p.props : {};
618
+ 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;
619
+ const r = {
620
+ component: p.component,
621
+ instanceId: typeof p.instanceId === "number" ? p.instanceId : 0,
622
+ commitId: typeof p.commitId === "number" ? p.commitId : 0,
623
+ renderCount: typeof p.renderCount === "number" ? p.renderCount : 0,
624
+ trigger: typeof p.trigger === "string" ? p.trigger : "parent",
625
+ avoidable: !!p.avoidable,
626
+ props: { prev: isRecord(props.prev) ? props.prev : {}, next: isRecord(props.next) ? props.next : {} },
627
+ propChanges: arr(p.propChanges),
628
+ stateChanges: arr(p.stateChanges),
629
+ hookChanges: arr(p.hookChanges),
630
+ parent,
631
+ owner: typeof p.owner === "string" ? p.owner : null,
632
+ path: Array.isArray(p.path) ? p.path.filter((x) => typeof x === "string") : [],
633
+ reasons: Array.isArray(p.reasons) ? p.reasons.filter((x) => typeof x === "string") : [],
634
+ time: typeof p.time === "number" ? p.time : 0,
635
+ receivedAt: typeof p.receivedAt === "number" ? p.receivedAt : 0
556
636
  };
637
+ if (typeof p.selfDuration === "number") r.selfDuration = p.selfDuration;
638
+ if (typeof p.treeDuration === "number") r.treeDuration = p.treeDuration;
639
+ if (typeof p.memoized === "boolean") r.memoized = p.memoized;
640
+ if (p.compiled === true) r.compiled = true;
641
+ if (typeof p.commitPriority === "string") r.commitPriority = p.commitPriority;
642
+ if (Array.isArray(p.hookState)) r.hookState = p.hookState.filter((h) => isRecord(h) && typeof h.path === "string");
643
+ if (Array.isArray(p.contexts)) r.contexts = p.contexts.filter((c) => isRecord(c) && typeof c.name === "string");
644
+ if (isRecord(p.state)) r.state = p.state;
645
+ if (Array.isArray(p.updaters)) r.updaters = p.updaters.filter((u) => typeof u === "string");
646
+ if (p.commitCause === "effect-after-commit" || p.commitCause === "suspense-resolved") r.commitCause = p.commitCause;
647
+ if (typeof p.afterCommit === "number") r.afterCommit = p.afterCommit;
648
+ if (typeof p.key === "string") r.key = p.key;
649
+ if (isRecord(p.source) && typeof p.source.fileName === "string") r.source = p.source;
650
+ return r;
651
+ }
652
+ function valueNode(v, depth = 0) {
653
+ if (v === null || v === void 0) return el("span", { class: "v nil", text: String(v) });
654
+ const t = typeof v;
655
+ if (typeof v === "string") {
656
+ if (v.startsWith(FN_PREFIX)) return el("span", { class: "v fn", text: v });
657
+ if (/^<[^>]+>$/.test(v)) return el("span", { class: "v", text: v });
658
+ return el("span", { class: "v str", text: JSON.stringify(v) });
659
+ }
660
+ if (t === "number" || t === "bigint") return el("span", { class: "v num", text: String(v) });
661
+ if (t === "boolean") return el("span", { class: "v bool", text: String(v) });
662
+ if (Array.isArray(v)) {
663
+ const short = v.length <= 4 && v.every((x) => typeof x !== "object" || x === null);
664
+ if (short) {
665
+ const s = el("span", { class: "v" }, "[");
666
+ v.forEach((x, i) => {
667
+ if (i) s.append(", ");
668
+ s.append(valueNode(x, depth + 1));
669
+ });
670
+ s.append("]");
671
+ return s;
672
+ }
673
+ return objectDetails(`Array(${v.length})`, v);
674
+ }
675
+ const o = v;
676
+ if (o.$type === "element") {
677
+ const props = isRecord(o.props) ? o.props : {};
678
+ const label2 = `<${String(o.name)}${o.key !== void 0 ? ` key=${JSON.stringify(o.key)}` : ""}>`;
679
+ return Object.keys(props).length ? objectDetails(label2, props) : el("span", { class: "v", text: label2 });
680
+ }
681
+ if (o.$type === "Date") return el("span", { class: "v", text: `Date(${String(o.value)})` });
682
+ if (o.$type === "RegExp") return el("span", { class: "v", text: String(o.value) });
683
+ if (o.$type === "Map" && Array.isArray(o.entries)) return objectDetails(`Map(${o.entries.length})`, o.entries);
684
+ if (o.$type === "Set" && Array.isArray(o.values)) return objectDetails(`Set(${o.values.length})`, o.values);
685
+ const keys = Object.keys(o).filter((k) => k !== "$type");
686
+ const label = `${o.$type ? String(o.$type) + " " : ""}{${keys.slice(0, 3).join(", ")}${keys.length > 3 ? ", ..." : ""}}`;
687
+ return objectDetails(label, o);
688
+ }
689
+ function objectDetails(label, obj) {
690
+ const d = el("details", { class: "obj" }, [el("summary", { text: label })]);
691
+ d.addEventListener(
692
+ "toggle",
693
+ () => {
694
+ if (d.open && !d.querySelector("pre")) d.append(el("pre", { text: JSON.stringify(obj, null, 2) }));
695
+ },
696
+ { once: true }
697
+ );
698
+ return d;
699
+ }
700
+ function analyzeCommit2(reports) {
701
+ const first = reports[0];
702
+ return { ...analyzeCommit(reports), receivedAt: first ? first.receivedAt : 0, fixes: rankFixes(reports) };
703
+ }
704
+ function rootCauseSummary2(name, commits, analyze = (_, reports) => analyzeCommit2(reports)) {
705
+ const s = rootCauseSummary(name, commits, analyze);
706
+ return { ...s, fixes: rankFixes(s.affected) };
557
707
  }
558
708
  var PRIORITY_LABEL = { immediate: "discrete input", "user-blocking": "continuous input", normal: "transition / async", low: "low", idle: "idle" };
559
709
  function sourceContext(text, line, around = 3) {
@@ -710,7 +860,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
710
860
  if (r.parent) by.append(el("div", { text: `Triggered by <${r.parent.name}> (${r.parent.trigger})` }));
711
861
  else by.append(el("div", { text: "Update started in this component" }));
712
862
  if (r.owner) by.append(el("div", { class: "meta", text: `Created by <${r.owner}>` }));
713
- if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
863
+ if (r.compiled) by.append(el("div", { class: "meta", text: "Compiled by React Compiler (output memoized per input; identical inputs make the render cheap)" }));
864
+ else if (r.memoized === false) by.append(el("div", { class: "meta", text: "Not memoized (re-renders whenever its parent does)" }));
714
865
  else if (r.memoized === true) by.append(el("div", { class: "meta", text: "Memoized (React.memo / PureComponent)" }));
715
866
  if (r.updaters && r.updaters.length) by.append(el("div", { text: `Update scheduled by ${r.updaters.map((u) => `<${u}>`).join(", ")}` }));
716
867
  if (r.commitCause === "effect-after-commit") by.append(el("div", { class: "cause effect", text: `Effect loop: state set right after commit #${r.afterCommit ?? "?"}` }));
@@ -852,6 +1003,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
852
1003
  reports: [],
853
1004
  commits: /* @__PURE__ */ new Map(),
854
1005
  commitOrder: [],
1006
+ apps: [],
1007
+ appId: null,
855
1008
  selectedKey: null,
856
1009
  selectedReport: null,
857
1010
  selectedCommit: null,
@@ -877,7 +1030,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
877
1030
  recording: null,
878
1031
  selectedSession: null,
879
1032
  compareWith: null,
880
- byInstance: false
1033
+ byInstance: false,
1034
+ timeWindow: null,
1035
+ stripCollapsed: false
881
1036
  };
882
1037
  let persistTimer = null;
883
1038
  let queue = [];
@@ -937,6 +1092,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
937
1092
  const settingsBtn = iconButton("\u2699", "Settings", "Settings", () => toggleSettings());
938
1093
  const status = el("span", { class: "status", title: "" }, [el("span", { class: "dot" }), el("span", { class: "status-text", text: "no page" })]);
939
1094
  const tabChip = el("span", { class: "tab-chip", hidden: true, title: "The tab this panel follows" });
1095
+ const appPicker = el("select", { class: "app-picker", hidden: true, title: "Which connected app this panel watches", "aria-label": "App" });
1096
+ appPicker.addEventListener("change", () => transport.selectApp?.(appPicker.value));
940
1097
  const undock = transport.undock ? [
941
1098
  el("span", { class: "sep" }),
942
1099
  iconButton("\u2AFF", "Side panel", "Show this panel next to the page (Chrome side panel)", () => void transport.undock("sidepanel").catch((e) => toast(String(e.message || e)))),
@@ -958,11 +1115,31 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
958
1115
  ...undock,
959
1116
  el("span", { class: "spacer" }),
960
1117
  tabChip,
1118
+ appPicker,
961
1119
  status,
962
1120
  settingsBtn
963
1121
  ]);
964
1122
  const summary = el("div", { class: "summary" });
965
1123
  const banner = el("div", { class: "banner", hidden: true });
1124
+ const stripChevron = el("span", { class: "chevron", text: "\u25BE" });
1125
+ const stripCount = el("span", { class: "count", text: "0 commits" });
1126
+ const stripChip = el("button", {
1127
+ class: "chip",
1128
+ hidden: true,
1129
+ title: "Clear the time window (Esc)",
1130
+ onclick: (e) => {
1131
+ e.stopPropagation();
1132
+ clearWindow();
1133
+ }
1134
+ });
1135
+ const stripBars = el("div", { class: "bars" });
1136
+ const stripFrom = el("span", { class: "from", text: "" });
1137
+ const stripTo = el("span", { class: "to", text: "now" });
1138
+ const stripHeader = el("div", { class: "timeline-header", onclick: () => toggleStrip() }, [stripChevron, el("span", { class: "title", text: "Timeline" }), stripChip, stripCount]);
1139
+ const timeline = el("div", { class: "timeline", hidden: true, role: "group", "aria-label": "Commit timeline" }, [
1140
+ stripHeader,
1141
+ el("div", { class: "timeline-body" }, [stripBars, el("div", { class: "timeline-axis" }, [stripFrom, stripTo])])
1142
+ ]);
966
1143
  const viewsBar = el("div", { class: "views" });
967
1144
  const VIEWS = [
968
1145
  ["tree", "Tree"],
@@ -1025,7 +1202,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1025
1202
  details.setAttribute("aria-label", "Details");
1026
1203
  settings.setAttribute("role", "dialog");
1027
1204
  settings.setAttribute("aria-label", "Settings");
1028
- root.append(toolbar, summary, banner, main, stream, toastEl);
1205
+ stripBars.setAttribute("role", "group");
1206
+ stripBars.setAttribute("aria-label", "Commits over time; arrow keys move between commits, Enter selects, drag to filter every view to a time window");
1207
+ root.append(toolbar, summary, banner, timeline, main, stream, toastEl);
1029
1208
  root.addEventListener("keydown", onGlobalKey);
1030
1209
  function setCompact(on) {
1031
1210
  if (state.compact === on) return;
@@ -1144,7 +1323,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1144
1323
  collapsed: [...state.collapsed],
1145
1324
  treeWidth: state.treeWidth,
1146
1325
  flashOn: state.flashOn,
1147
- byInstance: state.byInstance
1326
+ byInstance: state.byInstance,
1327
+ stripCollapsed: state.stripCollapsed
1148
1328
  };
1149
1329
  transport.storage.set("panel", saved);
1150
1330
  }, 150);
@@ -1196,6 +1376,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1196
1376
  instancesBtn.classList.toggle("active", state.byInstance);
1197
1377
  rebuildTree();
1198
1378
  }
1379
+ if (typeof saved.stripCollapsed === "boolean" && saved.stripCollapsed !== state.stripCollapsed) toggleStrip(saved.stripCollapsed);
1199
1380
  if (saved.tab === "history" || saved.tab === "fix") state.tab = saved.tab;
1200
1381
  if (saved.view && viewButtons.has(saved.view)) state.view = saved.view;
1201
1382
  for (const n of state.nodesByKey.values()) n.expanded = !state.collapsed.has(n.key);
@@ -1250,7 +1431,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1250
1431
  function analysisFor(key, reports) {
1251
1432
  const cached = commitAnalyses.get(key);
1252
1433
  if (cached && cached.len === reports.length) return cached.analysis;
1253
- const analysis = analyzeCommit(reports);
1434
+ const analysis = analyzeCommit2(reports);
1254
1435
  commitAnalyses.set(key, { len: reports.length, analysis });
1255
1436
  return analysis;
1256
1437
  }
@@ -1351,6 +1532,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1351
1532
  function renderHeavy(details2) {
1352
1533
  heavyAt = Date.now();
1353
1534
  if (state.view !== "tree") renderLeft();
1535
+ renderStrip();
1354
1536
  if (details2) renderDetails();
1355
1537
  }
1356
1538
  function scheduleHeavy(details2) {
@@ -1397,12 +1579,14 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1397
1579
  state.selectedCommit = null;
1398
1580
  state.selectedFix = null;
1399
1581
  state.selectedRoot = null;
1582
+ state.timeWindow = null;
1400
1583
  if (state.tab === "commit" || state.tab === "fixlist" || state.tab === "root") state.tab = "latest";
1401
1584
  queue = [];
1402
1585
  renderLeft();
1403
1586
  renderDetails();
1404
1587
  renderStream();
1405
1588
  renderSummary();
1589
+ renderStrip();
1406
1590
  }
1407
1591
  let filterCache = { filter: "", value: null, regex: null, text: "" };
1408
1592
  function parsedFilter() {
@@ -1443,17 +1627,45 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1443
1627
  }
1444
1628
  return text.includes(q);
1445
1629
  }
1630
+ const inWindow = (r) => {
1631
+ const w = state.timeWindow;
1632
+ return !w || r.receivedAt >= w.from && r.receivedAt <= w.to;
1633
+ };
1634
+ const windowKey = () => {
1635
+ const w = state.timeWindow;
1636
+ return w ? `${w.from}-${w.to}` : "";
1637
+ };
1638
+ const NO_COUNTS = { total: 0, avoidable: 0 };
1639
+ let windowNodes = null;
1640
+ function nodeCounts(node) {
1641
+ if (!state.timeWindow) return node;
1642
+ const key = `${dataGen}|${windowKey()}|${state.byInstance}`;
1643
+ if (!windowNodes || windowNodes.key !== key) {
1644
+ const map = /* @__PURE__ */ new Map();
1645
+ for (const r of state.reports) {
1646
+ if (!inWindow(r)) continue;
1647
+ const k = nodeOfReport(r).key;
1648
+ let c = map.get(k);
1649
+ if (!c) map.set(k, c = { total: 0, avoidable: 0 });
1650
+ c.total++;
1651
+ if (r.avoidable) c.avoidable++;
1652
+ }
1653
+ windowNodes = { key, map };
1654
+ }
1655
+ return windowNodes.map.get(node.key) || NO_COUNTS;
1656
+ }
1446
1657
  function visible(node) {
1447
- const own = (!state.avoidableOnly || node.avoidable > 0) && matchesFilter(node.name) && node.total > 0 && (valueQuery() === null || node.reports.some(matchesValues));
1658
+ const c = nodeCounts(node);
1659
+ const own = (!state.avoidableOnly || c.avoidable > 0) && matchesFilter(node.name) && c.total > 0 && (valueQuery() === null || node.reports.some(matchesValues));
1448
1660
  if (own) return true;
1449
- for (const c of node.children.values()) if (visible(c)) return true;
1661
+ for (const child of node.children.values()) if (visible(child)) return true;
1450
1662
  return false;
1451
1663
  }
1452
- const passes = (r) => (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
1664
+ const passes = (r) => inWindow(r) && (!state.avoidableOnly || r.avoidable) && matchesFilter(r.component) && matchesValues(r);
1453
1665
  let dataGen = 0;
1454
1666
  let filtered = { key: "", reports: [], fixes: null };
1455
1667
  function filteredReports() {
1456
- const key = `${dataGen}|${state.avoidableOnly}|${state.filter}`;
1668
+ const key = `${dataGen}|${state.avoidableOnly}|${state.filter}|${windowKey()}`;
1457
1669
  if (filtered.key !== key) filtered = { key, reports: state.reports.filter(passes), fixes: null };
1458
1670
  return filtered.reports;
1459
1671
  }
@@ -1471,6 +1683,154 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1471
1683
  renderDetails();
1472
1684
  persist();
1473
1685
  }
1686
+ let bars = [];
1687
+ const barEls = /* @__PURE__ */ new Map();
1688
+ const barNodes = () => [...stripBars.children];
1689
+ function renderStrip() {
1690
+ const order = state.commitOrder;
1691
+ const next = [];
1692
+ let max = 1;
1693
+ for (let i = Math.max(0, order.length - MAX_STRIP_BARS); i < order.length; i++) {
1694
+ const key = order[i];
1695
+ const reports = state.commits.get(key);
1696
+ if (!reports || !reports.length) continue;
1697
+ let avoidable = 0;
1698
+ let from = Infinity;
1699
+ let to = 0;
1700
+ for (const r of reports) {
1701
+ if (r.avoidable) avoidable++;
1702
+ if (r.receivedAt < from) from = r.receivedAt;
1703
+ if (r.receivedAt > to) to = r.receivedAt;
1704
+ }
1705
+ next.push({ key, total: reports.length, avoidable, from, to });
1706
+ if (reports.length > max) max = reports.length;
1707
+ }
1708
+ bars = next;
1709
+ if (!bars.length) {
1710
+ timeline.hidden = true;
1711
+ stripBars.textContent = "";
1712
+ barEls.clear();
1713
+ return;
1714
+ }
1715
+ timeline.hidden = false;
1716
+ const now = bars[bars.length - 1].to;
1717
+ const win = state.timeWindow;
1718
+ const live = /* @__PURE__ */ new Set();
1719
+ const frag = document.createDocumentFragment();
1720
+ for (let i = 0; i < bars.length; i++) {
1721
+ const b = bars[i];
1722
+ live.add(b.key);
1723
+ let node = barEls.get(b.key);
1724
+ if (!node) {
1725
+ node = el("button", { class: "bar", type: "button", "data-commit": String(b.key) }, [el("span", { class: "avoid" })]);
1726
+ barEls.set(b.key, node);
1727
+ }
1728
+ node.setAttribute("data-index", String(i));
1729
+ node.style.height = `${Math.max(MIN_BAR_PCT, Math.round(100 * Math.sqrt(b.total / max)))}%`;
1730
+ node.firstElementChild.style.height = `${Math.round(100 * b.avoidable / b.total)}%`;
1731
+ node.classList.toggle("has-avoid", b.avoidable > 0);
1732
+ node.classList.toggle("selected", state.selectedCommit === b.key && state.tab === "commit");
1733
+ node.classList.toggle("in-window", !!win && b.to >= win.from && b.from <= win.to);
1734
+ node.setAttribute("aria-label", `commit ${b.key}, ${plural(b.total, "render")}, ${b.avoidable} avoidable, ${spokenAgo(Math.max(0, now - b.to))}`);
1735
+ node.title = `#${b.key} \xB7 ${plural(b.total, "render")} \xB7 ${b.avoidable} avoidable`;
1736
+ frag.append(node);
1737
+ }
1738
+ stripBars.textContent = "";
1739
+ stripBars.append(frag);
1740
+ for (const key of [...barEls.keys()]) if (!live.has(key)) barEls.delete(key);
1741
+ const span = now - bars[0].from;
1742
+ stripFrom.textContent = span > 0 ? `-${fmtSpan(span)}` : "0ms";
1743
+ stripTo.textContent = "now";
1744
+ stripCount.textContent = plural(bars.length, "commit") + (order.length > bars.length ? ` of ${order.length}` : "");
1745
+ if (win) {
1746
+ let n = 0;
1747
+ for (const r of state.reports) if (inWindow(r)) n++;
1748
+ stripChip.textContent = `${fmtSpan(win.to - win.from)} window, ${plural(n, "report")} \xB7 Clear`;
1749
+ stripChip.hidden = false;
1750
+ } else stripChip.hidden = true;
1751
+ }
1752
+ function toggleStrip(value) {
1753
+ state.stripCollapsed = value === void 0 ? !state.stripCollapsed : value;
1754
+ timeline.classList.toggle("collapsed", state.stripCollapsed);
1755
+ stripChevron.textContent = state.stripCollapsed ? "\u25B8" : "\u25BE";
1756
+ if (value === void 0) persist();
1757
+ }
1758
+ function setWindow(from, to) {
1759
+ state.timeWindow = from <= to ? { from, to } : { from: to, to: from };
1760
+ afterWindowChange();
1761
+ }
1762
+ function clearWindow() {
1763
+ if (!state.timeWindow) return;
1764
+ state.timeWindow = null;
1765
+ afterWindowChange();
1766
+ }
1767
+ function afterWindowChange() {
1768
+ renderLeft();
1769
+ renderStream();
1770
+ renderDetails();
1771
+ renderStrip();
1772
+ }
1773
+ const barAt = (target) => {
1774
+ const node = target && typeof target.closest === "function" ? target.closest(".bar") : null;
1775
+ const i = node ? Number(node.getAttribute("data-index")) : -1;
1776
+ return Number.isInteger(i) && i >= 0 && i < bars.length ? i : -1;
1777
+ };
1778
+ let brush = null;
1779
+ let swallowClick = false;
1780
+ const paintBrush = () => {
1781
+ const lo = brush ? Math.min(brush.a, brush.b) : -1;
1782
+ const hi = brush ? Math.max(brush.a, brush.b) : -2;
1783
+ const nodes = barNodes();
1784
+ for (let i = 0; i < nodes.length; i++) nodes[i].classList.toggle("brushing", i >= lo && i <= hi);
1785
+ };
1786
+ stripBars.addEventListener("mousedown", (e) => {
1787
+ swallowClick = false;
1788
+ const i = barAt(e.target);
1789
+ if (i < 0) return;
1790
+ brush = { a: i, b: i };
1791
+ });
1792
+ stripBars.addEventListener("mousemove", (e) => {
1793
+ if (!brush) return;
1794
+ const i = barAt(e.target);
1795
+ if (i < 0 || i === brush.b) return;
1796
+ brush.b = i;
1797
+ paintBrush();
1798
+ });
1799
+ window.addEventListener("mouseup", (e) => {
1800
+ if (!brush) return;
1801
+ const i = barAt(e.target);
1802
+ if (i >= 0) brush.b = i;
1803
+ const { a, b } = brush;
1804
+ brush = null;
1805
+ paintBrush();
1806
+ if (a === b) return;
1807
+ swallowClick = true;
1808
+ setWindow(bars[Math.min(a, b)].from, bars[Math.max(a, b)].to);
1809
+ });
1810
+ stripBars.addEventListener("click", (e) => {
1811
+ if (swallowClick) {
1812
+ swallowClick = false;
1813
+ return;
1814
+ }
1815
+ const i = barAt(e.target);
1816
+ if (i >= 0) selectCommitBar(bars[i].key);
1817
+ });
1818
+ stripBars.addEventListener("keydown", (e) => {
1819
+ swallowClick = false;
1820
+ const key = e.key;
1821
+ if (key !== "ArrowLeft" && key !== "ArrowRight" && key !== "Home" && key !== "End") return;
1822
+ const nodes = barNodes();
1823
+ const i = nodes.indexOf(document.activeElement);
1824
+ if (i < 0) return;
1825
+ e.preventDefault();
1826
+ const to = key === "ArrowLeft" ? Math.max(0, i - 1) : key === "ArrowRight" ? Math.min(nodes.length - 1, i + 1) : key === "Home" ? 0 : nodes.length - 1;
1827
+ nodes[to].focus();
1828
+ });
1829
+ function selectCommitBar(key) {
1830
+ showCommit(key);
1831
+ if (state.view !== "commits") setView("commits");
1832
+ renderStrip();
1833
+ }
1474
1834
  function renderLeft() {
1475
1835
  if (state.view === "tree") renderTree();
1476
1836
  else if (state.view === "offenders") renderOffenders();
@@ -1480,10 +1840,73 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1480
1840
  }
1481
1841
  const rowEls = /* @__PURE__ */ new Map();
1482
1842
  const treeList = virtualList(tree, ROW_H, ({ node, depth }) => rowFor(node, depth));
1483
- const emptyEl = el("div", { class: "empty" }, [
1484
- el("div", { text: "No re-renders reported yet." }),
1485
- el("div", null, ["Call ", el("code", { text: "init({ notifier: createDevtoolsNotifier() })" }), " in the page, or enable injection in Settings, then interact with it."])
1486
- ]);
1843
+ const emptyEl = el("div", { class: "empty" });
1844
+ function trackingSentence(t) {
1845
+ const what = t.mode === "all" ? "Tracking every component" : t.mode === "memoized" ? "Tracking every React.memo and PureComponent" : "Tracking only components marked with track()";
1846
+ const parts = [what];
1847
+ if (t.include.length) parts.push(`${t.mode === "marked" ? "and" : "plus"} names matching ${t.include.join(", ")}`);
1848
+ if (t.exclude.length) parts.push(`except ${t.exclude.join(", ")}`);
1849
+ const rendered = `${t.renderedCount}${t.overflow ? "+" : ""} component${t.renderedCount === 1 ? "" : "s"} rendered`;
1850
+ return `${parts.join(", ")}; ${rendered}, ${t.trackedCount} of them tracked.`;
1851
+ }
1852
+ function renderEmpty() {
1853
+ emptyEl.textContent = "";
1854
+ emptyEl.append(el("div", { class: "empty-head", text: "No re-renders reported yet." }));
1855
+ const t = state.library?.tracking;
1856
+ if (!t) {
1857
+ emptyEl.append(
1858
+ el("div", null, ["Call ", el("code", { text: "init({ notifier: createDevtoolsNotifier() })" }), " in the page, or enable injection in Settings, then interact with it."])
1859
+ );
1860
+ return;
1861
+ }
1862
+ emptyEl.append(el("div", { class: "empty-tracking", text: trackingSentence(t) }));
1863
+ if (t.mode === "all") {
1864
+ emptyEl.append(el("div", { text: "Every component is tracked, so this is waiting for a re-render: interact with the page. The first render of a component is never reported." }));
1865
+ return;
1866
+ }
1867
+ const untracked = t.renderedCount - t.trackedCount;
1868
+ emptyEl.append(
1869
+ el("div", {
1870
+ text: untracked > 0 ? `${untracked} component${untracked === 1 ? "" : "s"} rendered without being tracked. Widen what is tracked, or interact with the page if the tracked ones simply have not re-rendered yet.` : "Interact with the page: the first render of a component is never reported, only re-renders."
1871
+ })
1872
+ );
1873
+ if (!transport.configure) return;
1874
+ const trackAll = el("button", { class: "primary", onclick: () => void applyTracking({ trackAllComponents: true }) }, "Track every component");
1875
+ const match = el("input", { class: "empty-match", type: "text", placeholder: "Name or /regex/", "aria-label": "Track components matching" });
1876
+ const addMatch = () => {
1877
+ const value = match.value.trim();
1878
+ if (!value) return;
1879
+ const include = (state.library?.tracking?.include || []).slice();
1880
+ if (!include.includes(value)) include.push(value);
1881
+ match.value = "";
1882
+ void applyTracking({ include });
1883
+ };
1884
+ match.addEventListener("keydown", (e) => {
1885
+ if (e.key === "Enter") addMatch();
1886
+ });
1887
+ emptyEl.append(
1888
+ el("div", { class: "empty-actions" }, [trackAll, match, el("button", { onclick: addMatch }, "Track components matching\u2026")])
1889
+ );
1890
+ }
1891
+ async function applyTracking(patch) {
1892
+ if (!transport.configure) return;
1893
+ try {
1894
+ const applied = await transport.configure(patch);
1895
+ if (state.library) {
1896
+ if (applied) state.library.options = applied;
1897
+ const t = state.library.tracking;
1898
+ if (t) {
1899
+ if (patch.trackAllComponents) t.mode = "all";
1900
+ if (patch.include) t.include = patch.include;
1901
+ }
1902
+ transport.storage?.set("settings", Object.assign({}, state.library.options));
1903
+ }
1904
+ toast("Applied");
1905
+ renderTree();
1906
+ } catch (e) {
1907
+ toast(`Failed: ${e.message}`);
1908
+ }
1909
+ }
1487
1910
  function renderTree() {
1488
1911
  const flat = [];
1489
1912
  const walk = (node, depth) => {
@@ -1495,6 +1918,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1495
1918
  };
1496
1919
  walk(state.tree, 0);
1497
1920
  if (flat.length === 0) {
1921
+ renderEmpty();
1498
1922
  if (!emptyEl.parentNode) tree.append(emptyEl);
1499
1923
  } else emptyEl.remove();
1500
1924
  if (rowEls.size > flat.length * 2 + 64) {
@@ -1551,8 +1975,9 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1551
1975
  chevron.textContent = node.expanded ? "\u25BE" : "\u25B8";
1552
1976
  const badges = row.querySelector(".badges");
1553
1977
  badges.textContent = "";
1554
- if (node.avoidable) badges.append(el("span", { class: "badge avoid", title: "avoidable re-renders", text: String(node.avoidable) }));
1555
- if (node.total) badges.append(el("span", { class: "badge", title: "re-renders", text: String(node.total) }));
1978
+ const counts = nodeCounts(node);
1979
+ if (counts.avoidable) badges.append(el("span", { class: "badge avoid", title: "avoidable re-renders", text: String(counts.avoidable) }));
1980
+ if (counts.total) badges.append(el("span", { class: "badge", title: "re-renders", text: String(counts.total) }));
1556
1981
  if (node.flash) {
1557
1982
  node.flash = false;
1558
1983
  if (Date.now() - (node.flashAt || 0) < 1e3) {
@@ -1607,7 +2032,11 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1607
2032
  toggleSettings(false);
1608
2033
  return;
1609
2034
  }
1610
- if (inField) target.blur();
2035
+ if (inField) {
2036
+ target.blur();
2037
+ return;
2038
+ }
2039
+ clearWindow();
1611
2040
  return;
1612
2041
  }
1613
2042
  if (inField || e.ctrlKey || e.metaKey || e.altKey) return;
@@ -1715,6 +2144,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1715
2144
  state.tab = "commit";
1716
2145
  renderLeft();
1717
2146
  renderDetails();
2147
+ renderStrip();
1718
2148
  }
1719
2149
  function showRoot(name) {
1720
2150
  state.selectedRoot = name;
@@ -1981,7 +2411,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1981
2411
  }
1982
2412
  function renderRootDetails() {
1983
2413
  const name = state.selectedRoot;
1984
- const s = rootCauseSummary(name, state.commits, analysisFor);
2414
+ const s = rootCauseSummary2(name, state.commits, analysisFor);
1985
2415
  details.append(
1986
2416
  el("div", { class: "details-header" }, [
1987
2417
  el("span", { class: "title" }, ["Root cause ", el("span", { class: "name", text: `<${name}>` })]),
@@ -2291,6 +2721,16 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2291
2721
  status.querySelector(".status-text").textContent = text;
2292
2722
  tabChip.hidden = !state.tabLabel;
2293
2723
  tabChip.textContent = state.tabLabel || "";
2724
+ appPicker.hidden = state.apps.length < 2;
2725
+ if (!appPicker.hidden) {
2726
+ const want = state.apps.map((a) => `${a.id}:${a.label}`).join("|");
2727
+ if (appPicker.dataset.apps !== want) {
2728
+ appPicker.dataset.apps = want;
2729
+ appPicker.textContent = "";
2730
+ for (const a of state.apps) appPicker.append(el("option", { value: a.id, text: a.label || a.id }));
2731
+ }
2732
+ if (state.appId) appPicker.value = state.appId;
2733
+ }
2294
2734
  banner.textContent = "";
2295
2735
  const warnings = [];
2296
2736
  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.`);
@@ -2310,6 +2750,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2310
2750
  function setLibrary(info) {
2311
2751
  state.library = info;
2312
2752
  renderStatus();
2753
+ if (emptyEl.parentNode) renderEmpty();
2313
2754
  if (state.settingsOpen) void renderSettings();
2314
2755
  if (state.flashOn) transport.flashAvoidable?.(true);
2315
2756
  }
@@ -2434,6 +2875,33 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2434
2875
  const max = el("input", { type: "number", min: "0", value: String(current.maxReportsPerComponent || 0) });
2435
2876
  max.addEventListener("change", () => void applyOptions({ maxReportsPerComponent: Math.max(0, Number(max.value) || 0) }));
2436
2877
  sec.append(el("label", { class: "opt col" }, ["Stop printing a component after N reports (0 = never)", max]));
2878
+ const help = el("div", { class: "section" }, [el("h3", { text: "Help" })]);
2879
+ help.append(
2880
+ 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." }),
2881
+ el("a", { class: "report-link", href: issueUrl(), target: "_blank", rel: "noreferrer", text: "Report a problem \u2197" })
2882
+ );
2883
+ body.append(help);
2884
+ }
2885
+ function issueUrl() {
2886
+ const lib = state.library;
2887
+ const ext = typeof chrome !== "undefined" && chrome.runtime && typeof chrome.runtime.getManifest === "function" ? chrome.runtime.getManifest().version : "n/a";
2888
+ const react = lib?.react?.map((r) => `${r.version || "?"}${r.bundleType === 0 ? " (prod)" : ""}`).join(", ") || "not detected";
2889
+ const transportLabel = state.tabLabel || (state.relay ? "content script" : state.polling ? "polling" : "none");
2890
+ const lines = [
2891
+ "### What happened",
2892
+ "",
2893
+ "(what you did, what you expected, what the panel showed)",
2894
+ "",
2895
+ "### Environment",
2896
+ `- extension ${ext}, panel protocol ${PROTOCOL}, transport: ${transportLabel}`,
2897
+ `- library ${lib?.library || "not detected"} (protocol ${lib?.protocol ?? "?"}), React ${react}${lib?.production ? ", production build" : ""}`,
2898
+ `- options: ${JSON.stringify(lib?.options || {})}`,
2899
+ `- 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}`,
2900
+ `- buffered: ${state.reports.length} reports, ${totals.avoidable} avoidable; top fix: ${bestFix ? bestFix.label : "none"}`,
2901
+ `- browser: ${typeof navigator !== "undefined" ? navigator.userAgent : "n/a"}`
2902
+ ];
2903
+ const params2 = new URLSearchParams({ title: "Panel: ", body: lines.join("\n"), labels: "bug" });
2904
+ return `https://github.com/NexaLeaf/rerender-lens/issues/new?${params2.toString()}`;
2437
2905
  }
2438
2906
  function handle(message) {
2439
2907
  if (!isRecord(message)) return;
@@ -2460,6 +2928,13 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2460
2928
  state.library = null;
2461
2929
  renderStatus();
2462
2930
  break;
2931
+ case "apps": {
2932
+ const p = isRecord(message.payload) ? message.payload : {};
2933
+ state.apps = Array.isArray(p.list) ? p.list : [];
2934
+ state.appId = typeof p.selected === "string" ? p.selected : null;
2935
+ renderStatus();
2936
+ break;
2937
+ }
2463
2938
  case "hello":
2464
2939
  if (isRecord(message.payload)) setLibrary(message.payload);
2465
2940
  break;
@@ -2590,12 +3065,28 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2590
3065
  const info = await io.bridge("info");
2591
3066
  if (info) {
2592
3067
  emit({ type: "hello", version: info.protocol, payload: info });
3068
+ startInfoRefresh();
2593
3069
  return true;
2594
3070
  }
2595
3071
  } catch {
2596
3072
  }
2597
3073
  return false;
2598
3074
  }
3075
+ let infoTimer = null;
3076
+ function startInfoRefresh() {
3077
+ if (infoTimer) return;
3078
+ infoTimer = setInterval(() => {
3079
+ if (disposed) return stopInfoRefresh();
3080
+ io.bridge("info").then((info) => {
3081
+ if (info) emit({ type: "hello", version: info.protocol, payload: info });
3082
+ else stopInfoRefresh();
3083
+ }).catch(() => stopInfoRefresh());
3084
+ }, INFO_REFRESH_MS);
3085
+ }
3086
+ function stopInfoRefresh() {
3087
+ if (infoTimer) clearInterval(infoTimer);
3088
+ infoTimer = null;
3089
+ }
2599
3090
  let syncGen = 0;
2600
3091
  let syncTimer = null;
2601
3092
  let disposed = false;
@@ -2603,6 +3094,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2603
3094
  syncGen++;
2604
3095
  if (syncTimer) clearTimeout(syncTimer);
2605
3096
  syncTimer = null;
3097
+ stopInfoRefresh();
2606
3098
  }
2607
3099
  function syncWithRetry(onReady, initialDelay = 0) {
2608
3100
  cancelSync();
@@ -3060,6 +3552,8 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3060
3552
  function floodReports(n) {
3061
3553
  const out = [];
3062
3554
  const components = Math.max(10, Math.floor(n / 10));
3555
+ const spread = 9e4;
3556
+ const t0 = Date.now() - spread;
3063
3557
  for (let i = 0; i < n; i++) {
3064
3558
  const id = i % components;
3065
3559
  const depth = 1 + id % 6;
@@ -3067,6 +3561,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3067
3561
  for (let d = 1; d < depth; d++) path.push(`Section${(id * 7 + d) % 40}`);
3068
3562
  const avoidable = id % 3 !== 0;
3069
3563
  out.push({
3564
+ receivedAt: t0 + Math.round(i / Math.max(1, n - 1) * spread),
3070
3565
  component: `Item${id}`,
3071
3566
  path,
3072
3567
  trigger: avoidable ? "parent" : "props",
@@ -3136,14 +3631,22 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3136
3631
  return transport;
3137
3632
  }
3138
3633
  function createRelayClientTransport(relayUrl, ES = EventSource) {
3139
- const base = relayUrl.replace(/\/$/, "");
3634
+ const [rawBase, relayQuery = ""] = relayUrl.split("?");
3635
+ const base = (rawBase ?? "").replace(/\/$/, "");
3636
+ const relayToken = new URLSearchParams(relayQuery).get("token");
3637
+ const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
3140
3638
  let listener = null;
3141
3639
  let stream = null;
3142
3640
  let appsOnline = null;
3641
+ let roster = [];
3642
+ let selected = null;
3143
3643
  const emit = (m) => {
3144
3644
  if (listener) listener(m);
3145
3645
  };
3146
- const post = (message) => fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }).then(() => void 0);
3646
+ const post = (message) => {
3647
+ const body = selected && isRecord(message) && message.__rerenderLensCmd === true ? { ...message, app: selected } : message;
3648
+ return fetch(`${base}/message${auth ? `?${auth}` : ""}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }).then(() => void 0);
3649
+ };
3147
3650
  const { bridge, reply } = commandBridge(post, 2e3);
3148
3651
  const handleData = (data) => {
3149
3652
  let parsed;
@@ -3155,12 +3658,21 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3155
3658
  for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
3156
3659
  if (!isRecord(m) || reply(m)) continue;
3157
3660
  if (m.__rerenderLens === true && m.type === "relay") {
3158
- const apps = isRecord(m.payload) && typeof m.payload.apps === "number" ? m.payload.apps : 0;
3661
+ const p = isRecord(m.payload) ? m.payload : {};
3662
+ const apps = typeof p.apps === "number" ? p.apps : 0;
3663
+ roster = Array.isArray(p.list) ? p.list : [];
3159
3664
  const wasOnline = appsOnline;
3665
+ const wasSelected = selected;
3160
3666
  appsOnline = apps;
3161
- if (apps > 0 && wasOnline !== null && wasOnline === 0) void attach();
3667
+ if (!roster.length) selected = null;
3668
+ else if (!selected || !roster.some((a) => a.id === selected)) selected = roster[0].id;
3669
+ emit({ type: "apps", payload: { list: roster, selected } });
3670
+ if (apps > 0 && (wasOnline === null || wasOnline === 0 || selected !== wasSelected)) void attach();
3162
3671
  else if (apps === 0) emit({ type: "disconnected" });
3163
- } 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 });
3672
+ } else if (m.__rerenderLens === true && typeof m.type === "string") {
3673
+ if (typeof m.app === "string" && selected && m.app !== selected) continue;
3674
+ emit({ type: m.type, version: typeof m.version === "number" ? m.version : void 0, payload: m.payload });
3675
+ }
3164
3676
  }
3165
3677
  };
3166
3678
  async function attach() {
@@ -3177,17 +3689,23 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3177
3689
  const transport = {
3178
3690
  origin: base,
3179
3691
  tabLabel: `relay ${base.replace(/^https?:\/\//, "")}`,
3692
+ selectApp(id) {
3693
+ if (id === selected || !roster.some((a) => a.id === id)) return;
3694
+ selected = id;
3695
+ emit({ type: "apps", payload: { list: roster, selected } });
3696
+ emit({ type: "navigated" });
3697
+ void attach();
3698
+ },
3180
3699
  subscribe(fn) {
3181
3700
  listener = fn;
3182
3701
  const open = () => {
3183
- stream = new ES(`${base}/events?role=panel`);
3702
+ stream = new ES(`${base}/events?role=panel${auth ? `&${auth}` : ""}`);
3184
3703
  stream.onmessage = (e) => handleData(e.data);
3185
3704
  stream.onerror = () => {
3186
3705
  emit({ type: "disconnected" });
3187
3706
  };
3188
3707
  };
3189
3708
  open();
3190
- void attach();
3191
3709
  },
3192
3710
  replay: () => void bridge("replay"),
3193
3711
  clear: () => void bridge("clear"),
@@ -3272,7 +3790,7 @@ setState((prev) => (deepEqual(prev, next) ? prev : next));`
3272
3790
  createBroadcastTransport,
3273
3791
  createRelayClientTransport,
3274
3792
  sourceContext,
3275
- analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit, contextAttribution, cascadeTree, rootCauseSummary, summarizeSession, compareSessions }
3793
+ analysis: { firstDifferentPath, diffLeaves, fixesFor, rankFixes, rootCauseOf, analyzeCommit: analyzeCommit2, contextAttribution, cascadeTree, rootCauseSummary: rootCauseSummary2, summarizeSession, compareSessions }
3276
3794
  };
3277
3795
  window.RerenderLensPanel = api;
3278
3796
  var hasChrome = typeof chrome !== "undefined" && !!chrome && !!chrome.runtime && !!chrome.runtime.id;