browsertrack 0.0.1 → 0.1.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 (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. package/vitest.config.ts +9 -0
@@ -0,0 +1,2080 @@
1
+ import {
2
+ getSemanticSelector,
3
+ redactUrl,
4
+ truncate
5
+ } from "./chunk-6A7FFDIB.js";
6
+
7
+ // packages/client/src/breadcrumbs.ts
8
+ var BreadcrumbBuffer = class {
9
+ capacity;
10
+ buffer = [];
11
+ constructor(capacity = 50) {
12
+ this.capacity = capacity;
13
+ }
14
+ add(breadcrumb) {
15
+ const item = {
16
+ ...breadcrumb,
17
+ timestamp: breadcrumb.timestamp || Date.now()
18
+ };
19
+ this.buffer.push(item);
20
+ if (this.buffer.length > this.capacity) {
21
+ this.buffer.shift();
22
+ }
23
+ }
24
+ getRecent() {
25
+ return [...this.buffer];
26
+ }
27
+ clear() {
28
+ this.buffer = [];
29
+ }
30
+ };
31
+
32
+ // packages/client/src/interceptors/interaction.ts
33
+ function extractElementSummary(el) {
34
+ const selector = getSemanticSelector(el);
35
+ const tag = el.tagName.toLowerCase();
36
+ const id = el.id || void 0;
37
+ const classes = Array.from(el.classList || []);
38
+ let boundingRect = void 0;
39
+ let visible = true;
40
+ try {
41
+ const rect = el.getBoundingClientRect();
42
+ boundingRect = {
43
+ x: Math.round(rect.x),
44
+ y: Math.round(rect.y),
45
+ width: Math.round(rect.width),
46
+ height: Math.round(rect.height),
47
+ top: Math.round(rect.top),
48
+ left: Math.round(rect.left),
49
+ bottom: Math.round(rect.bottom),
50
+ right: Math.round(rect.right)
51
+ };
52
+ const style = window.getComputedStyle(el);
53
+ visible = style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && rect.width > 0 && rect.height > 0;
54
+ } catch {
55
+ }
56
+ let innerText;
57
+ if (tag !== "input" && tag !== "textarea" && tag !== "select") {
58
+ innerText = truncate(el.textContent?.trim(), 80);
59
+ }
60
+ const outerHTML = truncate(el.outerHTML?.trim(), 200);
61
+ return {
62
+ selector,
63
+ tag,
64
+ id,
65
+ classes,
66
+ boundingRect,
67
+ visible,
68
+ innerText,
69
+ outerHTML
70
+ };
71
+ }
72
+ function setupInteractionInterceptors(onInteraction) {
73
+ if (typeof document === "undefined") return () => {
74
+ };
75
+ const onClick = (event) => {
76
+ try {
77
+ const target = event.target;
78
+ if (!target || !(target instanceof Element)) return;
79
+ const summary = extractElementSummary(target);
80
+ const message = `click ${summary.selector}${summary.innerText ? ` ("${summary.innerText}")` : ""}`;
81
+ onInteraction(
82
+ {
83
+ type: "click",
84
+ category: "ui",
85
+ message,
86
+ timestamp: Date.now(),
87
+ element: summary
88
+ },
89
+ summary
90
+ );
91
+ } catch {
92
+ }
93
+ };
94
+ const onSubmit = (event) => {
95
+ try {
96
+ const target = event.target;
97
+ if (!target || !(target instanceof Element)) return;
98
+ const summary = extractElementSummary(target);
99
+ const formAction = target.getAttribute("action") || "";
100
+ const message = `submit form ${summary.selector}${formAction ? ` (action: ${formAction})` : ""}`;
101
+ onInteraction(
102
+ {
103
+ type: "submit",
104
+ category: "ui",
105
+ message,
106
+ timestamp: Date.now(),
107
+ element: summary
108
+ },
109
+ summary
110
+ );
111
+ } catch {
112
+ }
113
+ };
114
+ document.addEventListener("click", onClick, { capture: true, passive: true });
115
+ document.addEventListener("submit", onSubmit, { capture: true, passive: true });
116
+ return () => {
117
+ document.removeEventListener("click", onClick, { capture: true });
118
+ document.removeEventListener("submit", onSubmit, { capture: true });
119
+ };
120
+ }
121
+
122
+ // packages/client/src/commands/handler.ts
123
+ var ClientCommandHandler = class {
124
+ screenshotDriver;
125
+ constructor(screenshotDriver) {
126
+ this.screenshotDriver = screenshotDriver;
127
+ }
128
+ async executeCommand(command) {
129
+ try {
130
+ switch (command.type) {
131
+ case "reload":
132
+ return this.handleReload(command);
133
+ case "navigate":
134
+ return this.handleNavigate(command);
135
+ case "get_page_state":
136
+ return this.handleGetPageState(command);
137
+ case "query_element":
138
+ return this.handleQueryElement(command);
139
+ case "capture_element":
140
+ return await this.handleCaptureElement(command);
141
+ case "check_overflow":
142
+ return this.handleCheckOverflow(command);
143
+ case "get_element_rect":
144
+ return this.handleGetElementRect(command);
145
+ case "get_element_style":
146
+ return this.handleGetElementStyle(command);
147
+ default:
148
+ return {
149
+ id: command.id,
150
+ ok: false,
151
+ error: `Unsupported command type: ${command.type}`
152
+ };
153
+ }
154
+ } catch (err) {
155
+ return {
156
+ id: command.id,
157
+ ok: false,
158
+ error: err?.message || "Command execution failed"
159
+ };
160
+ }
161
+ }
162
+ handleReload(command) {
163
+ if (typeof window !== "undefined" && window.location) {
164
+ setTimeout(() => {
165
+ window.location.reload();
166
+ }, 50);
167
+ return { id: command.id, ok: true, result: { message: "Reload initiated" } };
168
+ }
169
+ return { id: command.id, ok: false, error: "No browser window available" };
170
+ }
171
+ handleNavigate(command) {
172
+ const targetUrl = command.params?.url;
173
+ if (!targetUrl) {
174
+ return { id: command.id, ok: false, error: "Missing target url parameter" };
175
+ }
176
+ if (typeof window !== "undefined" && window.location) {
177
+ setTimeout(() => {
178
+ window.location.href = targetUrl;
179
+ }, 50);
180
+ return { id: command.id, ok: true, result: { message: `Navigating to ${targetUrl}` } };
181
+ }
182
+ return { id: command.id, ok: false, error: "No browser window available" };
183
+ }
184
+ handleGetPageState(command) {
185
+ if (typeof window === "undefined" || typeof document === "undefined") {
186
+ return { id: command.id, ok: false, error: "No browser document available" };
187
+ }
188
+ const activeEl = document.activeElement && document.activeElement !== document.body ? extractElementSummary(document.activeElement) : void 0;
189
+ return {
190
+ id: command.id,
191
+ ok: true,
192
+ result: {
193
+ url: window.location.href,
194
+ route: window.location.pathname + window.location.search + window.location.hash,
195
+ title: document.title,
196
+ readyState: document.readyState,
197
+ activeElement: activeEl
198
+ }
199
+ };
200
+ }
201
+ handleQueryElement(command) {
202
+ if (typeof document === "undefined") {
203
+ return { id: command.id, ok: false, error: "No browser document available" };
204
+ }
205
+ const selector = command.params?.selector;
206
+ if (!selector) {
207
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
208
+ }
209
+ const element = document.querySelector(selector);
210
+ if (!element) {
211
+ return {
212
+ id: command.id,
213
+ ok: true,
214
+ result: { exists: false }
215
+ };
216
+ }
217
+ const summary = extractElementSummary(element);
218
+ return {
219
+ id: command.id,
220
+ ok: true,
221
+ result: {
222
+ exists: true,
223
+ visible: summary.visible,
224
+ tag: summary.tag,
225
+ id: summary.id,
226
+ classes: summary.classes,
227
+ boundingRect: summary.boundingRect,
228
+ innerText: summary.innerText,
229
+ outerHTML: summary.outerHTML
230
+ }
231
+ };
232
+ }
233
+ async handleCaptureElement(command) {
234
+ const selector = command.params?.selector;
235
+ let targetEl = null;
236
+ if (selector) {
237
+ targetEl = document.querySelector(selector);
238
+ if (!targetEl) {
239
+ return {
240
+ id: command.id,
241
+ ok: false,
242
+ reason: `ELEMENT_NOT_FOUND: ${selector}`,
243
+ error: `Element not found for selector: ${selector}`
244
+ };
245
+ }
246
+ } else {
247
+ targetEl = document.body || document.documentElement;
248
+ }
249
+ const captureRes = await this.screenshotDriver.captureElement(targetEl);
250
+ if (!captureRes.ok) {
251
+ return {
252
+ id: command.id,
253
+ ok: false,
254
+ reason: captureRes.reason || "CAPTURE_FAILED",
255
+ error: captureRes.reason || "Failed to capture element screenshot"
256
+ };
257
+ }
258
+ return {
259
+ id: command.id,
260
+ ok: true,
261
+ result: {
262
+ dataUrl: captureRes.dataUrl,
263
+ format: captureRes.format,
264
+ width: captureRes.width,
265
+ height: captureRes.height
266
+ }
267
+ };
268
+ }
269
+ handleCheckOverflow(command) {
270
+ if (typeof window === "undefined" || typeof document === "undefined") {
271
+ return { id: command.id, ok: false, error: "No browser window available" };
272
+ }
273
+ const selector = command.params?.selector;
274
+ if (!selector) {
275
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
276
+ }
277
+ const el = document.querySelector(selector);
278
+ if (!el) {
279
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
280
+ }
281
+ const rect = el.getBoundingClientRect();
282
+ const vw = window.innerWidth;
283
+ const vh = window.innerHeight;
284
+ const overflowRightPx = Math.max(0, Math.round(rect.right - vw));
285
+ const overflowBottomPx = Math.max(0, Math.round(rect.bottom - vh));
286
+ const isViewportOverflow = overflowRightPx > 0 || rect.left < 0;
287
+ let parentOverflow = false;
288
+ if (el.parentElement) {
289
+ parentOverflow = el.parentElement.scrollWidth > el.parentElement.clientWidth;
290
+ }
291
+ return {
292
+ id: command.id,
293
+ ok: true,
294
+ result: {
295
+ selector,
296
+ overflow: isViewportOverflow || parentOverflow,
297
+ viewportWidth: vw,
298
+ viewportHeight: vh,
299
+ rect: {
300
+ x: Math.round(rect.x),
301
+ y: Math.round(rect.y),
302
+ width: Math.round(rect.width),
303
+ height: Math.round(rect.height),
304
+ top: Math.round(rect.top),
305
+ left: Math.round(rect.left),
306
+ bottom: Math.round(rect.bottom),
307
+ right: Math.round(rect.right)
308
+ },
309
+ overflowRightPx,
310
+ overflowBottomPx,
311
+ parentOverflow
312
+ }
313
+ };
314
+ }
315
+ handleGetElementRect(command) {
316
+ if (typeof document === "undefined") {
317
+ return { id: command.id, ok: false, error: "No browser document available" };
318
+ }
319
+ const selector = command.params?.selector;
320
+ if (!selector) {
321
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
322
+ }
323
+ const el = document.querySelector(selector);
324
+ if (!el) {
325
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
326
+ }
327
+ const rect = el.getBoundingClientRect();
328
+ return {
329
+ id: command.id,
330
+ ok: true,
331
+ result: {
332
+ x: Math.round(rect.x),
333
+ y: Math.round(rect.y),
334
+ width: Math.round(rect.width),
335
+ height: Math.round(rect.height),
336
+ top: Math.round(rect.top),
337
+ left: Math.round(rect.left),
338
+ bottom: Math.round(rect.bottom),
339
+ right: Math.round(rect.right)
340
+ }
341
+ };
342
+ }
343
+ handleGetElementStyle(command) {
344
+ if (typeof window === "undefined" || typeof document === "undefined") {
345
+ return { id: command.id, ok: false, error: "No browser window available" };
346
+ }
347
+ const selector = command.params?.selector;
348
+ if (!selector) {
349
+ return { id: command.id, ok: false, error: "Missing selector parameter" };
350
+ }
351
+ const el = document.querySelector(selector);
352
+ if (!el) {
353
+ return { id: command.id, ok: false, error: `Element not found: ${selector}` };
354
+ }
355
+ const computed = window.getComputedStyle(el);
356
+ const properties = command.params?.properties || ["overflow", "overflowX", "overflowY", "width", "maxWidth", "display", "position"];
357
+ const styles = {};
358
+ for (const prop of properties) {
359
+ styles[prop] = computed.getPropertyValue(prop) || computed[prop] || "";
360
+ }
361
+ return {
362
+ id: command.id,
363
+ ok: true,
364
+ result: {
365
+ selector,
366
+ styles
367
+ }
368
+ };
369
+ }
370
+ };
371
+
372
+ // packages/client/src/config.ts
373
+ var DEFAULT_OPTIONS = {
374
+ daemonUrl: "ws://127.0.0.1:7331",
375
+ projectId: "",
376
+ maxBreadcrumbs: 50,
377
+ captureErrors: true,
378
+ captureConsole: true,
379
+ captureNetwork: true,
380
+ captureNavigation: true,
381
+ captureInteractions: true,
382
+ onErrorScreenshot: true,
383
+ notes: {
384
+ enabled: true,
385
+ shortcut: "Alt+Click",
386
+ showBadges: true,
387
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"]
388
+ },
389
+ debug: false
390
+ };
391
+
392
+ // packages/client/src/interceptors/console.ts
393
+ function setupConsoleInterceptors(onConsole) {
394
+ if (typeof console === "undefined") return () => {
395
+ };
396
+ const originalError = console.error;
397
+ const originalWarn = console.warn;
398
+ function formatArgs(args) {
399
+ return args.map((arg) => {
400
+ if (arg instanceof Error) {
401
+ return arg.stack || `${arg.name}: ${arg.message}`;
402
+ }
403
+ if (typeof arg === "object" && arg !== null) {
404
+ try {
405
+ return JSON.stringify(arg);
406
+ } catch {
407
+ return String(arg);
408
+ }
409
+ }
410
+ return String(arg);
411
+ }).join(" ");
412
+ }
413
+ console.error = function(...args) {
414
+ try {
415
+ const message = formatArgs(args);
416
+ const stack = new Error().stack;
417
+ onConsole({
418
+ level: "error",
419
+ message,
420
+ stack,
421
+ timestamp: Date.now()
422
+ });
423
+ } catch {
424
+ }
425
+ return originalError.apply(console, args);
426
+ };
427
+ console.warn = function(...args) {
428
+ try {
429
+ const message = formatArgs(args);
430
+ const stack = new Error().stack;
431
+ onConsole({
432
+ level: "warn",
433
+ message,
434
+ stack,
435
+ timestamp: Date.now()
436
+ });
437
+ } catch {
438
+ }
439
+ return originalWarn.apply(console, args);
440
+ };
441
+ return () => {
442
+ console.error = originalError;
443
+ console.warn = originalWarn;
444
+ };
445
+ }
446
+
447
+ // packages/client/src/interceptors/navigation.ts
448
+ function setupNavigationInterceptors(onNavigation) {
449
+ if (typeof window === "undefined" || typeof history === "undefined") return () => {
450
+ };
451
+ let currentUrl = redactUrl(window.location.href);
452
+ onNavigation({
453
+ to: currentUrl,
454
+ type: "initial",
455
+ timestamp: Date.now()
456
+ });
457
+ const originalPushState = history.pushState;
458
+ const originalReplaceState = history.replaceState;
459
+ history.pushState = function(data, unused, url) {
460
+ const from = currentUrl;
461
+ const result = originalPushState.apply(history, [data, unused, url]);
462
+ try {
463
+ const to = redactUrl(window.location.href);
464
+ currentUrl = to;
465
+ onNavigation({
466
+ from,
467
+ to,
468
+ type: "pushState",
469
+ timestamp: Date.now()
470
+ });
471
+ } catch {
472
+ }
473
+ return result;
474
+ };
475
+ history.replaceState = function(data, unused, url) {
476
+ const from = currentUrl;
477
+ const result = originalReplaceState.apply(history, [data, unused, url]);
478
+ try {
479
+ const to = redactUrl(window.location.href);
480
+ currentUrl = to;
481
+ onNavigation({
482
+ from,
483
+ to,
484
+ type: "replaceState",
485
+ timestamp: Date.now()
486
+ });
487
+ } catch {
488
+ }
489
+ return result;
490
+ };
491
+ const onPopState = () => {
492
+ const from = currentUrl;
493
+ const to = redactUrl(window.location.href);
494
+ currentUrl = to;
495
+ onNavigation({
496
+ from,
497
+ to,
498
+ type: "popstate",
499
+ timestamp: Date.now()
500
+ });
501
+ };
502
+ const onHashChange = () => {
503
+ const from = currentUrl;
504
+ const to = redactUrl(window.location.href);
505
+ currentUrl = to;
506
+ onNavigation({
507
+ from,
508
+ to,
509
+ type: "hashchange",
510
+ timestamp: Date.now()
511
+ });
512
+ };
513
+ window.addEventListener("popstate", onPopState);
514
+ window.addEventListener("hashchange", onHashChange);
515
+ return () => {
516
+ history.pushState = originalPushState;
517
+ history.replaceState = originalReplaceState;
518
+ window.removeEventListener("popstate", onPopState);
519
+ window.removeEventListener("hashchange", onHashChange);
520
+ };
521
+ }
522
+
523
+ // packages/client/src/interceptors/network.ts
524
+ function setupNetworkInterceptors(onNetwork) {
525
+ const cleanups = [];
526
+ if (typeof window !== "undefined" && typeof window.fetch === "function") {
527
+ const originalFetch = window.fetch;
528
+ window.fetch = async function(input, init2) {
529
+ const startTime = Date.now();
530
+ let urlStr = "";
531
+ let method = "GET";
532
+ try {
533
+ if (typeof input === "string") {
534
+ urlStr = input;
535
+ } else if (input instanceof URL) {
536
+ urlStr = input.toString();
537
+ } else if (input && typeof input === "object" && "url" in input) {
538
+ urlStr = input.url;
539
+ method = input.method || "GET";
540
+ }
541
+ if (init2 && init2.method) {
542
+ method = init2.method.toUpperCase();
543
+ }
544
+ } catch {
545
+ urlStr = "unknown_url";
546
+ }
547
+ const safeUrl = redactUrl(urlStr);
548
+ try {
549
+ const response = await originalFetch.apply(window, [input, init2]);
550
+ const durationMs = Date.now() - startTime;
551
+ onNetwork({
552
+ url: safeUrl,
553
+ method,
554
+ status: response.status,
555
+ statusText: response.statusText,
556
+ durationMs,
557
+ timestamp: startTime
558
+ });
559
+ return response;
560
+ } catch (err) {
561
+ const durationMs = Date.now() - startTime;
562
+ const isAbort = err?.name === "AbortError";
563
+ onNetwork({
564
+ url: safeUrl,
565
+ method,
566
+ durationMs,
567
+ error: err?.message || "Network request failed",
568
+ aborted: isAbort,
569
+ timestamp: startTime
570
+ });
571
+ throw err;
572
+ }
573
+ };
574
+ cleanups.push(() => {
575
+ window.fetch = originalFetch;
576
+ });
577
+ }
578
+ if (typeof window !== "undefined" && typeof window.XMLHttpRequest === "function") {
579
+ const originalOpen = XMLHttpRequest.prototype.open;
580
+ const originalSend = XMLHttpRequest.prototype.send;
581
+ const originalAbort = XMLHttpRequest.prototype.abort;
582
+ const xhrMap = /* @__PURE__ */ new WeakMap();
583
+ XMLHttpRequest.prototype.open = function(...args) {
584
+ try {
585
+ const method = String(args[0] || "GET").toUpperCase();
586
+ const rawUrl = String(args[1] || "");
587
+ xhrMap.set(this, {
588
+ url: redactUrl(rawUrl),
589
+ method,
590
+ startTime: Date.now()
591
+ });
592
+ } catch {
593
+ }
594
+ return originalOpen.apply(this, args);
595
+ };
596
+ XMLHttpRequest.prototype.abort = function() {
597
+ const meta = xhrMap.get(this);
598
+ if (meta) {
599
+ meta.aborted = true;
600
+ }
601
+ return originalAbort.apply(this);
602
+ };
603
+ XMLHttpRequest.prototype.send = function(body) {
604
+ const meta = xhrMap.get(this) || { url: "unknown_url", method: "GET", startTime: Date.now() };
605
+ const onComplete = () => {
606
+ try {
607
+ const durationMs = Date.now() - meta.startTime;
608
+ onNetwork({
609
+ url: meta.url,
610
+ method: meta.method,
611
+ status: this.status,
612
+ statusText: this.statusText,
613
+ durationMs,
614
+ aborted: meta.aborted,
615
+ timestamp: meta.startTime
616
+ });
617
+ } catch {
618
+ }
619
+ };
620
+ const onError = () => {
621
+ try {
622
+ const durationMs = Date.now() - meta.startTime;
623
+ onNetwork({
624
+ url: meta.url,
625
+ method: meta.method,
626
+ status: this.status || 0,
627
+ durationMs,
628
+ error: "XHR Network Error",
629
+ aborted: meta.aborted,
630
+ timestamp: meta.startTime
631
+ });
632
+ } catch {
633
+ }
634
+ };
635
+ this.addEventListener("load", onComplete, { once: true });
636
+ this.addEventListener("error", onError, { once: true });
637
+ this.addEventListener("timeout", onError, { once: true });
638
+ return originalSend.apply(this, [body]);
639
+ };
640
+ cleanups.push(() => {
641
+ XMLHttpRequest.prototype.open = originalOpen;
642
+ XMLHttpRequest.prototype.send = originalSend;
643
+ XMLHttpRequest.prototype.abort = originalAbort;
644
+ });
645
+ }
646
+ return () => {
647
+ for (const cleanup of cleanups) {
648
+ try {
649
+ cleanup();
650
+ } catch {
651
+ }
652
+ }
653
+ };
654
+ }
655
+
656
+ // packages/client/src/interceptors/runtime.ts
657
+ function setupRuntimeInterceptors(onRuntimeError) {
658
+ if (typeof window === "undefined") return () => {
659
+ };
660
+ const errorHandler = (event) => {
661
+ try {
662
+ const errorEvent = {
663
+ message: event.message || event.error && event.error.message || "Script error",
664
+ stack: event.error && event.error.stack || void 0,
665
+ filename: event.filename || void 0,
666
+ lineno: event.lineno || void 0,
667
+ colno: event.colno || void 0,
668
+ errorType: event.error && event.error.name || "Error",
669
+ timestamp: Date.now()
670
+ };
671
+ onRuntimeError(errorEvent, event.error);
672
+ } catch {
673
+ }
674
+ };
675
+ const unhandledRejectionHandler = (event) => {
676
+ try {
677
+ const reason = event.reason;
678
+ let message = "Unhandled Promise Rejection";
679
+ let stack;
680
+ let errorType = "UnhandledRejection";
681
+ if (reason instanceof Error) {
682
+ message = reason.message;
683
+ stack = reason.stack;
684
+ errorType = reason.name || "UnhandledRejection";
685
+ } else if (typeof reason === "string") {
686
+ message = reason;
687
+ } else if (reason && typeof reason === "object") {
688
+ try {
689
+ message = JSON.stringify(reason);
690
+ } catch {
691
+ message = String(reason);
692
+ }
693
+ }
694
+ const errorEvent = {
695
+ message,
696
+ stack,
697
+ errorType,
698
+ timestamp: Date.now()
699
+ };
700
+ onRuntimeError(errorEvent, reason);
701
+ } catch {
702
+ }
703
+ };
704
+ window.addEventListener("error", errorHandler);
705
+ window.addEventListener("unhandledrejection", unhandledRejectionHandler);
706
+ return () => {
707
+ window.removeEventListener("error", errorHandler);
708
+ window.removeEventListener("unhandledrejection", unhandledRejectionHandler);
709
+ };
710
+ }
711
+
712
+ // packages/client/src/screenshot/browser-script-driver.ts
713
+ import { snapdom } from "@zumer/snapdom";
714
+ var BrowserScriptScreenshotDriver = class {
715
+ name = "BrowserScriptScreenshotDriver";
716
+ async captureSelector(selector) {
717
+ try {
718
+ if (typeof document === "undefined") {
719
+ return { ok: false, reason: "NO_DOCUMENT_ENVIRONMENT" };
720
+ }
721
+ const el = document.querySelector(selector);
722
+ if (!el) {
723
+ return { ok: false, reason: `ELEMENT_NOT_FOUND: ${selector}` };
724
+ }
725
+ return await this.captureElement(el);
726
+ } catch (err) {
727
+ return { ok: false, reason: err?.message || "UNKNOWN_CAPTURE_ERROR" };
728
+ }
729
+ }
730
+ async captureElement(element) {
731
+ try {
732
+ if (typeof window === "undefined" || typeof document === "undefined") {
733
+ return { ok: false, reason: "NO_BROWSER_ENVIRONMENT" };
734
+ }
735
+ const rect = element.getBoundingClientRect();
736
+ const width = Math.max(Math.round(rect.width), 10);
737
+ const height = Math.max(Math.round(rect.height), 10);
738
+ const capturePromise = (async () => {
739
+ try {
740
+ let imgElement = void 0;
741
+ let format = "webp";
742
+ try {
743
+ if (typeof snapdom === "function") {
744
+ const captureRes = await snapdom(element, { scale: 1, embedFonts: false });
745
+ if (captureRes && typeof captureRes.toWebp === "function") {
746
+ imgElement = await captureRes.toWebp();
747
+ } else if (captureRes && typeof captureRes.toPng === "function") {
748
+ imgElement = await captureRes.toPng();
749
+ format = "png";
750
+ } else if (captureRes?.url) {
751
+ return {
752
+ ok: true,
753
+ dataUrl: captureRes.url,
754
+ format: "webp",
755
+ width,
756
+ height
757
+ };
758
+ }
759
+ } else if (snapdom?.toWebp) {
760
+ imgElement = await snapdom.toWebp(element, {
761
+ scale: 1,
762
+ embedFonts: false
763
+ });
764
+ } else if (snapdom?.toPng) {
765
+ imgElement = await snapdom.toPng(element, {
766
+ scale: 1,
767
+ embedFonts: false
768
+ });
769
+ format = "png";
770
+ }
771
+ } catch {
772
+ }
773
+ if (imgElement && imgElement.src) {
774
+ return {
775
+ ok: true,
776
+ dataUrl: imgElement.src,
777
+ format: imgElement.src.startsWith("data:image/webp") ? "webp" : format,
778
+ width: imgElement.naturalWidth || width,
779
+ height: imgElement.naturalHeight || height
780
+ };
781
+ }
782
+ } catch (snapErr) {
783
+ const errMsg = String(snapErr?.message || snapErr);
784
+ if (errMsg.includes("CORS") || errMsg.includes("SecurityError") || errMsg.includes("tainted")) {
785
+ return { ok: false, reason: "CROSS_ORIGIN_RESOURCE" };
786
+ }
787
+ }
788
+ return await this.captureWithSvgFallback(element, width, height);
789
+ })();
790
+ const timeoutPromise = new Promise((resolve) => {
791
+ setTimeout(() => resolve({ ok: false, reason: "CAPTURE_TIMEOUT" }), 3e3);
792
+ });
793
+ return await Promise.race([capturePromise, timeoutPromise]);
794
+ } catch (err) {
795
+ return { ok: false, reason: err?.message || "CAPTURE_EXCEPTION" };
796
+ }
797
+ }
798
+ async captureWithSvgFallback(element, width, height) {
799
+ try {
800
+ const clone = element.cloneNode(true);
801
+ const computed = window.getComputedStyle(element);
802
+ let cssText = "";
803
+ for (let i = 0; i < computed.length; i++) {
804
+ const prop = computed[i];
805
+ cssText += `${prop}:${computed.getPropertyValue(prop)};`;
806
+ }
807
+ clone.style.cssText = cssText;
808
+ clone.style.margin = "0";
809
+ clone.style.position = "static";
810
+ const svg = `
811
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
812
+ <foreignObject width="100%" height="100%">
813
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px;">
814
+ ${clone.outerHTML}
815
+ </div>
816
+ </foreignObject>
817
+ </svg>
818
+ `.trim();
819
+ const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
820
+ const url = URL.createObjectURL(svgBlob);
821
+ return await new Promise((resolve) => {
822
+ const img = new Image();
823
+ img.onload = () => {
824
+ try {
825
+ const canvas = document.createElement("canvas");
826
+ canvas.width = width;
827
+ canvas.height = height;
828
+ const ctx = canvas.getContext("2d");
829
+ if (!ctx) {
830
+ URL.revokeObjectURL(url);
831
+ resolve({ ok: false, reason: "CANVAS_CONTEXT_UNAVAILABLE" });
832
+ return;
833
+ }
834
+ ctx.drawImage(img, 0, 0);
835
+ URL.revokeObjectURL(url);
836
+ let dataUrl;
837
+ try {
838
+ dataUrl = canvas.toDataURL("image/webp", 0.85);
839
+ } catch {
840
+ dataUrl = canvas.toDataURL("image/png");
841
+ }
842
+ resolve({
843
+ ok: true,
844
+ dataUrl,
845
+ format: dataUrl.startsWith("data:image/webp") ? "webp" : "png",
846
+ width,
847
+ height
848
+ });
849
+ } catch (e) {
850
+ URL.revokeObjectURL(url);
851
+ if (e?.name === "SecurityError" || String(e).includes("tainted")) {
852
+ resolve({ ok: false, reason: "CROSS_ORIGIN_RESOURCE" });
853
+ } else {
854
+ resolve({ ok: false, reason: e?.message || "CANVAS_EXPORT_FAILED" });
855
+ }
856
+ }
857
+ };
858
+ img.onerror = () => {
859
+ URL.revokeObjectURL(url);
860
+ resolve({ ok: false, reason: "IMAGE_LOAD_FAILED" });
861
+ };
862
+ img.src = url;
863
+ });
864
+ } catch (e) {
865
+ return { ok: false, reason: e?.message || "FALLBACK_CAPTURE_FAILED" };
866
+ }
867
+ }
868
+ };
869
+
870
+ // packages/client/src/transport/websocket.ts
871
+ var WebSocketTransport = class {
872
+ wsUrl;
873
+ projectId;
874
+ debug;
875
+ ws = null;
876
+ sessionId = null;
877
+ queue = [];
878
+ maxQueueSize = 50;
879
+ reconnectAttempts = 0;
880
+ maxReconnectDelay = 1e4;
881
+ reconnectTimer = null;
882
+ commandHandler;
883
+ isDestroyed = false;
884
+ constructor(options) {
885
+ this.wsUrl = options.url || "ws://127.0.0.1:7331";
886
+ this.projectId = options.projectId;
887
+ this.debug = !!options.debug;
888
+ }
889
+ setCommandHandler(handler) {
890
+ this.commandHandler = handler;
891
+ }
892
+ getSessionId() {
893
+ return this.sessionId;
894
+ }
895
+ isConnected() {
896
+ return this.ws !== null && this.ws.readyState === (typeof WebSocket !== "undefined" ? WebSocket.OPEN : 1);
897
+ }
898
+ connect() {
899
+ if (this.isDestroyed || typeof WebSocket === "undefined") return;
900
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
901
+ return;
902
+ }
903
+ try {
904
+ this.ws = new WebSocket(this.wsUrl);
905
+ this.ws.onopen = () => {
906
+ this.reconnectAttempts = 0;
907
+ this.sendHello();
908
+ this.flushQueue();
909
+ };
910
+ this.ws.onmessage = async (event) => {
911
+ try {
912
+ const raw = typeof event.data === "string" ? event.data : "";
913
+ if (!raw) return;
914
+ const msg = JSON.parse(raw);
915
+ if (msg.type === "hello_ack") {
916
+ this.sessionId = msg.sessionId;
917
+ if (this.debug) {
918
+ console.log("[BrowserTrack] Connected, session ID:", this.sessionId);
919
+ }
920
+ return;
921
+ }
922
+ if (msg.type === "command" && this.commandHandler) {
923
+ const command = msg.command;
924
+ const res = await this.commandHandler.executeCommand(command);
925
+ this.send({
926
+ type: "command_response",
927
+ sessionId: this.sessionId,
928
+ response: res
929
+ });
930
+ }
931
+ } catch {
932
+ }
933
+ };
934
+ this.ws.onerror = () => {
935
+ };
936
+ this.ws.onclose = () => {
937
+ this.ws = null;
938
+ this.scheduleReconnect();
939
+ };
940
+ } catch {
941
+ this.scheduleReconnect();
942
+ }
943
+ }
944
+ send(payload) {
945
+ const raw = JSON.stringify(payload);
946
+ if (this.isConnected()) {
947
+ try {
948
+ this.ws.send(raw);
949
+ } catch {
950
+ this.enqueue(raw);
951
+ }
952
+ } else {
953
+ this.enqueue(raw);
954
+ }
955
+ }
956
+ enqueue(raw) {
957
+ if (this.queue.length >= this.maxQueueSize) {
958
+ this.queue.shift();
959
+ }
960
+ this.queue.push(raw);
961
+ }
962
+ flushQueue() {
963
+ if (!this.isConnected()) return;
964
+ while (this.queue.length > 0) {
965
+ const item = this.queue.shift();
966
+ if (item) {
967
+ try {
968
+ this.ws.send(item);
969
+ } catch {
970
+ this.queue.unshift(item);
971
+ break;
972
+ }
973
+ }
974
+ }
975
+ }
976
+ sendHello() {
977
+ if (typeof window === "undefined") return;
978
+ const hello = {
979
+ type: "hello",
980
+ origin: window.location.origin,
981
+ url: window.location.href,
982
+ title: document.title,
983
+ userAgent: navigator.userAgent,
984
+ timestamp: Date.now(),
985
+ projectId: this.projectId
986
+ };
987
+ try {
988
+ this.ws.send(JSON.stringify(hello));
989
+ } catch {
990
+ }
991
+ }
992
+ scheduleReconnect() {
993
+ if (this.isDestroyed || this.reconnectTimer) return;
994
+ this.reconnectAttempts++;
995
+ const delay = Math.min(1e3 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
996
+ this.reconnectTimer = setTimeout(() => {
997
+ this.reconnectTimer = null;
998
+ this.connect();
999
+ }, delay);
1000
+ }
1001
+ destroy() {
1002
+ this.isDestroyed = true;
1003
+ if (this.reconnectTimer) {
1004
+ clearTimeout(this.reconnectTimer);
1005
+ this.reconnectTimer = null;
1006
+ }
1007
+ if (this.ws) {
1008
+ try {
1009
+ this.ws.close();
1010
+ } catch {
1011
+ }
1012
+ this.ws = null;
1013
+ }
1014
+ this.queue = [];
1015
+ }
1016
+ };
1017
+
1018
+ // packages/client/src/notes/inspector.ts
1019
+ var NoteInspector = class {
1020
+ transport;
1021
+ screenshotDriver;
1022
+ options;
1023
+ container = null;
1024
+ shadowRoot = null;
1025
+ highlightOverlay = null;
1026
+ regionOverlay = null;
1027
+ regionBox = null;
1028
+ toolbarElement = null;
1029
+ modalOverlay = null;
1030
+ activeMode = "idle";
1031
+ hoveredElement = null;
1032
+ selectedElement = null;
1033
+ selectedRegion = null;
1034
+ isDraggingRegion = false;
1035
+ dragStartX = 0;
1036
+ dragStartY = 0;
1037
+ cleanups = [];
1038
+ constructor(transport, screenshotDriver, options = {}) {
1039
+ this.transport = transport;
1040
+ this.screenshotDriver = screenshotDriver;
1041
+ this.options = {
1042
+ shortcut: "Alt+Click",
1043
+ maskSelectors: ['input[type="password"]', "[data-sensitive]"],
1044
+ showToolbar: true,
1045
+ ...options
1046
+ };
1047
+ }
1048
+ init() {
1049
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1050
+ if (document.readyState === "loading") {
1051
+ document.addEventListener("DOMContentLoaded", () => {
1052
+ this.ensureContainer();
1053
+ });
1054
+ } else {
1055
+ this.ensureContainer();
1056
+ }
1057
+ this.setupListeners();
1058
+ }
1059
+ setMode(mode) {
1060
+ this.activeMode = mode;
1061
+ this.updateToolbarState();
1062
+ if (mode === "element") {
1063
+ this.hideRegionOverlay();
1064
+ } else if (mode === "region") {
1065
+ this.hideHighlight();
1066
+ this.showRegionOverlay();
1067
+ } else if (mode === "page") {
1068
+ this.hideRegionOverlay();
1069
+ this.hideHighlight();
1070
+ this.openNoteEditor(document.body, "page");
1071
+ } else {
1072
+ this.hideRegionOverlay();
1073
+ this.hideHighlight();
1074
+ }
1075
+ }
1076
+ ensureContainer() {
1077
+ if (typeof document === "undefined") return null;
1078
+ if (!this.container) {
1079
+ this.container = document.createElement("div");
1080
+ this.container.id = "browsertrack-inspector-host";
1081
+ this.container.style.cssText = "all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;";
1082
+ this.shadowRoot = this.container.attachShadow({ mode: "open" });
1083
+ const style = document.createElement("style");
1084
+ style.textContent = `
1085
+ :host {
1086
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1087
+ color-scheme: dark;
1088
+ -webkit-font-smoothing: antialiased;
1089
+ }
1090
+
1091
+ /* 1. Element Highlight Overlay */
1092
+ .bt-highlight {
1093
+ position: fixed;
1094
+ border: 2px solid #3b82f6;
1095
+ background: rgba(59, 130, 246, 0.15);
1096
+ border-radius: 4px;
1097
+ pointer-events: none;
1098
+ transition: all 0.05s ease-out;
1099
+ z-index: 2147483640;
1100
+ box-sizing: border-box;
1101
+ }
1102
+
1103
+ .bt-badge {
1104
+ position: absolute;
1105
+ top: -24px;
1106
+ left: 0;
1107
+ background: #1e293b;
1108
+ color: #38bdf8;
1109
+ border: 1px solid #3b82f6;
1110
+ font-size: 11px;
1111
+ font-weight: 600;
1112
+ padding: 2px 6px;
1113
+ border-radius: 3px;
1114
+ white-space: nowrap;
1115
+ pointer-events: none;
1116
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
1117
+ }
1118
+
1119
+ /* 2. Region Selection Overlay */
1120
+ .bt-region-overlay {
1121
+ position: fixed;
1122
+ inset: 0;
1123
+ width: 100vw;
1124
+ height: 100vh;
1125
+ cursor: crosshair;
1126
+ z-index: 2147483642;
1127
+ pointer-events: auto;
1128
+ background: rgba(15, 23, 42, 0.2);
1129
+ }
1130
+
1131
+ .bt-region-box {
1132
+ position: fixed;
1133
+ border: 2px dashed #38bdf8;
1134
+ background: rgba(56, 189, 248, 0.2);
1135
+ box-sizing: border-box;
1136
+ pointer-events: none;
1137
+ z-index: 2147483643;
1138
+ }
1139
+
1140
+ .bt-region-badge {
1141
+ position: absolute;
1142
+ top: -24px;
1143
+ left: 0;
1144
+ background: #0f172a;
1145
+ color: #38bdf8;
1146
+ border: 1px solid #0284c7;
1147
+ font-size: 11px;
1148
+ font-weight: 600;
1149
+ padding: 2px 6px;
1150
+ border-radius: 3px;
1151
+ white-space: nowrap;
1152
+ pointer-events: none;
1153
+ }
1154
+
1155
+ /* 3. Floating Quick Toolbar */
1156
+ .bt-toolbar {
1157
+ position: fixed;
1158
+ bottom: 20px;
1159
+ right: 20px;
1160
+ background: #1e293b;
1161
+ border: 1px solid #334155;
1162
+ border-radius: 30px;
1163
+ padding: 4px 6px;
1164
+ display: flex;
1165
+ align-items: center;
1166
+ gap: 4px;
1167
+ box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -4px rgba(0,0,0,0.5);
1168
+ pointer-events: auto;
1169
+ z-index: 2147483646;
1170
+ user-select: none;
1171
+ transition: all 0.2s ease;
1172
+ }
1173
+
1174
+ .bt-toolbar-btn {
1175
+ background: transparent;
1176
+ border: none;
1177
+ color: #94a3b8;
1178
+ font-size: 12px;
1179
+ font-weight: 500;
1180
+ padding: 6px 10px;
1181
+ border-radius: 20px;
1182
+ cursor: pointer;
1183
+ display: flex;
1184
+ align-items: center;
1185
+ gap: 5px;
1186
+ transition: all 0.15s ease;
1187
+ }
1188
+
1189
+ .bt-toolbar-btn:hover {
1190
+ background: #334155;
1191
+ color: #f8fafc;
1192
+ }
1193
+
1194
+ .bt-toolbar-btn.active {
1195
+ background: #2563eb;
1196
+ color: #ffffff;
1197
+ font-weight: 600;
1198
+ }
1199
+
1200
+ .bt-toolbar-divider {
1201
+ width: 1px;
1202
+ height: 16px;
1203
+ background: #334155;
1204
+ }
1205
+
1206
+ /* 4. Modal Backdrop & Editor */
1207
+ .bt-modal-backdrop {
1208
+ position: fixed;
1209
+ top: 0;
1210
+ left: 0;
1211
+ width: 100vw;
1212
+ height: 100vh;
1213
+ background: rgba(15, 23, 42, 0.75);
1214
+ backdrop-filter: blur(4px);
1215
+ display: flex;
1216
+ align-items: center;
1217
+ justify-content: center;
1218
+ pointer-events: auto;
1219
+ z-index: 2147483647;
1220
+ opacity: 1;
1221
+ box-sizing: border-box;
1222
+ }
1223
+
1224
+ .bt-modal {
1225
+ background: #1e293b;
1226
+ color: #f8fafc;
1227
+ border: 1px solid #334155;
1228
+ border-radius: 12px;
1229
+ padding: 18px;
1230
+ width: 420px;
1231
+ max-width: 90vw;
1232
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.7), 0 8px 10px -6px rgba(0, 0, 0, 0.7);
1233
+ display: flex;
1234
+ flex-direction: column;
1235
+ gap: 12px;
1236
+ box-sizing: border-box;
1237
+ }
1238
+
1239
+ .bt-modal-header {
1240
+ display: flex;
1241
+ justify-content: space-between;
1242
+ align-items: center;
1243
+ }
1244
+
1245
+ .bt-modal-title {
1246
+ font-size: 14px;
1247
+ font-weight: 600;
1248
+ display: flex;
1249
+ align-items: center;
1250
+ gap: 6px;
1251
+ color: #f8fafc;
1252
+ }
1253
+
1254
+ .bt-mode-badge {
1255
+ font-size: 10px;
1256
+ font-weight: 700;
1257
+ text-transform: uppercase;
1258
+ background: rgba(59, 130, 246, 0.2);
1259
+ color: #60a5fa;
1260
+ border: 1px solid #3b82f6;
1261
+ padding: 2px 6px;
1262
+ border-radius: 4px;
1263
+ }
1264
+
1265
+ .bt-close-btn {
1266
+ background: transparent;
1267
+ border: none;
1268
+ color: #94a3b8;
1269
+ font-size: 16px;
1270
+ cursor: pointer;
1271
+ padding: 2px 6px;
1272
+ border-radius: 4px;
1273
+ }
1274
+ .bt-close-btn:hover {
1275
+ background: #334155;
1276
+ color: #ffffff;
1277
+ }
1278
+
1279
+ .bt-target-pill {
1280
+ background: #0f172a;
1281
+ color: #94a3b8;
1282
+ border: 1px solid #334155;
1283
+ border-radius: 6px;
1284
+ padding: 6px 10px;
1285
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1286
+ font-size: 11px;
1287
+ overflow: hidden;
1288
+ text-overflow: ellipsis;
1289
+ white-space: nowrap;
1290
+ }
1291
+
1292
+ .bt-textarea {
1293
+ background: #0f172a;
1294
+ color: #f8fafc;
1295
+ border: 1px solid #334155;
1296
+ border-radius: 6px;
1297
+ padding: 10px;
1298
+ font-size: 13px;
1299
+ resize: vertical;
1300
+ min-height: 85px;
1301
+ outline: none;
1302
+ box-sizing: border-box;
1303
+ width: 100%;
1304
+ font-family: inherit;
1305
+ }
1306
+
1307
+ .bt-textarea:focus {
1308
+ border-color: #3b82f6;
1309
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
1310
+ }
1311
+
1312
+ .bt-modal-actions {
1313
+ display: flex;
1314
+ justify-content: flex-end;
1315
+ gap: 8px;
1316
+ }
1317
+
1318
+ .bt-btn {
1319
+ padding: 8px 14px;
1320
+ border-radius: 6px;
1321
+ font-size: 12px;
1322
+ font-weight: 600;
1323
+ cursor: pointer;
1324
+ border: 1px solid transparent;
1325
+ transition: all 0.1s ease;
1326
+ font-family: inherit;
1327
+ }
1328
+
1329
+ .bt-btn-cancel {
1330
+ background: transparent;
1331
+ color: #94a3b8;
1332
+ }
1333
+ .bt-btn-cancel:hover {
1334
+ background: #334155;
1335
+ color: #f8fafc;
1336
+ }
1337
+
1338
+ .bt-btn-save {
1339
+ background: #2563eb;
1340
+ color: #ffffff;
1341
+ }
1342
+ .bt-btn-save:hover {
1343
+ background: #1d4ed8;
1344
+ }
1345
+ .bt-btn-save:disabled {
1346
+ opacity: 0.6;
1347
+ cursor: not-allowed;
1348
+ }
1349
+ `;
1350
+ this.shadowRoot.appendChild(style);
1351
+ this.highlightOverlay = document.createElement("div");
1352
+ this.highlightOverlay.className = "bt-highlight";
1353
+ this.highlightOverlay.style.display = "none";
1354
+ this.shadowRoot.appendChild(this.highlightOverlay);
1355
+ if (this.options.showToolbar) {
1356
+ this.createToolbar();
1357
+ }
1358
+ }
1359
+ if (this.container && !this.container.isConnected) {
1360
+ const mountParent = document.body || document.documentElement;
1361
+ if (mountParent) {
1362
+ mountParent.appendChild(this.container);
1363
+ }
1364
+ }
1365
+ return this.shadowRoot;
1366
+ }
1367
+ createToolbar() {
1368
+ if (!this.shadowRoot || this.toolbarElement) return;
1369
+ this.toolbarElement = document.createElement("div");
1370
+ this.toolbarElement.className = "bt-toolbar";
1371
+ this.toolbarElement.innerHTML = `
1372
+ <button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
1373
+ <span>\u{1F3AF}</span> Element
1374
+ </button>
1375
+ <button class="bt-toolbar-btn" id="bt-mode-region" title="Drag to select screen region">
1376
+ <span>\u{1F4D0}</span> Region
1377
+ </button>
1378
+ <button class="bt-toolbar-btn" id="bt-mode-page" title="Leave full-page note">
1379
+ <span>\u{1F4C4}</span> Page
1380
+ </button>
1381
+ `;
1382
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1383
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1384
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1385
+ btnElement.onclick = () => {
1386
+ this.setMode(this.activeMode === "element" ? "idle" : "element");
1387
+ };
1388
+ btnRegion.onclick = () => {
1389
+ this.setMode(this.activeMode === "region" ? "idle" : "region");
1390
+ };
1391
+ btnPage.onclick = () => {
1392
+ this.setMode("page");
1393
+ };
1394
+ this.shadowRoot.appendChild(this.toolbarElement);
1395
+ }
1396
+ updateToolbarState() {
1397
+ if (!this.toolbarElement) return;
1398
+ const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
1399
+ const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
1400
+ const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
1401
+ btnElement?.classList.toggle("active", this.activeMode === "element");
1402
+ btnRegion?.classList.toggle("active", this.activeMode === "region");
1403
+ btnPage?.classList.toggle("active", this.activeMode === "page");
1404
+ }
1405
+ setupListeners() {
1406
+ const onMouseMove = (e) => {
1407
+ if (this.modalOverlay || this.isDraggingRegion || this.activeMode === "region") return;
1408
+ if (e.altKey || this.activeMode === "element") {
1409
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1410
+ if (target && target !== this.container && !this.container?.contains(target)) {
1411
+ this.hoveredElement = target;
1412
+ this.updateHighlight(target);
1413
+ return;
1414
+ }
1415
+ }
1416
+ if (!e.altKey && this.activeMode !== "element") {
1417
+ this.hideHighlight();
1418
+ }
1419
+ };
1420
+ const onClick = (e) => {
1421
+ if (this.modalOverlay || this.activeMode === "region") return;
1422
+ if (e.altKey || this.activeMode === "element") {
1423
+ e.preventDefault();
1424
+ e.stopPropagation();
1425
+ const target = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
1426
+ if (target && target !== this.container && !this.container?.contains(target)) {
1427
+ this.selectedElement = target;
1428
+ this.openNoteEditor(target, "element");
1429
+ if (this.activeMode === "element") {
1430
+ this.setMode("idle");
1431
+ }
1432
+ }
1433
+ }
1434
+ };
1435
+ const onKeyUp = (e) => {
1436
+ if (e.key === "Alt" && !this.modalOverlay && this.activeMode !== "element") {
1437
+ this.hideHighlight();
1438
+ }
1439
+ };
1440
+ window.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
1441
+ window.addEventListener("click", onClick, { capture: true });
1442
+ window.addEventListener("keyup", onKeyUp, { capture: true });
1443
+ this.cleanups.push(() => {
1444
+ window.removeEventListener("mousemove", onMouseMove, { capture: true });
1445
+ window.removeEventListener("click", onClick, { capture: true });
1446
+ window.removeEventListener("keyup", onKeyUp, { capture: true });
1447
+ });
1448
+ }
1449
+ showRegionOverlay() {
1450
+ const root = this.ensureContainer();
1451
+ if (!root) return;
1452
+ if (!this.regionOverlay) {
1453
+ this.regionOverlay = document.createElement("div");
1454
+ this.regionOverlay.className = "bt-region-overlay";
1455
+ this.regionBox = document.createElement("div");
1456
+ this.regionBox.className = "bt-region-box";
1457
+ this.regionBox.style.display = "none";
1458
+ this.regionOverlay.appendChild(this.regionBox);
1459
+ this.regionOverlay.onmousedown = (e) => {
1460
+ this.isDraggingRegion = true;
1461
+ this.dragStartX = e.clientX;
1462
+ this.dragStartY = e.clientY;
1463
+ if (this.regionBox) {
1464
+ this.regionBox.style.display = "block";
1465
+ this.regionBox.style.left = `${e.clientX}px`;
1466
+ this.regionBox.style.top = `${e.clientY}px`;
1467
+ this.regionBox.style.width = "0px";
1468
+ this.regionBox.style.height = "0px";
1469
+ }
1470
+ };
1471
+ this.regionOverlay.onmousemove = (e) => {
1472
+ if (!this.isDraggingRegion || !this.regionBox) return;
1473
+ const currentX = e.clientX;
1474
+ const currentY = e.clientY;
1475
+ const left = Math.min(this.dragStartX, currentX);
1476
+ const top = Math.min(this.dragStartY, currentY);
1477
+ const width = Math.abs(currentX - this.dragStartX);
1478
+ const height = Math.abs(currentY - this.dragStartY);
1479
+ this.regionBox.style.left = `${left}px`;
1480
+ this.regionBox.style.top = `${top}px`;
1481
+ this.regionBox.style.width = `${width}px`;
1482
+ this.regionBox.style.height = `${height}px`;
1483
+ let badge = this.regionBox.querySelector(".bt-region-badge");
1484
+ if (!badge) {
1485
+ badge = document.createElement("div");
1486
+ badge.className = "bt-region-badge";
1487
+ this.regionBox.appendChild(badge);
1488
+ }
1489
+ badge.textContent = `Region: ${Math.round(width)} \xD7 ${Math.round(height)} px`;
1490
+ };
1491
+ this.regionOverlay.onmouseup = (e) => {
1492
+ if (!this.isDraggingRegion) return;
1493
+ this.isDraggingRegion = false;
1494
+ const currentX = e.clientX;
1495
+ const currentY = e.clientY;
1496
+ const left = Math.min(this.dragStartX, currentX);
1497
+ const top = Math.min(this.dragStartY, currentY);
1498
+ const width = Math.abs(currentX - this.dragStartX);
1499
+ const height = Math.abs(currentY - this.dragStartY);
1500
+ if (width > 20 && height > 20) {
1501
+ this.selectedRegion = {
1502
+ x: Math.round(left),
1503
+ y: Math.round(top),
1504
+ width: Math.round(width),
1505
+ height: Math.round(height)
1506
+ };
1507
+ this.hideRegionOverlay();
1508
+ this.setMode("idle");
1509
+ this.openRegionNoteEditor(this.selectedRegion);
1510
+ } else {
1511
+ this.hideRegionOverlay();
1512
+ this.setMode("idle");
1513
+ }
1514
+ };
1515
+ }
1516
+ root.appendChild(this.regionOverlay);
1517
+ }
1518
+ hideRegionOverlay() {
1519
+ if (this.regionOverlay && this.regionOverlay.parentElement) {
1520
+ this.regionOverlay.parentElement.removeChild(this.regionOverlay);
1521
+ }
1522
+ if (this.regionBox) {
1523
+ this.regionBox.style.display = "none";
1524
+ }
1525
+ }
1526
+ updateHighlight(el) {
1527
+ const root = this.ensureContainer();
1528
+ if (!root || !this.highlightOverlay) return;
1529
+ const rect = el.getBoundingClientRect();
1530
+ this.highlightOverlay.style.display = "block";
1531
+ this.highlightOverlay.style.top = `${rect.top}px`;
1532
+ this.highlightOverlay.style.left = `${rect.left}px`;
1533
+ this.highlightOverlay.style.width = `${rect.width}px`;
1534
+ this.highlightOverlay.style.height = `${rect.height}px`;
1535
+ const selector = getSemanticSelector(el);
1536
+ const badgeText = `${selector} \xB7 ${Math.round(rect.width)}\xD7${Math.round(rect.height)}`;
1537
+ let badge = this.highlightOverlay.querySelector(".bt-badge");
1538
+ if (!badge) {
1539
+ badge = document.createElement("div");
1540
+ badge.className = "bt-badge";
1541
+ this.highlightOverlay.appendChild(badge);
1542
+ }
1543
+ badge.textContent = badgeText;
1544
+ }
1545
+ hideHighlight() {
1546
+ if (this.highlightOverlay) {
1547
+ this.highlightOverlay.style.display = "none";
1548
+ }
1549
+ }
1550
+ openNoteEditor(el, noteType = "element") {
1551
+ const root = this.ensureContainer();
1552
+ if (!root) return;
1553
+ const targetEl = el || document.body;
1554
+ this.selectedElement = targetEl;
1555
+ if (noteType === "element") {
1556
+ this.updateHighlight(targetEl);
1557
+ }
1558
+ this.renderNoteModal({
1559
+ title: "\u{1F4DD} Add Visual Note",
1560
+ modeBadge: noteType.toUpperCase(),
1561
+ pillText: noteType === "page" ? `Page Viewport: ${window.innerWidth} \xD7 ${window.innerHeight} px` : `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}\xD7${Math.round(targetEl.getBoundingClientRect().height)}) \xB7 Viewport: ${window.innerWidth}\xD7${window.innerHeight}`,
1562
+ onSave: async (message) => {
1563
+ await this.saveVisualNote(targetEl, message, noteType);
1564
+ }
1565
+ });
1566
+ }
1567
+ openRegionNoteEditor(region) {
1568
+ this.renderNoteModal({
1569
+ title: "\u{1F4DD} Add Region Note",
1570
+ modeBadge: "REGION",
1571
+ pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} \xD7 ${region.height} px)`,
1572
+ onSave: async (message) => {
1573
+ await this.saveRegionVisualNote(region, message);
1574
+ }
1575
+ });
1576
+ }
1577
+ renderNoteModal(options) {
1578
+ const root = this.ensureContainer();
1579
+ if (!root) return;
1580
+ if (this.modalOverlay && this.modalOverlay.parentElement) {
1581
+ this.modalOverlay.parentElement.removeChild(this.modalOverlay);
1582
+ this.modalOverlay = null;
1583
+ }
1584
+ const backdrop = document.createElement("div");
1585
+ backdrop.className = "bt-modal-backdrop";
1586
+ this.modalOverlay = backdrop;
1587
+ backdrop.innerHTML = `
1588
+ <div class="bt-modal">
1589
+ <div class="bt-modal-header">
1590
+ <div class="bt-modal-title">
1591
+ <span>${options.title}</span>
1592
+ <span class="bt-mode-badge">${options.modeBadge}</span>
1593
+ </div>
1594
+ <button class="bt-close-btn" id="btn-close" title="Close">\u2715</button>
1595
+ </div>
1596
+ <div class="bt-target-pill" title="${options.pillText}">
1597
+ \u{1F3AF} ${options.pillText}
1598
+ </div>
1599
+ <textarea class="bt-textarea" placeholder="Describe the layout issue, styling bug, or note for AI agent..." autofocus></textarea>
1600
+ <div class="bt-modal-actions">
1601
+ <button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
1602
+ <button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>
1603
+ </div>
1604
+ </div>
1605
+ `;
1606
+ const textarea = backdrop.querySelector("textarea");
1607
+ const btnClose = backdrop.querySelector("#btn-close");
1608
+ const btnCancel = backdrop.querySelector("#btn-cancel");
1609
+ const btnSave = backdrop.querySelector("#btn-save");
1610
+ const closeModal = () => {
1611
+ if (this.modalOverlay) {
1612
+ root.removeChild(this.modalOverlay);
1613
+ this.modalOverlay = null;
1614
+ this.hideHighlight();
1615
+ this.hideRegionOverlay();
1616
+ }
1617
+ };
1618
+ btnClose.onclick = closeModal;
1619
+ btnCancel.onclick = closeModal;
1620
+ backdrop.onclick = (e) => {
1621
+ if (e.target === backdrop) {
1622
+ closeModal();
1623
+ }
1624
+ };
1625
+ const submitNote = async () => {
1626
+ const message = textarea.value.trim();
1627
+ if (!message) return;
1628
+ btnSave.textContent = "Saving...";
1629
+ btnSave.disabled = true;
1630
+ try {
1631
+ await options.onSave(message);
1632
+ } finally {
1633
+ closeModal();
1634
+ }
1635
+ };
1636
+ btnSave.onclick = submitNote;
1637
+ textarea.onkeydown = (e) => {
1638
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey || !e.shiftKey)) {
1639
+ e.preventDefault();
1640
+ submitNote();
1641
+ }
1642
+ if (e.key === "Escape") {
1643
+ closeModal();
1644
+ }
1645
+ };
1646
+ root.appendChild(backdrop);
1647
+ setTimeout(() => textarea?.focus(), 50);
1648
+ }
1649
+ async saveVisualNote(targetEl, message, noteType = "element") {
1650
+ const rect = targetEl.getBoundingClientRect();
1651
+ const selector = noteType === "page" ? "body" : getSemanticSelector(targetEl);
1652
+ let screenshotDataUrl;
1653
+ try {
1654
+ const snap = await this.screenshotDriver.captureElement(targetEl);
1655
+ if (snap.ok) {
1656
+ screenshotDataUrl = snap.dataUrl;
1657
+ }
1658
+ } catch {
1659
+ }
1660
+ const elementContext = this.extractElementContext(targetEl);
1661
+ const target = {
1662
+ selector,
1663
+ boundingRect: {
1664
+ x: Math.round(rect.x),
1665
+ y: Math.round(rect.y),
1666
+ width: Math.round(rect.width),
1667
+ height: Math.round(rect.height),
1668
+ top: Math.round(rect.top),
1669
+ left: Math.round(rect.left),
1670
+ bottom: Math.round(rect.bottom),
1671
+ right: Math.round(rect.right)
1672
+ },
1673
+ visible: rect.width > 0 && rect.height > 0,
1674
+ confidence: selector.startsWith("[data-test") ? "high" : selector.startsWith("#") ? "medium" : "low"
1675
+ };
1676
+ const notePayload = {
1677
+ type: "create_note",
1678
+ sessionId: this.transport.getSessionId() || "",
1679
+ noteType,
1680
+ message,
1681
+ route: window.location.pathname + window.location.search,
1682
+ url: window.location.href,
1683
+ viewport: {
1684
+ width: window.innerWidth,
1685
+ height: window.innerHeight,
1686
+ devicePixelRatio: window.devicePixelRatio || 1
1687
+ },
1688
+ scroll: {
1689
+ scrollX: window.scrollX,
1690
+ scrollY: window.scrollY
1691
+ },
1692
+ target,
1693
+ elementContext,
1694
+ screenshot: screenshotDataUrl,
1695
+ timestamp: Date.now()
1696
+ };
1697
+ this.transport.send(notePayload);
1698
+ if (this.options.onNoteCreated) {
1699
+ this.options.onNoteCreated(notePayload);
1700
+ }
1701
+ }
1702
+ async saveRegionVisualNote(region, message) {
1703
+ let screenshotDataUrl;
1704
+ try {
1705
+ const snap = await this.screenshotDriver.captureElement(document.body || document.documentElement);
1706
+ if (snap.ok && snap.dataUrl) {
1707
+ screenshotDataUrl = await this.cropDataUrl(snap.dataUrl, region);
1708
+ }
1709
+ } catch {
1710
+ }
1711
+ const notePayload = {
1712
+ type: "create_note",
1713
+ sessionId: this.transport.getSessionId() || "",
1714
+ noteType: "region",
1715
+ message,
1716
+ route: window.location.pathname + window.location.search,
1717
+ url: window.location.href,
1718
+ viewport: {
1719
+ width: window.innerWidth,
1720
+ height: window.innerHeight,
1721
+ devicePixelRatio: window.devicePixelRatio || 1
1722
+ },
1723
+ scroll: {
1724
+ scrollX: window.scrollX,
1725
+ scrollY: window.scrollY
1726
+ },
1727
+ region,
1728
+ screenshot: screenshotDataUrl,
1729
+ timestamp: Date.now()
1730
+ };
1731
+ this.transport.send(notePayload);
1732
+ if (this.options.onNoteCreated) {
1733
+ this.options.onNoteCreated(notePayload);
1734
+ }
1735
+ }
1736
+ async cropDataUrl(dataUrl, region) {
1737
+ return new Promise((resolve) => {
1738
+ const img = new Image();
1739
+ img.onload = () => {
1740
+ try {
1741
+ const canvas = document.createElement("canvas");
1742
+ canvas.width = region.width;
1743
+ canvas.height = region.height;
1744
+ const ctx = canvas.getContext("2d");
1745
+ if (!ctx) {
1746
+ resolve(dataUrl);
1747
+ return;
1748
+ }
1749
+ const dpr = window.devicePixelRatio || 1;
1750
+ ctx.drawImage(
1751
+ img,
1752
+ region.x * dpr,
1753
+ region.y * dpr,
1754
+ region.width * dpr,
1755
+ region.height * dpr,
1756
+ 0,
1757
+ 0,
1758
+ region.width,
1759
+ region.height
1760
+ );
1761
+ resolve(canvas.toDataURL("image/webp", 0.9));
1762
+ } catch {
1763
+ resolve(dataUrl);
1764
+ }
1765
+ };
1766
+ img.onerror = () => resolve(dataUrl);
1767
+ img.src = dataUrl;
1768
+ });
1769
+ }
1770
+ extractElementContext(el) {
1771
+ const selector = getSemanticSelector(el);
1772
+ const tag = el.tagName.toLowerCase();
1773
+ const attributes = {};
1774
+ for (let i = 0; i < el.attributes.length; i++) {
1775
+ const attr = el.attributes[i];
1776
+ if (attr.name === "value" && el.type === "password") {
1777
+ attributes[attr.name] = "[REDACTED]";
1778
+ } else {
1779
+ attributes[attr.name] = attr.value;
1780
+ }
1781
+ }
1782
+ let outerHTML = "";
1783
+ try {
1784
+ const clone = el.cloneNode(true);
1785
+ for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
1786
+ passInput.setAttribute("value", "[REDACTED]");
1787
+ }
1788
+ outerHTML = truncate(clone.outerHTML, 10240);
1789
+ } catch {
1790
+ outerHTML = truncate(el.outerHTML, 10240);
1791
+ }
1792
+ let parent;
1793
+ if (el.parentElement && el.parentElement !== document.body) {
1794
+ parent = {
1795
+ selector: getSemanticSelector(el.parentElement),
1796
+ tag: el.parentElement.tagName.toLowerCase()
1797
+ };
1798
+ }
1799
+ return {
1800
+ selector,
1801
+ tag,
1802
+ attributes,
1803
+ outerHTML,
1804
+ innerText: truncate(el.textContent?.trim(), 200),
1805
+ parent
1806
+ };
1807
+ }
1808
+ destroy() {
1809
+ for (const cleanup of this.cleanups) {
1810
+ try {
1811
+ cleanup();
1812
+ } catch {
1813
+ }
1814
+ }
1815
+ this.cleanups = [];
1816
+ if (this.container && this.container.parentElement) {
1817
+ this.container.parentElement.removeChild(this.container);
1818
+ }
1819
+ this.container = null;
1820
+ this.shadowRoot = null;
1821
+ this.toolbarElement = null;
1822
+ }
1823
+ };
1824
+
1825
+ // packages/client/src/client.ts
1826
+ var BrowserTrackClient = class {
1827
+ options;
1828
+ breadcrumbs;
1829
+ transport;
1830
+ screenshotDriver;
1831
+ commandHandler;
1832
+ inspector;
1833
+ lastElement;
1834
+ currentRoute = "/";
1835
+ cleanups = [];
1836
+ isInitialized = false;
1837
+ constructor(options = {}) {
1838
+ this.options = {
1839
+ ...DEFAULT_OPTIONS,
1840
+ ...options,
1841
+ notes: {
1842
+ ...DEFAULT_OPTIONS.notes,
1843
+ ...options.notes || {}
1844
+ }
1845
+ };
1846
+ this.breadcrumbs = new BreadcrumbBuffer(this.options.maxBreadcrumbs);
1847
+ this.transport = new WebSocketTransport({
1848
+ url: this.options.daemonUrl,
1849
+ projectId: this.options.projectId,
1850
+ debug: this.options.debug
1851
+ });
1852
+ this.screenshotDriver = new BrowserScriptScreenshotDriver();
1853
+ this.commandHandler = new ClientCommandHandler(this.screenshotDriver);
1854
+ this.transport.setCommandHandler(this.commandHandler);
1855
+ if (this.options.notes.enabled) {
1856
+ this.inspector = new NoteInspector(this.transport, this.screenshotDriver, {
1857
+ shortcut: this.options.notes.shortcut,
1858
+ maskSelectors: this.options.notes.maskSelectors
1859
+ });
1860
+ }
1861
+ }
1862
+ init() {
1863
+ if (this.isInitialized || typeof window === "undefined") return this;
1864
+ this.isInitialized = true;
1865
+ try {
1866
+ this.currentRoute = window.location.pathname + window.location.search;
1867
+ } catch {
1868
+ this.currentRoute = "/";
1869
+ }
1870
+ this.transport.connect();
1871
+ if (this.options.captureErrors) {
1872
+ const cleanupRuntime = setupRuntimeInterceptors(async (errEvent) => {
1873
+ await this.handleRuntimeError(errEvent);
1874
+ });
1875
+ this.cleanups.push(cleanupRuntime);
1876
+ }
1877
+ if (this.options.captureConsole) {
1878
+ const cleanupConsole = setupConsoleInterceptors(async (consoleEvent) => {
1879
+ await this.handleConsole(consoleEvent);
1880
+ });
1881
+ this.cleanups.push(cleanupConsole);
1882
+ }
1883
+ if (this.options.captureNetwork) {
1884
+ const cleanupNetwork = setupNetworkInterceptors((netEvent) => {
1885
+ this.handleNetwork(netEvent);
1886
+ });
1887
+ this.cleanups.push(cleanupNetwork);
1888
+ }
1889
+ if (this.options.captureNavigation) {
1890
+ const cleanupNav = setupNavigationInterceptors((navEvent) => {
1891
+ this.handleNavigation(navEvent);
1892
+ });
1893
+ this.cleanups.push(cleanupNav);
1894
+ }
1895
+ if (this.options.captureInteractions) {
1896
+ const cleanupInteraction = setupInteractionInterceptors((breadcrumb, elSummary) => {
1897
+ if (elSummary) {
1898
+ this.lastElement = elSummary;
1899
+ }
1900
+ this.breadcrumbs.add(breadcrumb);
1901
+ });
1902
+ this.cleanups.push(cleanupInteraction);
1903
+ }
1904
+ if (this.inspector) {
1905
+ this.inspector.init();
1906
+ }
1907
+ return this;
1908
+ }
1909
+ openNoteEditor(el, noteType = "element") {
1910
+ if (this.inspector) {
1911
+ this.inspector.openNoteEditor(el, noteType);
1912
+ }
1913
+ }
1914
+ startRegionSelection() {
1915
+ if (this.inspector) {
1916
+ this.inspector.setMode("region");
1917
+ }
1918
+ }
1919
+ startElementSelection() {
1920
+ if (this.inspector) {
1921
+ this.inspector.setMode("element");
1922
+ }
1923
+ }
1924
+ setInspectMode(mode) {
1925
+ if (this.inspector) {
1926
+ this.inspector.setMode(mode);
1927
+ }
1928
+ }
1929
+ async createNote(targetEl, message, noteType = "element") {
1930
+ if (this.inspector) {
1931
+ await this.inspector.saveVisualNote(targetEl, message, noteType);
1932
+ }
1933
+ }
1934
+ async handleRuntimeError(errorEvent) {
1935
+ this.breadcrumbs.add({
1936
+ type: "error",
1937
+ category: "runtime",
1938
+ message: `${errorEvent.errorType}: ${errorEvent.message}`,
1939
+ timestamp: errorEvent.timestamp,
1940
+ level: "error",
1941
+ data: {
1942
+ filename: errorEvent.filename,
1943
+ lineno: errorEvent.lineno,
1944
+ colno: errorEvent.colno
1945
+ }
1946
+ });
1947
+ let screenshotDataUrl;
1948
+ if (this.options.onErrorScreenshot && this.lastElement?.selector) {
1949
+ try {
1950
+ const snap = await this.screenshotDriver.captureSelector(this.lastElement.selector);
1951
+ if (snap.ok) {
1952
+ screenshotDataUrl = snap.dataUrl;
1953
+ }
1954
+ } catch {
1955
+ }
1956
+ }
1957
+ this.sendEvent("runtime_error", errorEvent, screenshotDataUrl);
1958
+ }
1959
+ async handleConsole(consoleEvent) {
1960
+ this.breadcrumbs.add({
1961
+ type: "console",
1962
+ category: "console",
1963
+ message: `console.${consoleEvent.level}: ${consoleEvent.message}`,
1964
+ timestamp: consoleEvent.timestamp,
1965
+ level: consoleEvent.level === "error" ? "error" : consoleEvent.level === "warn" ? "warn" : "info"
1966
+ });
1967
+ if (consoleEvent.level === "error" || consoleEvent.level === "warn") {
1968
+ this.sendEvent("console", consoleEvent);
1969
+ }
1970
+ }
1971
+ handleNetwork(netEvent) {
1972
+ const isError = netEvent.status && netEvent.status >= 400 || !!netEvent.error;
1973
+ const msg = `${netEvent.method} ${netEvent.url} -> ${netEvent.status || "ERR"} (${netEvent.durationMs}ms)`;
1974
+ this.breadcrumbs.add({
1975
+ type: netEvent.id?.startsWith("xhr") ? "xhr" : "fetch",
1976
+ category: "network",
1977
+ message: msg,
1978
+ timestamp: netEvent.timestamp,
1979
+ level: isError ? "error" : "info",
1980
+ data: {
1981
+ status: netEvent.status,
1982
+ durationMs: netEvent.durationMs,
1983
+ error: netEvent.error,
1984
+ aborted: netEvent.aborted
1985
+ }
1986
+ });
1987
+ this.sendEvent("fetch", netEvent);
1988
+ }
1989
+ handleNavigation(navEvent) {
1990
+ this.currentRoute = navEvent.to;
1991
+ this.breadcrumbs.add({
1992
+ type: "navigation",
1993
+ category: "navigation",
1994
+ message: `navigate ${navEvent.to} (${navEvent.type})`,
1995
+ timestamp: navEvent.timestamp,
1996
+ level: "info",
1997
+ data: { from: navEvent.from, to: navEvent.to, type: navEvent.type }
1998
+ });
1999
+ this.sendEvent("navigation", navEvent);
2000
+ }
2001
+ sendEvent(eventType, payload, screenshotDataUrl) {
2002
+ if (typeof window === "undefined") return;
2003
+ const message = {
2004
+ type: "event",
2005
+ sessionId: this.transport.getSessionId() || "",
2006
+ eventType,
2007
+ payload,
2008
+ breadcrumbs: this.breadcrumbs.getRecent(),
2009
+ lastElement: this.lastElement,
2010
+ route: this.currentRoute,
2011
+ url: window.location.href,
2012
+ title: document.title,
2013
+ timestamp: Date.now(),
2014
+ screenshot: screenshotDataUrl
2015
+ };
2016
+ this.transport.send(message);
2017
+ }
2018
+ getBreadcrumbs() {
2019
+ return this.breadcrumbs.getRecent();
2020
+ }
2021
+ getLastElement() {
2022
+ return this.lastElement;
2023
+ }
2024
+ destroy() {
2025
+ for (const cleanup of this.cleanups) {
2026
+ try {
2027
+ cleanup();
2028
+ } catch {
2029
+ }
2030
+ }
2031
+ this.cleanups = [];
2032
+ this.transport.destroy();
2033
+ if (this.inspector) {
2034
+ this.inspector.destroy();
2035
+ }
2036
+ this.breadcrumbs.clear();
2037
+ this.isInitialized = false;
2038
+ }
2039
+ };
2040
+
2041
+ // packages/client/src/index.ts
2042
+ var defaultClient = null;
2043
+ function init(options = {}) {
2044
+ if (defaultClient) {
2045
+ return defaultClient;
2046
+ }
2047
+ defaultClient = new BrowserTrackClient(options);
2048
+ defaultClient.init();
2049
+ return defaultClient;
2050
+ }
2051
+ function getClient() {
2052
+ return defaultClient;
2053
+ }
2054
+ if (typeof window !== "undefined") {
2055
+ window.__BROWSERTRACK__ = {
2056
+ init,
2057
+ getClient
2058
+ };
2059
+ try {
2060
+ const currentScript = document.currentScript;
2061
+ const autoInit = currentScript?.getAttribute("data-auto-init") !== "false";
2062
+ const daemonUrl = currentScript?.getAttribute("data-daemon-url") || void 0;
2063
+ const projectId = currentScript?.getAttribute("data-project-id") || void 0;
2064
+ if (autoInit) {
2065
+ init({ daemonUrl, projectId });
2066
+ }
2067
+ } catch {
2068
+ }
2069
+ }
2070
+
2071
+ export {
2072
+ BreadcrumbBuffer,
2073
+ DEFAULT_OPTIONS,
2074
+ BrowserScriptScreenshotDriver,
2075
+ NoteInspector,
2076
+ BrowserTrackClient,
2077
+ init,
2078
+ getClient
2079
+ };
2080
+ //# sourceMappingURL=chunk-X7C5CHBO.js.map