browsertrack 0.1.2 → 0.2.1

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 (52) hide show
  1. package/AGENTS.md +6 -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-G2Y3CXCY.js → chunk-464D4U2U.js} +86 -3
  5. package/dist/chunk-464D4U2U.js.map +1 -0
  6. package/dist/{chunk-SPCIROIU.js → chunk-7OCOQGDN.js} +24 -5
  7. package/dist/chunk-7OCOQGDN.js.map +1 -0
  8. package/dist/{chunk-SKCMT2DE.js → chunk-INXDWPJW.js} +806 -160
  9. package/dist/chunk-INXDWPJW.js.map +1 -0
  10. package/dist/cli/index.js +202 -6
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/client/index.cjs +809 -161
  13. package/dist/client/index.d.ts +62 -11
  14. package/dist/client/index.js +7 -3
  15. package/dist/client.iife.js +206 -27
  16. package/dist/{notes-CBvN91Wf.d.ts → commands-fjuqKzkm.d.ts} +125 -91
  17. package/dist/core/index.d.ts +2 -2
  18. package/dist/daemon/index.d.ts +3 -3
  19. package/dist/daemon/index.js +2 -2
  20. package/dist/{engine-CeT9URuN.d.ts → engine-CmchnMDq.d.ts} +13 -2
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +4 -4
  23. package/dist/mcp/index.d.ts +4 -4
  24. package/dist/mcp/index.js +2 -2
  25. package/dist/{projects-CY8ungMt.d.ts → projects-DB7S312i.d.ts} +1 -1
  26. package/dist/{server-Dd8NX2Mk.d.ts → server-DiVmTrIR.d.ts} +1 -1
  27. package/docs/component-resolver.md +108 -0
  28. package/docs/index.md +3 -1
  29. package/docs/mcp-reference.md +15 -2
  30. package/docs/scenarios-flows.md +86 -0
  31. package/docs/visual-notes.md +36 -0
  32. package/package.json +1 -1
  33. package/packages/client/src/client.ts +18 -1
  34. package/packages/client/src/config.ts +84 -1
  35. package/packages/client/src/index.ts +4 -1
  36. package/packages/client/src/interceptors/interaction.ts +3 -0
  37. package/packages/client/src/notes/inspector.ts +640 -166
  38. package/packages/client/src/source/resolver.ts +272 -0
  39. package/packages/core/src/types/events.ts +3 -0
  40. package/packages/core/src/types/notes.ts +36 -0
  41. package/packages/daemon/src/notes/engine.ts +6 -0
  42. package/packages/daemon/src/server/ws.ts +23 -1
  43. package/packages/daemon/src/storage/db.ts +106 -3
  44. package/packages/mcp/src/handlers.ts +59 -0
  45. package/packages/mcp/src/tools.ts +28 -0
  46. package/test/client/component-resolver.test.ts +141 -0
  47. package/test/client/interceptors.test.ts +120 -0
  48. package/test/daemon/scenario-storage.test.ts +158 -0
  49. package/dist/chunk-G2Y3CXCY.js.map +0 -1
  50. package/dist/chunk-ILRYKMME.js.map +0 -1
  51. package/dist/chunk-SKCMT2DE.js.map +0 -1
  52. package/dist/chunk-SPCIROIU.js.map +0 -1
@@ -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,18 +11,29 @@ 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
 
20
+ export interface ActiveScenarioState {
21
+ id: string;
22
+ title: string;
23
+ stepNumber: number;
24
+ }
25
+
15
26
  /**
16
- * Isolated Visual Note Inspector rendering in Shadow DOM.
27
+ * Isolated Visual Note & Multi-Step Scenario Inspector rendering in Shadow DOM.
17
28
  * Supports:
18
29
  * 1. Element selection (hover highlight & click)
19
30
  * 2. Region / Area selection (drag-and-drop rectangle on screen with cancel banner)
20
31
  * 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
32
+ * 4. Multi-Step Flow / Scenario Recording (sequential 'Save & Next Step' continuous capture)
33
+ * 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
34
+ * 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
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)
24
37
  */
25
38
  export class NoteInspector {
26
39
  private transport: WebSocketTransport;
@@ -37,8 +50,10 @@ export class NoteInspector {
37
50
  private toolbarElement: HTMLDivElement | null = null;
38
51
  private modalOverlay: HTMLDivElement | null = null;
39
52
  private cardOverlay: HTMLDivElement | null = null;
53
+ private toastContainer: HTMLDivElement | null = null;
40
54
 
41
55
  private activeMode: NoteInspectMode = 'idle';
56
+ private activeScenario: ActiveScenarioState | null = null;
42
57
  private hoveredElement: HTMLElement | null = null;
43
58
  private selectedElement: HTMLElement | null = null;
44
59
  private selectedRegion: RegionContext | null = null;
@@ -49,15 +64,20 @@ export class NoteInspector {
49
64
 
50
65
  private savedNotes: VisualNote[] = [];
51
66
  private showMarkers = true;
67
+ private isHidden = false;
52
68
  private cleanups: (() => void)[] = [];
53
69
 
54
70
  constructor(transport: WebSocketTransport, screenshotDriver: ScreenshotDriver, options: InspectorOptions = {}) {
55
71
  this.transport = transport;
56
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;
57
76
  this.options = {
58
77
  shortcut: 'Alt+Click',
59
78
  maskSelectors: ['input[type="password"]', '[data-sensitive]'],
60
79
  showToolbar: true,
80
+ showBadges: true,
61
81
  ...options,
62
82
  };
63
83
  }
@@ -90,6 +110,46 @@ export class NoteInspector {
90
110
  this.renderMarkers();
91
111
  }
92
112
 
113
+ public showToast(message: string, icon = '✨', durationMs = 2500): void {
114
+ if (typeof document === 'undefined') return;
115
+ const root = this.ensureContainer();
116
+ if (!root || !this.toastContainer) return;
117
+
118
+ const toast = document.createElement('div');
119
+ toast.className = 'bt-toast';
120
+ toast.innerHTML = `<span>${icon}</span> <span>${message}</span>`;
121
+ this.toastContainer.appendChild(toast);
122
+
123
+ setTimeout(() => {
124
+ toast.classList.add('bt-toast-fadeout');
125
+ setTimeout(() => {
126
+ if (toast.parentElement) {
127
+ toast.parentElement.removeChild(toast);
128
+ }
129
+ }, 250);
130
+ }, durationMs);
131
+ }
132
+
133
+ public startScenario(title?: string): void {
134
+ const defaultTitle = title || `Scenario ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
135
+ this.activeScenario = {
136
+ id: `scen_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
137
+ title: defaultTitle,
138
+ stepNumber: 1,
139
+ };
140
+ this.updateToolbarState();
141
+ this.setMode('element');
142
+ this.showToast(`Started flow: "${defaultTitle}"`, '🎬');
143
+ }
144
+
145
+ public finishScenario(): void {
146
+ const title = this.activeScenario?.title;
147
+ this.activeScenario = null;
148
+ this.updateToolbarState();
149
+ this.setMode('idle');
150
+ this.showToast(title ? `Finished flow: "${title}"` : 'Flow recording finished', '✓');
151
+ }
152
+
93
153
  public setMode(mode: NoteInspectMode): void {
94
154
  this.activeMode = mode;
95
155
  this.updateToolbarState();
@@ -109,6 +169,44 @@ export class NoteInspector {
109
169
  }
110
170
  }
111
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
+
112
210
  private ensureContainer(): ShadowRoot | null {
113
211
  if (typeof document === 'undefined') return null;
114
212
 
@@ -117,6 +215,9 @@ export class NoteInspector {
117
215
  this.container.id = 'browsertrack-inspector-host';
118
216
  this.container.style.cssText =
119
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
+ }
120
221
 
121
222
  this.shadowRoot = this.container.attachShadow({ mode: 'open' });
122
223
 
@@ -276,6 +377,12 @@ export class NoteInspector {
276
377
  z-index: 2147483641;
277
378
  }
278
379
 
380
+ .bt-note-marker-step {
381
+ background: linear-gradient(135deg, #f59e0b, #d97706);
382
+ border-color: #fef3c7;
383
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.4), 0 0 0 1px rgba(217, 119, 6, 0.5);
384
+ }
385
+
279
386
  @keyframes bt-pop-in {
280
387
  0% { transform: scale(0.6); opacity: 0; }
281
388
  100% { transform: scale(1); opacity: 1; }
@@ -286,6 +393,10 @@ export class NoteInspector {
286
393
  box-shadow: 0 8px 20px rgba(37,99,235,0.6), 0 0 0 2px #60a5fa;
287
394
  }
288
395
 
396
+ .bt-note-marker-step:hover {
397
+ box-shadow: 0 8px 20px rgba(245, 158, 11, 0.7), 0 0 0 2px #fde68a;
398
+ }
399
+
289
400
  .bt-marker-resolved {
290
401
  background: linear-gradient(135deg, #475569, #334155);
291
402
  border-color: #94a3b8;
@@ -382,6 +493,19 @@ export class NoteInspector {
382
493
  box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
383
494
  }
384
495
 
496
+ .bt-toolbar-btn.active-scenario {
497
+ background: linear-gradient(135deg, #f59e0b, #d97706);
498
+ color: #ffffff;
499
+ font-weight: 700;
500
+ box-shadow: 0 2px 8px rgba(245, 158, 11, 0.4);
501
+ animation: bt-pulse 2s infinite;
502
+ }
503
+
504
+ @keyframes bt-pulse {
505
+ 0%, 100% { opacity: 1; }
506
+ 50% { opacity: 0.85; }
507
+ }
508
+
385
509
  .bt-count-pill {
386
510
  background: rgba(255, 255, 255, 0.2);
387
511
  color: #ffffff;
@@ -429,7 +553,7 @@ export class NoteInspector {
429
553
  border: 1px solid #334155;
430
554
  border-radius: 14px;
431
555
  padding: 20px;
432
- width: 450px;
556
+ width: 470px;
433
557
  max-width: 92vw;
434
558
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05);
435
559
  display: flex;
@@ -452,6 +576,7 @@ export class NoteInspector {
452
576
  align-items: center;
453
577
  gap: 8px;
454
578
  color: #f8fafc;
579
+ flex-wrap: wrap;
455
580
  }
456
581
 
457
582
  .bt-mode-badge {
@@ -481,6 +606,12 @@ export class NoteInspector {
481
606
  border: 1px solid rgba(168, 85, 247, 0.4);
482
607
  }
483
608
 
609
+ .bt-badge-step {
610
+ background: rgba(245, 158, 11, 0.15);
611
+ color: #fbbf24;
612
+ border: 1px solid rgba(245, 158, 11, 0.4);
613
+ }
614
+
484
615
  .bt-badge-status-open {
485
616
  background: rgba(16, 185, 129, 0.15);
486
617
  color: #34d399;
@@ -532,6 +663,59 @@ export class NoteInspector {
532
663
  color: #cbd5e1;
533
664
  }
534
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
+
682
+ .bt-scenario-stepper {
683
+ display: flex;
684
+ align-items: center;
685
+ justify-content: space-between;
686
+ background: #090d16;
687
+ border: 1px solid #1e293b;
688
+ border-radius: 8px;
689
+ padding: 6px 10px;
690
+ font-size: 12px;
691
+ font-weight: 600;
692
+ color: #f59e0b;
693
+ }
694
+
695
+ .bt-step-nav-btn {
696
+ background: #1e293b;
697
+ color: #f8fafc;
698
+ border: 1px solid #334155;
699
+ padding: 4px 10px;
700
+ border-radius: 6px;
701
+ cursor: pointer;
702
+ font-size: 11px;
703
+ font-weight: 600;
704
+ transition: all 0.12s ease;
705
+ font-family: inherit;
706
+ }
707
+
708
+ .bt-step-nav-btn:hover:not(:disabled) {
709
+ background: #2563eb;
710
+ border-color: #3b82f6;
711
+ color: #ffffff;
712
+ }
713
+
714
+ .bt-step-nav-btn:disabled {
715
+ opacity: 0.3;
716
+ cursor: not-allowed;
717
+ }
718
+
535
719
  .bt-note-message-box {
536
720
  background: #090d16;
537
721
  border: 1px solid #1e293b;
@@ -576,8 +760,9 @@ export class NoteInspector {
576
760
  display: flex;
577
761
  justify-content: space-between;
578
762
  align-items: center;
579
- gap: 12px;
763
+ gap: 10px;
580
764
  margin-top: 2px;
765
+ flex-wrap: wrap;
581
766
  }
582
767
 
583
768
  .bt-kbd-hint {
@@ -605,12 +790,13 @@ export class NoteInspector {
605
790
  justify-content: flex-end;
606
791
  align-items: center;
607
792
  gap: 8px;
793
+ flex-wrap: wrap;
608
794
  }
609
795
 
610
796
  .bt-btn {
611
- padding: 8px 16px;
797
+ padding: 7px 14px;
612
798
  border-radius: 7px;
613
- font-size: 12.5px;
799
+ font-size: 12px;
614
800
  font-weight: 600;
615
801
  cursor: pointer;
616
802
  border: 1px solid transparent;
@@ -619,7 +805,7 @@ export class NoteInspector {
619
805
  display: inline-flex;
620
806
  align-items: center;
621
807
  justify-content: center;
622
- gap: 6px;
808
+ gap: 5px;
623
809
  }
624
810
 
625
811
  .bt-btn-cancel {
@@ -641,6 +827,25 @@ export class NoteInspector {
641
827
  box-shadow: 0 4px 10px rgba(37, 99, 235, 0.45);
642
828
  }
643
829
 
830
+ .bt-btn-next-step {
831
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
832
+ color: #ffffff;
833
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
834
+ }
835
+ .bt-btn-next-step:hover {
836
+ background: linear-gradient(135deg, #4f46e5, #4338ca);
837
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.55);
838
+ }
839
+
840
+ .bt-btn-finish-flow {
841
+ background: linear-gradient(135deg, #10b981, #059669);
842
+ color: #ffffff;
843
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.35);
844
+ }
845
+ .bt-btn-finish-flow:hover {
846
+ background: linear-gradient(135deg, #059669, #047857);
847
+ }
848
+
644
849
  .bt-btn-resolve {
645
850
  background: rgba(16, 185, 129, 0.15);
646
851
  color: #6ee7b7;
@@ -660,6 +865,47 @@ export class NoteInspector {
660
865
  background: rgba(239, 68, 68, 0.3);
661
866
  color: #ffffff;
662
867
  }
868
+
869
+ /* 6. Toast Notifications */
870
+ .bt-toast-container {
871
+ position: fixed;
872
+ bottom: 74px;
873
+ right: 24px;
874
+ display: flex;
875
+ flex-direction: column;
876
+ gap: 8px;
877
+ pointer-events: none;
878
+ z-index: 2147483647;
879
+ align-items: flex-end;
880
+ }
881
+
882
+ .bt-toast {
883
+ background: #0f172a;
884
+ color: #f8fafc;
885
+ border: 1px solid #334155;
886
+ border-radius: 30px;
887
+ padding: 8px 16px;
888
+ font-size: 12.5px;
889
+ font-weight: 500;
890
+ display: inline-flex;
891
+ align-items: center;
892
+ gap: 8px;
893
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
894
+ pointer-events: auto;
895
+ animation: bt-toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
896
+ transition: opacity 0.25s ease, transform 0.25s ease;
897
+ user-select: none;
898
+ }
899
+
900
+ .bt-toast-fadeout {
901
+ opacity: 0;
902
+ transform: translateY(6px);
903
+ }
904
+
905
+ @keyframes bt-toast-in {
906
+ from { opacity: 0; transform: translateY(8px) scale(0.95); }
907
+ to { opacity: 1; transform: translateY(0) scale(1); }
908
+ }
663
909
  `;
664
910
 
665
911
  this.shadowRoot.appendChild(style);
@@ -675,8 +921,13 @@ export class NoteInspector {
675
921
  this.markersContainer.className = 'bt-markers-layer';
676
922
  this.shadowRoot.appendChild(this.markersContainer);
677
923
 
924
+ // Toast container layer
925
+ this.toastContainer = document.createElement('div');
926
+ this.toastContainer.className = 'bt-toast-container';
927
+ this.shadowRoot.appendChild(this.toastContainer);
928
+
678
929
  // Floating Toolbar
679
- if (this.options.showToolbar) {
930
+ if (this.options.showToolbar && !this.isHidden) {
680
931
  this.createToolbar();
681
932
  }
682
933
  }
@@ -707,6 +958,10 @@ export class NoteInspector {
707
958
  <span>📄</span> Page
708
959
  </button>
709
960
  <div class="bt-toolbar-divider"></div>
961
+ <button class="bt-toolbar-btn" id="bt-mode-flow" title="Record multi-step reproduction flow">
962
+ <span>🎬</span> Flow
963
+ </button>
964
+ <div class="bt-toolbar-divider"></div>
710
965
  <button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
711
966
  <span>📌</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
712
967
  </button>
@@ -715,6 +970,7 @@ export class NoteInspector {
715
970
  const btnElement = this.toolbarElement.querySelector('#bt-mode-element') as HTMLButtonElement;
716
971
  const btnRegion = this.toolbarElement.querySelector('#bt-mode-region') as HTMLButtonElement;
717
972
  const btnPage = this.toolbarElement.querySelector('#bt-mode-page') as HTMLButtonElement;
973
+ const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow') as HTMLButtonElement;
718
974
  const btnToggleNotes = this.toolbarElement.querySelector('#bt-toggle-notes') as HTMLButtonElement;
719
975
 
720
976
  btnElement.onclick = (e: MouseEvent) => {
@@ -732,6 +988,15 @@ export class NoteInspector {
732
988
  this.setMode('page');
733
989
  };
734
990
 
991
+ btnFlow.onclick = (e: MouseEvent) => {
992
+ e.stopPropagation();
993
+ if (this.activeScenario) {
994
+ this.finishScenario();
995
+ } else {
996
+ this.startScenario();
997
+ }
998
+ };
999
+
735
1000
  btnToggleNotes.onclick = (e: MouseEvent) => {
736
1001
  e.stopPropagation();
737
1002
  this.showMarkers = !this.showMarkers;
@@ -748,10 +1013,23 @@ export class NoteInspector {
748
1013
  const btnElement = this.toolbarElement.querySelector('#bt-mode-element');
749
1014
  const btnRegion = this.toolbarElement.querySelector('#bt-mode-region');
750
1015
  const btnPage = this.toolbarElement.querySelector('#bt-mode-page');
1016
+ const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow');
751
1017
 
752
1018
  btnElement?.classList.toggle('active', this.activeMode === 'element');
753
1019
  btnRegion?.classList.toggle('active', this.activeMode === 'region');
754
1020
  btnPage?.classList.toggle('active', this.activeMode === 'page');
1021
+
1022
+ if (btnFlow) {
1023
+ if (this.activeScenario) {
1024
+ btnFlow.className = 'bt-toolbar-btn active-scenario';
1025
+ btnFlow.innerHTML = `<span>🎬</span> Step ${this.activeScenario.stepNumber} (Finish)`;
1026
+ btnFlow.setAttribute('title', `Click to finish recording "${this.activeScenario.title}"`);
1027
+ } else {
1028
+ btnFlow.className = 'bt-toolbar-btn';
1029
+ btnFlow.innerHTML = `<span>🎬</span> Flow`;
1030
+ btnFlow.setAttribute('title', 'Record multi-step reproduction flow');
1031
+ }
1032
+ }
755
1033
  }
756
1034
 
757
1035
  private updateToolbarCount(): void {
@@ -768,7 +1046,7 @@ export class NoteInspector {
768
1046
  if (!root || !this.markersContainer) return;
769
1047
 
770
1048
  this.markersContainer.innerHTML = '';
771
- if (!this.showMarkers) return;
1049
+ if (!this.showMarkers || this.isHidden) return;
772
1050
 
773
1051
  const currentPath = window.location.pathname;
774
1052
  // Filter open notes on this route or global
@@ -784,6 +1062,10 @@ export class NoteInspector {
784
1062
  return;
785
1063
  }
786
1064
 
1065
+ const isStep = !!note.scenarioId && note.stepNumber != null;
1066
+ const stepLabel = isStep ? `🎬 Step ${note.stepNumber}` : `#${index + 1}`;
1067
+ const markerClass = `bt-note-marker ${isStep ? 'bt-note-marker-step' : ''}`;
1068
+
787
1069
  if (note.type === 'region' && note.region) {
788
1070
  // Region box
789
1071
  const regBox = document.createElement('div');
@@ -796,11 +1078,11 @@ export class NoteInspector {
796
1078
 
797
1079
  // Pin on top-left of region
798
1080
  const pin = document.createElement('div');
799
- pin.className = 'bt-note-marker';
1081
+ pin.className = markerClass;
800
1082
  pin.style.left = `${Math.max(4, note.region.x - 12)}px`;
801
1083
  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>`;
1084
+ pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
1085
+ pin.innerHTML = `<span>${isStep ? '🎬' : '📐'}</span> <span>${stepLabel}</span>`;
804
1086
  pin.onclick = (e) => {
805
1087
  e.stopPropagation();
806
1088
  this.openNoteCard(note);
@@ -820,12 +1102,12 @@ export class NoteInspector {
820
1102
  const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
821
1103
  if (rect) {
822
1104
  const pin = document.createElement('div');
823
- pin.className = 'bt-note-marker';
1105
+ pin.className = markerClass;
824
1106
  pin.setAttribute('data-note-id', note.id);
825
- pin.title = note.message;
1107
+ pin.title = isStep ? `[${note.scenarioTitle || 'Scenario'}] Step ${note.stepNumber}: ${note.message}` : note.message;
826
1108
  pin.style.left = `${Math.max(4, rect.left - 10)}px`;
827
1109
  pin.style.top = `${Math.max(4, rect.top - 12)}px`;
828
- pin.innerHTML = `<span>📝</span> <span>#${index + 1}</span>`;
1110
+ pin.innerHTML = `<span>${isStep ? '🎬' : '📝'}</span> <span>${stepLabel}</span>`;
829
1111
 
830
1112
  pin.onclick = (e) => {
831
1113
  e.stopPropagation();
@@ -845,10 +1127,11 @@ export class NoteInspector {
845
1127
  if (pageNotes.length > 0) {
846
1128
  const pageDock = document.createElement('div');
847
1129
  pageDock.className = 'bt-page-notes-dock';
848
- pageNotes.forEach((pNote, idx) => {
1130
+ pageNotes.forEach((pNote) => {
849
1131
  const pill = document.createElement('div');
850
1132
  pill.className = 'bt-page-note-pill';
851
- pill.innerHTML = `<span>📄</span> <span>Page Note (${truncate(pNote.message, 25)})</span>`;
1133
+ const label = pNote.scenarioId && pNote.stepNumber ? `Step ${pNote.stepNumber}: ` : '';
1134
+ pill.innerHTML = `<span>📄</span> <span>${label}${truncate(pNote.message, 25)}</span>`;
852
1135
  pill.onclick = (e) => {
853
1136
  e.stopPropagation();
854
1137
  this.openNoteCard(pNote);
@@ -889,17 +1172,71 @@ export class NoteInspector {
889
1172
 
890
1173
  const formattedDate = new Date(note.createdAt).toLocaleString();
891
1174
 
1175
+ // Scenario flow steps navigation
1176
+ let scenarioStepsHtml = '';
1177
+ let scenarioSteps: VisualNote[] = [];
1178
+ if (note.scenarioId) {
1179
+ scenarioSteps = this.savedNotes
1180
+ .filter((n) => n.scenarioId === note.scenarioId)
1181
+ .sort((a, b) => (a.stepNumber || 0) - (b.stepNumber || 0));
1182
+
1183
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1184
+ const prevStep = currentIndex > 0 ? scenarioSteps[currentIndex - 1] : null;
1185
+ const nextStep = currentIndex >= 0 && currentIndex < scenarioSteps.length - 1 ? scenarioSteps[currentIndex + 1] : null;
1186
+
1187
+ scenarioStepsHtml = `
1188
+ <div class="bt-scenario-stepper">
1189
+ <button class="bt-step-nav-btn" id="btn-prev-step" ${!prevStep ? 'disabled' : ''}>
1190
+ ◀ Step ${prevStep ? prevStep.stepNumber : ''}
1191
+ </button>
1192
+ <span>🎬 Step ${note.stepNumber || 1} of ${scenarioSteps.length}</span>
1193
+ <button class="bt-step-nav-btn" id="btn-next-step-card" ${!nextStep ? 'disabled' : ''}>
1194
+ Step ${nextStep ? nextStep.stepNumber : ''} ▶
1195
+ </button>
1196
+ </div>
1197
+ `;
1198
+ }
1199
+
892
1200
  backdrop.innerHTML = `
893
1201
  <div class="bt-modal">
894
1202
  <div class="bt-modal-header">
895
1203
  <div class="bt-modal-title">
896
- <span>${icon} Visual Note Details</span>
1204
+ <span>${note.scenarioId ? '🎬 ' + (note.scenarioTitle || 'Scenario Flow') : icon + ' Visual Note Details'}</span>
897
1205
  <span class="bt-mode-badge ${badgeClass}">${note.type.toUpperCase()}</span>
1206
+ ${note.scenarioId && note.stepNumber ? `<span class="bt-mode-badge bt-badge-step">STEP ${note.stepNumber}</span>` : ''}
898
1207
  <span class="bt-mode-badge ${statusClass}">${note.status}</span>
899
1208
  </div>
900
1209
  <button class="bt-close-btn" id="btn-card-close" title="Close (Esc)">✕</button>
901
1210
  </div>
902
1211
 
1212
+ ${scenarioStepsHtml}
1213
+
1214
+ ${
1215
+ note.elementContext?.componentSource?.componentName
1216
+ ? `<div class="bt-component-pill" title="${
1217
+ note.elementContext.componentSource.sourceFile
1218
+ ? note.elementContext.componentSource.sourceFile +
1219
+ (note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : '')
1220
+ : note.elementContext.componentSource.componentName
1221
+ }">
1222
+ <span>🧬</span>
1223
+ <span style="font-weight: 600; color: #a5b4fc;">&lt;${note.elementContext.componentSource.componentName}&gt;</span>
1224
+ ${
1225
+ note.elementContext.componentSource.sourceFile
1226
+ ? `<span style="color: #64748b; font-size: 11px; margin-left: 4px;">${note.elementContext.componentSource.sourceFile}${
1227
+ note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : ''
1228
+ }</span>`
1229
+ : ''
1230
+ }
1231
+ ${
1232
+ note.elementContext.componentSource.framework
1233
+ ? `<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>`
1234
+ : ''
1235
+ }
1236
+ </div>`
1237
+ : ''
1238
+ }
1239
+
903
1240
  <div class="bt-target-pill" title="${contextText}">
904
1241
  <span>🏷️</span>
905
1242
  <span class="bt-pill-content">${contextText}</span>
@@ -913,9 +1250,18 @@ export class NoteInspector {
913
1250
  </div>
914
1251
 
915
1252
  <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>
1253
+ <div style="display: flex; gap: 6px;">
1254
+ <button class="bt-btn bt-btn-delete" id="btn-card-delete" title="Delete this note">
1255
+ <span>🗑️</span> Delete
1256
+ </button>
1257
+ ${
1258
+ note.scenarioId
1259
+ ? `<button class="bt-btn bt-btn-delete" id="btn-card-delete-flow" title="Delete entire scenario flow">
1260
+ <span>🗑️</span> Delete Flow
1261
+ </button>`
1262
+ : ''
1263
+ }
1264
+ </div>
919
1265
  <div class="bt-modal-actions">
920
1266
  <button class="bt-btn bt-btn-cancel" id="btn-card-dismiss">Close</button>
921
1267
  <button class="bt-btn ${note.status === 'OPEN' ? 'bt-btn-resolve' : 'bt-btn-save'}" id="btn-card-resolve">
@@ -930,6 +1276,9 @@ export class NoteInspector {
930
1276
  const btnDismiss = backdrop.querySelector('#btn-card-dismiss') as HTMLButtonElement;
931
1277
  const btnResolve = backdrop.querySelector('#btn-card-resolve') as HTMLButtonElement;
932
1278
  const btnDelete = backdrop.querySelector('#btn-card-delete') as HTMLButtonElement;
1279
+ const btnDeleteFlow = backdrop.querySelector('#btn-card-delete-flow') as HTMLButtonElement | null;
1280
+ const btnPrevStep = backdrop.querySelector('#btn-prev-step') as HTMLButtonElement | null;
1281
+ const btnNextStepCard = backdrop.querySelector('#btn-next-step-card') as HTMLButtonElement | null;
933
1282
 
934
1283
  const closeCard = () => {
935
1284
  if (this.cardOverlay) {
@@ -945,27 +1294,56 @@ export class NoteInspector {
945
1294
  if (e.target === backdrop) closeCard();
946
1295
  };
947
1296
 
1297
+ if (btnPrevStep && scenarioSteps.length > 0) {
1298
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1299
+ if (currentIndex > 0) {
1300
+ btnPrevStep.onclick = () => {
1301
+ this.openNoteCard(scenarioSteps[currentIndex - 1]);
1302
+ };
1303
+ }
1304
+ }
1305
+
1306
+ if (btnNextStepCard && scenarioSteps.length > 0) {
1307
+ const currentIndex = scenarioSteps.findIndex((s) => s.id === note.id);
1308
+ if (currentIndex >= 0 && currentIndex < scenarioSteps.length - 1) {
1309
+ btnNextStepCard.onclick = () => {
1310
+ this.openNoteCard(scenarioSteps[currentIndex + 1]);
1311
+ };
1312
+ }
1313
+ }
1314
+
948
1315
  btnResolve.onclick = () => {
949
1316
  if (note.status === 'OPEN') {
950
1317
  this.transport.send({ type: 'resolve_note', noteId: note.id });
1318
+ this.showToast('Note marked as resolved', '✅');
951
1319
  } else {
952
1320
  this.transport.send({ type: 'reopen_note', noteId: note.id });
1321
+ this.showToast('Note reopened', '↺');
953
1322
  }
954
1323
  closeCard();
955
1324
  };
956
1325
 
957
1326
  btnDelete.onclick = () => {
958
1327
  this.transport.send({ type: 'delete_note', noteId: note.id });
1328
+ this.showToast('Note deleted', '🗑️');
959
1329
  closeCard();
960
1330
  };
961
1331
 
1332
+ if (btnDeleteFlow && note.scenarioId) {
1333
+ btnDeleteFlow.onclick = () => {
1334
+ this.transport.send({ type: 'delete_scenario', scenarioId: note.scenarioId });
1335
+ this.showToast('Scenario flow deleted', '🗑️');
1336
+ closeCard();
1337
+ };
1338
+ }
1339
+
962
1340
  root.appendChild(backdrop);
963
1341
  }
964
1342
 
965
1343
  private setupListeners(): void {
966
1344
  // 1. Mouse move: hover highlight in element mode or with Alt key
967
1345
  const onMouseMove = (e: MouseEvent) => {
968
- if (this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
1346
+ if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
969
1347
 
970
1348
  // Don't highlight elements if mouse is over our own inspector UI
971
1349
  const path = e.composedPath ? e.composedPath() : [];
@@ -990,7 +1368,7 @@ export class NoteInspector {
990
1368
 
991
1369
  // 2. Click: Alt+Click or Element mode click selects element and opens editor
992
1370
  const onClick = (e: MouseEvent) => {
993
- if (this.modalOverlay || this.cardOverlay) return;
1371
+ if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
994
1372
 
995
1373
  // If clicking inside inspector toolbar or container, do NOT intercept
996
1374
  const path = e.composedPath ? e.composedPath() : [];
@@ -1008,7 +1386,7 @@ export class NoteInspector {
1008
1386
  if (target && target !== this.container && !this.container?.contains(target)) {
1009
1387
  this.selectedElement = target;
1010
1388
  this.openNoteEditor(target, 'element');
1011
- if (this.activeMode === 'element') {
1389
+ if (this.activeMode === 'element' && !this.activeScenario) {
1012
1390
  this.setMode('idle');
1013
1391
  }
1014
1392
  }
@@ -1022,7 +1400,9 @@ export class NoteInspector {
1022
1400
  this.shadowRoot.removeChild(this.cardOverlay);
1023
1401
  this.cardOverlay = null;
1024
1402
  } else if (this.activeMode === 'region' || this.activeMode === 'element') {
1025
- this.setMode('idle');
1403
+ if (!this.activeScenario) {
1404
+ this.setMode('idle');
1405
+ }
1026
1406
  }
1027
1407
  }
1028
1408
  };
@@ -1153,31 +1533,30 @@ export class NoteInspector {
1153
1533
  width: Math.round(width),
1154
1534
  height: Math.round(height),
1155
1535
  };
1156
- this.hideRegionOverlay();
1157
- this.setMode('idle');
1158
1536
  this.openRegionNoteEditor(this.selectedRegion);
1159
- } else {
1160
- this.hideRegionOverlay();
1537
+ }
1538
+
1539
+ if (this.regionBox) {
1540
+ this.regionBox.style.display = 'none';
1541
+ }
1542
+ if (!this.activeScenario) {
1161
1543
  this.setMode('idle');
1162
1544
  }
1163
1545
  };
1164
- }
1165
1546
 
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
1547
  root.appendChild(this.regionOverlay);
1548
+ } else {
1549
+ this.regionOverlay.style.display = 'block';
1171
1550
  }
1172
1551
  }
1173
1552
 
1174
1553
  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';
1554
+ if (this.regionOverlay) {
1555
+ this.regionOverlay.style.display = 'none';
1556
+ this.isDraggingRegion = false;
1557
+ if (this.regionBox) {
1558
+ this.regionBox.style.display = 'none';
1559
+ }
1181
1560
  }
1182
1561
  }
1183
1562
 
@@ -1187,21 +1566,21 @@ export class NoteInspector {
1187
1566
 
1188
1567
  const rect = el.getBoundingClientRect();
1189
1568
  this.highlightOverlay.style.display = 'block';
1190
- this.highlightOverlay.style.top = `${rect.top}px`;
1191
1569
  this.highlightOverlay.style.left = `${rect.left}px`;
1570
+ this.highlightOverlay.style.top = `${rect.top}px`;
1192
1571
  this.highlightOverlay.style.width = `${rect.width}px`;
1193
1572
  this.highlightOverlay.style.height = `${rect.height}px`;
1194
1573
 
1195
- const selector = getSemanticSelector(el);
1196
- const badgeText = `${selector} · ${Math.round(rect.width)}×${Math.round(rect.height)}`;
1197
-
1198
1574
  let badge = this.highlightOverlay.querySelector('.bt-badge') as HTMLDivElement | null;
1199
1575
  if (!badge) {
1200
1576
  badge = document.createElement('div');
1201
1577
  badge.className = 'bt-badge';
1202
1578
  this.highlightOverlay.appendChild(badge);
1203
1579
  }
1204
- badge.textContent = badgeText;
1580
+
1581
+ const selector = getSemanticSelector(el);
1582
+ const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
1583
+ badge.textContent = `${label} (${Math.round(rect.width)} × ${Math.round(rect.height)} px)`;
1205
1584
  }
1206
1585
 
1207
1586
  private hideHighlight(): void {
@@ -1220,26 +1599,79 @@ export class NoteInspector {
1220
1599
  this.updateHighlight(targetEl);
1221
1600
  }
1222
1601
 
1602
+ const comp = noteType === 'element' ? resolveComponentSource(targetEl) : undefined;
1603
+ const compPrefix = comp?.componentName
1604
+ ? `🧬 <${comp.componentName}>${comp.sourceFile ? ' (' + comp.sourceFile + (comp.sourceLine ? ':' + comp.sourceLine : '') + ')' : ''} · `
1605
+ : '';
1606
+
1223
1607
  this.renderNoteModal({
1224
- title: 'Add Visual Note',
1225
- modeBadge: noteType.toUpperCase(),
1608
+ title: this.activeScenario
1609
+ ? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
1610
+ : 'Add Visual Note',
1611
+ modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : noteType.toUpperCase(),
1226
1612
  pillText:
1227
1613
  noteType === 'page'
1228
1614
  ? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
1229
- : `${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);
1615
+ : `${compPrefix}${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
1616
+ onSave: async (message, action) => {
1617
+ let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
1618
+
1619
+ if (action === 'next_step' && !this.activeScenario) {
1620
+ this.startScenario();
1621
+ }
1622
+
1623
+ if (this.activeScenario) {
1624
+ scenarioParam = {
1625
+ scenarioId: this.activeScenario.id,
1626
+ stepNumber: this.activeScenario.stepNumber,
1627
+ scenarioTitle: this.activeScenario.title,
1628
+ };
1629
+ }
1630
+
1631
+ await this.saveVisualNote(targetEl, message, noteType, scenarioParam);
1632
+
1633
+ if (action === 'next_step' && this.activeScenario) {
1634
+ this.activeScenario.stepNumber++;
1635
+ this.updateToolbarState();
1636
+ this.setMode('element');
1637
+ } else if (action === 'finish_flow' && this.activeScenario) {
1638
+ this.finishScenario();
1639
+ }
1232
1640
  },
1233
1641
  });
1234
1642
  }
1235
1643
 
1236
1644
  public openRegionNoteEditor(region: RegionContext): void {
1237
1645
  this.renderNoteModal({
1238
- title: 'Add Region Note',
1239
- modeBadge: 'REGION',
1646
+ title: this.activeScenario
1647
+ ? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
1648
+ : 'Add Region Note',
1649
+ modeBadge: this.activeScenario ? `STEP ${this.activeScenario.stepNumber}` : 'REGION',
1240
1650
  pillText: `Selected Area: x:${region.x}, y:${region.y} (${region.width} × ${region.height} px)`,
1241
- onSave: async (message) => {
1242
- await this.saveRegionVisualNote(region, message);
1651
+ onSave: async (message, action) => {
1652
+ let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
1653
+
1654
+ if (action === 'next_step' && !this.activeScenario) {
1655
+ this.startScenario();
1656
+ }
1657
+
1658
+ if (this.activeScenario) {
1659
+ scenarioParam = {
1660
+ scenarioId: this.activeScenario.id,
1661
+ stepNumber: this.activeScenario.stepNumber,
1662
+ scenarioTitle: this.activeScenario.title,
1663
+ };
1664
+ }
1665
+
1666
+ await this.saveRegionVisualNote(region, message, scenarioParam);
1667
+
1668
+ if (action === 'next_step' && this.activeScenario) {
1669
+ this.activeScenario.stepNumber++;
1670
+ this.updateToolbarState();
1671
+ this.setMode('element');
1672
+ } else if (action === 'finish_flow' && this.activeScenario) {
1673
+ this.finishScenario();
1674
+ }
1243
1675
  },
1244
1676
  });
1245
1677
  }
@@ -1248,7 +1680,7 @@ export class NoteInspector {
1248
1680
  title: string;
1249
1681
  modeBadge: string;
1250
1682
  pillText: string;
1251
- onSave: (message: string) => Promise<void>;
1683
+ onSave: (message: string, action: 'save' | 'next_step' | 'finish_flow') => Promise<void>;
1252
1684
  }): void {
1253
1685
  const root = this.ensureContainer();
1254
1686
  if (!root) return;
@@ -1263,15 +1695,16 @@ export class NoteInspector {
1263
1695
  backdrop.className = 'bt-modal-backdrop';
1264
1696
  this.modalOverlay = backdrop;
1265
1697
 
1266
- const badgeClass = `bt-badge-${options.modeBadge.toLowerCase()}`;
1267
- const icon = options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
1698
+ const isStep = options.modeBadge.startsWith('STEP');
1699
+ const badgeClass = isStep ? 'bt-badge-step' : `bt-badge-${options.modeBadge.toLowerCase()}`;
1700
+ const icon = isStep ? '🎬' : options.modeBadge === 'REGION' ? '📐' : options.modeBadge === 'PAGE' ? '📄' : '🎯';
1268
1701
  const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
1269
1702
 
1270
1703
  backdrop.innerHTML = `
1271
1704
  <div class="bt-modal">
1272
1705
  <div class="bt-modal-header">
1273
1706
  <div class="bt-modal-title">
1274
- <span>📝 ${options.title}</span>
1707
+ <span>${icon} ${options.title}</span>
1275
1708
  <span class="bt-mode-badge ${badgeClass}">${options.modeBadge}</span>
1276
1709
  </div>
1277
1710
  <button class="bt-close-btn" id="btn-close" title="Close (Esc)">✕</button>
@@ -1280,14 +1713,23 @@ export class NoteInspector {
1280
1713
  <span>${icon}</span>
1281
1714
  <span class="bt-pill-content">${options.pillText}</span>
1282
1715
  </div>
1283
- <textarea class="bt-textarea" placeholder="Describe the layout issue, styling bug, or note for AI agent..." autofocus></textarea>
1716
+ <textarea class="bt-textarea" placeholder="Describe the layout issue, user action, or note for AI agent..." autofocus></textarea>
1284
1717
  <div class="bt-modal-footer">
1285
1718
  <div class="bt-kbd-hint">
1286
1719
  <kbd>${isMac ? '⌘' : 'Ctrl'}+Enter</kbd> save · <kbd>Esc</kbd> cancel
1287
1720
  </div>
1288
1721
  <div class="bt-modal-actions">
1289
1722
  <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>
1723
+ <button class="bt-btn bt-btn-next-step" id="btn-next-step" title="Save this step and immediately select the next element">
1724
+ <span>➡️</span> ${this.activeScenario ? 'Save & Next Step' : 'Save as Step 1 (Flow)'}
1725
+ </button>
1726
+ ${
1727
+ this.activeScenario
1728
+ ? `<button class="bt-btn bt-btn-finish-flow" id="btn-finish-flow" title="Save final step and complete scenario">
1729
+ <span>✓</span> Save & Finish Flow
1730
+ </button>`
1731
+ : `<button class="bt-btn bt-btn-save" id="btn-save">Save Note</button>`
1732
+ }
1291
1733
  </div>
1292
1734
  </div>
1293
1735
  </div>
@@ -1296,7 +1738,9 @@ export class NoteInspector {
1296
1738
  const textarea = backdrop.querySelector('textarea') as HTMLTextAreaElement;
1297
1739
  const btnClose = backdrop.querySelector('#btn-close') as HTMLButtonElement;
1298
1740
  const btnCancel = backdrop.querySelector('#btn-cancel') as HTMLButtonElement;
1299
- const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement;
1741
+ const btnSave = backdrop.querySelector('#btn-save') as HTMLButtonElement | null;
1742
+ const btnNextStep = backdrop.querySelector('#btn-next-step') as HTMLButtonElement;
1743
+ const btnFinishFlow = backdrop.querySelector('#btn-finish-flow') as HTMLButtonElement | null;
1300
1744
 
1301
1745
  const closeModal = () => {
1302
1746
  if (this.modalOverlay) {
@@ -1316,26 +1760,33 @@ export class NoteInspector {
1316
1760
  }
1317
1761
  };
1318
1762
 
1319
- const submitNote = async () => {
1763
+ const handleAction = async (action: 'save' | 'next_step' | 'finish_flow') => {
1320
1764
  const message = textarea.value.trim();
1321
1765
  if (!message) return;
1322
1766
 
1323
- btnSave.textContent = 'Saving...';
1324
- btnSave.disabled = true;
1767
+ btnNextStep.disabled = true;
1768
+ if (btnSave) btnSave.disabled = true;
1769
+ if (btnFinishFlow) btnFinishFlow.disabled = true;
1325
1770
 
1326
1771
  try {
1327
- await options.onSave(message);
1772
+ await options.onSave(message, action);
1328
1773
  } finally {
1329
1774
  closeModal();
1330
1775
  }
1331
1776
  };
1332
1777
 
1333
- btnSave.onclick = submitNote;
1778
+ if (btnSave) {
1779
+ btnSave.onclick = () => handleAction('save');
1780
+ }
1781
+ btnNextStep.onclick = () => handleAction('next_step');
1782
+ if (btnFinishFlow) {
1783
+ btnFinishFlow.onclick = () => handleAction('finish_flow');
1784
+ }
1334
1785
 
1335
1786
  textarea.onkeydown = (e: KeyboardEvent) => {
1336
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey || !e.shiftKey)) {
1787
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1337
1788
  e.preventDefault();
1338
- submitNote();
1789
+ handleAction(this.activeScenario ? 'next_step' : 'save');
1339
1790
  }
1340
1791
  if (e.key === 'Escape') {
1341
1792
  closeModal();
@@ -1346,7 +1797,12 @@ export class NoteInspector {
1346
1797
  setTimeout(() => textarea?.focus(), 50);
1347
1798
  }
1348
1799
 
1349
- public async saveVisualNote(targetEl: HTMLElement, message: string, noteType: 'element' | 'page' = 'element'): Promise<void> {
1800
+ public async saveVisualNote(
1801
+ targetEl: HTMLElement,
1802
+ message: string,
1803
+ noteType: 'element' | 'page' = 'element',
1804
+ scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
1805
+ ): Promise<void> {
1350
1806
  const rect = targetEl.getBoundingClientRect();
1351
1807
  const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
1352
1808
 
@@ -1381,25 +1837,30 @@ export class NoteInspector {
1381
1837
  confidence: selector.startsWith('[data-test') ? 'high' : selector.startsWith('#') ? 'medium' : 'low',
1382
1838
  };
1383
1839
 
1840
+ const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
1841
+ const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
1842
+ const viewport = typeof window !== 'undefined'
1843
+ ? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
1844
+ : { width: 1280, height: 800, devicePixelRatio: 1 };
1845
+ const scroll = typeof window !== 'undefined'
1846
+ ? { scrollX: window.scrollX, scrollY: window.scrollY }
1847
+ : { scrollX: 0, scrollY: 0 };
1848
+
1384
1849
  const notePayload = {
1385
1850
  type: 'create_note',
1386
1851
  sessionId: this.transport.getSessionId() || '',
1387
1852
  noteType,
1388
1853
  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
- },
1854
+ route,
1855
+ url,
1856
+ viewport,
1857
+ scroll,
1400
1858
  target,
1401
1859
  elementContext,
1402
1860
  screenshot: screenshotDataUrl,
1861
+ scenarioId: scenario?.scenarioId,
1862
+ stepNumber: scenario?.stepNumber,
1863
+ scenarioTitle: scenario?.scenarioTitle,
1403
1864
  timestamp: Date.now(),
1404
1865
  };
1405
1866
 
@@ -1407,9 +1868,19 @@ export class NoteInspector {
1407
1868
  if (this.options.onNoteCreated) {
1408
1869
  this.options.onNoteCreated(notePayload as any);
1409
1870
  }
1871
+
1872
+ if (scenario?.scenarioId) {
1873
+ this.showToast(`Step ${scenario.stepNumber || 1} recorded`, '🎬');
1874
+ } else {
1875
+ this.showToast('Visual note saved', '✨');
1876
+ }
1410
1877
  }
1411
1878
 
1412
- public async saveRegionVisualNote(region: RegionContext, message: string): Promise<void> {
1879
+ public async saveRegionVisualNote(
1880
+ region: RegionContext,
1881
+ message: string,
1882
+ scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
1883
+ ): Promise<void> {
1413
1884
  // 1. Capture full page screenshot and crop to region
1414
1885
  let screenshotDataUrl: string | undefined;
1415
1886
  try {
@@ -1421,24 +1892,29 @@ export class NoteInspector {
1421
1892
  // Defensive
1422
1893
  }
1423
1894
 
1895
+ const route = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
1896
+ const url = typeof window !== 'undefined' ? window.location.href : 'http://localhost/';
1897
+ const viewport = typeof window !== 'undefined'
1898
+ ? { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio || 1 }
1899
+ : { width: 1280, height: 800, devicePixelRatio: 1 };
1900
+ const scroll = typeof window !== 'undefined'
1901
+ ? { scrollX: window.scrollX, scrollY: window.scrollY }
1902
+ : { scrollX: 0, scrollY: 0 };
1903
+
1424
1904
  const notePayload = {
1425
1905
  type: 'create_note',
1426
1906
  sessionId: this.transport.getSessionId() || '',
1427
1907
  noteType: 'region',
1428
1908
  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
- },
1909
+ route,
1910
+ url,
1911
+ viewport,
1912
+ scroll,
1440
1913
  region,
1441
1914
  screenshot: screenshotDataUrl,
1915
+ scenarioId: scenario?.scenarioId,
1916
+ stepNumber: scenario?.stepNumber,
1917
+ scenarioTitle: scenario?.scenarioTitle,
1442
1918
  timestamp: Date.now(),
1443
1919
  };
1444
1920
 
@@ -1446,108 +1922,106 @@ export class NoteInspector {
1446
1922
  if (this.options.onNoteCreated) {
1447
1923
  this.options.onNoteCreated(notePayload as any);
1448
1924
  }
1449
- }
1450
1925
 
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
- });
1926
+ if (scenario?.scenarioId) {
1927
+ this.showToast(`Step ${scenario.stepNumber || 1} region recorded`, '🎬');
1928
+ } else {
1929
+ this.showToast('Region note saved', '📐');
1930
+ }
1485
1931
  }
1486
1932
 
1487
1933
  private extractElementContext(el: HTMLElement): ElementContext {
1488
1934
  const selector = getSemanticSelector(el);
1489
- const tag = el.tagName.toLowerCase();
1490
-
1491
- // Attributes (sanitize passwords/tokens)
1492
1935
  const attributes: Record<string, string> = {};
1936
+
1493
1937
  for (let i = 0; i < el.attributes.length; i++) {
1494
1938
  const attr = el.attributes[i];
1495
- if (attr.name === 'value' && (el as HTMLInputElement).type === 'password') {
1939
+ if (
1940
+ this.options.maskSelectors?.some((mask) => {
1941
+ try {
1942
+ return el.matches(mask);
1943
+ } catch {
1944
+ return false;
1945
+ }
1946
+ }) &&
1947
+ (attr.name === 'value' || attr.name === 'data-secret')
1948
+ ) {
1496
1949
  attributes[attr.name] = '[REDACTED]';
1497
1950
  } else {
1498
1951
  attributes[attr.name] = attr.value;
1499
1952
  }
1500
1953
  }
1501
1954
 
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);
1955
+ let outerHTML = el.outerHTML;
1956
+ if (outerHTML && outerHTML.length > 1000) {
1957
+ outerHTML = truncate(outerHTML, 1000);
1512
1958
  }
1513
1959
 
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
- };
1960
+ let innerText = el.innerText || el.textContent || '';
1961
+ if (innerText && innerText.length > 200) {
1962
+ innerText = truncate(innerText, 200);
1520
1963
  }
1521
1964
 
1965
+ const componentSource = resolveComponentSource(el);
1966
+
1522
1967
  return {
1523
1968
  selector,
1524
- tag,
1969
+ tag: el.tagName.toLowerCase(),
1525
1970
  attributes,
1526
1971
  outerHTML,
1527
- innerText: truncate(el.textContent?.trim(), 200),
1528
- parent,
1972
+ innerText,
1973
+ componentSource,
1974
+ parent: el.parentElement
1975
+ ? {
1976
+ selector: getSemanticSelector(el.parentElement),
1977
+ tag: el.parentElement.tagName.toLowerCase(),
1978
+ }
1979
+ : undefined,
1529
1980
  };
1530
1981
  }
1531
1982
 
1983
+ private async cropDataUrl(dataUrl: string, region: RegionContext): Promise<string> {
1984
+ return new Promise((resolve) => {
1985
+ const img = new Image();
1986
+ img.onload = () => {
1987
+ const canvas = document.createElement('canvas');
1988
+ canvas.width = region.width;
1989
+ canvas.height = region.height;
1990
+ const ctx = canvas.getContext('2d');
1991
+ if (!ctx) {
1992
+ resolve(dataUrl);
1993
+ return;
1994
+ }
1995
+
1996
+ const dpr = window.devicePixelRatio || 1;
1997
+ ctx.drawImage(
1998
+ img,
1999
+ region.x * dpr,
2000
+ region.y * dpr,
2001
+ region.width * dpr,
2002
+ region.height * dpr,
2003
+ 0,
2004
+ 0,
2005
+ region.width,
2006
+ region.height
2007
+ );
2008
+ resolve(canvas.toDataURL('image/png'));
2009
+ };
2010
+ img.onerror = () => resolve(dataUrl);
2011
+ img.src = dataUrl;
2012
+ });
2013
+ }
2014
+
1532
2015
  public destroy(): void {
1533
2016
  for (const cleanup of this.cleanups) {
1534
- try {
1535
- cleanup();
1536
- } catch {}
2017
+ cleanup();
1537
2018
  }
1538
2019
  this.cleanups = [];
1539
2020
 
1540
- if (this.container && this.container.parentElement) {
1541
- this.container.parentElement.removeChild(this.container);
2021
+ if (this.container && this.container.parentNode) {
2022
+ this.container.parentNode.removeChild(this.container);
2023
+ this.container = null;
2024
+ this.shadowRoot = null;
1542
2025
  }
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
2026
  }
1553
2027
  }