browsertrack 0.2.0 → 0.2.2

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 (67) hide show
  1. package/AGENTS.md +11 -6
  2. package/README.md +2 -0
  3. package/dist/{chunk-WB7ZKWK7.js → chunk-4HRLW6YF.js} +509 -109
  4. package/dist/chunk-4HRLW6YF.js.map +1 -0
  5. package/dist/{chunk-3HOXPTM2.js → chunk-AYSVE6NG.js} +808 -53
  6. package/dist/chunk-AYSVE6NG.js.map +1 -0
  7. package/dist/{chunk-6VA7GBAO.js → chunk-QRZ57ME3.js} +70 -2
  8. package/dist/chunk-QRZ57ME3.js.map +1 -0
  9. package/dist/{chunk-UP5JKCFY.js → chunk-TWEYRBDU.js} +281 -44
  10. package/dist/chunk-TWEYRBDU.js.map +1 -0
  11. package/dist/cli/index.js +1040 -546
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/client/index.cjs +536 -109
  14. package/dist/client/index.d.ts +36 -4
  15. package/dist/client/index.js +8 -4
  16. package/dist/client.iife.js +42 -19
  17. package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
  18. package/dist/core/index.d.ts +26 -3
  19. package/dist/core/index.js +9 -1
  20. package/dist/daemon/index.d.ts +4 -4
  21. package/dist/daemon/index.js +6 -8
  22. package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +14 -7
  25. package/dist/mcp/index.d.ts +4 -4
  26. package/dist/mcp/index.js +7 -4
  27. package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
  28. package/dist/{server-BztYp1Zc.d.ts → server-DjV7RWQM.d.ts} +10 -2
  29. package/docs/cli.md +4 -1
  30. package/docs/component-resolver.md +108 -0
  31. package/docs/getting-started.md +60 -6
  32. package/docs/index.md +1 -0
  33. package/docs/mcp-reference.md +44 -2
  34. package/docs/visual-notes.md +36 -0
  35. package/package.json +1 -1
  36. package/packages/cli/src/index.ts +247 -151
  37. package/packages/client/src/client.ts +18 -1
  38. package/packages/client/src/config.ts +84 -1
  39. package/packages/client/src/index.ts +4 -1
  40. package/packages/client/src/interceptors/interaction.ts +3 -0
  41. package/packages/client/src/interceptors/navigation.ts +38 -26
  42. package/packages/client/src/interceptors/network.ts +22 -17
  43. package/packages/client/src/notes/inspector.ts +186 -51
  44. package/packages/client/src/source/resolver.ts +278 -0
  45. package/packages/client/src/transport/websocket.ts +23 -18
  46. package/packages/core/src/index.ts +1 -0
  47. package/packages/core/src/safety.ts +86 -0
  48. package/packages/core/src/types/events.ts +3 -0
  49. package/packages/core/src/types/notes.ts +11 -0
  50. package/packages/daemon/src/server/daemon.ts +7 -1
  51. package/packages/daemon/src/server/http.ts +125 -5
  52. package/packages/daemon/src/server/ws.ts +33 -29
  53. package/packages/daemon/src/storage/db.ts +57 -35
  54. package/packages/mcp/src/handlers.ts +115 -45
  55. package/packages/mcp/src/server.ts +202 -2
  56. package/test/client/component-resolver.test.ts +141 -0
  57. package/test/client/interceptors.test.ts +56 -0
  58. package/test/core/safety.test.ts +106 -0
  59. package/test/daemon/storage.test.ts +36 -0
  60. package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
  61. package/test/mcp/auto-start.test.ts +87 -0
  62. package/dist/chunk-3HOXPTM2.js.map +0 -1
  63. package/dist/chunk-6VA7GBAO.js.map +0 -1
  64. package/dist/chunk-7OCOQGDN.js +0 -635
  65. package/dist/chunk-7OCOQGDN.js.map +0 -1
  66. package/dist/chunk-UP5JKCFY.js.map +0 -1
  67. package/dist/chunk-WB7ZKWK7.js.map +0 -1
@@ -7,6 +7,7 @@ export * from './breadcrumbs.js';
7
7
  export * from './screenshot/driver.js';
8
8
  export * from './screenshot/browser-script-driver.js';
9
9
  export * from './notes/inspector.js';
10
+ export * from './source/resolver.js';
10
11
 
11
12
  let defaultClient: BrowserTrackClient | null = null;
12
13
 
@@ -40,9 +41,11 @@ if (typeof window !== 'undefined') {
40
41
  const autoInit = currentScript?.getAttribute('data-auto-init') !== 'false';
41
42
  const daemonUrl = currentScript?.getAttribute('data-daemon-url') || undefined;
42
43
  const projectId = currentScript?.getAttribute('data-project-id') || undefined;
44
+ const hidden = currentScript?.getAttribute('data-hidden') === 'true';
45
+ const hideQueryParam = currentScript?.getAttribute('data-hide-query-param') || undefined;
43
46
 
44
47
  if (autoInit) {
45
- init({ daemonUrl, projectId });
48
+ init({ daemonUrl, projectId, hidden, hideQueryParam });
46
49
  }
47
50
  } catch {
48
51
  // If auto-start fails, don't crash
@@ -1,5 +1,6 @@
1
1
  import type { Breadcrumb, ElementSummary } from '../../../core/src/index.js';
2
2
  import { getSemanticSelector, truncate } from '../../../core/src/index.js';
3
+ import { resolveComponentSource } from '../source/resolver.js';
3
4
 
4
5
  export type InteractionCallback = (breadcrumb: Breadcrumb, elementSummary?: ElementSummary) => void;
5
6
 
@@ -12,6 +13,7 @@ export function extractElementSummary(el: HTMLElement | Element): ElementSummary
12
13
  const tag = el.tagName.toLowerCase();
13
14
  const id = el.id || undefined;
14
15
  const classes = Array.from(el.classList || []);
16
+ const componentSource = resolveComponentSource(el as HTMLElement);
15
17
 
16
18
  let boundingRect: ElementSummary['boundingRect'] = undefined;
17
19
  let visible = true;
@@ -53,6 +55,7 @@ export function extractElementSummary(el: HTMLElement | Element): ElementSummary
53
55
  visible,
54
56
  innerText,
55
57
  outerHTML,
58
+ componentSource,
56
59
  };
57
60
  }
58
61
 
@@ -9,14 +9,18 @@ export type NavigationCallback = (event: NavigationEvent) => void;
9
9
  export function setupNavigationInterceptors(onNavigation: NavigationCallback): () => void {
10
10
  if (typeof window === 'undefined' || typeof history === 'undefined') return () => {};
11
11
 
12
- let currentUrl = redactUrl(window.location.href);
13
-
14
- // Initial navigation
15
- onNavigation({
16
- to: currentUrl,
17
- type: 'initial',
18
- timestamp: Date.now(),
19
- });
12
+ let currentUrl = '';
13
+ try {
14
+ currentUrl = redactUrl(window.location.href);
15
+ // Initial navigation
16
+ onNavigation({
17
+ to: currentUrl,
18
+ type: 'initial',
19
+ timestamp: Date.now(),
20
+ });
21
+ } catch {
22
+ // Defensive
23
+ }
20
24
 
21
25
  const originalPushState = history.pushState;
22
26
  const originalReplaceState = history.replaceState;
@@ -58,27 +62,35 @@ export function setupNavigationInterceptors(onNavigation: NavigationCallback): (
58
62
  };
59
63
 
60
64
  const onPopState = () => {
61
- const from = currentUrl;
62
- const to = redactUrl(window.location.href);
63
- currentUrl = to;
64
- onNavigation({
65
- from,
66
- to,
67
- type: 'popstate',
68
- timestamp: Date.now(),
69
- });
65
+ try {
66
+ const from = currentUrl;
67
+ const to = redactUrl(window.location.href);
68
+ currentUrl = to;
69
+ onNavigation({
70
+ from,
71
+ to,
72
+ type: 'popstate',
73
+ timestamp: Date.now(),
74
+ });
75
+ } catch {
76
+ // Defensive
77
+ }
70
78
  };
71
79
 
72
80
  const onHashChange = () => {
73
- const from = currentUrl;
74
- const to = redactUrl(window.location.href);
75
- currentUrl = to;
76
- onNavigation({
77
- from,
78
- to,
79
- type: 'hashchange',
80
- timestamp: Date.now(),
81
- });
81
+ try {
82
+ const from = currentUrl;
83
+ const to = redactUrl(window.location.href);
84
+ currentUrl = to;
85
+ onNavigation({
86
+ from,
87
+ to,
88
+ type: 'hashchange',
89
+ timestamp: Date.now(),
90
+ });
91
+ } catch {
92
+ // Defensive
93
+ }
82
94
  };
83
95
 
84
96
  window.addEventListener('popstate', onPopState);
@@ -37,35 +37,40 @@ export function setupNetworkInterceptors(onNetwork: NetworkCallback): () => void
37
37
 
38
38
  const safeUrl = redactUrl(urlStr);
39
39
 
40
+ let response: Response;
40
41
  try {
41
- const response = await originalFetch.apply(window, [input as any, init]);
42
- const durationMs = Date.now() - startTime;
43
-
44
- onNetwork({
45
- url: safeUrl,
46
- method,
47
- status: response.status,
48
- statusText: response.statusText,
49
- durationMs,
50
- timestamp: startTime,
51
- });
52
-
53
- return response;
42
+ response = await originalFetch.apply(window, [input as any, init]);
54
43
  } catch (err: any) {
55
44
  const durationMs = Date.now() - startTime;
56
45
  const isAbort = err?.name === 'AbortError';
57
46
 
47
+ try {
48
+ onNetwork({
49
+ url: safeUrl,
50
+ method,
51
+ durationMs,
52
+ error: err?.message || 'Network request failed',
53
+ aborted: isAbort,
54
+ timestamp: startTime,
55
+ });
56
+ } catch {}
57
+
58
+ throw err;
59
+ }
60
+
61
+ const durationMs = Date.now() - startTime;
62
+ try {
58
63
  onNetwork({
59
64
  url: safeUrl,
60
65
  method,
66
+ status: response.status,
67
+ statusText: response.statusText,
61
68
  durationMs,
62
- error: err?.message || 'Network request failed',
63
- aborted: isAbort,
64
69
  timestamp: startTime,
65
70
  });
71
+ } catch {}
66
72
 
67
- throw err;
68
- }
73
+ return response;
69
74
  };
70
75
 
71
76
  cleanups.push(() => {
@@ -2,6 +2,8 @@ import type { ElementContext, NoteTarget, RegionContext, VisualNote } from '../.
2
2
  import { getSemanticSelector, truncate } from '../../../core/src/index.js';
3
3
  import type { ScreenshotDriver } from '../screenshot/driver.js';
4
4
  import type { WebSocketTransport } from '../transport/websocket.js';
5
+ import { shouldHideUIFromUrl } from '../config.js';
6
+ import { resolveComponentSource } from '../source/resolver.js';
5
7
 
6
8
  export type NoteInspectMode = 'element' | 'region' | 'page' | 'idle';
7
9
 
@@ -9,6 +11,9 @@ export interface InspectorOptions {
9
11
  shortcut?: string; // e.g. "Alt+Click"
10
12
  maskSelectors?: string[];
11
13
  showToolbar?: boolean;
14
+ showBadges?: boolean;
15
+ hidden?: boolean;
16
+ hideQueryParam?: string | string[];
12
17
  onNoteCreated?: (note: Partial<VisualNote>) => void;
13
18
  }
14
19
 
@@ -28,6 +33,7 @@ export interface ActiveScenarioState {
28
33
  * 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
29
34
  * 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
30
35
  * 7. Floating quick dock / toolbar in bottom-right corner with Notes count and Flow controls
36
+ * 8. Automatic invisibility via query parameter (?bt=0, ?bt=false, ?bt=hidden, ?no_bt, custom query params)
31
37
  */
32
38
  export class NoteInspector {
33
39
  private transport: WebSocketTransport;
@@ -58,15 +64,20 @@ export class NoteInspector {
58
64
 
59
65
  private savedNotes: VisualNote[] = [];
60
66
  private showMarkers = true;
67
+ private isHidden = false;
61
68
  private cleanups: (() => void)[] = [];
62
69
 
63
70
  constructor(transport: WebSocketTransport, screenshotDriver: ScreenshotDriver, options: InspectorOptions = {}) {
64
71
  this.transport = transport;
65
72
  this.screenshotDriver = screenshotDriver;
73
+ const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
74
+ this.isHidden = options.hidden === true || hiddenByQuery;
75
+ this.showMarkers = options.showBadges !== false && !this.isHidden;
66
76
  this.options = {
67
77
  shortcut: 'Alt+Click',
68
78
  maskSelectors: ['input[type="password"]', '[data-sensitive]'],
69
79
  showToolbar: true,
80
+ showBadges: true,
70
81
  ...options,
71
82
  };
72
83
  }
@@ -158,6 +169,44 @@ export class NoteInspector {
158
169
  }
159
170
  }
160
171
 
172
+ public isVisible(): boolean {
173
+ return !this.isHidden;
174
+ }
175
+
176
+ public setVisible(visible: boolean): void {
177
+ this.isHidden = !visible;
178
+ this.showMarkers = this.options.showBadges !== false && !this.isHidden;
179
+
180
+ if (this.container) {
181
+ this.container.style.display = this.isHidden ? 'none' : 'block';
182
+ }
183
+
184
+ if (this.toolbarElement) {
185
+ this.toolbarElement.style.display = this.isHidden ? 'none' : 'flex';
186
+ } else if (!this.isHidden && this.options.showToolbar !== false) {
187
+ this.createToolbar();
188
+ }
189
+
190
+ if (this.isHidden) {
191
+ this.hideHighlight();
192
+ this.hideRegionOverlay();
193
+ if (this.modalOverlay && this.shadowRoot) {
194
+ this.shadowRoot.removeChild(this.modalOverlay);
195
+ this.modalOverlay = null;
196
+ }
197
+ if (this.cardOverlay && this.shadowRoot) {
198
+ this.shadowRoot.removeChild(this.cardOverlay);
199
+ this.cardOverlay = null;
200
+ }
201
+ if (this.markersContainer) {
202
+ this.markersContainer.innerHTML = '';
203
+ }
204
+ } else {
205
+ this.renderMarkers();
206
+ this.updateToolbarCount();
207
+ }
208
+ }
209
+
161
210
  private ensureContainer(): ShadowRoot | null {
162
211
  if (typeof document === 'undefined') return null;
163
212
 
@@ -166,6 +215,9 @@ export class NoteInspector {
166
215
  this.container.id = 'browsertrack-inspector-host';
167
216
  this.container.style.cssText =
168
217
  'all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;';
218
+ if (this.isHidden) {
219
+ this.container.style.display = 'none';
220
+ }
169
221
 
170
222
  this.shadowRoot = this.container.attachShadow({ mode: 'open' });
171
223
 
@@ -611,6 +663,22 @@ export class NoteInspector {
611
663
  color: #cbd5e1;
612
664
  }
613
665
 
666
+ .bt-component-pill {
667
+ display: flex;
668
+ align-items: center;
669
+ gap: 6px;
670
+ background: rgba(15, 23, 42, 0.95);
671
+ border: 1px solid rgba(99, 102, 241, 0.35);
672
+ border-radius: 6px;
673
+ padding: 6px 10px;
674
+ font-size: 11.5px;
675
+ color: #c7d2fe;
676
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
677
+ overflow: hidden;
678
+ text-overflow: ellipsis;
679
+ white-space: nowrap;
680
+ }
681
+
614
682
  .bt-scenario-stepper {
615
683
  display: flex;
616
684
  align-items: center;
@@ -859,7 +927,7 @@ export class NoteInspector {
859
927
  this.shadowRoot.appendChild(this.toastContainer);
860
928
 
861
929
  // Floating Toolbar
862
- if (this.options.showToolbar) {
930
+ if (this.options.showToolbar && !this.isHidden) {
863
931
  this.createToolbar();
864
932
  }
865
933
  }
@@ -978,13 +1046,20 @@ export class NoteInspector {
978
1046
  if (!root || !this.markersContainer) return;
979
1047
 
980
1048
  this.markersContainer.innerHTML = '';
981
- if (!this.showMarkers) return;
1049
+ if (!this.showMarkers || this.isHidden) return;
982
1050
 
983
1051
  const currentPath = window.location.pathname;
984
1052
  // Filter open notes on this route or global
985
- const activeNotes = this.savedNotes.filter(
986
- (n) => n.status === 'OPEN' && (n.route === currentPath || !n.route || n.route === '/' || window.location.href.includes(n.route))
987
- );
1053
+ const activeNotes = this.savedNotes.filter((n) => {
1054
+ if (n.status !== 'OPEN') return false;
1055
+ if (!n.route) return true;
1056
+ if (n.route === currentPath) return true;
1057
+ // Support hash or query routes (e.g. /#modal or /?tab=settings)
1058
+ if (n.route.includes('#') || n.route.includes('?')) {
1059
+ return window.location.href.includes(n.route);
1060
+ }
1061
+ return false;
1062
+ });
988
1063
 
989
1064
  const pageNotes: VisualNote[] = [];
990
1065
 
@@ -1143,6 +1218,32 @@ export class NoteInspector {
1143
1218
 
1144
1219
  ${scenarioStepsHtml}
1145
1220
 
1221
+ ${
1222
+ note.elementContext?.componentSource?.componentName
1223
+ ? `<div class="bt-component-pill" title="${
1224
+ note.elementContext.componentSource.sourceFile
1225
+ ? note.elementContext.componentSource.sourceFile +
1226
+ (note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : '')
1227
+ : note.elementContext.componentSource.componentName
1228
+ }">
1229
+ <span>🧬</span>
1230
+ <span style="font-weight: 600; color: #a5b4fc;">&lt;${note.elementContext.componentSource.componentName}&gt;</span>
1231
+ ${
1232
+ note.elementContext.componentSource.sourceFile
1233
+ ? `<span style="color: #64748b; font-size: 11px; margin-left: 4px;">${note.elementContext.componentSource.sourceFile}${
1234
+ note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : ''
1235
+ }</span>`
1236
+ : ''
1237
+ }
1238
+ ${
1239
+ note.elementContext.componentSource.framework
1240
+ ? `<span class="bt-mode-badge" style="background: rgba(99, 102, 241, 0.15); color: #818cf8; margin-left: auto; font-size: 10px;">${note.elementContext.componentSource.framework.toUpperCase()}</span>`
1241
+ : ''
1242
+ }
1243
+ </div>`
1244
+ : ''
1245
+ }
1246
+
1146
1247
  <div class="bt-target-pill" title="${contextText}">
1147
1248
  <span>🏷️</span>
1148
1249
  <span class="bt-pill-content">${contextText}</span>
@@ -1249,81 +1350,103 @@ export class NoteInspector {
1249
1350
  private setupListeners(): void {
1250
1351
  // 1. Mouse move: hover highlight in element mode or with Alt key
1251
1352
  const onMouseMove = (e: MouseEvent) => {
1252
- if (this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
1253
-
1254
- // Don't highlight elements if mouse is over our own inspector UI
1255
- const path = e.composedPath ? e.composedPath() : [];
1256
- if (this.container && path.includes(this.container)) {
1257
- this.hideHighlight();
1258
- return;
1259
- }
1353
+ try {
1354
+ if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
1260
1355
 
1261
- if (e.altKey || this.activeMode === 'element') {
1262
- const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
1263
- if (target && target !== this.container && !this.container?.contains(target)) {
1264
- this.hoveredElement = target;
1265
- this.updateHighlight(target);
1356
+ // Don't highlight elements if mouse is over our own inspector UI
1357
+ const path = e.composedPath ? e.composedPath() : [];
1358
+ if (this.container && path.includes(this.container)) {
1359
+ this.hideHighlight();
1266
1360
  return;
1267
1361
  }
1268
- }
1269
1362
 
1270
- if (!e.altKey && this.activeMode !== 'element') {
1271
- this.hideHighlight();
1363
+ if (e.altKey || this.activeMode === 'element') {
1364
+ const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
1365
+ if (target && target !== this.container && !this.container?.contains(target)) {
1366
+ this.hoveredElement = target;
1367
+ this.updateHighlight(target);
1368
+ return;
1369
+ }
1370
+ }
1371
+
1372
+ if (!e.altKey && this.activeMode !== 'element') {
1373
+ this.hideHighlight();
1374
+ }
1375
+ } catch {
1376
+ // Defensive: never let inspector hover crash host app
1272
1377
  }
1273
1378
  };
1274
1379
 
1275
1380
  // 2. Click: Alt+Click or Element mode click selects element and opens editor
1276
1381
  const onClick = (e: MouseEvent) => {
1277
- if (this.modalOverlay || this.cardOverlay) return;
1382
+ try {
1383
+ if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
1278
1384
 
1279
- // If clicking inside inspector toolbar or container, do NOT intercept
1280
- const path = e.composedPath ? e.composedPath() : [];
1281
- if (this.container && path.includes(this.container)) {
1282
- return;
1283
- }
1385
+ // If clicking inside inspector toolbar or container, do NOT intercept
1386
+ const path = e.composedPath ? e.composedPath() : [];
1387
+ if (this.container && path.includes(this.container)) {
1388
+ return;
1389
+ }
1284
1390
 
1285
- if (this.activeMode === 'region') return;
1391
+ if (this.activeMode === 'region') return;
1286
1392
 
1287
- if (e.altKey || this.activeMode === 'element') {
1288
- e.preventDefault();
1289
- e.stopPropagation();
1393
+ if (e.altKey || this.activeMode === 'element') {
1394
+ e.preventDefault();
1395
+ e.stopPropagation();
1290
1396
 
1291
- const target = (this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY)) as HTMLElement | null;
1292
- if (target && target !== this.container && !this.container?.contains(target)) {
1293
- this.selectedElement = target;
1294
- this.openNoteEditor(target, 'element');
1295
- if (this.activeMode === 'element' && !this.activeScenario) {
1296
- this.setMode('idle');
1397
+ const target = (this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY)) as HTMLElement | null;
1398
+ if (target && target !== this.container && !this.container?.contains(target)) {
1399
+ this.selectedElement = target;
1400
+ this.openNoteEditor(target, 'element');
1401
+ if (this.activeMode === 'element' && !this.activeScenario) {
1402
+ this.setMode('idle');
1403
+ }
1297
1404
  }
1298
1405
  }
1406
+ } catch {
1407
+ // Defensive
1299
1408
  }
1300
1409
  };
1301
1410
 
1302
1411
  // 3. Key handling: Escape cancels active modes, Alt release hides highlight
1303
1412
  const onKeyDown = (e: KeyboardEvent) => {
1304
- if (e.key === 'Escape') {
1305
- if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
1306
- this.shadowRoot.removeChild(this.cardOverlay);
1307
- this.cardOverlay = null;
1308
- } else if (this.activeMode === 'region' || this.activeMode === 'element') {
1309
- if (!this.activeScenario) {
1310
- this.setMode('idle');
1413
+ try {
1414
+ if (e.key === 'Escape') {
1415
+ if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
1416
+ this.shadowRoot.removeChild(this.cardOverlay);
1417
+ this.cardOverlay = null;
1418
+ } else if (this.activeMode === 'region' || this.activeMode === 'element') {
1419
+ if (!this.activeScenario) {
1420
+ this.setMode('idle');
1421
+ }
1311
1422
  }
1312
1423
  }
1424
+ } catch {
1425
+ // Defensive
1313
1426
  }
1314
1427
  };
1315
1428
 
1316
1429
  const onKeyUp = (e: KeyboardEvent) => {
1317
- if (e.key === 'Alt' && !this.modalOverlay && !this.cardOverlay && this.activeMode !== 'element') {
1318
- this.hideHighlight();
1430
+ try {
1431
+ if (e.key === 'Alt' && !this.modalOverlay && !this.cardOverlay && this.activeMode !== 'element') {
1432
+ this.hideHighlight();
1433
+ }
1434
+ } catch {
1435
+ // Defensive
1319
1436
  }
1320
1437
  };
1321
1438
 
1322
1439
  // 4. Scroll & resize: reposition markers accurately
1323
1440
  const onScrollOrResize = () => {
1324
- requestAnimationFrame(() => {
1325
- this.updateMarkerPositions();
1326
- });
1441
+ try {
1442
+ requestAnimationFrame(() => {
1443
+ try {
1444
+ this.updateMarkerPositions();
1445
+ } catch {}
1446
+ });
1447
+ } catch {
1448
+ // Defensive
1449
+ }
1327
1450
  };
1328
1451
 
1329
1452
  window.addEventListener('mousemove', onMouseMove, { capture: true, passive: true });
@@ -1505,6 +1628,11 @@ export class NoteInspector {
1505
1628
  this.updateHighlight(targetEl);
1506
1629
  }
1507
1630
 
1631
+ const comp = noteType === 'element' ? resolveComponentSource(targetEl) : undefined;
1632
+ const compPrefix = comp?.componentName
1633
+ ? `🧬 <${comp.componentName}>${comp.sourceFile ? ' (' + comp.sourceFile + (comp.sourceLine ? ':' + comp.sourceLine : '') + ')' : ''} · `
1634
+ : '';
1635
+
1508
1636
  this.renderNoteModal({
1509
1637
  title: this.activeScenario
1510
1638
  ? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
@@ -1513,7 +1641,7 @@ export class NoteInspector {
1513
1641
  pillText:
1514
1642
  noteType === 'page'
1515
1643
  ? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
1516
- : `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
1644
+ : `${compPrefix}${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
1517
1645
  onSave: async (message, action) => {
1518
1646
  let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
1519
1647
 
@@ -1863,12 +1991,15 @@ export class NoteInspector {
1863
1991
  innerText = truncate(innerText, 200);
1864
1992
  }
1865
1993
 
1994
+ const componentSource = resolveComponentSource(el);
1995
+
1866
1996
  return {
1867
1997
  selector,
1868
1998
  tag: el.tagName.toLowerCase(),
1869
1999
  attributes,
1870
2000
  outerHTML,
1871
2001
  innerText,
2002
+ componentSource,
1872
2003
  parent: el.parentElement
1873
2004
  ? {
1874
2005
  selector: getSemanticSelector(el.parentElement),
@@ -1903,7 +2034,11 @@ export class NoteInspector {
1903
2034
  region.width,
1904
2035
  region.height
1905
2036
  );
1906
- resolve(canvas.toDataURL('image/png'));
2037
+ try {
2038
+ resolve(canvas.toDataURL('image/png'));
2039
+ } catch {
2040
+ resolve(dataUrl);
2041
+ }
1907
2042
  };
1908
2043
  img.onerror = () => resolve(dataUrl);
1909
2044
  img.src = dataUrl;