browsertrack 0.1.2 → 0.2.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 (43) hide show
  1. package/AGENTS.md +4 -2
  2. package/dist/{chunk-ILRYKMME.js → chunk-3HOXPTM2.js} +97 -3
  3. package/dist/chunk-3HOXPTM2.js.map +1 -0
  4. package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
  5. package/dist/chunk-7OCOQGDN.js.map +1 -0
  6. package/dist/{chunk-G2Y3CXCY.js → chunk-UP5JKCFY.js} +84 -2
  7. package/dist/chunk-UP5JKCFY.js.map +1 -0
  8. package/dist/{chunk-SKCMT2DE.js → chunk-WB7ZKWK7.js} +453 -152
  9. package/dist/chunk-WB7ZKWK7.js.map +1 -0
  10. package/dist/cli/index.js +200 -5
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/client/index.cjs +452 -151
  13. package/dist/client/index.d.ts +28 -9
  14. package/dist/client/index.js +1 -1
  15. package/dist/client.iife.js +185 -29
  16. package/dist/core/index.d.ts +2 -2
  17. package/dist/daemon/index.d.ts +3 -3
  18. package/dist/daemon/index.js +2 -2
  19. package/dist/{engine-CeT9URuN.d.ts → engine-B43IohQY.d.ts} +13 -2
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.js +4 -4
  22. package/dist/mcp/index.d.ts +4 -4
  23. package/dist/mcp/index.js +2 -2
  24. package/dist/{notes-CBvN91Wf.d.ts → notes-BMnonq46.d.ts} +24 -1
  25. package/dist/{projects-CY8ungMt.d.ts → projects-D5J-egVN.d.ts} +1 -1
  26. package/dist/{server-Dd8NX2Mk.d.ts → server-BztYp1Zc.d.ts} +1 -1
  27. package/docs/index.md +2 -1
  28. package/docs/mcp-reference.md +14 -1
  29. package/docs/scenarios-flows.md +86 -0
  30. package/package.json +1 -1
  31. package/packages/client/src/notes/inspector.ts +533 -161
  32. package/packages/core/src/types/notes.ts +25 -0
  33. package/packages/daemon/src/notes/engine.ts +6 -0
  34. package/packages/daemon/src/server/ws.ts +23 -1
  35. package/packages/daemon/src/storage/db.ts +106 -3
  36. package/packages/mcp/src/handlers.ts +58 -0
  37. package/packages/mcp/src/tools.ts +28 -0
  38. package/test/client/interceptors.test.ts +64 -0
  39. package/test/daemon/scenario-storage.test.ts +158 -0
  40. package/dist/chunk-G2Y3CXCY.js.map +0 -1
  41. package/dist/chunk-ILRYKMME.js.map +0 -1
  42. package/dist/chunk-SKCMT2DE.js.map +0 -1
  43. package/dist/chunk-SPCIROIU.js.map +0 -1
@@ -12,15 +12,22 @@ export interface InspectorOptions {
12
12
  onNoteCreated?: (note: Partial<VisualNote>) => void;
13
13
  }
14
14
 
15
+ export interface ActiveScenarioState {
16
+ id: string;
17
+ title: string;
18
+ stepNumber: number;
19
+ }
20
+
15
21
  /**
16
- * Isolated Visual Note Inspector rendering in Shadow DOM.
22
+ * Isolated Visual Note & Multi-Step Scenario Inspector rendering in Shadow DOM.
17
23
  * Supports:
18
24
  * 1. Element selection (hover highlight & click)
19
25
  * 2. Region / Area selection (drag-and-drop rectangle on screen with cancel banner)
20
26
  * 3. Whole page note
21
- * 4. Real-time Saved Note Markers (pins on elements/regions when page loads or syncs)
22
- * 5. Interactive Note Detail Card (view message, target, resolve/delete actions)
23
- * 6. Floating quick dock / toolbar in bottom-right corner with Notes count toggle
27
+ * 4. Multi-Step Flow / Scenario Recording (sequential 'Save & Next Step' continuous capture)
28
+ * 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
29
+ * 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
30
+ * 7. Floating quick dock / toolbar in bottom-right corner with Notes count and Flow controls
24
31
  */
25
32
  export class NoteInspector {
26
33
  private transport: WebSocketTransport;
@@ -37,8 +44,10 @@ export class NoteInspector {
37
44
  private toolbarElement: HTMLDivElement | null = null;
38
45
  private modalOverlay: HTMLDivElement | null = null;
39
46
  private cardOverlay: HTMLDivElement | null = null;
47
+ private toastContainer: HTMLDivElement | null = null;
40
48
 
41
49
  private activeMode: NoteInspectMode = 'idle';
50
+ private activeScenario: ActiveScenarioState | null = null;
42
51
  private hoveredElement: HTMLElement | null = null;
43
52
  private selectedElement: HTMLElement | null = null;
44
53
  private selectedRegion: RegionContext | null = null;
@@ -90,6 +99,46 @@ export class NoteInspector {
90
99
  this.renderMarkers();
91
100
  }
92
101
 
102
+ public showToast(message: string, icon = '✨', durationMs = 2500): void {
103
+ if (typeof document === 'undefined') return;
104
+ const root = this.ensureContainer();
105
+ if (!root || !this.toastContainer) return;
106
+
107
+ const toast = document.createElement('div');
108
+ toast.className = 'bt-toast';
109
+ toast.innerHTML = `<span>${icon}</span> <span>${message}</span>`;
110
+ this.toastContainer.appendChild(toast);
111
+
112
+ setTimeout(() => {
113
+ toast.classList.add('bt-toast-fadeout');
114
+ setTimeout(() => {
115
+ if (toast.parentElement) {
116
+ toast.parentElement.removeChild(toast);
117
+ }
118
+ }, 250);
119
+ }, durationMs);
120
+ }
121
+
122
+ public startScenario(title?: string): void {
123
+ const defaultTitle = title || `Scenario ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
124
+ this.activeScenario = {
125
+ id: `scen_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
126
+ title: defaultTitle,
127
+ stepNumber: 1,
128
+ };
129
+ this.updateToolbarState();
130
+ this.setMode('element');
131
+ this.showToast(`Started flow: "${defaultTitle}"`, '🎬');
132
+ }
133
+
134
+ public finishScenario(): void {
135
+ const title = this.activeScenario?.title;
136
+ this.activeScenario = null;
137
+ this.updateToolbarState();
138
+ this.setMode('idle');
139
+ this.showToast(title ? `Finished flow: "${title}"` : 'Flow recording finished', '✓');
140
+ }
141
+
93
142
  public setMode(mode: NoteInspectMode): void {
94
143
  this.activeMode = mode;
95
144
  this.updateToolbarState();
@@ -276,6 +325,12 @@ export class NoteInspector {
276
325
  z-index: 2147483641;
277
326
  }
278
327
 
328
+ .bt-note-marker-step {
329
+ background: linear-gradient(135deg, #f59e0b, #d97706);
330
+ border-color: #fef3c7;
331
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4), 0 0 0 1px rgba(217, 119, 6, 0.5);
332
+ }
333
+
279
334
  @keyframes bt-pop-in {
280
335
  0% { transform: scale(0.6); opacity: 0; }
281
336
  100% { transform: scale(1); opacity: 1; }
@@ -286,6 +341,10 @@ export class NoteInspector {
286
341
  box-shadow: 0 8px 20px rgba(37,99,235,0.6), 0 0 0 2px #60a5fa;
287
342
  }
288
343
 
344
+ .bt-note-marker-step:hover {
345
+ box-shadow: 0 8px 20px rgba(245, 158, 11, 0.7), 0 0 0 2px #fde68a;
346
+ }
347
+
289
348
  .bt-marker-resolved {
290
349
  background: linear-gradient(135deg, #475569, #334155);
291
350
  border-color: #94a3b8;
@@ -382,6 +441,19 @@ export class NoteInspector {
382
441
  box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
383
442
  }
384
443
 
444
+ .bt-toolbar-btn.active-scenario {
445
+ background: linear-gradient(135deg, #f59e0b, #d97706);
446
+ color: #ffffff;
447
+ font-weight: 700;
448
+ box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4);
449
+ animation: bt-pulse 2s infinite;
450
+ }
451
+
452
+ @keyframes bt-pulse {
453
+ 0%, 100% { opacity: 1; }
454
+ 50% { opacity: 0.85; }
455
+ }
456
+
385
457
  .bt-count-pill {
386
458
  background: rgba(255, 255, 255, 0.2);
387
459
  color: #ffffff;
@@ -429,7 +501,7 @@ export class NoteInspector {
429
501
  border: 1px solid #334155;
430
502
  border-radius: 14px;
431
503
  padding: 20px;
432
- width: 450px;
504
+ width: 470px;
433
505
  max-width: 92vw;
434
506
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
435
507
  display: flex;
@@ -452,6 +524,7 @@ export class NoteInspector {
452
524
  align-items: center;
453
525
  gap: 8px;
454
526
  color: #f8fafc;
527
+ flex-wrap: wrap;
455
528
  }
456
529
 
457
530
  .bt-mode-badge {
@@ -481,6 +554,12 @@ export class NoteInspector {
481
554
  border: 1px solid rgba(168, 85, 247, 0.4);
482
555
  }
483
556
 
557
+ .bt-badge-step {
558
+ background: rgba(245, 158, 11, 0.15);
559
+ color: #fbbf24;
560
+ border: 1px solid rgba(245, 158, 11, 0.4);
561
+ }
562
+
484
563
  .bt-badge-status-open {
485
564
  background: rgba(16, 185, 129, 0.15);
486
565
  color: #34d399;
@@ -532,6 +611,43 @@ export class NoteInspector {
532
611
  color: #cbd5e1;
533
612
  }
534
613
 
614
+ .bt-scenario-stepper {
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: space-between;
618
+ background: #090d16;
619
+ border: 1px solid #1e293b;
620
+ border-radius: 8px;
621
+ padding: 6px 10px;
622
+ font-size: 12px;
623
+ font-weight: 600;
624
+ color: #f59e0b;
625
+ }
626
+
627
+ .bt-step-nav-btn {
628
+ background: #1e293b;
629
+ color: #f8fafc;
630
+ border: 1px solid #334155;
631
+ padding: 4px 10px;
632
+ border-radius: 6px;
633
+ cursor: pointer;
634
+ font-size: 11px;
635
+ font-weight: 600;
636
+ transition: all 0.12s ease;
637
+ font-family: inherit;
638
+ }
639
+
640
+ .bt-step-nav-btn:hover:not(:disabled) {
641
+ background: #2563eb;
642
+ border-color: #3b82f6;
643
+ color: #ffffff;
644
+ }
645
+
646
+ .bt-step-nav-btn:disabled {
647
+ opacity: 0.3;
648
+ cursor: not-allowed;
649
+ }
650
+
535
651
  .bt-note-message-box {
536
652
  background: #090d16;
537
653
  border: 1px solid #1e293b;
@@ -576,8 +692,9 @@ export class NoteInspector {
576
692
  display: flex;
577
693
  justify-content: space-between;
578
694
  align-items: center;
579
- gap: 12px;
695
+ gap: 10px;
580
696
  margin-top: 2px;
697
+ flex-wrap: wrap;
581
698
  }
582
699
 
583
700
  .bt-kbd-hint {
@@ -605,12 +722,13 @@ export class NoteInspector {
605
722
  justify-content: flex-end;
606
723
  align-items: center;
607
724
  gap: 8px;
725
+ flex-wrap: wrap;
608
726
  }
609
727
 
610
728
  .bt-btn {
611
- padding: 8px 16px;
729
+ padding: 7px 14px;
612
730
  border-radius: 7px;
613
- font-size: 12.5px;
731
+ font-size: 12px;
614
732
  font-weight: 600;
615
733
  cursor: pointer;
616
734
  border: 1px solid transparent;
@@ -619,7 +737,7 @@ export class NoteInspector {
619
737
  display: inline-flex;
620
738
  align-items: center;
621
739
  justify-content: center;
622
- gap: 6px;
740
+ gap: 5px;
623
741
  }
624
742
 
625
743
  .bt-btn-cancel {
@@ -641,6 +759,25 @@ export class NoteInspector {
641
759
  box-shadow: 0 4px 10px rgba(37, 99, 235, 0.45);
642
760
  }
643
761
 
762
+ .bt-btn-next-step {
763
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
764
+ color: #ffffff;
765
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
766
+ }
767
+ .bt-btn-next-step:hover {
768
+ background: linear-gradient(135deg, #4f46e5, #4338ca);
769
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.55);
770
+ }
771
+
772
+ .bt-btn-finish-flow {
773
+ background: linear-gradient(135deg, #10b981, #059669);
774
+ color: #ffffff;
775
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.35);
776
+ }
777
+ .bt-btn-finish-flow:hover {
778
+ background: linear-gradient(135deg, #059669, #047857);
779
+ }
780
+
644
781
  .bt-btn-resolve {
645
782
  background: rgba(16, 185, 129, 0.15);
646
783
  color: #6ee7b7;
@@ -660,6 +797,47 @@ export class NoteInspector {
660
797
  background: rgba(239, 68, 68, 0.3);
661
798
  color: #ffffff;
662
799
  }
800
+
801
+ /* 6. Toast Notifications */
802
+ .bt-toast-container {
803
+ position: fixed;
804
+ bottom: 74px;
805
+ right: 24px;
806
+ display: flex;
807
+ flex-direction: column;
808
+ gap: 8px;
809
+ pointer-events: none;
810
+ z-index: 2147483647;
811
+ align-items: flex-end;
812
+ }
813
+
814
+ .bt-toast {
815
+ background: #0f172a;
816
+ color: #f8fafc;
817
+ border: 1px solid #334155;
818
+ border-radius: 30px;
819
+ padding: 8px 16px;
820
+ font-size: 12.5px;
821
+ font-weight: 500;
822
+ display: inline-flex;
823
+ align-items: center;
824
+ gap: 8px;
825
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
826
+ pointer-events: auto;
827
+ animation: bt-toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
828
+ transition: opacity 0.25s ease, transform 0.25s ease;
829
+ user-select: none;
830
+ }
831
+
832
+ .bt-toast-fadeout {
833
+ opacity: 0;
834
+ transform: translateY(6px);
835
+ }
836
+
837
+ @keyframes bt-toast-in {
838
+ from { opacity: 0; transform: translateY(8px) scale(0.95); }
839
+ to { opacity: 1; transform: translateY(0) scale(1); }
840
+ }
663
841
  `;
664
842
 
665
843
  this.shadowRoot.appendChild(style);
@@ -675,6 +853,11 @@ export class NoteInspector {
675
853
  this.markersContainer.className = 'bt-markers-layer';
676
854
  this.shadowRoot.appendChild(this.markersContainer);
677
855
 
856
+ // Toast container layer
857
+ this.toastContainer = document.createElement('div');
858
+ this.toastContainer.className = 'bt-toast-container';
859
+ this.shadowRoot.appendChild(this.toastContainer);
860
+
678
861
  // Floating Toolbar
679
862
  if (this.options.showToolbar) {
680
863
  this.createToolbar();
@@ -707,6 +890,10 @@ export class NoteInspector {
707
890
  <span>📄</span> Page
708
891
  </button>
709
892
  <div class="bt-toolbar-divider"></div>
893
+ <button class="bt-toolbar-btn" id="bt-mode-flow" title="Record multi-step reproduction flow">
894
+ <span>🎬</span> Flow
895
+ </button>
896
+ <div class="bt-toolbar-divider"></div>
710
897
  <button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
711
898
  <span>📌</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
712
899
  </button>
@@ -715,6 +902,7 @@ export class NoteInspector {
715
902
  const btnElement = this.toolbarElement.querySelector('#bt-mode-element') as HTMLButtonElement;
716
903
  const btnRegion = this.toolbarElement.querySelector('#bt-mode-region') as HTMLButtonElement;
717
904
  const btnPage = this.toolbarElement.querySelector('#bt-mode-page') as HTMLButtonElement;
905
+ const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow') as HTMLButtonElement;
718
906
  const btnToggleNotes = this.toolbarElement.querySelector('#bt-toggle-notes') as HTMLButtonElement;
719
907
 
720
908
  btnElement.onclick = (e: MouseEvent) => {
@@ -732,6 +920,15 @@ export class NoteInspector {
732
920
  this.setMode('page');
733
921
  };
734
922
 
923
+ btnFlow.onclick = (e: MouseEvent) => {
924
+ e.stopPropagation();
925
+ if (this.activeScenario) {
926
+ this.finishScenario();
927
+ } else {
928
+ this.startScenario();
929
+ }
930
+ };
931
+
735
932
  btnToggleNotes.onclick = (e: MouseEvent) => {
736
933
  e.stopPropagation();
737
934
  this.showMarkers = !this.showMarkers;
@@ -748,10 +945,23 @@ export class NoteInspector {
748
945
  const btnElement = this.toolbarElement.querySelector('#bt-mode-element');
749
946
  const btnRegion = this.toolbarElement.querySelector('#bt-mode-region');
750
947
  const btnPage = this.toolbarElement.querySelector('#bt-mode-page');
948
+ const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow');
751
949
 
752
950
  btnElement?.classList.toggle('active', this.activeMode === 'element');
753
951
  btnRegion?.classList.toggle('active', this.activeMode === 'region');
754
952
  btnPage?.classList.toggle('active', this.activeMode === 'page');
953
+
954
+ if (btnFlow) {
955
+ if (this.activeScenario) {
956
+ btnFlow.className = 'bt-toolbar-btn active-scenario';
957
+ btnFlow.innerHTML = `<span>🎬</span> Step ${this.activeScenario.stepNumber} (Finish)`;
958
+ btnFlow.setAttribute('title', `Click to finish recording "${this.activeScenario.title}"`);
959
+ } else {
960
+ btnFlow.className = 'bt-toolbar-btn';
961
+ btnFlow.innerHTML = `<span>🎬</span> Flow`;
962
+ btnFlow.setAttribute('title', 'Record multi-step reproduction flow');
963
+ }
964
+ }
755
965
  }
756
966
 
757
967
  private updateToolbarCount(): void {
@@ -784,6 +994,10 @@ export class NoteInspector {
784
994
  return;
785
995
  }
786
996
 
997
+ const isStep = !!note.scenarioId && note.stepNumber != null;
998
+ const stepLabel = isStep ? `🎬 Step ${note.stepNumber}` : `#${index + 1}`;
999
+ const markerClass = `bt-note-marker ${isStep ? 'bt-note-marker-step' : ''}`;
1000
+
787
1001
  if (note.type === 'region' && note.region) {
788
1002
  // Region box
789
1003
  const regBox = document.createElement('div');
@@ -796,11 +1010,11 @@ export class NoteInspector {
796
1010
 
797
1011
  // Pin on top-left of region
798
1012
  const pin = document.createElement('div');
799
- pin.className = 'bt-note-marker';
1013
+ pin.className = markerClass;
800
1014
  pin.style.left = `${Math.max(4, note.region.x - 12)}px`;
801
1015
  pin.style.top = `${Math.max(4, note.region.y - 12)}px`;
802
- pin.title = note.message;
803
- pin.innerHTML = `<span>📐</span> <span>#${index + 1}</span>`;
1016
+ pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
1017
+ pin.innerHTML = `<span>${isStep ? '🎬' : '📐'}</span> <span>${stepLabel}</span>`;
804
1018
  pin.onclick = (e) => {
805
1019
  e.stopPropagation();
806
1020
  this.openNoteCard(note);
@@ -820,12 +1034,12 @@ export class NoteInspector {
820
1034
  const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
821
1035
  if (rect) {
822
1036
  const pin = document.createElement('div');
823
- pin.className = 'bt-note-marker';
1037
+ pin.className = markerClass;
824
1038
  pin.setAttribute('data-note-id', note.id);
825
- pin.title = note.message;
1039
+ pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
826
1040
  pin.style.left = `${Math.max(4, rect.left - 10)}px`;
827
1041
  pin.style.top = `${Math.max(4, rect.top - 12)}px`;
828
- pin.innerHTML = `<span>📝</span> <span>#${index + 1}</span>`;
1042
+ pin.innerHTML = `<span>${isStep ? '🎬' : '📝'}</span> <span>${stepLabel}</span>`;
829
1043
 
830
1044
  pin.onclick = (e) => {
831
1045
  e.stopPropagation();
@@ -845,10 +1059,11 @@ export class NoteInspector {
845
1059
  if (pageNotes.length > 0) {
846
1060
  const pageDock = document.createElement('div');
847
1061
  pageDock.className = 'bt-page-notes-dock';
848
- pageNotes.forEach((pNote, idx) => {
1062
+ pageNotes.forEach((pNote) => {
849
1063
  const pill = document.createElement('div');
850
1064
  pill.className = 'bt-page-note-pill';
851
- pill.innerHTML = `<span>📄</span> <span>Page Note (${truncate(pNote.message, 25)})</span>`;
1065
+ const label = pNote.scenarioId && pNote.stepNumber ? `Step ${pNote.stepNumber}: ` : '';
1066
+ pill.innerHTML = `<span>📄</span> <span>${label}${truncate(pNote.message, 25)}</span>`;
852
1067
  pill.onclick = (e) => {
853
1068
  e.stopPropagation();
854
1069
  this.openNoteCard(pNote);
@@ -889,17 +1104,45 @@ export class NoteInspector {
889
1104
 
890
1105
  const formattedDate = new Date(note.createdAt).toLocaleString();
891
1106
 
1107
+ // Scenario flow steps navigation
1108
+ let scenarioStepsHtml = '';
1109
+ let scenarioSteps: VisualNote[] = [];
1110
+ if (note.scenarioId) {
1111
+ scenarioSteps = this.savedNotes
1112
+ .filter((n) => n.scenarioId === note.scenarioId)
1113
+ .sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
1114
+
1115
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1116
+ const prevStep = currentIndex > 0 ? scenarioSteps[currentIndex - 1] : null;
1117
+ const nextStep = currentIndex >= 0 && currentIndex < scenarioSteps.length - 1 ? scenarioSteps[currentIndex + 1] : null;
1118
+
1119
+ scenarioStepsHtml = `
1120
+ <div class="bt-scenario-stepper">
1121
+ <button class="bt-step-nav-btn" id="btn-prev-step" ${!prevStep ? 'disabled' : ''}>
1122
+ ◀ Step ${prevStep ? prevStep.stepNumber : ''}
1123
+ </button>
1124
+ <span>🎬 Step ${note.stepNumber || 1} of ${scenarioSteps.length}</span>
1125
+ <button class="bt-step-nav-btn" id="btn-next-step-card" ${!nextStep ? 'disabled' : ''}>
1126
+ Step ${nextStep ? nextStep.stepNumber : ''} ▶
1127
+ </button>
1128
+ </div>
1129
+ `;
1130
+ }
1131
+
892
1132
  backdrop.innerHTML = `
893
1133
  <div class="bt-modal">
894
1134
  <div class="bt-modal-header">
895
1135
  <div class="bt-modal-title">
896
- <span>${icon} Visual Note Details</span>
1136
+ <span>${note.scenarioId ? '🎬 ' + (note.scenarioTitle || 'Scenario Flow') : icon + ' Visual Note Details'}</span>
897
1137
  <span class="bt-mode-badge ${badgeClass}">${note.type.toUpperCase()}</span>
1138
+ ${note.scenarioId && note.stepNumber ? `<span class="bt-mode-badge bt-badge-step">STEP ${note.stepNumber}</span>` : ''}
898
1139
  <span class="bt-mode-badge ${statusClass}">${note.status}</span>
899
1140
  </div>
900
1141
  <button class="bt-close-btn" id="btn-card-close" title="Close (Esc)">✕</button>
901
1142
  </div>
902
1143
 
1144
+ ${scenarioStepsHtml}
1145
+
903
1146
  <div class="bt-target-pill" title="${contextText}">
904
1147
  <span>🏷️</span>
905
1148
  <span class="bt-pill-content">${contextText}</span>
@@ -913,9 +1156,18 @@ export class NoteInspector {
913
1156
  </div>
914
1157
 
915
1158
  <div class="bt-modal-footer">
916
- <button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
917
- <span>🗑️</span> Delete
918
- </button>
1159
+ <div style="display: flex; gap: 6px;">
1160
+ <button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
1161
+ <span>🗑️</span> Delete
1162
+ </button>
1163
+ ${
1164
+ note.scenarioId
1165
+ ? `<button class="bt-btn bt-btn-delete" id="btn-card-delete-flow" title="Delete entire scenario flow">
1166
+ <span>🗑️</span> Delete Flow
1167
+ </button>`
1168
+ : ''
1169
+ }
1170
+ </div>
919
1171
  <div class="bt-modal-actions">
920
1172
  <button class="bt-btn bt-btn-cancel" id="btn-card-dismiss">Close</button>
921
1173
  <button class="bt-btn ${note.status === 'OPEN' ? 'bt-btn-resolve' : 'bt-btn-save'}" id="btn-card-resolve">
@@ -930,6 +1182,9 @@ export class NoteInspector {
930
1182
  const btnDismiss = backdrop.querySelector('#btn-card-dismiss') as HTMLButtonElement;
931
1183
  const btnResolve = backdrop.querySelector('#btn-card-resolve') as HTMLButtonElement;
932
1184
  const btnDelete = backdrop.querySelector('#btn-card-delete') as HTMLButtonElement;
1185
+ const btnDeleteFlow = backdrop.querySelector('#btn-card-delete-flow') as HTMLButtonElement | null;
1186
+ const btnPrevStep = backdrop.querySelector('#btn-prev-step') as HTMLButtonElement | null;
1187
+ const btnNextStepCard = backdrop.querySelector('#btn-next-step-card') as HTMLButtonElement | null;
933
1188
 
934
1189
  const closeCard = () => {
935
1190
  if (this.cardOverlay) {
@@ -945,20 +1200,49 @@ export class NoteInspector {
945
1200
  if (e.target === backdrop) closeCard();
946
1201
  };
947
1202
 
1203
+ if (btnPrevStep && scenarioSteps.length > 0) {
1204
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1205
+ if (currentIndex > 0) {
1206
+ btnPrevStep.onclick = () => {
1207
+ this.openNoteCard(scenarioSteps[currentIndex - 1]);
1208
+ };
1209
+ }
1210
+ }
1211
+
1212
+ if (btnNextStepCard && scenarioSteps.length > 0) {
1213
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1214
+ if (currentIndex >= 0 && currentIndex < scenarioSteps.length - 1) {
1215
+ btnNextStepCard.onclick = () => {
1216
+ this.openNoteCard(scenarioSteps[currentIndex + 1]);
1217
+ };
1218
+ }
1219
+ }
1220
+
948
1221
  btnResolve.onclick = () => {
949
1222
  if (note.status === 'OPEN') {
950
1223
  this.transport.send({ type: 'resolve_note', noteId: note.id });
1224
+ this.showToast('Note marked as resolved', '✅');
951
1225
  } else {
952
1226
  this.transport.send({ type: 'reopen_note', noteId: note.id });
1227
+ this.showToast('Note reopened', '↺');
953
1228
  }
954
1229
  closeCard();
955
1230
  };
956
1231
 
957
1232
  btnDelete.onclick = () => {
958
1233
  this.transport.send({ type: 'delete_note', noteId: note.id });
1234
+ this.showToast('Note deleted', '🗑️');
959
1235
  closeCard();
960
1236
  };
961
1237
 
1238
+ if (btnDeleteFlow && note.scenarioId) {
1239
+ btnDeleteFlow.onclick = () => {
1240
+ this.transport.send({ type: 'delete_scenario', scenarioId: note.scenarioId });
1241
+ this.showToast('Scenario flow deleted', '🗑️');
1242
+ closeCard();
1243
+ };
1244
+ }
1245
+
962
1246
  root.appendChild(backdrop);
963
1247
  }
964
1248
 
@@ -1008,7 +1292,7 @@ export class NoteInspector {
1008
1292
  if (target && target !== this.container && !this.container?.contains(target)) {
1009
1293
  this.selectedElement = target;
1010
1294
  this.openNoteEditor(target, 'element');
1011
- if (this.activeMode === 'element') {
1295
+ if (this.activeMode === 'element' && !this.activeScenario) {
1012
1296
  this.setMode('idle');
1013
1297
  }
1014
1298
  }
@@ -1022,7 +1306,9 @@ export class NoteInspector {
1022
1306
  this.shadowRoot.removeChild(this.cardOverlay);
1023
1307
  this.cardOverlay = null;
1024
1308
  } else if (this.activeMode === 'region' || this.activeMode === 'element') {
1025
- this.setMode('idle');
1309
+ if (!this.activeScenario) {
1310
+ this.setMode('idle');
1311
+ }
1026
1312
  }
1027
1313
  }
1028
1314
  };
@@ -1153,31 +1439,30 @@ export class NoteInspector {
1153
1439
  width: Math.round(width),
1154
1440
  height: Math.round(height),
1155
1441
  };
1156
- this.hideRegionOverlay();
1157
- this.setMode('idle');
1158
1442
  this.openRegionNoteEditor(this.selectedRegion);
1159
- } else {
1160
- this.hideRegionOverlay();
1443
+ }
1444
+
1445
+ if (this.regionBox) {
1446
+ this.regionBox.style.display = 'none';
1447
+ }
1448
+ if (!this.activeScenario) {
1161
1449
  this.setMode('idle');
1162
1450
  }
1163
1451
  };
1164
- }
1165
1452
 
1166
- // Always ensure regionOverlay is mounted before toolbar so toolbar is interactable
1167
- if (this.toolbarElement && this.toolbarElement.parentNode === root) {
1168
- root.insertBefore(this.regionOverlay, this.toolbarElement);
1169
- } else {
1170
1453
  root.appendChild(this.regionOverlay);
1454
+ } else {
1455
+ this.regionOverlay.style.display = 'block';
1171
1456
  }
1172
1457
  }
1173
1458
 
1174
1459
  private hideRegionOverlay(): void {
1175
- this.isDraggingRegion = false;
1176
- if (this.regionOverlay && this.regionOverlay.parentNode) {
1177
- this.regionOverlay.parentNode.removeChild(this.regionOverlay);
1178
- }
1179
- if (this.regionBox) {
1180
- this.regionBox.style.display = 'none';
1460
+ if (this.regionOverlay) {
1461
+ this.regionOverlay.style.display = 'none';
1462
+ this.isDraggingRegion = false;
1463
+ if (this.regionBox) {
1464
+ this.regionBox.style.display = 'none';
1465
+ }
1181
1466
  }
1182
1467
  }
1183
1468
 
@@ -1187,21 +1472,21 @@ export class NoteInspector {
1187
1472
 
1188
1473
  const rect = el.getBoundingClientRect();
1189
1474
  this.highlightOverlay.style.display = 'block';
1190
- this.highlightOverlay.style.top = `${rect.top}px`;
1191
1475
  this.highlightOverlay.style.left = `${rect.left}px`;
1476
+ this.highlightOverlay.style.top = `${rect.top}px`;
1192
1477
  this.highlightOverlay.style.width = `${rect.width}px`;
1193
1478
  this.highlightOverlay.style.height = `${rect.height}px`;
1194
1479
 
1195
- const selector = getSemanticSelector(el);
1196
- const badgeText = `${selector} · ${Math.round(rect.width)}×${Math.round(rect.height)}`;
1197
-
1198
1480
  let badge = this.highlightOverlay.querySelector('.bt-badge') as HTMLDivElement | null;
1199
1481
  if (!badge) {
1200
1482
  badge = document.createElement('div');
1201
1483
  badge.className = 'bt-badge';
1202
1484
  this.highlightOverlay.appendChild(badge);
1203
1485
  }
1204
- badge.textContent = badgeText;
1486
+
1487
+ const selector = getSemanticSelector(el);
1488
+ const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
1489
+ badge.textContent = `${label} (${Math.round(rect.width)} × ${Math.round(rect.height)} px)`;
1205
1490
  }
1206
1491
 
1207
1492
  private hideHighlight(): void {
@@ -1221,25 +1506,73 @@ export class NoteInspector {
1221
1506
  }
1222
1507
 
1223
1508
  this.renderNoteModal({
1224
- title: 'Add Visual Note',
1225
- modeBadge: noteType.toUpperCase(),
1509
+ title: this.activeScenario
1510
+ ? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
1511
+ : 'Add Visual Note',
1512
+ modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : noteType.toUpperCase(),
1226
1513
  pillText:
1227
1514
  noteType === 'page'
1228
1515
  ? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
1229
1516
  : `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
1230
- onSave: async (message) => {
1231
- await this.saveVisualNote(targetEl, message, noteType);
1517
+ onSave: async (message, action) => {
1518
+ let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
1519
+
1520
+ if (action === 'next_step' && !this.activeScenario) {
1521
+ this.startScenario();
1522
+ }
1523
+
1524
+ if (this.activeScenario) {
1525
+ scenarioParam = {
1526
+ scenarioId: this.activeScenario.id,
1527
+ stepNumber: this.activeScenario.stepNumber,
1528
+ scenarioTitle: this.activeScenario.title,
1529
+ };
1530
+ }
1531
+
1532
+ await this.saveVisualNote(targetEl, message, noteType, scenarioParam);
1533
+
1534
+ if (action === 'next_step' && this.activeScenario) {
1535
+ this.activeScenario.stepNumber++;
1536
+ this.updateToolbarState();
1537
+ this.setMode('element');
1538
+ } else if (action === 'finish_flow' && this.activeScenario) {
1539
+ this.finishScenario();
1540
+ }
1232
1541
  },
1233
1542
  });
1234
1543
  }
1235
1544
 
1236
1545
  public openRegionNoteEditor(region: RegionContext): void {
1237
1546
  this.renderNoteModal({
1238
- title: 'Add Region Note',
1239
- modeBadge: 'REGION',
1547
+ title: this.activeScenario
1548
+ ? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
1549
+ : 'Add Region Note',
1550
+ modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : 'REGION',
1240
1551
  pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} × ${region.height} px)`,
1241
- onSave: async (message) => {
1242
- await this.saveRegionVisualNote(region, message);
1552
+ onSave: async (message, action) => {
1553
+ let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
1554
+
1555
+ if (action === 'next_step' && !this.activeScenario) {
1556
+ this.startScenario();
1557
+ }
1558
+
1559
+ if (this.activeScenario) {
1560
+ scenarioParam = {
1561
+ scenarioId: this.activeScenario.id,
1562
+ stepNumber: this.activeScenario.stepNumber,
1563
+ scenarioTitle: this.activeScenario.title,
1564
+ };
1565
+ }
1566
+
1567
+ await this.saveRegionVisualNote(region, message, scenarioParam);
1568
+
1569
+ if (action === 'next_step' && this.activeScenario) {
1570
+ this.activeScenario.stepNumber++;
1571
+ this.updateToolbarState();
1572
+ this.setMode('element');
1573
+ } else if (action === 'finish_flow' && this.activeScenario) {
1574
+ this.finishScenario();
1575
+ }
1243
1576
  },
1244
1577
  });
1245
1578
  }
@@ -1248,7 +1581,7 @@ export class NoteInspector {
1248
1581
  title: string;
1249
1582
  modeBadge: string;
1250
1583
  pillText: string;
1251
- onSave: (message: string) => Promise<void>;
1584
+ onSave: (message: string, action: 'save' | 'next_step' | 'finish_flow') => Promise<void>;
1252
1585
  }): void {
1253
1586
  const root = this.ensureContainer();
1254
1587
  if (!root) return;
@@ -1263,15 +1596,16 @@ export class NoteInspector {
1263
1596
  backdrop.className = 'bt-modal-backdrop';
1264
1597
  this.modalOverlay = backdrop;
1265
1598
 
1266
- const badgeClass = `bt-badge-${options.modeBadge.toLowerCase()}`;
1267
- const icon = options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
1599
+ const isStep = options.modeBadge.startsWith('STEP');
1600
+ const badgeClass = isStep ? 'bt-badge-step' : `bt-badge-${options.modeBadge.toLowerCase()}`;
1601
+ const icon = isStep ? '🎬' : options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
1268
1602
  const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
1269
1603
 
1270
1604
  backdrop.innerHTML = `
1271
1605
  <div class="bt-modal">
1272
1606
  <div class="bt-modal-header">
1273
1607
  <div class="bt-modal-title">
1274
- <span>📝 ${options.title}</span>
1608
+ <span>${icon} ${options.title}</span>
1275
1609
  <span class="bt-mode-badge ${badgeClass}">${options.modeBadge}</span>
1276
1610
  </div>
1277
1611
  <button class="bt-close-btn" id="btn-close" title="Close (Esc)">✕</button>
@@ -1280,14 +1614,23 @@ export class NoteInspector {
1280
1614
  <span>${icon}</span>
1281
1615
  <span class="bt-pill-content">${options.pillText}</span>
1282
1616
  </div>
1283
- <textarea class="bt-textarea" placeholder="Describe the layout issue, styling bug, or note for AI agent..." autofocus></textarea>
1617
+ <textarea class="bt-textarea" placeholder="Describe the layout issue, user action, or note for AI agent..." autofocus></textarea>
1284
1618
  <div class="bt-modal-footer">
1285
1619
  <div class="bt-kbd-hint">
1286
1620
  <kbd>${isMac ? '⌘' : 'Ctrl'}+Enter</kbd> save · <kbd>Esc</kbd> cancel
1287
1621
  </div>
1288
1622
  <div class="bt-modal-actions">
1289
1623
  <button class="bt-btn bt-btn-cancel" id="btn-cancel">Cancel</button>
1290
- <button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>
1624
+ <button class="bt-btn bt-btn-next-step" id="btn-next-step" title="Save this step and immediately select the next element">
1625
+ <span>➡️</span> ${this.activeScenario ? 'Save & Next Step' : 'Save as Step 1 (Flow)'}
1626
+ </button>
1627
+ ${
1628
+ this.activeScenario
1629
+ ? `<button class="bt-btn bt-btn-finish-flow" id="btn-finish-flow" title="Save final step and complete scenario">
1630
+ <span>✓</span> Save & Finish Flow
1631
+ </button>`
1632
+ : `<button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>`
1633
+ }
1291
1634
  </div>
1292
1635
  </div>
1293
1636
  </div>
@@ -1296,7 +1639,9 @@ export class NoteInspector {
1296
1639
  const textarea = backdrop.querySelector('textarea') as HTMLTextAreaElement;
1297
1640
  const btnClose = backdrop.querySelector('#btn-close') as HTMLButtonElement;
1298
1641
  const btnCancel = backdrop.querySelector('#btn-cancel') as HTMLButtonElement;
1299
- const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement;
1642
+ const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement | null;
1643
+ const btnNextStep = backdrop.querySelector('#btn-next-step') as HTMLButtonElement;
1644
+ const btnFinishFlow = backdrop.querySelector('#btn-finish-flow') as HTMLButtonElement | null;
1300
1645
 
1301
1646
  const closeModal = () => {
1302
1647
  if (this.modalOverlay) {
@@ -1316,26 +1661,33 @@ export class NoteInspector {
1316
1661
  }
1317
1662
  };
1318
1663
 
1319
- const submitNote = async () => {
1664
+ const handleAction = async (action: 'save' | 'next_step' | 'finish_flow') => {
1320
1665
  const message = textarea.value.trim();
1321
1666
  if (!message) return;
1322
1667
 
1323
- btnSave.textContent = 'Saving...';
1324
- btnSave.disabled = true;
1668
+ btnNextStep.disabled = true;
1669
+ if (btnSave) btnSave.disabled = true;
1670
+ if (btnFinishFlow) btnFinishFlow.disabled = true;
1325
1671
 
1326
1672
  try {
1327
- await options.onSave(message);
1673
+ await options.onSave(message, action);
1328
1674
  } finally {
1329
1675
  closeModal();
1330
1676
  }
1331
1677
  };
1332
1678
 
1333
- btnSave.onclick = submitNote;
1679
+ if (btnSave) {
1680
+ btnSave.onclick = () => handleAction('save');
1681
+ }
1682
+ btnNextStep.onclick = () => handleAction('next_step');
1683
+ if (btnFinishFlow) {
1684
+ btnFinishFlow.onclick = () => handleAction('finish_flow');
1685
+ }
1334
1686
 
1335
1687
  textarea.onkeydown = (e: KeyboardEvent) => {
1336
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey || !e.shiftKey)) {
1688
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1337
1689
  e.preventDefault();
1338
- submitNote();
1690
+ handleAction(this.activeScenario ? 'next_step' : 'save');
1339
1691
  }
1340
1692
  if (e.key === 'Escape') {
1341
1693
  closeModal();
@@ -1346,7 +1698,12 @@ export class NoteInspector {
1346
1698
  setTimeout(() => textarea?.focus(), 50);
1347
1699
  }
1348
1700
 
1349
- public async saveVisualNote(targetEl: HTMLElement, message: string, noteType: 'element' | 'page' = 'element'): Promise<void> {
1701
+ public async saveVisualNote(
1702
+ targetEl: HTMLElement,
1703
+ message: string,
1704
+ noteType: 'element' | 'page' = 'element',
1705
+ scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
1706
+ ): Promise<void> {
1350
1707
  const rect = targetEl.getBoundingClientRect();
1351
1708
  const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
1352
1709
 
@@ -1381,25 +1738,30 @@ export class NoteInspector {
1381
1738
  confidence: selector.startsWith('[data-test') ? 'high' : selector.startsWith('#') ? 'medium' : 'low',
1382
1739
  };
1383
1740
 
1741
+ const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
1742
+ const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
1743
+ const viewport = typeof window !== 'undefined'
1744
+ ? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
1745
+ : { width: 1280, height: 800, devicePixelRatio: 1 };
1746
+ const scroll = typeof window !== 'undefined'
1747
+ ? { scrollX: window.scrollX, scrollY: window.scrollY }
1748
+ : { scrollX: 0, scrollY: 0 };
1749
+
1384
1750
  const notePayload = {
1385
1751
  type: 'create_note',
1386
1752
  sessionId: this.transport.getSessionId() || '',
1387
1753
  noteType,
1388
1754
  message,
1389
- route: window.location.pathname + window.location.search,
1390
- url: window.location.href,
1391
- viewport: {
1392
- width: window.innerWidth,
1393
- height: window.innerHeight,
1394
- devicePixelRatio: window.devicePixelRatio || 1,
1395
- },
1396
- scroll: {
1397
- scrollX: window.scrollX,
1398
- scrollY: window.scrollY,
1399
- },
1755
+ route,
1756
+ url,
1757
+ viewport,
1758
+ scroll,
1400
1759
  target,
1401
1760
  elementContext,
1402
1761
  screenshot: screenshotDataUrl,
1762
+ scenarioId: scenario?.scenarioId,
1763
+ stepNumber: scenario?.stepNumber,
1764
+ scenarioTitle: scenario?.scenarioTitle,
1403
1765
  timestamp: Date.now(),
1404
1766
  };
1405
1767
 
@@ -1407,9 +1769,19 @@ export class NoteInspector {
1407
1769
  if (this.options.onNoteCreated) {
1408
1770
  this.options.onNoteCreated(notePayload as any);
1409
1771
  }
1772
+
1773
+ if (scenario?.scenarioId) {
1774
+ this.showToast(`Step ${scenario.stepNumber || 1} recorded`, '🎬');
1775
+ } else {
1776
+ this.showToast('Visual note saved', '✨');
1777
+ }
1410
1778
  }
1411
1779
 
1412
- public async saveRegionVisualNote(region: RegionContext, message: string): Promise<void> {
1780
+ public async saveRegionVisualNote(
1781
+ region: RegionContext,
1782
+ message: string,
1783
+ scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
1784
+ ): Promise<void> {
1413
1785
  // 1. Capture full page screenshot and crop to region
1414
1786
  let screenshotDataUrl: string | undefined;
1415
1787
  try {
@@ -1421,24 +1793,29 @@ export class NoteInspector {
1421
1793
  // Defensive
1422
1794
  }
1423
1795
 
1796
+ const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
1797
+ const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
1798
+ const viewport = typeof window !== 'undefined'
1799
+ ? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
1800
+ : { width: 1280, height: 800, devicePixelRatio: 1 };
1801
+ const scroll = typeof window !== 'undefined'
1802
+ ? { scrollX: window.scrollX, scrollY: window.scrollY }
1803
+ : { scrollX: 0, scrollY: 0 };
1804
+
1424
1805
  const notePayload = {
1425
1806
  type: 'create_note',
1426
1807
  sessionId: this.transport.getSessionId() || '',
1427
1808
  noteType: 'region',
1428
1809
  message,
1429
- route: window.location.pathname + window.location.search,
1430
- url: window.location.href,
1431
- viewport: {
1432
- width: window.innerWidth,
1433
- height: window.innerHeight,
1434
- devicePixelRatio: window.devicePixelRatio || 1,
1435
- },
1436
- scroll: {
1437
- scrollX: window.scrollX,
1438
- scrollY: window.scrollY,
1439
- },
1810
+ route,
1811
+ url,
1812
+ viewport,
1813
+ scroll,
1440
1814
  region,
1441
1815
  screenshot: screenshotDataUrl,
1816
+ scenarioId: scenario?.scenarioId,
1817
+ stepNumber: scenario?.stepNumber,
1818
+ scenarioTitle: scenario?.scenarioTitle,
1442
1819
  timestamp: Date.now(),
1443
1820
  };
1444
1821
 
@@ -1446,108 +1823,103 @@ export class NoteInspector {
1446
1823
  if (this.options.onNoteCreated) {
1447
1824
  this.options.onNoteCreated(notePayload as any);
1448
1825
  }
1449
- }
1450
1826
 
1451
- private async cropDataUrl(dataUrl: string, region: RegionContext): Promise<string> {
1452
- return new Promise((resolve) => {
1453
- const img = new Image();
1454
- img.onload = () => {
1455
- try {
1456
- const canvas = document.createElement('canvas');
1457
- canvas.width = region.width;
1458
- canvas.height = region.height;
1459
- const ctx = canvas.getContext('2d');
1460
- if (!ctx) {
1461
- resolve(dataUrl);
1462
- return;
1463
- }
1464
-
1465
- const dpr = window.devicePixelRatio || 1;
1466
- ctx.drawImage(
1467
- img,
1468
- region.x * dpr,
1469
- region.y * dpr,
1470
- region.width * dpr,
1471
- region.height * dpr,
1472
- 0,
1473
- 0,
1474
- region.width,
1475
- region.height
1476
- );
1477
- resolve(canvas.toDataURL('image/webp', 0.9));
1478
- } catch {
1479
- resolve(dataUrl);
1480
- }
1481
- };
1482
- img.onerror = () => resolve(dataUrl);
1483
- img.src = dataUrl;
1484
- });
1827
+ if (scenario?.scenarioId) {
1828
+ this.showToast(`Step ${scenario.stepNumber || 1} region recorded`, '🎬');
1829
+ } else {
1830
+ this.showToast('Region note saved', '📐');
1831
+ }
1485
1832
  }
1486
1833
 
1487
1834
  private extractElementContext(el: HTMLElement): ElementContext {
1488
1835
  const selector = getSemanticSelector(el);
1489
- const tag = el.tagName.toLowerCase();
1490
-
1491
- // Attributes (sanitize passwords/tokens)
1492
1836
  const attributes: Record<string, string> = {};
1837
+
1493
1838
  for (let i = 0; i < el.attributes.length; i++) {
1494
1839
  const attr = el.attributes[i];
1495
- if (attr.name === 'value' && (el as HTMLInputElement).type === 'password') {
1840
+ if (
1841
+ this.options.maskSelectors?.some((mask) => {
1842
+ try {
1843
+ return el.matches(mask);
1844
+ } catch {
1845
+ return false;
1846
+ }
1847
+ }) &&
1848
+ (attr.name === 'value' || attr.name === 'data-secret')
1849
+ ) {
1496
1850
  attributes[attr.name] = '[REDACTED]';
1497
1851
  } else {
1498
1852
  attributes[attr.name] = attr.value;
1499
1853
  }
1500
1854
  }
1501
1855
 
1502
- // Clone element to sanitize inner sensitive fields
1503
- let outerHTML = '';
1504
- try {
1505
- const clone = el.cloneNode(true) as HTMLElement;
1506
- for (const passInput of Array.from(clone.querySelectorAll('input[type="password"]'))) {
1507
- passInput.setAttribute('value', '[REDACTED]');
1508
- }
1509
- outerHTML = truncate(clone.outerHTML, 10240); // 10 KB max
1510
- } catch {
1511
- outerHTML = truncate(el.outerHTML, 10240);
1856
+ let outerHTML = el.outerHTML;
1857
+ if (outerHTML && outerHTML.length > 1000) {
1858
+ outerHTML = truncate(outerHTML, 1000);
1512
1859
  }
1513
1860
 
1514
- let parent: { selector: string; tag: string } | undefined;
1515
- if (el.parentElement && el.parentElement !== document.body) {
1516
- parent = {
1517
- selector: getSemanticSelector(el.parentElement),
1518
- tag: el.parentElement.tagName.toLowerCase(),
1519
- };
1861
+ let innerText = el.innerText || el.textContent || '';
1862
+ if (innerText && innerText.length > 200) {
1863
+ innerText = truncate(innerText, 200);
1520
1864
  }
1521
1865
 
1522
1866
  return {
1523
1867
  selector,
1524
- tag,
1868
+ tag: el.tagName.toLowerCase(),
1525
1869
  attributes,
1526
1870
  outerHTML,
1527
- innerText: truncate(el.textContent?.trim(), 200),
1528
- parent,
1871
+ innerText,
1872
+ parent: el.parentElement
1873
+ ? {
1874
+ selector: getSemanticSelector(el.parentElement),
1875
+ tag: el.parentElement.tagName.toLowerCase(),
1876
+ }
1877
+ : undefined,
1529
1878
  };
1530
1879
  }
1531
1880
 
1881
+ private async cropDataUrl(dataUrl: string, region: RegionContext): Promise<string> {
1882
+ return new Promise((resolve) => {
1883
+ const img = new Image();
1884
+ img.onload = () => {
1885
+ const canvas = document.createElement('canvas');
1886
+ canvas.width = region.width;
1887
+ canvas.height = region.height;
1888
+ const ctx = canvas.getContext('2d');
1889
+ if (!ctx) {
1890
+ resolve(dataUrl);
1891
+ return;
1892
+ }
1893
+
1894
+ const dpr = window.devicePixelRatio || 1;
1895
+ ctx.drawImage(
1896
+ img,
1897
+ region.x * dpr,
1898
+ region.y * dpr,
1899
+ region.width * dpr,
1900
+ region.height * dpr,
1901
+ 0,
1902
+ 0,
1903
+ region.width,
1904
+ region.height
1905
+ );
1906
+ resolve(canvas.toDataURL('image/png'));
1907
+ };
1908
+ img.onerror = () => resolve(dataUrl);
1909
+ img.src = dataUrl;
1910
+ });
1911
+ }
1912
+
1532
1913
  public destroy(): void {
1533
1914
  for (const cleanup of this.cleanups) {
1534
- try {
1535
- cleanup();
1536
- } catch {}
1915
+ cleanup();
1537
1916
  }
1538
1917
  this.cleanups = [];
1539
1918
 
1540
- if (this.container && this.container.parentElement) {
1541
- this.container.parentElement.removeChild(this.container);
1919
+ if (this.container && this.container.parentNode) {
1920
+ this.container.parentNode.removeChild(this.container);
1921
+ this.container = null;
1922
+ this.shadowRoot = null;
1542
1923
  }
1543
- this.container = null;
1544
- this.shadowRoot = null;
1545
- this.toolbarElement = null;
1546
- this.regionOverlay = null;
1547
- this.regionBox = null;
1548
- this.regionBanner = null;
1549
- this.markersContainer = null;
1550
- this.modalOverlay = null;
1551
- this.cardOverlay = null;
1552
1924
  }
1553
1925
  }