@treelocator/runtime 0.5.2 → 0.6.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 (163) hide show
  1. package/.eslintignore +1 -0
  2. package/dist/_generated_styles.d.ts +1 -1
  3. package/dist/_generated_styles.js +20 -0
  4. package/dist/_generated_tree_icon.d.ts +1 -1
  5. package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
  6. package/dist/adapters/HtmlElementTreeNode.js +4 -6
  7. package/dist/adapters/createTreeNode.js +17 -44
  8. package/dist/adapters/detectFramework.d.ts +8 -0
  9. package/dist/adapters/detectFramework.js +25 -0
  10. package/dist/adapters/detectFramework.test.d.ts +1 -0
  11. package/dist/adapters/detectFramework.test.js +60 -0
  12. package/dist/adapters/jsx/jsxAdapter.js +54 -89
  13. package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
  14. package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
  15. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
  16. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
  17. package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
  18. package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
  19. package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
  20. package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
  21. package/dist/adapters/resolveAdapter.d.ts +1 -1
  22. package/dist/adapters/resolveAdapter.js +4 -8
  23. package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
  24. package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
  25. package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
  26. package/dist/adapters/vue/vueAdapter.test.js +222 -0
  27. package/dist/browserApi.d.ts +148 -0
  28. package/dist/browserApi.js +146 -5
  29. package/dist/browserApi.test.d.ts +1 -0
  30. package/dist/browserApi.test.js +287 -0
  31. package/dist/components/RecordingPillButton.d.ts +11 -0
  32. package/dist/components/RecordingPillButton.js +202 -0
  33. package/dist/components/RecordingResults.d.ts +2 -0
  34. package/dist/components/RecordingResults.js +213 -78
  35. package/dist/components/Runtime.js +161 -554
  36. package/dist/components/SettingsPanel.d.ts +5 -0
  37. package/dist/components/SettingsPanel.js +312 -0
  38. package/dist/consoleCapture.d.ts +9 -0
  39. package/dist/consoleCapture.js +95 -0
  40. package/dist/functions/cssRuleInspector.d.ts +83 -0
  41. package/dist/functions/cssRuleInspector.js +608 -0
  42. package/dist/functions/cssRuleInspector.test.d.ts +1 -0
  43. package/dist/functions/cssRuleInspector.test.js +439 -0
  44. package/dist/functions/deduplicateLabels.test.d.ts +1 -0
  45. package/dist/functions/deduplicateLabels.test.js +178 -0
  46. package/dist/functions/enrichAncestrySourceMaps.js +0 -1
  47. package/dist/functions/extractComputedStyles.d.ts +51 -0
  48. package/dist/functions/extractComputedStyles.js +447 -0
  49. package/dist/functions/extractComputedStyles.test.d.ts +1 -0
  50. package/dist/functions/extractComputedStyles.test.js +549 -0
  51. package/dist/functions/formatAncestryChain.d.ts +8 -0
  52. package/dist/functions/formatAncestryChain.js +21 -1
  53. package/dist/functions/formatAncestryChain.test.js +18 -0
  54. package/dist/functions/getUsableName.test.d.ts +1 -0
  55. package/dist/functions/getUsableName.test.js +219 -0
  56. package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
  57. package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
  58. package/dist/functions/mergeRects.test.js +210 -1
  59. package/dist/functions/namedSnapshots.d.ts +52 -0
  60. package/dist/functions/namedSnapshots.js +161 -0
  61. package/dist/functions/namedSnapshots.test.d.ts +1 -0
  62. package/dist/functions/namedSnapshots.test.js +85 -0
  63. package/dist/functions/normalizeFilePath.test.d.ts +1 -0
  64. package/dist/functions/normalizeFilePath.test.js +66 -0
  65. package/dist/functions/parseDataId.test.d.ts +1 -0
  66. package/dist/functions/parseDataId.test.js +101 -0
  67. package/dist/hooks/getStorage.d.ts +3 -0
  68. package/dist/hooks/getStorage.js +17 -0
  69. package/dist/hooks/useEventListeners.d.ts +15 -0
  70. package/dist/hooks/useEventListeners.js +56 -0
  71. package/dist/hooks/useLocatorStorage.d.ts +18 -0
  72. package/dist/hooks/useLocatorStorage.js +41 -0
  73. package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
  74. package/dist/hooks/useLocatorStorage.test.js +124 -0
  75. package/dist/hooks/useRecordingState.d.ts +43 -0
  76. package/dist/hooks/useRecordingState.js +387 -0
  77. package/dist/hooks/useSettings.d.ts +13 -0
  78. package/dist/hooks/useSettings.js +66 -0
  79. package/dist/index.d.ts +5 -2
  80. package/dist/index.js +4 -2
  81. package/dist/initRuntime.d.ts +3 -1
  82. package/dist/initRuntime.js +4 -1
  83. package/dist/mcpBridge.d.ts +61 -0
  84. package/dist/mcpBridge.js +534 -0
  85. package/dist/mcpBridge.test.d.ts +1 -0
  86. package/dist/mcpBridge.test.js +248 -0
  87. package/dist/output.css +20 -0
  88. package/dist/visualDiff/diff.d.ts +9 -0
  89. package/dist/visualDiff/diff.js +209 -0
  90. package/dist/visualDiff/diff.test.d.ts +1 -0
  91. package/dist/visualDiff/diff.test.js +253 -0
  92. package/dist/visualDiff/settle.d.ts +3 -0
  93. package/dist/visualDiff/settle.js +50 -0
  94. package/dist/visualDiff/settle.test.d.ts +1 -0
  95. package/dist/visualDiff/settle.test.js +65 -0
  96. package/dist/visualDiff/snapshot.d.ts +4 -0
  97. package/dist/visualDiff/snapshot.js +84 -0
  98. package/dist/visualDiff/snapshot.test.d.ts +1 -0
  99. package/dist/visualDiff/snapshot.test.js +245 -0
  100. package/dist/visualDiff/types.d.ts +37 -0
  101. package/dist/visualDiff/types.js +1 -0
  102. package/package.json +2 -2
  103. package/scripts/wrapCSS.js +1 -1
  104. package/scripts/wrapImage.js +1 -1
  105. package/src/_generated_styles.ts +21 -1
  106. package/src/_generated_tree_icon.ts +1 -1
  107. package/src/adapters/HtmlElementTreeNode.ts +10 -7
  108. package/src/adapters/createTreeNode.ts +12 -51
  109. package/src/adapters/detectFramework.test.ts +73 -0
  110. package/src/adapters/detectFramework.ts +28 -0
  111. package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
  112. package/src/adapters/jsx/jsxAdapter.ts +53 -106
  113. package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
  114. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
  115. package/src/adapters/react/findDebugSource.ts +5 -6
  116. package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
  117. package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
  118. package/src/adapters/react/reactAdapter.ts +1 -2
  119. package/src/adapters/resolveAdapter.ts +4 -14
  120. package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
  121. package/src/adapters/vue/vueAdapter.test.ts +259 -0
  122. package/src/browserApi.test.ts +329 -0
  123. package/src/browserApi.ts +351 -4
  124. package/src/components/RecordingPillButton.tsx +301 -0
  125. package/src/components/RecordingResults.tsx +114 -13
  126. package/src/components/Runtime.tsx +176 -621
  127. package/src/components/SettingsPanel.tsx +339 -0
  128. package/src/consoleCapture.ts +113 -0
  129. package/src/functions/cssRuleInspector.test.ts +517 -0
  130. package/src/functions/cssRuleInspector.ts +708 -0
  131. package/src/functions/deduplicateLabels.test.ts +115 -0
  132. package/src/functions/enrichAncestrySourceMaps.ts +6 -3
  133. package/src/functions/extractComputedStyles.test.ts +681 -0
  134. package/src/functions/extractComputedStyles.ts +768 -0
  135. package/src/functions/formatAncestryChain.test.ts +23 -1
  136. package/src/functions/formatAncestryChain.ts +22 -1
  137. package/src/functions/getUsableName.test.ts +242 -0
  138. package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
  139. package/src/functions/mergeRects.test.ts +111 -1
  140. package/src/functions/namedSnapshots.test.ts +106 -0
  141. package/src/functions/namedSnapshots.ts +232 -0
  142. package/src/functions/normalizeFilePath.test.ts +80 -0
  143. package/src/functions/parseDataId.test.ts +125 -0
  144. package/src/hooks/getStorage.ts +26 -0
  145. package/src/hooks/useEventListeners.ts +97 -0
  146. package/src/hooks/useLocatorStorage.test.ts +127 -0
  147. package/src/hooks/useLocatorStorage.ts +60 -0
  148. package/src/hooks/useRecordingState.ts +516 -0
  149. package/src/hooks/useSettings.ts +83 -0
  150. package/src/index.ts +10 -5
  151. package/src/initRuntime.ts +5 -0
  152. package/src/mcpBridge.test.ts +260 -0
  153. package/src/mcpBridge.ts +677 -0
  154. package/src/visualDiff/diff.test.ts +167 -0
  155. package/src/visualDiff/diff.ts +242 -0
  156. package/src/visualDiff/settle.test.ts +77 -0
  157. package/src/visualDiff/settle.ts +62 -0
  158. package/src/visualDiff/snapshot.test.ts +200 -0
  159. package/src/visualDiff/snapshot.ts +119 -0
  160. package/src/visualDiff/types.ts +40 -0
  161. package/tsconfig.json +3 -1
  162. package/vitest.config.ts +18 -0
  163. package/jest.config.ts +0 -195
@@ -0,0 +1,339 @@
1
+ import { For } from "solid-js";
2
+ import {
3
+ DEFAULT_SETTINGS,
4
+ resetSettings,
5
+ setSetting,
6
+ settings,
7
+ type TreelocatorSettings,
8
+ } from "../hooks/useSettings";
9
+
10
+ type SettingsPanelProps = {
11
+ onDismiss: () => void;
12
+ };
13
+
14
+ type ToggleKey =
15
+ | "anomalyTracking"
16
+ | "visualDiff"
17
+ | "computedStyles"
18
+ | "computedStylesIncludeDefaults";
19
+ type NumberKey = "sampleRate" | "maxDurationMs" | "jumpMinAbsolute" | "lagMinDelay";
20
+
21
+ const TOGGLES: { key: ToggleKey; label: string; hint: string }[] = [
22
+ {
23
+ key: "anomalyTracking",
24
+ label: "Anomaly tracking",
25
+ hint: "Detect jitter, jumps, stutter, stuck frames",
26
+ },
27
+ {
28
+ key: "visualDiff",
29
+ label: "Visual diff",
30
+ hint: "Snapshot before/after element tree",
31
+ },
32
+ {
33
+ key: "computedStyles",
34
+ label: "Computed styles",
35
+ hint: "Include styles in alt+click clipboard output",
36
+ },
37
+ {
38
+ key: "computedStylesIncludeDefaults",
39
+ label: "Include default styles",
40
+ hint: "Show browser-default values like display:block and font-weight:bold",
41
+ },
42
+ ];
43
+
44
+ const NUMBERS: {
45
+ key: NumberKey;
46
+ label: string;
47
+ hint: string;
48
+ unit: string;
49
+ min: number;
50
+ max: number;
51
+ step: number;
52
+ }[] = [
53
+ {
54
+ key: "sampleRate",
55
+ label: "Sample rate",
56
+ hint: "Max frames per second to capture",
57
+ unit: "Hz",
58
+ min: 1,
59
+ max: 60,
60
+ step: 1,
61
+ },
62
+ {
63
+ key: "maxDurationMs",
64
+ label: "Max duration",
65
+ hint: "Auto-stop recording after this long",
66
+ unit: "ms",
67
+ min: 1000,
68
+ max: 120000,
69
+ step: 1000,
70
+ },
71
+ {
72
+ key: "jumpMinAbsolute",
73
+ label: "Jump threshold",
74
+ hint: "Minimum px delta to flag a jump",
75
+ unit: "px",
76
+ min: 1,
77
+ max: 1000,
78
+ step: 1,
79
+ },
80
+ {
81
+ key: "lagMinDelay",
82
+ label: "Lag threshold",
83
+ hint: "Minimum rAF delay to flag lag",
84
+ unit: "ms",
85
+ min: 1,
86
+ max: 1000,
87
+ step: 1,
88
+ },
89
+ ];
90
+
91
+ const rowStyle = {
92
+ display: "flex",
93
+ "align-items": "center",
94
+ "justify-content": "space-between",
95
+ gap: "12px",
96
+ padding: "8px 0",
97
+ "border-bottom": "1px solid rgba(255, 255, 255, 0.05)",
98
+ };
99
+
100
+ const labelStyle = { "min-width": "0", flex: "1 1 auto" };
101
+ const labelTitle = { "font-weight": "600", color: "#fff" };
102
+ const labelHint = {
103
+ color: "#9ca3af",
104
+ "font-size": "11px",
105
+ "margin-top": "2px",
106
+ "line-height": "1.3",
107
+ };
108
+
109
+ const sectionTitleStyle = {
110
+ "margin-bottom": "4px",
111
+ color: "#9ca3af",
112
+ "font-size": "11px",
113
+ "text-transform": "uppercase",
114
+ "letter-spacing": "0.5px",
115
+ };
116
+
117
+ function Toggle(props: {
118
+ checked: boolean;
119
+ onChange: (v: boolean) => void;
120
+ }) {
121
+ return (
122
+ <div
123
+ role="switch"
124
+ aria-checked={props.checked}
125
+ onClick={() => props.onChange(!props.checked)}
126
+ style={{
127
+ cursor: "pointer",
128
+ width: "32px",
129
+ height: "18px",
130
+ "border-radius": "9px",
131
+ background: props.checked
132
+ ? "rgba(59, 130, 246, 0.6)"
133
+ : "rgba(255, 255, 255, 0.12)",
134
+ position: "relative",
135
+ transition: "background 0.15s",
136
+ "flex-shrink": "0",
137
+ }}
138
+ >
139
+ <div
140
+ style={{
141
+ position: "absolute",
142
+ top: "2px",
143
+ left: props.checked ? "16px" : "2px",
144
+ width: "14px",
145
+ height: "14px",
146
+ "border-radius": "50%",
147
+ background: "#fff",
148
+ transition: "left 0.15s",
149
+ }}
150
+ />
151
+ </div>
152
+ );
153
+ }
154
+
155
+ function NumberInput(props: {
156
+ value: number;
157
+ min: number;
158
+ max: number;
159
+ step: number;
160
+ unit: string;
161
+ onChange: (v: number) => void;
162
+ }) {
163
+ return (
164
+ <div
165
+ style={{
166
+ display: "flex",
167
+ "align-items": "center",
168
+ gap: "4px",
169
+ "flex-shrink": "0",
170
+ }}
171
+ >
172
+ <input
173
+ type="number"
174
+ value={props.value}
175
+ min={props.min}
176
+ max={props.max}
177
+ step={props.step}
178
+ onInput={(e) => {
179
+ const n = Number(e.currentTarget.value);
180
+ if (!Number.isFinite(n)) return;
181
+ const clamped = Math.min(props.max, Math.max(props.min, n));
182
+ props.onChange(clamped);
183
+ }}
184
+ style={{
185
+ width: "72px",
186
+ padding: "4px 6px",
187
+ "border-radius": "4px",
188
+ background: "rgba(255, 255, 255, 0.08)",
189
+ border: "1px solid rgba(255, 255, 255, 0.1)",
190
+ color: "#fff",
191
+ "font-family": "inherit",
192
+ "font-size": "11px",
193
+ "text-align": "right",
194
+ outline: "none",
195
+ }}
196
+ />
197
+ <span style={{ color: "#9ca3af", "font-size": "11px", "min-width": "18px" }}>
198
+ {props.unit}
199
+ </span>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export function SettingsPanel(props: SettingsPanelProps) {
205
+ const current = () => settings();
206
+
207
+ return (
208
+ <div
209
+ data-treelocator-settings-panel
210
+ style={{
211
+ position: "fixed",
212
+ bottom: "180px",
213
+ right: "20px",
214
+ "z-index": "2147483646",
215
+ width: "340px",
216
+ "max-height": "400px",
217
+ "overflow-y": "auto",
218
+ background: "rgba(15, 15, 15, 0.92)",
219
+ "backdrop-filter": "blur(12px)",
220
+ "border-radius": "12px",
221
+ border: "1px solid rgba(255, 255, 255, 0.1)",
222
+ "box-shadow": "0 8px 32px rgba(0, 0, 0, 0.4)",
223
+ color: "#e5e5e5",
224
+ "font-family": "system-ui, -apple-system, sans-serif",
225
+ "font-size": "12px",
226
+ "pointer-events": "auto",
227
+ }}
228
+ >
229
+ {/* Header */}
230
+ <div
231
+ style={{
232
+ display: "flex",
233
+ "align-items": "center",
234
+ "justify-content": "space-between",
235
+ padding: "12px 14px 8px",
236
+ "border-bottom": "1px solid rgba(255, 255, 255, 0.08)",
237
+ }}
238
+ >
239
+ <div>
240
+ <div style={{ "font-weight": "600", "font-size": "13px", color: "#fff" }}>
241
+ Settings
242
+ </div>
243
+ <div style={{ color: "#9ca3af", "margin-top": "2px" }}>
244
+ Persisted to localStorage
245
+ </div>
246
+ </div>
247
+ <div style={{ display: "flex", "align-items": "center", gap: "4px" }}>
248
+ <div
249
+ style={{
250
+ cursor: "pointer",
251
+ padding: "4px 10px",
252
+ "border-radius": "4px",
253
+ background: "rgba(255, 255, 255, 0.08)",
254
+ color: "#9ca3af",
255
+ "font-size": "11px",
256
+ "font-weight": "600",
257
+ }}
258
+ onClick={resetSettings}
259
+ >
260
+ Reset
261
+ </div>
262
+ <div
263
+ style={{
264
+ cursor: "pointer",
265
+ padding: "4px 8px",
266
+ "border-radius": "4px",
267
+ color: "#9ca3af",
268
+ "font-size": "16px",
269
+ "line-height": "1",
270
+ }}
271
+ onClick={props.onDismiss}
272
+ >
273
+ &times;
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ {/* Feature toggles */}
279
+ <div style={{ padding: "8px 14px" }}>
280
+ <div style={sectionTitleStyle}>Features</div>
281
+ <For each={TOGGLES}>
282
+ {(item) => (
283
+ <div style={rowStyle}>
284
+ <div style={labelStyle}>
285
+ <div style={labelTitle}>{item.label}</div>
286
+ <div style={labelHint}>{item.hint}</div>
287
+ </div>
288
+ <Toggle
289
+ checked={current()[item.key]}
290
+ onChange={(v) => setSetting(item.key, v)}
291
+ />
292
+ </div>
293
+ )}
294
+ </For>
295
+ </div>
296
+
297
+ {/* Thresholds */}
298
+ <div
299
+ style={{
300
+ padding: "8px 14px 12px",
301
+ "border-top": "1px solid rgba(255, 255, 255, 0.08)",
302
+ }}
303
+ >
304
+ <div style={sectionTitleStyle}>Thresholds</div>
305
+ <For each={NUMBERS}>
306
+ {(item) => (
307
+ <div style={rowStyle}>
308
+ <div style={labelStyle}>
309
+ <div style={labelTitle}>{item.label}</div>
310
+ <div style={labelHint}>{item.hint}</div>
311
+ </div>
312
+ <NumberInput
313
+ value={current()[item.key]}
314
+ min={item.min}
315
+ max={item.max}
316
+ step={item.step}
317
+ unit={item.unit}
318
+ onChange={(v) =>
319
+ setSetting(item.key as keyof TreelocatorSettings, v as never)
320
+ }
321
+ />
322
+ </div>
323
+ )}
324
+ </For>
325
+ <div
326
+ style={{
327
+ color: "#6b7280",
328
+ "font-size": "11px",
329
+ "margin-top": "8px",
330
+ }}
331
+ >
332
+ Defaults: sample {DEFAULT_SETTINGS.sampleRate}Hz, max{" "}
333
+ {DEFAULT_SETTINGS.maxDurationMs}ms, jump {DEFAULT_SETTINGS.jumpMinAbsolute}px,
334
+ lag {DEFAULT_SETTINGS.lagMinDelay}ms
335
+ </div>
336
+ </div>
337
+ </div>
338
+ );
339
+ }
@@ -0,0 +1,113 @@
1
+ export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
2
+
3
+ export interface ConsoleEntry {
4
+ level: ConsoleLevel;
5
+ timestamp: number;
6
+ message: string;
7
+ }
8
+
9
+ const MAX_ENTRIES = 500;
10
+ const LEVELS: ConsoleLevel[] = ["log", "info", "warn", "error", "debug"];
11
+
12
+ const CAPTURE_FLAG = "__treelocator_console_captured__";
13
+ const entries: ConsoleEntry[] = [];
14
+
15
+ function formatArg(value: unknown, seen: WeakSet<object>): string {
16
+ if (value === null) return "null";
17
+ if (value === undefined) return "undefined";
18
+ const t = typeof value;
19
+ if (t === "string") return value as string;
20
+ if (t === "number" || t === "boolean" || t === "bigint") return String(value);
21
+ if (t === "function") {
22
+ const name = (value as { name?: string }).name || "anonymous";
23
+ return `[Function: ${name}]`;
24
+ }
25
+ if (value instanceof Error) {
26
+ return `${value.name}: ${value.message}`;
27
+ }
28
+ if (typeof Element !== "undefined" && value instanceof Element) {
29
+ const el = value as Element;
30
+ const id = el.id ? `#${el.id}` : "";
31
+ const cls = el.className && typeof el.className === "string"
32
+ ? `.${el.className.trim().split(/\s+/).join(".")}`
33
+ : "";
34
+ return `<${el.tagName.toLowerCase()}${id}${cls}>`;
35
+ }
36
+ if (t === "object") {
37
+ if (seen.has(value as object)) return "[Circular]";
38
+ seen.add(value as object);
39
+ try {
40
+ const localSeen = new WeakSet<object>();
41
+ return JSON.stringify(value, (_key, v) => {
42
+ if (v === null || v === undefined) return v;
43
+ if (typeof v === "bigint") return v.toString();
44
+ if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
45
+ if (typeof v === "object") {
46
+ if (localSeen.has(v)) return "[Circular]";
47
+ localSeen.add(v);
48
+ }
49
+ return v;
50
+ });
51
+ } catch {
52
+ try {
53
+ return String(value);
54
+ } catch {
55
+ return "[Unserializable]";
56
+ }
57
+ }
58
+ }
59
+ return String(value);
60
+ }
61
+
62
+ function formatArgs(args: unknown[]): string {
63
+ const seen = new WeakSet<object>();
64
+ return args.map((arg) => formatArg(arg, seen)).join(" ");
65
+ }
66
+
67
+ function pushEntry(level: ConsoleLevel, args: unknown[]): void {
68
+ entries.push({
69
+ level,
70
+ timestamp: Date.now(),
71
+ message: formatArgs(args),
72
+ });
73
+ if (entries.length > MAX_ENTRIES) {
74
+ entries.splice(0, entries.length - MAX_ENTRIES);
75
+ }
76
+ }
77
+
78
+ export function installConsoleCapture(): void {
79
+ if (typeof console === "undefined") return;
80
+ const target = console as unknown as Record<string, unknown>;
81
+ if (target[CAPTURE_FLAG]) return;
82
+ target[CAPTURE_FLAG] = true;
83
+
84
+ for (const level of LEVELS) {
85
+ const original = target[level];
86
+ if (typeof original !== "function") continue;
87
+ const wrapped = function (this: unknown, ...args: unknown[]) {
88
+ try {
89
+ pushEntry(level, args);
90
+ } catch {
91
+ // Swallow — capture must never break logging.
92
+ }
93
+ return (original as (...a: unknown[]) => unknown).apply(this, args);
94
+ };
95
+ try {
96
+ target[level] = wrapped;
97
+ } catch {
98
+ // Some environments freeze console — ignore.
99
+ }
100
+ }
101
+ }
102
+
103
+ export function getConsoleEntries(last?: number): ConsoleEntry[] {
104
+ if (typeof last !== "number" || !Number.isFinite(last) || last <= 0) {
105
+ return entries.slice();
106
+ }
107
+ const n = Math.min(Math.floor(last), entries.length);
108
+ return entries.slice(entries.length - n);
109
+ }
110
+
111
+ export function clearConsoleEntries(): void {
112
+ entries.length = 0;
113
+ }