browsertrack 0.2.1 → 0.2.3

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 (55) hide show
  1. package/AGENTS.md +9 -6
  2. package/README.md +2 -0
  3. package/dist/{chunk-464D4U2U.js → chunk-5NR5K3ER.js} +299 -44
  4. package/dist/chunk-5NR5K3ER.js.map +1 -0
  5. package/dist/{chunk-3HOXPTM2.js → chunk-G5CIZSQM.js} +808 -53
  6. package/dist/chunk-G5CIZSQM.js.map +1 -0
  7. package/dist/{chunk-6VA7GBAO.js → chunk-ONPW7AYL.js} +144 -12
  8. package/dist/chunk-ONPW7AYL.js.map +1 -0
  9. package/dist/{chunk-INXDWPJW.js → chunk-PG4JJDCV.js} +353 -119
  10. package/dist/chunk-PG4JJDCV.js.map +1 -0
  11. package/dist/cli/index.js +1058 -546
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/client/index.cjs +449 -128
  14. package/dist/client/index.d.ts +2 -0
  15. package/dist/client/index.js +2 -2
  16. package/dist/client.iife.js +121 -15
  17. package/dist/core/index.d.ts +32 -2
  18. package/dist/core/index.js +11 -1
  19. package/dist/daemon/index.d.ts +2 -2
  20. package/dist/daemon/index.js +6 -8
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +16 -7
  23. package/dist/mcp/index.d.ts +1 -1
  24. package/dist/mcp/index.js +7 -4
  25. package/dist/{server-DiVmTrIR.d.ts → server-BRG-RQQP.d.ts} +10 -1
  26. package/docs/cli.md +4 -1
  27. package/docs/getting-started.md +60 -6
  28. package/docs/mcp-reference.md +42 -0
  29. package/package.json +1 -1
  30. package/packages/cli/src/index.ts +247 -151
  31. package/packages/client/src/interceptors/navigation.ts +38 -26
  32. package/packages/client/src/interceptors/network.ts +22 -17
  33. package/packages/client/src/notes/inspector.ts +289 -65
  34. package/packages/client/src/source/resolver.ts +9 -3
  35. package/packages/client/src/transport/websocket.ts +23 -18
  36. package/packages/core/src/index.ts +1 -0
  37. package/packages/core/src/safety.ts +86 -0
  38. package/packages/core/src/selector.ts +110 -15
  39. package/packages/daemon/src/server/daemon.ts +7 -1
  40. package/packages/daemon/src/server/http.ts +125 -5
  41. package/packages/daemon/src/server/ws.ts +33 -29
  42. package/packages/daemon/src/storage/db.ts +57 -35
  43. package/packages/mcp/src/handlers.ts +121 -45
  44. package/packages/mcp/src/server.ts +221 -3
  45. package/test/core/safety.test.ts +106 -0
  46. package/test/core/selector.test.ts +133 -0
  47. package/test/daemon/storage.test.ts +36 -0
  48. package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
  49. package/test/mcp/auto-start.test.ts +87 -0
  50. package/dist/chunk-3HOXPTM2.js.map +0 -1
  51. package/dist/chunk-464D4U2U.js.map +0 -1
  52. package/dist/chunk-6VA7GBAO.js.map +0 -1
  53. package/dist/chunk-7OCOQGDN.js +0 -635
  54. package/dist/chunk-7OCOQGDN.js.map +0 -1
  55. package/dist/chunk-INXDWPJW.js.map +0 -1
@@ -133,7 +133,8 @@ function redactUrl(rawUrl) {
133
133
  function getSemanticSelector(element, options = {}) {
134
134
  if (!element || !element.tagName) return "unknown";
135
135
  const maxClasses = options.maxClasses ?? 2;
136
- const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa");
136
+ const maxParentDepth = options.maxParentDepth ?? 3;
137
+ const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa") || element.getAttribute("data-component");
137
138
  if (testId) {
138
139
  return `[data-testid="${testId}"]`;
139
140
  }
@@ -141,34 +142,122 @@ function getSemanticSelector(element, options = {}) {
141
142
  if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
142
143
  return `#${id}`;
143
144
  }
145
+ const tag = element.tagName.toLowerCase();
144
146
  const ariaLabel = element.getAttribute("aria-label");
145
- if (ariaLabel && ariaLabel.length < 30) {
146
- return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
147
+ if (ariaLabel && ariaLabel.length < 40) {
148
+ return `${tag}[aria-label="${ariaLabel}"]`;
149
+ }
150
+ const role = element.getAttribute("role");
151
+ if (role && !["presentation", "none"].includes(role)) {
152
+ const roleSelector = `${tag}[role="${role}"]`;
153
+ if (tag === "button" || tag === "a" || tag === "nav" || tag === "dialog") {
154
+ return roleSelector;
155
+ }
147
156
  }
148
157
  const nameAttr = element.getAttribute("name");
149
158
  if (nameAttr) {
150
- return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
151
- }
152
- const tag = element.tagName.toLowerCase();
153
- const classList = Array.from(element.classList || []).filter((c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c)).slice(0, maxClasses);
159
+ return `${tag}[name="${nameAttr}"]`;
160
+ }
161
+ const titleAttr = element.getAttribute("title");
162
+ if (titleAttr && titleAttr.length < 30) {
163
+ return `${tag}[title="${titleAttr}"]`;
164
+ }
165
+ const rawClassList = Array.from(element.classList || []);
166
+ const classList = rawClassList.filter(
167
+ (c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c) && // filter dynamic hashed utility classes
168
+ !c.includes(":") && // Tailwind pseudo-class prefixes (hover:, md:, etc.)
169
+ !c.includes("[")
170
+ // Tailwind arbitrary value classes
171
+ ).slice(0, maxClasses);
154
172
  if (classList.length > 0) {
155
173
  const classStr = classList.map((c) => `.${c}`).join("");
156
174
  return `${tag}${classStr}`;
157
175
  }
158
- if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
159
- const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
160
- if (parentSelector && parentSelector !== "unknown" && !parentSelector.includes(">")) {
161
- return `${parentSelector} > ${tag}`;
176
+ const hasParent = element.parentElement && (typeof document === "undefined" || element.parentElement !== document.body && element.parentElement !== document.documentElement);
177
+ if (hasParent) {
178
+ let suffix = "";
179
+ if (element.parentElement.children && element.parentElement.children.length > 1) {
180
+ const sameTagSiblings = Array.from(element.parentElement.children).filter(
181
+ (child) => child.tagName && child.tagName.toLowerCase() === tag
182
+ );
183
+ if (sameTagSiblings.length > 1) {
184
+ const index = sameTagSiblings.indexOf(element) + 1;
185
+ if (index > 0) {
186
+ suffix = `:nth-of-type(${index})`;
187
+ }
188
+ }
189
+ }
190
+ if (maxParentDepth > 0) {
191
+ const parentSelector = getSemanticSelector(element.parentElement, {
192
+ maxClasses: 1,
193
+ maxParentDepth: maxParentDepth - 1
194
+ });
195
+ if (parentSelector && parentSelector !== "unknown" && parentSelector !== "body" && parentSelector !== "html") {
196
+ return `${parentSelector} > ${tag}${suffix}`;
197
+ }
198
+ }
199
+ if (suffix) {
200
+ return `${tag}${suffix}`;
162
201
  }
163
202
  }
164
203
  return tag;
165
204
  }
205
+ function resolveMeaningfulTarget(element) {
206
+ if (!element || typeof element !== "object") {
207
+ return element;
208
+ }
209
+ const el = element;
210
+ const isSvg = el.tagName.toLowerCase() === "svg" || el.namespaceURI?.includes("svg");
211
+ if (isSvg || el.closest?.("svg")) {
212
+ const interactiveParent = el.closest?.(
213
+ 'button, a, [role="button"], [role="link"], [role="tab"], summary, label, [data-testid], [data-component]'
214
+ );
215
+ if (interactiveParent) return interactiveParent;
216
+ const svgEl = isSvg ? el : el.closest?.("svg");
217
+ if (svgEl) return svgEl;
218
+ }
219
+ const leafTags = ["span", "i", "em", "b", "strong", "small", "mark", "code"];
220
+ const tag = el.tagName.toLowerCase();
221
+ if (leafTags.includes(tag) && !el.id && !el.getAttribute("data-testid") && !el.getAttribute("data-component") && !el.getAttribute("aria-label")) {
222
+ const interactiveParent = el.closest?.(
223
+ 'button, a, [role="button"], [role="link"], [role="tab"], [role="menuitem"], summary, label, [data-testid], [data-component]'
224
+ );
225
+ if (interactiveParent) return interactiveParent;
226
+ }
227
+ return el;
228
+ }
166
229
  function truncate(str, maxLength = 200) {
167
230
  if (!str) return "";
168
231
  if (str.length <= maxLength) return str;
169
232
  return str.slice(0, maxLength) + "...";
170
233
  }
171
234
 
235
+ // packages/core/src/safety.ts
236
+ function safeJsonStringify(val, fallback = "{}") {
237
+ if (val === void 0) return fallback;
238
+ try {
239
+ const seen = /* @__PURE__ */ new WeakSet();
240
+ return JSON.stringify(val, (key, value) => {
241
+ if (typeof value === "object" && value !== null) {
242
+ if (seen.has(value)) {
243
+ return "[Circular]";
244
+ }
245
+ seen.add(value);
246
+ }
247
+ if (typeof value === "bigint") {
248
+ return value.toString();
249
+ }
250
+ return value;
251
+ });
252
+ } catch {
253
+ try {
254
+ return JSON.stringify(String(val));
255
+ } catch {
256
+ return fallback;
257
+ }
258
+ }
259
+ }
260
+
172
261
  // packages/client/src/source/resolver.ts
173
262
  function resolveComponentSource(el) {
174
263
  if (!el || typeof el !== "object") return void 0;
@@ -218,7 +307,9 @@ function resolveReactComponent(el) {
218
307
  }
219
308
  }
220
309
  let curr = hostFiber;
221
- while (curr) {
310
+ let depth = 0;
311
+ while (curr && depth < 50) {
312
+ depth++;
222
313
  const type = curr.type;
223
314
  const name = getReactComponentName(type);
224
315
  if (name && !name.startsWith("html:") && name !== "Fragment") {
@@ -273,7 +364,9 @@ function resolveVueComponent(el) {
273
364
  const sourceFile = type.__file;
274
365
  const hierarchy = [];
275
366
  let curr = vueParent;
276
- while (curr) {
367
+ let depth = 0;
368
+ while (curr && depth < 50) {
369
+ depth++;
277
370
  const cType = curr.type || {};
278
371
  const cName = cType.name || cType.__name || cType.displayName;
279
372
  if (cName && !hierarchy.includes(cName)) {
@@ -310,7 +403,9 @@ function resolveVueComponent(el) {
310
403
  function resolveSvelteComponent(el) {
311
404
  try {
312
405
  let curr = el;
313
- while (curr) {
406
+ let depth = 0;
407
+ while (curr && depth < 50) {
408
+ depth++;
314
409
  const meta = curr.__svelte_meta;
315
410
  if (meta && meta.loc) {
316
411
  return {
@@ -849,12 +944,16 @@ function setupConsoleInterceptors(onConsole) {
849
944
  function setupNavigationInterceptors(onNavigation) {
850
945
  if (typeof window === "undefined" || typeof history === "undefined") return () => {
851
946
  };
852
- let currentUrl = redactUrl(window.location.href);
853
- onNavigation({
854
- to: currentUrl,
855
- type: "initial",
856
- timestamp: Date.now()
857
- });
947
+ let currentUrl = "";
948
+ try {
949
+ currentUrl = redactUrl(window.location.href);
950
+ onNavigation({
951
+ to: currentUrl,
952
+ type: "initial",
953
+ timestamp: Date.now()
954
+ });
955
+ } catch {
956
+ }
858
957
  const originalPushState = history.pushState;
859
958
  const originalReplaceState = history.replaceState;
860
959
  history.pushState = function(data, unused, url) {
@@ -890,26 +989,32 @@ function setupNavigationInterceptors(onNavigation) {
890
989
  return result;
891
990
  };
892
991
  const onPopState = () => {
893
- const from = currentUrl;
894
- const to = redactUrl(window.location.href);
895
- currentUrl = to;
896
- onNavigation({
897
- from,
898
- to,
899
- type: "popstate",
900
- timestamp: Date.now()
901
- });
992
+ try {
993
+ const from = currentUrl;
994
+ const to = redactUrl(window.location.href);
995
+ currentUrl = to;
996
+ onNavigation({
997
+ from,
998
+ to,
999
+ type: "popstate",
1000
+ timestamp: Date.now()
1001
+ });
1002
+ } catch {
1003
+ }
902
1004
  };
903
1005
  const onHashChange = () => {
904
- const from = currentUrl;
905
- const to = redactUrl(window.location.href);
906
- currentUrl = to;
907
- onNavigation({
908
- from,
909
- to,
910
- type: "hashchange",
911
- timestamp: Date.now()
912
- });
1006
+ try {
1007
+ const from = currentUrl;
1008
+ const to = redactUrl(window.location.href);
1009
+ currentUrl = to;
1010
+ onNavigation({
1011
+ from,
1012
+ to,
1013
+ type: "hashchange",
1014
+ timestamp: Date.now()
1015
+ });
1016
+ } catch {
1017
+ }
913
1018
  };
914
1019
  window.addEventListener("popstate", onPopState);
915
1020
  window.addEventListener("hashchange", onHashChange);
@@ -946,31 +1051,38 @@ function setupNetworkInterceptors(onNetwork) {
946
1051
  urlStr = "unknown_url";
947
1052
  }
948
1053
  const safeUrl = redactUrl(urlStr);
1054
+ let response;
949
1055
  try {
950
- const response = await originalFetch.apply(window, [input, init2]);
951
- const durationMs = Date.now() - startTime;
952
- onNetwork({
953
- url: safeUrl,
954
- method,
955
- status: response.status,
956
- statusText: response.statusText,
957
- durationMs,
958
- timestamp: startTime
959
- });
960
- return response;
1056
+ response = await originalFetch.apply(window, [input, init2]);
961
1057
  } catch (err) {
962
- const durationMs = Date.now() - startTime;
1058
+ const durationMs2 = Date.now() - startTime;
963
1059
  const isAbort = err?.name === "AbortError";
1060
+ try {
1061
+ onNetwork({
1062
+ url: safeUrl,
1063
+ method,
1064
+ durationMs: durationMs2,
1065
+ error: err?.message || "Network request failed",
1066
+ aborted: isAbort,
1067
+ timestamp: startTime
1068
+ });
1069
+ } catch {
1070
+ }
1071
+ throw err;
1072
+ }
1073
+ const durationMs = Date.now() - startTime;
1074
+ try {
964
1075
  onNetwork({
965
1076
  url: safeUrl,
966
1077
  method,
1078
+ status: response.status,
1079
+ statusText: response.statusText,
967
1080
  durationMs,
968
- error: err?.message || "Network request failed",
969
- aborted: isAbort,
970
1081
  timestamp: startTime
971
1082
  });
972
- throw err;
1083
+ } catch {
973
1084
  }
1085
+ return response;
974
1086
  };
975
1087
  cleanups.push(() => {
976
1088
  window.fetch = originalFetch;
@@ -1358,15 +1470,18 @@ var WebSocketTransport = class {
1358
1470
  }
1359
1471
  }
1360
1472
  send(payload) {
1361
- const raw = JSON.stringify(payload);
1362
- if (this.isConnected()) {
1363
- try {
1364
- this.ws.send(raw);
1365
- } catch {
1473
+ try {
1474
+ const raw = safeJsonStringify(payload);
1475
+ if (this.isConnected()) {
1476
+ try {
1477
+ this.ws.send(raw);
1478
+ } catch {
1479
+ this.enqueue(raw);
1480
+ }
1481
+ } else {
1366
1482
  this.enqueue(raw);
1367
1483
  }
1368
- } else {
1369
- this.enqueue(raw);
1484
+ } catch {
1370
1485
  }
1371
1486
  }
1372
1487
  enqueue(raw) {
@@ -1391,17 +1506,17 @@ var WebSocketTransport = class {
1391
1506
  }
1392
1507
  sendHello() {
1393
1508
  if (typeof window === "undefined") return;
1394
- const hello = {
1395
- type: "hello",
1396
- origin: window.location.origin,
1397
- url: window.location.href,
1398
- title: document.title,
1399
- userAgent: navigator.userAgent,
1400
- timestamp: Date.now(),
1401
- projectId: this.projectId
1402
- };
1403
1509
  try {
1404
- this.ws.send(JSON.stringify(hello));
1510
+ const hello = {
1511
+ type: "hello",
1512
+ origin: window.location.origin,
1513
+ url: window.location.href,
1514
+ title: document.title,
1515
+ userAgent: navigator.userAgent,
1516
+ timestamp: Date.now(),
1517
+ projectId: this.projectId
1518
+ };
1519
+ this.ws.send(safeJsonStringify(hello));
1405
1520
  } catch {
1406
1521
  }
1407
1522
  }
@@ -1459,12 +1574,19 @@ var NoteInspector = class {
1459
1574
  __publicField(this, "savedNotes", []);
1460
1575
  __publicField(this, "showMarkers", true);
1461
1576
  __publicField(this, "isHidden", false);
1577
+ __publicField(this, "isToolbarCollapsed", false);
1462
1578
  __publicField(this, "cleanups", []);
1463
1579
  this.transport = transport;
1464
1580
  this.screenshotDriver = screenshotDriver;
1465
1581
  const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
1466
1582
  this.isHidden = options.hidden === true || hiddenByQuery;
1467
1583
  this.showMarkers = options.showBadges !== false && !this.isHidden;
1584
+ try {
1585
+ if (typeof window !== "undefined" && window.sessionStorage) {
1586
+ this.isToolbarCollapsed = window.sessionStorage.getItem("bt_toolbar_collapsed") === "true";
1587
+ }
1588
+ } catch {
1589
+ }
1468
1590
  this.options = {
1469
1591
  shortcut: "Alt+Click",
1470
1592
  maskSelectors: ['input[type="password"]', "[data-sensitive]"],
@@ -1592,11 +1714,28 @@ var NoteInspector = class {
1592
1714
  this.shadowRoot = this.container.attachShadow({ mode: "open" });
1593
1715
  const style = document.createElement("style");
1594
1716
  style.textContent = `
1595
- :host {
1596
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1597
- color-scheme: dark;
1717
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
1718
+
1719
+ :host, :host *, :host *::before, :host *::after {
1720
+ box-sizing: border-box;
1598
1721
  -webkit-font-smoothing: antialiased;
1599
1722
  -moz-osx-font-smoothing: grayscale;
1723
+ text-rendering: optimizeLegibility;
1724
+ }
1725
+
1726
+ :host {
1727
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1728
+ color-scheme: dark;
1729
+ letter-spacing: -0.012em;
1730
+ }
1731
+
1732
+ button, input, textarea, select {
1733
+ font-family: inherit;
1734
+ letter-spacing: inherit;
1735
+ }
1736
+
1737
+ code, pre, kbd, .bt-mono, .bt-badge, .bt-region-badge, .bt-target-pill, .bt-component-pill {
1738
+ font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1600
1739
  }
1601
1740
 
1602
1741
  /* 1. Element Highlight Overlay */
@@ -1830,7 +1969,7 @@ var NoteInspector = class {
1830
1969
  pointer-events: auto;
1831
1970
  z-index: 2147483646;
1832
1971
  user-select: none;
1833
- transition: all 0.2s ease;
1972
+ transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
1834
1973
  }
1835
1974
 
1836
1975
  .bt-toolbar-btn {
@@ -1891,6 +2030,86 @@ var NoteInspector = class {
1891
2030
  margin: 0 2px;
1892
2031
  }
1893
2032
 
2033
+ .bt-toolbar-collapse-btn {
2034
+ background: transparent;
2035
+ border: none;
2036
+ color: #64748b;
2037
+ font-size: 11px;
2038
+ font-weight: 700;
2039
+ padding: 6px 8px;
2040
+ border-radius: 20px;
2041
+ cursor: pointer;
2042
+ display: flex;
2043
+ align-items: center;
2044
+ justify-content: center;
2045
+ transition: all 0.15s ease;
2046
+ font-family: inherit;
2047
+ }
2048
+
2049
+ .bt-toolbar-collapse-btn:hover {
2050
+ background: #1e293b;
2051
+ color: #f8fafc;
2052
+ }
2053
+
2054
+ /* Collapsed Toolbar State */
2055
+ .bt-toolbar.bt-toolbar-collapsed {
2056
+ padding: 0;
2057
+ background: transparent;
2058
+ border: none;
2059
+ box-shadow: none;
2060
+ }
2061
+
2062
+ .bt-toolbar.bt-toolbar-collapsed .bt-toolbar-btn,
2063
+ .bt-toolbar.bt-toolbar-collapsed .bt-toolbar-divider,
2064
+ .bt-toolbar.bt-toolbar-collapsed .bt-toolbar-collapse-btn {
2065
+ display: none !important;
2066
+ }
2067
+
2068
+ .bt-toolbar-trigger {
2069
+ display: none;
2070
+ background: #0f172a;
2071
+ border: 1px solid #334155;
2072
+ border-radius: 30px;
2073
+ padding: 6px 14px;
2074
+ color: #38bdf8;
2075
+ font-size: 12px;
2076
+ font-weight: 600;
2077
+ cursor: pointer;
2078
+ align-items: center;
2079
+ gap: 8px;
2080
+ box-shadow: 0 10px 25px -3px rgba(0,0,0,0.6), 0 4px 6px -4px rgba(0,0,0,0.4);
2081
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
2082
+ font-family: inherit;
2083
+ user-select: none;
2084
+ }
2085
+
2086
+ .bt-toolbar.bt-toolbar-collapsed .bt-toolbar-trigger {
2087
+ display: inline-flex;
2088
+ }
2089
+
2090
+ .bt-toolbar-trigger:hover {
2091
+ background: #1e293b;
2092
+ color: #ffffff;
2093
+ transform: translateY(-2px);
2094
+ box-shadow: 0 12px 28px -3px rgba(0,0,0,0.7);
2095
+ border-color: #38bdf8;
2096
+ }
2097
+
2098
+ .bt-pulse-dot {
2099
+ width: 7px;
2100
+ height: 7px;
2101
+ border-radius: 50%;
2102
+ background: #38bdf8;
2103
+ box-shadow: 0 0 8px #38bdf8;
2104
+ display: inline-block;
2105
+ animation: bt-dot-pulse 2s infinite;
2106
+ }
2107
+
2108
+ @keyframes bt-dot-pulse {
2109
+ 0%, 100% { opacity: 1; transform: scale(1); }
2110
+ 50% { opacity: 0.4; transform: scale(0.85); }
2111
+ }
2112
+
1894
2113
  /* 5. Modals & Popover Card */
1895
2114
  .bt-modal-backdrop {
1896
2115
  position: fixed;
@@ -2301,8 +2520,13 @@ var NoteInspector = class {
2301
2520
  createToolbar() {
2302
2521
  if (!this.shadowRoot || this.toolbarElement) return;
2303
2522
  this.toolbarElement = document.createElement("div");
2304
- this.toolbarElement.className = "bt-toolbar";
2523
+ this.toolbarElement.className = `bt-toolbar ${this.isToolbarCollapsed ? "bt-toolbar-collapsed" : ""}`;
2305
2524
  this.toolbarElement.innerHTML = `
2525
+ <button class="bt-toolbar-trigger" id="bt-toolbar-expand" title="Expand BrowserTrack toolbar (Alt+T)">
2526
+ <span class="bt-pulse-dot"></span>
2527
+ <span>\u26A1 BrowserTrack</span>
2528
+ <span class="bt-count-pill" id="bt-notes-count-collapsed">0</span>
2529
+ </button>
2306
2530
  <button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
2307
2531
  <span>\u{1F3AF}</span> Element
2308
2532
  </button>
@@ -2320,12 +2544,26 @@ var NoteInspector = class {
2320
2544
  <button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
2321
2545
  <span>\u{1F4CC}</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
2322
2546
  </button>
2547
+ <div class="bt-toolbar-divider"></div>
2548
+ <button class="bt-toolbar-collapse-btn" id="bt-toolbar-collapse" title="Collapse toolbar (Alt+T)">
2549
+ <span>\u276F</span>
2550
+ </button>
2323
2551
  `;
2552
+ const btnExpand = this.toolbarElement.querySelector("#bt-toolbar-expand");
2553
+ const btnCollapse = this.toolbarElement.querySelector("#bt-toolbar-collapse");
2324
2554
  const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
2325
2555
  const btnRegion = this.toolbarElement.querySelector("#bt-mode-region");
2326
2556
  const btnPage = this.toolbarElement.querySelector("#bt-mode-page");
2327
2557
  const btnFlow = this.toolbarElement.querySelector("#bt-mode-flow");
2328
2558
  const btnToggleNotes = this.toolbarElement.querySelector("#bt-toggle-notes");
2559
+ btnExpand.onclick = (e) => {
2560
+ e.stopPropagation();
2561
+ this.toggleToolbarCollapse(false);
2562
+ };
2563
+ btnCollapse.onclick = (e) => {
2564
+ e.stopPropagation();
2565
+ this.toggleToolbarCollapse(true);
2566
+ };
2329
2567
  btnElement.onclick = (e) => {
2330
2568
  e.stopPropagation();
2331
2569
  this.setMode(this.activeMode === "element" ? "idle" : "element");
@@ -2354,6 +2592,17 @@ var NoteInspector = class {
2354
2592
  };
2355
2593
  this.shadowRoot.appendChild(this.toolbarElement);
2356
2594
  }
2595
+ toggleToolbarCollapse(force) {
2596
+ if (!this.toolbarElement) return;
2597
+ this.isToolbarCollapsed = typeof force === "boolean" ? force : !this.isToolbarCollapsed;
2598
+ this.toolbarElement.classList.toggle("bt-toolbar-collapsed", this.isToolbarCollapsed);
2599
+ try {
2600
+ if (typeof window !== "undefined" && window.sessionStorage) {
2601
+ window.sessionStorage.setItem("bt_toolbar_collapsed", String(this.isToolbarCollapsed));
2602
+ }
2603
+ } catch {
2604
+ }
2605
+ }
2357
2606
  updateToolbarState() {
2358
2607
  if (!this.toolbarElement) return;
2359
2608
  const btnElement = this.toolbarElement.querySelector("#bt-mode-element");
@@ -2377,11 +2626,15 @@ var NoteInspector = class {
2377
2626
  }
2378
2627
  updateToolbarCount() {
2379
2628
  if (!this.toolbarElement) return;
2629
+ const openCount = this.savedNotes.filter((n) => n.status === "OPEN").length;
2380
2630
  const countEl = this.toolbarElement.querySelector("#bt-notes-count");
2381
2631
  if (countEl) {
2382
- const openCount = this.savedNotes.filter((n) => n.status === "OPEN").length;
2383
2632
  countEl.textContent = String(openCount);
2384
2633
  }
2634
+ const countCollapsedEl = this.toolbarElement.querySelector("#bt-notes-count-collapsed");
2635
+ if (countCollapsedEl) {
2636
+ countCollapsedEl.textContent = String(openCount);
2637
+ }
2385
2638
  }
2386
2639
  renderMarkers() {
2387
2640
  const root = this.ensureContainer();
@@ -2389,9 +2642,15 @@ var NoteInspector = class {
2389
2642
  this.markersContainer.innerHTML = "";
2390
2643
  if (!this.showMarkers || this.isHidden) return;
2391
2644
  const currentPath = window.location.pathname;
2392
- const activeNotes = this.savedNotes.filter(
2393
- (n) => n.status === "OPEN" && (n.route === currentPath || !n.route || n.route === "/" || window.location.href.includes(n.route))
2394
- );
2645
+ const activeNotes = this.savedNotes.filter((n) => {
2646
+ if (n.status !== "OPEN") return false;
2647
+ if (!n.route) return true;
2648
+ if (n.route === currentPath) return true;
2649
+ if (n.route.includes("#") || n.route.includes("?")) {
2650
+ return window.location.href.includes(n.route);
2651
+ }
2652
+ return false;
2653
+ });
2395
2654
  const pageNotes = [];
2396
2655
  activeNotes.forEach((note, index) => {
2397
2656
  if (note.type === "page") {
@@ -2431,6 +2690,10 @@ var NoteInspector = class {
2431
2690
  }
2432
2691
  const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
2433
2692
  if (rect) {
2693
+ const isOffscreen = targetEl && (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth);
2694
+ if (isOffscreen) {
2695
+ return;
2696
+ }
2434
2697
  const pin = document.createElement("div");
2435
2698
  pin.className = markerClass;
2436
2699
  pin.setAttribute("data-note-id", note.id);
@@ -2616,65 +2879,90 @@ var NoteInspector = class {
2616
2879
  }
2617
2880
  setupListeners() {
2618
2881
  const onMouseMove = (e) => {
2619
- if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === "region") return;
2620
- const path = e.composedPath ? e.composedPath() : [];
2621
- if (this.container && path.includes(this.container)) {
2622
- this.hideHighlight();
2623
- return;
2624
- }
2625
- if (e.altKey || this.activeMode === "element") {
2626
- const target = document.elementFromPoint(e.clientX, e.clientY);
2627
- if (target && target !== this.container && !this.container?.contains(target)) {
2628
- this.hoveredElement = target;
2629
- this.updateHighlight(target);
2882
+ try {
2883
+ if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === "region") return;
2884
+ const path = e.composedPath ? e.composedPath() : [];
2885
+ if (this.container && path.includes(this.container)) {
2886
+ this.hideHighlight();
2630
2887
  return;
2631
2888
  }
2632
- }
2633
- if (!e.altKey && this.activeMode !== "element") {
2634
- this.hideHighlight();
2889
+ if (e.altKey || this.activeMode === "element") {
2890
+ const rawTarget = document.elementFromPoint(e.clientX, e.clientY);
2891
+ const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
2892
+ if (target && target !== this.container && !this.container?.contains(target)) {
2893
+ this.hoveredElement = target;
2894
+ this.updateHighlight(target);
2895
+ return;
2896
+ }
2897
+ }
2898
+ if (!e.altKey && this.activeMode !== "element") {
2899
+ this.hideHighlight();
2900
+ }
2901
+ } catch {
2635
2902
  }
2636
2903
  };
2637
2904
  const onClick = (e) => {
2638
- if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
2639
- const path = e.composedPath ? e.composedPath() : [];
2640
- if (this.container && path.includes(this.container)) {
2641
- return;
2642
- }
2643
- if (this.activeMode === "region") return;
2644
- if (e.altKey || this.activeMode === "element") {
2645
- e.preventDefault();
2646
- e.stopPropagation();
2647
- const target = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
2648
- if (target && target !== this.container && !this.container?.contains(target)) {
2649
- this.selectedElement = target;
2650
- this.openNoteEditor(target, "element");
2651
- if (this.activeMode === "element" && !this.activeScenario) {
2652
- this.setMode("idle");
2905
+ try {
2906
+ if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
2907
+ const path = e.composedPath ? e.composedPath() : [];
2908
+ if (this.container && path.includes(this.container)) {
2909
+ return;
2910
+ }
2911
+ if (this.activeMode === "region") return;
2912
+ if (e.altKey || this.activeMode === "element") {
2913
+ e.preventDefault();
2914
+ e.stopPropagation();
2915
+ const rawTarget = this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY);
2916
+ const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
2917
+ if (target && target !== this.container && !this.container?.contains(target)) {
2918
+ this.selectedElement = target;
2919
+ this.openNoteEditor(target, "element");
2920
+ if (this.activeMode === "element" && !this.activeScenario) {
2921
+ this.setMode("idle");
2922
+ }
2653
2923
  }
2654
2924
  }
2925
+ } catch {
2655
2926
  }
2656
2927
  };
2657
2928
  const onKeyDown = (e) => {
2658
- if (e.key === "Escape") {
2659
- if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
2660
- this.shadowRoot.removeChild(this.cardOverlay);
2661
- this.cardOverlay = null;
2662
- } else if (this.activeMode === "region" || this.activeMode === "element") {
2663
- if (!this.activeScenario) {
2664
- this.setMode("idle");
2929
+ try {
2930
+ if (e.altKey && (e.key === "t" || e.key === "T")) {
2931
+ e.preventDefault();
2932
+ this.toggleToolbarCollapse();
2933
+ return;
2934
+ }
2935
+ if (e.key === "Escape") {
2936
+ if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
2937
+ this.shadowRoot.removeChild(this.cardOverlay);
2938
+ this.cardOverlay = null;
2939
+ } else if (this.activeMode === "region" || this.activeMode === "element") {
2940
+ if (!this.activeScenario) {
2941
+ this.setMode("idle");
2942
+ }
2665
2943
  }
2666
2944
  }
2945
+ } catch {
2667
2946
  }
2668
2947
  };
2669
2948
  const onKeyUp = (e) => {
2670
- if (e.key === "Alt" && !this.modalOverlay && !this.cardOverlay && this.activeMode !== "element") {
2671
- this.hideHighlight();
2949
+ try {
2950
+ if (e.key === "Alt" && !this.modalOverlay && !this.cardOverlay && this.activeMode !== "element") {
2951
+ this.hideHighlight();
2952
+ }
2953
+ } catch {
2672
2954
  }
2673
2955
  };
2674
2956
  const onScrollOrResize = () => {
2675
- requestAnimationFrame(() => {
2676
- this.updateMarkerPositions();
2677
- });
2957
+ try {
2958
+ requestAnimationFrame(() => {
2959
+ try {
2960
+ this.updateMarkerPositions();
2961
+ } catch {
2962
+ }
2963
+ });
2964
+ } catch {
2965
+ }
2678
2966
  };
2679
2967
  window.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
2680
2968
  window.addEventListener("click", onClick, { capture: true });
@@ -2751,6 +3039,11 @@ var NoteInspector = class {
2751
3039
  this.regionBox.appendChild(badge);
2752
3040
  }
2753
3041
  badge.textContent = `Region: ${Math.round(width)} \xD7 ${Math.round(height)} px`;
3042
+ if (top < 32) {
3043
+ badge.style.top = "4px";
3044
+ } else {
3045
+ badge.style.top = "-26px";
3046
+ }
2754
3047
  };
2755
3048
  this.regionOverlay.onmouseup = (e) => {
2756
3049
  if (!this.isDraggingRegion) return;
@@ -2809,6 +3102,18 @@ var NoteInspector = class {
2809
3102
  const selector = getSemanticSelector(el);
2810
3103
  const label = this.activeScenario ? `\u{1F3AC} Step ${this.activeScenario.stepNumber} \xB7 ${selector}` : selector;
2811
3104
  badge.textContent = `${label} (${Math.round(rect.width)} \xD7 ${Math.round(rect.height)} px)`;
3105
+ if (rect.top < 32) {
3106
+ badge.style.top = `${rect.height + 4}px`;
3107
+ } else {
3108
+ badge.style.top = "-26px";
3109
+ }
3110
+ if (rect.left + 260 > window.innerWidth) {
3111
+ badge.style.left = "auto";
3112
+ badge.style.right = "0";
3113
+ } else {
3114
+ badge.style.left = "0";
3115
+ badge.style.right = "auto";
3116
+ }
2812
3117
  }
2813
3118
  hideHighlight() {
2814
3119
  if (this.highlightOverlay) {
@@ -2951,10 +3256,11 @@ var NoteInspector = class {
2951
3256
  btnNextStep.disabled = true;
2952
3257
  if (btnSave) btnSave.disabled = true;
2953
3258
  if (btnFinishFlow) btnFinishFlow.disabled = true;
3259
+ closeModal();
2954
3260
  try {
2955
3261
  await options.onSave(message, action);
2956
- } finally {
2957
- closeModal();
3262
+ } catch {
3263
+ this.showToast("Failed to save note", "\u26A0\uFE0F");
2958
3264
  }
2959
3265
  };
2960
3266
  if (btnSave) {
@@ -2981,8 +3287,12 @@ var NoteInspector = class {
2981
3287
  const selector = noteType === "page" ? "body" : getSemanticSelector(targetEl);
2982
3288
  let screenshotDataUrl;
2983
3289
  try {
2984
- const snap = await this.screenshotDriver.captureElement(targetEl);
2985
- if (snap.ok) {
3290
+ const snapPromise = this.screenshotDriver.captureElement(targetEl);
3291
+ const timeoutPromise = new Promise(
3292
+ (resolve) => setTimeout(() => resolve({ ok: false }), 800)
3293
+ );
3294
+ const snap = await Promise.race([snapPromise, timeoutPromise]);
3295
+ if (snap && snap.ok && snap.dataUrl) {
2986
3296
  screenshotDataUrl = snap.dataUrl;
2987
3297
  }
2988
3298
  } catch {
@@ -3037,9 +3347,16 @@ var NoteInspector = class {
3037
3347
  async saveRegionVisualNote(region, message, scenario) {
3038
3348
  let screenshotDataUrl;
3039
3349
  try {
3040
- const snap = await this.screenshotDriver.captureElement(document.body || document.documentElement);
3041
- if (snap.ok && snap.dataUrl) {
3042
- screenshotDataUrl = await this.cropDataUrl(snap.dataUrl, region);
3350
+ const centerX = region.x + region.width / 2;
3351
+ const centerY = region.y + region.height / 2;
3352
+ const targetElement = document.elementFromPoint(centerX, centerY) || document.body;
3353
+ const snapPromise = this.screenshotDriver.captureElement(targetElement);
3354
+ const timeoutPromise = new Promise(
3355
+ (resolve) => setTimeout(() => resolve({ ok: false }), 800)
3356
+ );
3357
+ const snap = await Promise.race([snapPromise, timeoutPromise]);
3358
+ if (snap && snap.ok && snap.dataUrl) {
3359
+ screenshotDataUrl = snap.dataUrl;
3043
3360
  }
3044
3361
  } catch {
3045
3362
  }
@@ -3136,7 +3453,11 @@ var NoteInspector = class {
3136
3453
  region.width,
3137
3454
  region.height
3138
3455
  );
3139
- resolve(canvas.toDataURL("image/png"));
3456
+ try {
3457
+ resolve(canvas.toDataURL("image/png"));
3458
+ } catch {
3459
+ resolve(dataUrl);
3460
+ }
3140
3461
  };
3141
3462
  img.onerror = () => resolve(dataUrl);
3142
3463
  img.src = dataUrl;