@eyeglass/inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1932 @@
1
+ /**
2
+ * Eyeglass Inspector - Glass UI for visual element inspection
3
+ */
4
+ import { captureSnapshot } from './snapshot.js';
5
+ const BRIDGE_URL = 'http://localhost:3300';
6
+ const STORAGE_KEY = 'eyeglass_session';
7
+ const HISTORY_KEY = 'eyeglass_history';
8
+ const SESSION_TTL = 10000; // 10 seconds
9
+ // Eye cursor as base64-encoded SVG (16x16 eye icon, indigo color)
10
+ const EYE_CURSOR = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM2MzY2ZjEiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xIDEyczQtOCAxMS04IDExIDggMTEgOC00IDgtMTEgOC0xMS04LTExLTh6Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iIzYzNjZmMSIvPjwvc3ZnPg==") 8 8, crosshair`;
11
+ const STYLES = `
12
+ :host {
13
+ all: initial;
14
+ position: fixed;
15
+ top: 0;
16
+ left: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ z-index: 2147483647;
20
+ pointer-events: none;
21
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ font-size: 13px;
23
+ line-height: 1.5;
24
+ box-sizing: border-box;
25
+ --glass-bg: rgba(255, 255, 255, 0.72);
26
+ --glass-border: rgba(0, 0, 0, 0.25);
27
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
28
+ --divider: rgba(0, 0, 0, 0.18);
29
+ --text-primary: #0f172a;
30
+ --text-secondary: #64748b;
31
+ --text-muted: #94a3b8;
32
+ --accent: #6366f1;
33
+ --accent-soft: rgba(99, 102, 241, 0.1);
34
+ --success: #10b981;
35
+ --error: #ef4444;
36
+ --border-radius: 16px;
37
+ --border-radius-sm: 10px;
38
+ }
39
+
40
+ *, *::before, *::after {
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ /* Highlight overlay */
45
+ .highlight {
46
+ position: absolute;
47
+ z-index: 2147483640;
48
+ border: 2px solid var(--accent);
49
+ background: rgba(99, 102, 241, 0.06);
50
+ pointer-events: none;
51
+ border-radius: 6px;
52
+ transition: all 0.1s ease-out;
53
+ box-shadow:
54
+ 0 0 0 3px rgba(99, 102, 241, 0.08),
55
+ 0 2px 8px rgba(99, 102, 241, 0.1);
56
+ }
57
+
58
+ .highlight.no-transition {
59
+ transition: none;
60
+ }
61
+
62
+ /* Glass Panel */
63
+ .glass-panel {
64
+ position: absolute;
65
+ z-index: 2147483647;
66
+ background: var(--glass-bg);
67
+ backdrop-filter: blur(20px) saturate(180%);
68
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
69
+ border: 1px solid var(--glass-border);
70
+ border-radius: var(--border-radius);
71
+ box-shadow: var(--glass-shadow);
72
+ pointer-events: auto;
73
+ width: 340px;
74
+ overflow: hidden;
75
+ animation: panelIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
76
+ cursor: default;
77
+ }
78
+
79
+ .glass-panel *, .glass-panel *::before, .glass-panel *::after {
80
+ cursor: inherit;
81
+ }
82
+
83
+ .glass-panel button, .glass-panel input {
84
+ cursor: pointer;
85
+ }
86
+
87
+ .glass-panel input[type="text"] {
88
+ cursor: text;
89
+ }
90
+
91
+ @keyframes panelIn {
92
+ from {
93
+ opacity: 0;
94
+ transform: translateY(8px) scale(0.96);
95
+ }
96
+ to {
97
+ opacity: 1;
98
+ transform: translateY(0) scale(1);
99
+ }
100
+ }
101
+
102
+ /* Panel Header */
103
+ .panel-header {
104
+ padding: 14px 16px;
105
+ border-bottom: 1px solid var(--divider);
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 10px;
109
+ cursor: grab;
110
+ user-select: none;
111
+ }
112
+
113
+ .panel-header:active {
114
+ cursor: grabbing;
115
+ }
116
+
117
+ .component-tag {
118
+ font-family: 'SF Mono', 'Fira Code', monospace;
119
+ font-size: 12px;
120
+ font-weight: 500;
121
+ color: var(--accent);
122
+ background: var(--accent-soft);
123
+ padding: 4px 10px;
124
+ border-radius: 6px;
125
+ letter-spacing: -0.01em;
126
+ }
127
+
128
+ .file-path {
129
+ font-size: 11px;
130
+ color: var(--text-muted);
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
134
+ flex: 1;
135
+ }
136
+
137
+ .close-btn {
138
+ width: 24px;
139
+ height: 24px;
140
+ border: none;
141
+ background: transparent;
142
+ color: var(--text-muted);
143
+ cursor: pointer;
144
+ border-radius: 6px;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ transition: all 0.15s;
149
+ font-size: 18px;
150
+ line-height: 1;
151
+ }
152
+
153
+ .close-btn:hover {
154
+ background: rgba(0, 0, 0, 0.05);
155
+ color: var(--text-secondary);
156
+ }
157
+
158
+ /* User Request */
159
+ .user-request {
160
+ padding: 12px 16px;
161
+ background: rgba(0, 0, 0, 0.02);
162
+ border-bottom: 1px solid var(--divider);
163
+ }
164
+
165
+ .user-request-label {
166
+ font-size: 10px;
167
+ font-weight: 600;
168
+ text-transform: uppercase;
169
+ letter-spacing: 0.05em;
170
+ color: var(--text-muted);
171
+ margin-bottom: 4px;
172
+ }
173
+
174
+ .user-request-text {
175
+ color: var(--text-primary);
176
+ font-weight: 500;
177
+ }
178
+
179
+ /* Input Mode */
180
+ .input-area {
181
+ padding: 12px 16px 16px;
182
+ }
183
+
184
+ .input-field {
185
+ width: 100%;
186
+ padding: 10px 14px;
187
+ border: 1px solid rgba(0, 0, 0, 0.08);
188
+ border-radius: var(--border-radius-sm);
189
+ font-size: 13px;
190
+ font-family: inherit;
191
+ background: rgba(255, 255, 255, 0.6);
192
+ color: var(--text-primary);
193
+ outline: none;
194
+ transition: all 0.15s;
195
+ }
196
+
197
+ .input-field::placeholder {
198
+ color: var(--text-muted);
199
+ }
200
+
201
+ .input-field:focus {
202
+ border-color: var(--accent);
203
+ box-shadow: 0 0 0 3px var(--accent-soft);
204
+ background: white;
205
+ }
206
+
207
+ .btn-row {
208
+ display: flex;
209
+ gap: 8px;
210
+ margin-top: 10px;
211
+ }
212
+
213
+ .btn {
214
+ flex: 1;
215
+ padding: 9px 14px;
216
+ border: none;
217
+ border-radius: var(--border-radius-sm);
218
+ font-size: 12px;
219
+ font-weight: 600;
220
+ font-family: inherit;
221
+ cursor: pointer;
222
+ transition: all 0.15s;
223
+ }
224
+
225
+ .btn-primary {
226
+ background: var(--accent);
227
+ color: white;
228
+ }
229
+
230
+ .btn-primary:hover {
231
+ background: #4f46e5;
232
+ transform: translateY(-1px);
233
+ }
234
+
235
+ .btn-secondary {
236
+ background: rgba(0, 0, 0, 0.04);
237
+ color: var(--text-secondary);
238
+ }
239
+
240
+ .btn-secondary:hover {
241
+ background: rgba(0, 0, 0, 0.08);
242
+ }
243
+
244
+ /* Activity Feed */
245
+ .activity-feed {
246
+ max-height: 280px;
247
+ overflow-y: auto;
248
+ padding: 8px 0;
249
+ }
250
+
251
+ .activity-feed::-webkit-scrollbar {
252
+ width: 6px;
253
+ }
254
+
255
+ .activity-feed::-webkit-scrollbar-track {
256
+ background: transparent;
257
+ }
258
+
259
+ .activity-feed::-webkit-scrollbar-thumb {
260
+ background: rgba(0, 0, 0, 0.1);
261
+ border-radius: 3px;
262
+ }
263
+
264
+ .activity-item {
265
+ padding: 8px 16px;
266
+ display: flex;
267
+ gap: 10px;
268
+ align-items: flex-start;
269
+ animation: itemIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
270
+ }
271
+
272
+ @keyframes itemIn {
273
+ from {
274
+ opacity: 0;
275
+ transform: translateX(-8px);
276
+ }
277
+ to {
278
+ opacity: 1;
279
+ transform: translateX(0);
280
+ }
281
+ }
282
+
283
+ .activity-icon {
284
+ width: 20px;
285
+ height: 20px;
286
+ border-radius: 50%;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ flex-shrink: 0;
291
+ font-size: 10px;
292
+ }
293
+
294
+ .activity-icon.status { background: var(--accent-soft); color: var(--accent); }
295
+ .activity-icon.thought { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
296
+ .activity-icon.action { background: rgba(14, 165, 233, 0.1); color: #0ea5e9; }
297
+ .activity-icon.question { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
298
+ .activity-icon.success { background: rgba(16, 185, 129, 0.1); color: var(--success); }
299
+ .activity-icon.error { background: rgba(239, 68, 68, 0.1); color: var(--error); }
300
+
301
+ .activity-content {
302
+ flex: 1;
303
+ min-width: 0;
304
+ }
305
+
306
+ .activity-text {
307
+ color: var(--text-primary);
308
+ word-wrap: break-word;
309
+ }
310
+
311
+ .activity-text.muted {
312
+ color: var(--text-secondary);
313
+ }
314
+
315
+ .activity-target {
316
+ font-family: 'SF Mono', 'Fira Code', monospace;
317
+ font-size: 11px;
318
+ color: var(--text-muted);
319
+ margin-top: 2px;
320
+ }
321
+
322
+ /* Question UI */
323
+ .question-box {
324
+ background: rgba(245, 158, 11, 0.06);
325
+ border: 1px solid rgba(245, 158, 11, 0.15);
326
+ border-radius: var(--border-radius-sm);
327
+ padding: 12px;
328
+ margin: 8px 16px;
329
+ animation: questionIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
330
+ }
331
+
332
+ @keyframes questionIn {
333
+ from {
334
+ opacity: 0;
335
+ transform: scale(0.95);
336
+ }
337
+ to {
338
+ opacity: 1;
339
+ transform: scale(1);
340
+ }
341
+ }
342
+
343
+ .question-text {
344
+ font-weight: 500;
345
+ color: var(--text-primary);
346
+ margin-bottom: 10px;
347
+ }
348
+
349
+ .question-options {
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ gap: 6px;
353
+ }
354
+
355
+ .question-option {
356
+ padding: 7px 14px;
357
+ border: 1px solid rgba(0, 0, 0, 0.1);
358
+ border-radius: 8px;
359
+ background: white;
360
+ font-size: 12px;
361
+ font-weight: 500;
362
+ font-family: inherit;
363
+ color: var(--text-primary);
364
+ cursor: pointer;
365
+ transition: all 0.15s;
366
+ }
367
+
368
+ .question-option:hover {
369
+ border-color: var(--accent);
370
+ background: var(--accent-soft);
371
+ color: var(--accent);
372
+ }
373
+
374
+ /* Status Footer */
375
+ .panel-footer {
376
+ padding: 10px 16px;
377
+ border-top: 1px solid var(--divider);
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 8px;
381
+ }
382
+
383
+ .status-indicator {
384
+ width: 8px;
385
+ height: 8px;
386
+ border-radius: 50%;
387
+ background: var(--text-muted);
388
+ }
389
+
390
+ .status-indicator.pending {
391
+ background: var(--accent);
392
+ animation: pulse 1.5s ease-in-out infinite;
393
+ }
394
+
395
+ .status-indicator.fixing {
396
+ background: #0ea5e9;
397
+ animation: pulse 1s ease-in-out infinite;
398
+ }
399
+
400
+ .status-indicator.success { background: var(--success); }
401
+ .status-indicator.failed { background: var(--error); }
402
+
403
+ @keyframes pulse {
404
+ 0%, 100% { opacity: 1; transform: scale(1); }
405
+ 50% { opacity: 0.5; transform: scale(0.9); }
406
+ }
407
+
408
+ .status-text {
409
+ font-size: 12px;
410
+ color: var(--text-secondary);
411
+ flex: 1;
412
+ }
413
+
414
+ /* Done state */
415
+ .panel-footer.done {
416
+ background: rgba(16, 185, 129, 0.06);
417
+ }
418
+
419
+ .panel-footer.done .status-text {
420
+ color: var(--success);
421
+ font-weight: 500;
422
+ }
423
+
424
+ /* Result Toast - shows after page reload */
425
+ .result-toast {
426
+ position: fixed;
427
+ bottom: 24px;
428
+ right: 24px;
429
+ background: var(--glass-bg);
430
+ backdrop-filter: blur(20px) saturate(180%);
431
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
432
+ border: 1px solid var(--glass-border);
433
+ border-radius: var(--border-radius);
434
+ box-shadow: var(--glass-shadow);
435
+ padding: 14px 18px;
436
+ pointer-events: auto;
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 12px;
440
+ animation: toastIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
441
+ max-width: 320px;
442
+ }
443
+
444
+ @keyframes toastIn {
445
+ from {
446
+ opacity: 0;
447
+ transform: translateY(16px) scale(0.95);
448
+ }
449
+ to {
450
+ opacity: 1;
451
+ transform: translateY(0) scale(1);
452
+ }
453
+ }
454
+
455
+ .toast-icon {
456
+ width: 32px;
457
+ height: 32px;
458
+ border-radius: 50%;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ font-size: 16px;
463
+ flex-shrink: 0;
464
+ }
465
+
466
+ .toast-icon.success {
467
+ background: rgba(16, 185, 129, 0.15);
468
+ color: var(--success);
469
+ }
470
+
471
+ .toast-icon.failed {
472
+ background: rgba(239, 68, 68, 0.15);
473
+ color: var(--error);
474
+ }
475
+
476
+ .toast-content {
477
+ flex: 1;
478
+ min-width: 0;
479
+ }
480
+
481
+ .toast-title {
482
+ font-weight: 600;
483
+ color: var(--text-primary);
484
+ margin-bottom: 2px;
485
+ }
486
+
487
+ .toast-message {
488
+ font-size: 12px;
489
+ color: var(--text-secondary);
490
+ overflow: hidden;
491
+ text-overflow: ellipsis;
492
+ white-space: nowrap;
493
+ }
494
+
495
+ .toast-close {
496
+ width: 24px;
497
+ height: 24px;
498
+ border: none;
499
+ background: transparent;
500
+ color: var(--text-muted);
501
+ cursor: pointer;
502
+ border-radius: 6px;
503
+ display: flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ font-size: 16px;
507
+ }
508
+
509
+ .toast-close:hover {
510
+ background: rgba(0, 0, 0, 0.05);
511
+ color: var(--text-secondary);
512
+ }
513
+
514
+ /* Hub - Request History */
515
+ .hub {
516
+ position: fixed;
517
+ bottom: 16px;
518
+ left: 16px;
519
+ background: var(--glass-bg);
520
+ backdrop-filter: blur(20px) saturate(180%);
521
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
522
+ border: 1px solid var(--glass-border);
523
+ border-radius: 10px;
524
+ box-shadow: var(--glass-shadow);
525
+ pointer-events: auto;
526
+ min-width: 36px;
527
+ max-width: 200px;
528
+ overflow: hidden;
529
+ animation: hubIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
530
+ cursor: default;
531
+ }
532
+
533
+ .hub *, .hub *::before, .hub *::after {
534
+ cursor: inherit;
535
+ }
536
+
537
+ .hub button {
538
+ cursor: pointer;
539
+ }
540
+
541
+ @keyframes hubIn {
542
+ from {
543
+ opacity: 0;
544
+ transform: translateY(8px) scale(0.96);
545
+ }
546
+ to {
547
+ opacity: 1;
548
+ transform: translateY(0) scale(1);
549
+ }
550
+ }
551
+
552
+ .hub.disabled {
553
+ opacity: 0.5;
554
+ }
555
+
556
+ .hub-header {
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: space-between;
560
+ gap: 6px;
561
+ padding: 6px 8px;
562
+ cursor: pointer;
563
+ user-select: none;
564
+ }
565
+
566
+ .hub-header:hover {
567
+ background: rgba(0, 0, 0, 0.03);
568
+ }
569
+
570
+ .hub-header-left {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 6px;
574
+ }
575
+
576
+ .hub-logo {
577
+ width: 20px;
578
+ height: 20px;
579
+ background: var(--accent);
580
+ border-radius: 5px;
581
+ display: flex;
582
+ align-items: center;
583
+ justify-content: center;
584
+ font-size: 11px;
585
+ color: white;
586
+ flex-shrink: 0;
587
+ }
588
+
589
+ .hub-title {
590
+ font-size: 11px;
591
+ font-weight: 600;
592
+ color: var(--text-primary);
593
+ flex: 1;
594
+ }
595
+
596
+ .hub-badge {
597
+ font-size: 9px;
598
+ font-weight: 600;
599
+ background: var(--accent);
600
+ color: white;
601
+ padding: 1px 5px;
602
+ border-radius: 8px;
603
+ min-width: 14px;
604
+ text-align: center;
605
+ }
606
+
607
+ .hub-toggle {
608
+ width: 16px;
609
+ height: 16px;
610
+ border: none;
611
+ background: transparent;
612
+ color: var(--text-muted);
613
+ cursor: pointer;
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ font-size: 10px;
618
+ transition: transform 0.2s;
619
+ }
620
+
621
+ .hub-toggle.expanded {
622
+ transform: rotate(180deg);
623
+ }
624
+
625
+ .hub-disable {
626
+ width: 20px;
627
+ height: 20px;
628
+ border: none;
629
+ background: transparent;
630
+ color: var(--text-muted);
631
+ cursor: pointer;
632
+ border-radius: 4px;
633
+ display: flex;
634
+ align-items: center;
635
+ justify-content: center;
636
+ transition: all 0.15s;
637
+ flex-shrink: 0;
638
+ }
639
+
640
+ .hub-disable:hover {
641
+ background: rgba(0, 0, 0, 0.05);
642
+ color: var(--text-secondary);
643
+ }
644
+
645
+ .hub-disable.active {
646
+ color: var(--accent);
647
+ }
648
+
649
+ .hub-disable svg {
650
+ width: 14px;
651
+ height: 14px;
652
+ }
653
+
654
+ .hub-content {
655
+ border-top: 1px solid var(--divider);
656
+ max-height: 0;
657
+ overflow: hidden;
658
+ transition: max-height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
659
+ }
660
+
661
+ .hub-content.expanded {
662
+ max-height: 220px;
663
+ }
664
+
665
+ .hub-list {
666
+ max-height: 200px;
667
+ overflow-y: auto;
668
+ padding: 4px 0;
669
+ }
670
+
671
+ .hub-list::-webkit-scrollbar {
672
+ width: 6px;
673
+ }
674
+
675
+ .hub-list::-webkit-scrollbar-track {
676
+ background: transparent;
677
+ }
678
+
679
+ .hub-list::-webkit-scrollbar-thumb {
680
+ background: rgba(0, 0, 0, 0.1);
681
+ border-radius: 3px;
682
+ }
683
+
684
+ .hub-item {
685
+ padding: 5px 8px;
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 8px;
689
+ }
690
+
691
+ .hub-item:hover {
692
+ background: rgba(0, 0, 0, 0.02);
693
+ }
694
+
695
+ .hub-item-status {
696
+ width: 6px;
697
+ height: 6px;
698
+ border-radius: 50%;
699
+ flex-shrink: 0;
700
+ }
701
+
702
+ .hub-item-status.pending { background: var(--accent); animation: pulse 1.5s ease-in-out infinite; }
703
+ .hub-item-status.fixing { background: #0ea5e9; animation: pulse 1s ease-in-out infinite; }
704
+ .hub-item-status.success { background: var(--success); }
705
+ .hub-item-status.failed { background: var(--error); }
706
+
707
+ .hub-item-content {
708
+ flex: 1;
709
+ min-width: 0;
710
+ }
711
+
712
+ .hub-item-component {
713
+ font-size: 10px;
714
+ font-weight: 500;
715
+ color: var(--text-secondary);
716
+ }
717
+
718
+ .hub-item-note {
719
+ font-size: 11px;
720
+ color: var(--text-primary);
721
+ word-wrap: break-word;
722
+ }
723
+
724
+ .hub-item-undo {
725
+ width: 18px;
726
+ height: 18px;
727
+ border: none;
728
+ background: transparent;
729
+ color: var(--text-muted);
730
+ cursor: pointer;
731
+ border-radius: 3px;
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ font-size: 10px;
736
+ opacity: 0;
737
+ transition: all 0.15s;
738
+ flex-shrink: 0;
739
+ }
740
+
741
+ .hub-item:hover .hub-item-undo {
742
+ opacity: 1;
743
+ }
744
+
745
+ .hub-item-undo:hover {
746
+ background: rgba(239, 68, 68, 0.1);
747
+ color: var(--error);
748
+ }
749
+
750
+ .hub-empty {
751
+ padding: 10px 8px;
752
+ text-align: center;
753
+ font-size: 10px;
754
+ color: var(--text-muted);
755
+ }
756
+
757
+ /* Collapsed hub (minimal) */
758
+ .hub.collapsed .hub-title,
759
+ .hub.collapsed .hub-toggle {
760
+ display: none;
761
+ }
762
+
763
+ .hub.collapsed .hub-header {
764
+ padding: 5px;
765
+ }
766
+
767
+ .hub.collapsed .hub-header-left {
768
+ gap: 4px;
769
+ }
770
+
771
+ /* Multi-select styles */
772
+ .highlight.multi {
773
+ border-style: dashed;
774
+ border-width: 2px;
775
+ box-shadow:
776
+ 0 0 0 2px rgba(99, 102, 241, 0.06),
777
+ 0 2px 6px rgba(99, 102, 241, 0.08);
778
+ }
779
+
780
+ .highlight-badge {
781
+ position: absolute;
782
+ top: -10px;
783
+ left: -10px;
784
+ width: 20px;
785
+ height: 20px;
786
+ background: var(--accent);
787
+ color: white;
788
+ font-size: 11px;
789
+ font-weight: 600;
790
+ border-radius: 50%;
791
+ display: flex;
792
+ align-items: center;
793
+ justify-content: center;
794
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
795
+ }
796
+
797
+ .multi-select-icon {
798
+ width: 24px;
799
+ height: 24px;
800
+ border: none;
801
+ background: transparent;
802
+ color: var(--text-muted);
803
+ cursor: pointer;
804
+ border-radius: 6px;
805
+ display: flex;
806
+ align-items: center;
807
+ justify-content: center;
808
+ transition: all 0.15s;
809
+ font-size: 16px;
810
+ line-height: 1;
811
+ }
812
+
813
+ .multi-select-icon:hover {
814
+ background: var(--accent-soft);
815
+ color: var(--accent);
816
+ }
817
+
818
+ .multi-select-icon.active {
819
+ background: var(--accent);
820
+ color: white;
821
+ }
822
+
823
+ .selected-list {
824
+ padding: 8px 16px;
825
+ border-bottom: 1px solid var(--divider);
826
+ background: rgba(0, 0, 0, 0.02);
827
+ }
828
+
829
+ .selected-list-header {
830
+ display: flex;
831
+ justify-content: space-between;
832
+ align-items: center;
833
+ margin-bottom: 8px;
834
+ }
835
+
836
+ .selected-count {
837
+ font-size: 11px;
838
+ font-weight: 600;
839
+ color: var(--text-secondary);
840
+ }
841
+
842
+ .selected-chips {
843
+ display: flex;
844
+ flex-wrap: wrap;
845
+ gap: 6px;
846
+ }
847
+
848
+ .selected-chip {
849
+ display: flex;
850
+ align-items: center;
851
+ gap: 4px;
852
+ padding: 4px 8px;
853
+ background: var(--accent-soft);
854
+ border-radius: 6px;
855
+ font-size: 11px;
856
+ color: var(--accent);
857
+ font-weight: 500;
858
+ }
859
+
860
+ .selected-chip-number {
861
+ width: 16px;
862
+ height: 16px;
863
+ background: var(--accent);
864
+ color: white;
865
+ border-radius: 50%;
866
+ display: flex;
867
+ align-items: center;
868
+ justify-content: center;
869
+ font-size: 10px;
870
+ font-weight: 600;
871
+ }
872
+
873
+ .selected-chip-remove {
874
+ width: 16px;
875
+ height: 16px;
876
+ border: none;
877
+ background: transparent;
878
+ color: var(--accent);
879
+ cursor: pointer;
880
+ border-radius: 50%;
881
+ display: flex;
882
+ align-items: center;
883
+ justify-content: center;
884
+ font-size: 14px;
885
+ line-height: 1;
886
+ opacity: 0.7;
887
+ transition: all 0.15s;
888
+ padding: 0;
889
+ }
890
+
891
+ .selected-chip-remove:hover {
892
+ opacity: 1;
893
+ background: rgba(99, 102, 241, 0.2);
894
+ }
895
+
896
+ .multi-mode-hint {
897
+ padding: 8px 16px;
898
+ background: var(--accent-soft);
899
+ border-bottom: 1px solid var(--divider);
900
+ font-size: 11px;
901
+ color: var(--accent);
902
+ text-align: center;
903
+ }
904
+ `;
905
+ export class EyeglassInspector extends HTMLElement {
906
+ constructor() {
907
+ super();
908
+ this.highlight = null;
909
+ this.panel = null;
910
+ this.toast = null;
911
+ this.hub = null;
912
+ this.currentElement = null;
913
+ this.currentSnapshot = null;
914
+ this.interactionId = null;
915
+ this.frozen = false;
916
+ this.eventSource = null;
917
+ this.throttleTimeout = null;
918
+ this.mode = 'input';
919
+ this.activityEvents = [];
920
+ this.currentStatus = 'idle';
921
+ this.hubExpanded = false;
922
+ this.inspectorEnabled = true;
923
+ this.history = [];
924
+ this.isDragging = false;
925
+ this.dragOffset = { x: 0, y: 0 };
926
+ this.customPanelPosition = null;
927
+ // Multi-select state
928
+ this.multiSelectMode = false;
929
+ this.selectedElements = [];
930
+ this.selectedSnapshots = [];
931
+ this.multiSelectHighlights = [];
932
+ this.submittedSnapshots = []; // Track what was submitted for activity mode display
933
+ // Cursor style element (injected into document head)
934
+ this.cursorStyleElement = null;
935
+ // Scroll handling
936
+ this.scrollTimeout = null;
937
+ this.handlePanelDrag = (e) => {
938
+ if (!this.isDragging || !this.panel)
939
+ return;
940
+ const x = Math.max(0, Math.min(e.clientX - this.dragOffset.x, window.innerWidth - 340));
941
+ const y = Math.max(0, Math.min(e.clientY - this.dragOffset.y, window.innerHeight - 100));
942
+ this.customPanelPosition = { x, y };
943
+ this.panel.style.left = `${x}px`;
944
+ this.panel.style.top = `${y}px`;
945
+ };
946
+ this.handlePanelDragEnd = () => {
947
+ this.isDragging = false;
948
+ document.removeEventListener('mousemove', this.handlePanelDrag);
949
+ document.removeEventListener('mouseup', this.handlePanelDragEnd);
950
+ };
951
+ this.shadow = this.attachShadow({ mode: 'closed' });
952
+ }
953
+ connectedCallback() {
954
+ const style = document.createElement('style');
955
+ style.textContent = STYLES;
956
+ this.shadow.appendChild(style);
957
+ this.highlight = document.createElement('div');
958
+ this.highlight.className = 'highlight';
959
+ this.highlight.style.display = 'none';
960
+ this.shadow.appendChild(this.highlight);
961
+ this.handleMouseMove = this.handleMouseMove.bind(this);
962
+ this.handleClick = this.handleClick.bind(this);
963
+ this.handleKeyDown = this.handleKeyDown.bind(this);
964
+ this.handlePanelDragStart = this.handlePanelDragStart.bind(this);
965
+ this.handleScroll = this.handleScroll.bind(this);
966
+ document.addEventListener('mousemove', this.handleMouseMove, true);
967
+ document.addEventListener('click', this.handleClick, true);
968
+ document.addEventListener('keydown', this.handleKeyDown, true);
969
+ window.addEventListener('scroll', this.handleScroll, true);
970
+ this.loadHistory();
971
+ this.renderHub();
972
+ this.connectSSE();
973
+ this.restoreSession();
974
+ this.updateCursor();
975
+ }
976
+ saveSession(message) {
977
+ if (!this.interactionId)
978
+ return;
979
+ const session = {
980
+ interactionId: this.interactionId,
981
+ userNote: this._userNote || '',
982
+ componentName: this.currentSnapshot?.framework.componentName || this.currentSnapshot?.tagName || 'element',
983
+ status: this.currentStatus,
984
+ message,
985
+ timestamp: Date.now(),
986
+ };
987
+ try {
988
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session));
989
+ }
990
+ catch (e) {
991
+ // Ignore storage errors
992
+ }
993
+ }
994
+ restoreSession() {
995
+ try {
996
+ const stored = sessionStorage.getItem(STORAGE_KEY);
997
+ if (!stored)
998
+ return;
999
+ const session = JSON.parse(stored);
1000
+ // Check if session is still fresh
1001
+ if (Date.now() - session.timestamp > SESSION_TTL) {
1002
+ sessionStorage.removeItem(STORAGE_KEY);
1003
+ return;
1004
+ }
1005
+ // Only show toast for completed sessions
1006
+ if (session.status === 'success' || session.status === 'failed') {
1007
+ this.showResultToast(session);
1008
+ sessionStorage.removeItem(STORAGE_KEY);
1009
+ }
1010
+ }
1011
+ catch (e) {
1012
+ // Ignore parse errors
1013
+ }
1014
+ }
1015
+ showResultToast(session) {
1016
+ this.toast = document.createElement('div');
1017
+ this.toast.className = 'result-toast';
1018
+ const isSuccess = session.status === 'success';
1019
+ const icon = isSuccess ? '✓' : '✕';
1020
+ const title = isSuccess ? 'Done!' : 'Failed';
1021
+ this.toast.innerHTML = `
1022
+ <div class="toast-icon ${session.status}">${icon}</div>
1023
+ <div class="toast-content">
1024
+ <div class="toast-title">${title}</div>
1025
+ <div class="toast-message">${this.escapeHtml(session.message || session.userNote)}</div>
1026
+ </div>
1027
+ <button class="toast-close">&times;</button>
1028
+ `;
1029
+ const closeBtn = this.toast.querySelector('.toast-close');
1030
+ closeBtn.addEventListener('click', () => this.hideToast());
1031
+ this.shadow.appendChild(this.toast);
1032
+ // Auto-hide after 4 seconds
1033
+ setTimeout(() => this.hideToast(), 4000);
1034
+ }
1035
+ hideToast() {
1036
+ if (this.toast) {
1037
+ this.toast.remove();
1038
+ this.toast = null;
1039
+ }
1040
+ }
1041
+ loadHistory() {
1042
+ try {
1043
+ const stored = sessionStorage.getItem(HISTORY_KEY);
1044
+ if (stored) {
1045
+ this.history = JSON.parse(stored);
1046
+ }
1047
+ }
1048
+ catch (e) {
1049
+ this.history = [];
1050
+ }
1051
+ }
1052
+ saveHistory() {
1053
+ try {
1054
+ sessionStorage.setItem(HISTORY_KEY, JSON.stringify(this.history));
1055
+ }
1056
+ catch (e) {
1057
+ // Ignore storage errors
1058
+ }
1059
+ }
1060
+ addToHistory(item) {
1061
+ // Check if this interaction already exists
1062
+ const existingIndex = this.history.findIndex(h => h.interactionId === item.interactionId);
1063
+ if (existingIndex >= 0) {
1064
+ this.history[existingIndex] = item;
1065
+ }
1066
+ else {
1067
+ this.history.unshift(item);
1068
+ // Keep only last 20 items
1069
+ if (this.history.length > 20) {
1070
+ this.history = this.history.slice(0, 20);
1071
+ }
1072
+ }
1073
+ this.saveHistory();
1074
+ this.renderHub();
1075
+ }
1076
+ updateHistoryStatus(interactionId, status) {
1077
+ const item = this.history.find(h => h.interactionId === interactionId);
1078
+ if (item) {
1079
+ item.status = status;
1080
+ this.saveHistory();
1081
+ this.renderHub();
1082
+ }
1083
+ }
1084
+ renderHub() {
1085
+ if (!this.hub) {
1086
+ this.hub = document.createElement('div');
1087
+ this.hub.className = 'hub';
1088
+ this.shadow.appendChild(this.hub);
1089
+ }
1090
+ const collapsedClass = this.hubExpanded ? '' : 'collapsed';
1091
+ const disabledClass = this.inspectorEnabled ? '' : 'disabled';
1092
+ const expandedClass = this.hubExpanded ? 'expanded' : '';
1093
+ const activeCount = this.history.filter(h => h.status === 'pending' || h.status === 'fixing').length;
1094
+ this.hub.className = `hub ${collapsedClass} ${disabledClass}`.trim();
1095
+ const eyeOpenSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
1096
+ const eyeClosedSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`;
1097
+ this.hub.innerHTML = `
1098
+ <div class="hub-header">
1099
+ <div class="hub-header-left">
1100
+ <div class="hub-logo">👁</div>
1101
+ <span class="hub-title">Eyeglass</span>
1102
+ ${activeCount > 0 ? `<span class="hub-badge">${activeCount}</span>` : ''}
1103
+ <button class="hub-toggle ${expandedClass}" title="Toggle history">▼</button>
1104
+ </div>
1105
+ <button class="hub-disable ${this.inspectorEnabled ? 'active' : ''}" title="${this.inspectorEnabled ? 'Disable' : 'Enable'} inspector">
1106
+ ${this.inspectorEnabled ? eyeOpenSvg : eyeClosedSvg}
1107
+ </button>
1108
+ </div>
1109
+ <div class="hub-content ${expandedClass}">
1110
+ ${this.history.length > 0 ? `
1111
+ <div class="hub-list">
1112
+ ${this.history.map(item => `
1113
+ <div class="hub-item" data-id="${item.interactionId}">
1114
+ <div class="hub-item-status ${item.status}"></div>
1115
+ <div class="hub-item-content">
1116
+ <div class="hub-item-component">${this.escapeHtml(item.componentName)}</div>
1117
+ <div class="hub-item-note">${this.escapeHtml(item.userNote)}</div>
1118
+ </div>
1119
+ ${item.status === 'success' ? `
1120
+ <button class="hub-item-undo" data-id="${item.interactionId}" title="Undo">↩</button>
1121
+ ` : ''}
1122
+ </div>
1123
+ `).join('')}
1124
+ </div>
1125
+ ` : `
1126
+ <div class="hub-empty">No requests yet</div>
1127
+ `}
1128
+ </div>
1129
+ `;
1130
+ // Wire up event handlers
1131
+ const header = this.hub.querySelector('.hub-header');
1132
+ const toggleBtn = this.hub.querySelector('.hub-toggle');
1133
+ const disableBtn = this.hub.querySelector('.hub-disable');
1134
+ // Toggle expand/collapse on header click (except disable button)
1135
+ header.addEventListener('click', (e) => {
1136
+ if (e.target === disableBtn)
1137
+ return;
1138
+ this.hubExpanded = !this.hubExpanded;
1139
+ this.renderHub();
1140
+ });
1141
+ // Toggle inspector enabled state
1142
+ disableBtn.addEventListener('click', (e) => {
1143
+ e.stopPropagation();
1144
+ this.inspectorEnabled = !this.inspectorEnabled;
1145
+ if (!this.inspectorEnabled) {
1146
+ this.unfreeze();
1147
+ }
1148
+ this.updateCursor();
1149
+ this.renderHub();
1150
+ });
1151
+ // Wire up undo buttons
1152
+ this.hub.querySelectorAll('.hub-item-undo').forEach(btn => {
1153
+ btn.addEventListener('click', (e) => {
1154
+ e.stopPropagation();
1155
+ const id = e.currentTarget.dataset.id;
1156
+ this.requestUndo(id);
1157
+ });
1158
+ });
1159
+ }
1160
+ async requestUndo(interactionId) {
1161
+ const itemIndex = this.history.findIndex(h => h.interactionId === interactionId);
1162
+ if (itemIndex === -1)
1163
+ return;
1164
+ // Mark as pending while undo is in progress
1165
+ this.history[itemIndex].status = 'pending';
1166
+ this.saveHistory();
1167
+ this.renderHub();
1168
+ try {
1169
+ const response = await fetch(`${BRIDGE_URL}/undo`, {
1170
+ method: 'POST',
1171
+ headers: { 'Content-Type': 'application/json' },
1172
+ body: JSON.stringify({ interactionId }),
1173
+ });
1174
+ if (response.ok) {
1175
+ // Remove from history on successful undo
1176
+ this.history.splice(itemIndex, 1);
1177
+ this.saveHistory();
1178
+ this.renderHub();
1179
+ }
1180
+ else {
1181
+ // Mark as failed if undo didn't work
1182
+ this.history[itemIndex].status = 'failed';
1183
+ this.saveHistory();
1184
+ this.renderHub();
1185
+ }
1186
+ }
1187
+ catch (err) {
1188
+ // Mark as failed on error
1189
+ if (this.history[itemIndex]) {
1190
+ this.history[itemIndex].status = 'failed';
1191
+ this.saveHistory();
1192
+ this.renderHub();
1193
+ }
1194
+ console.warn('Undo request failed:', err);
1195
+ }
1196
+ }
1197
+ disconnectedCallback() {
1198
+ document.removeEventListener('mousemove', this.handleMouseMove, true);
1199
+ document.removeEventListener('click', this.handleClick, true);
1200
+ document.removeEventListener('keydown', this.handleKeyDown, true);
1201
+ window.removeEventListener('scroll', this.handleScroll, true);
1202
+ this.eventSource?.close();
1203
+ // Clean up cursor style
1204
+ if (this.cursorStyleElement) {
1205
+ this.cursorStyleElement.remove();
1206
+ this.cursorStyleElement = null;
1207
+ }
1208
+ }
1209
+ connectSSE() {
1210
+ this.eventSource = new EventSource(`${BRIDGE_URL}/events`);
1211
+ this.eventSource.onmessage = (event) => {
1212
+ try {
1213
+ const data = JSON.parse(event.data);
1214
+ if (data.type === 'activity') {
1215
+ this.handleActivityEvent(data.payload);
1216
+ }
1217
+ }
1218
+ catch (e) {
1219
+ // Ignore parse errors
1220
+ }
1221
+ };
1222
+ this.eventSource.onerror = () => {
1223
+ this.eventSource?.close();
1224
+ setTimeout(() => this.connectSSE(), 3000);
1225
+ };
1226
+ }
1227
+ handleActivityEvent(event) {
1228
+ // Update history for any matching interaction, even if not current
1229
+ if (event.type === 'status') {
1230
+ this.updateHistoryStatus(event.interactionId, event.status);
1231
+ }
1232
+ if (event.interactionId !== this.interactionId)
1233
+ return;
1234
+ this.activityEvents.push(event);
1235
+ if (event.type === 'status') {
1236
+ this.currentStatus = event.status;
1237
+ // Persist session so we can show result after page reload
1238
+ this.saveSession(event.message);
1239
+ if (event.status === 'success' || event.status === 'failed') {
1240
+ setTimeout(() => this.unfreeze(), 4000);
1241
+ }
1242
+ }
1243
+ this.renderPanel();
1244
+ }
1245
+ handleMouseMove(e) {
1246
+ // In multi-select mode, continue raycasting even when frozen
1247
+ if (!this.multiSelectMode && this.frozen)
1248
+ return;
1249
+ if (!this.inspectorEnabled)
1250
+ return;
1251
+ // Don't raycast if hovering over our own UI (hub, panel, etc.)
1252
+ // When pointer-events: auto elements in our shadow DOM are hovered,
1253
+ // the host element will be in the composed path
1254
+ const path = e.composedPath();
1255
+ if (path.includes(this)) {
1256
+ if (!this.multiSelectMode) {
1257
+ this.hideHighlight();
1258
+ }
1259
+ return;
1260
+ }
1261
+ if (this.throttleTimeout)
1262
+ return;
1263
+ this.throttleTimeout = window.setTimeout(() => {
1264
+ this.throttleTimeout = null;
1265
+ }, 16);
1266
+ this.style.pointerEvents = 'none';
1267
+ const target = document.elementFromPoint(e.clientX, e.clientY);
1268
+ this.style.pointerEvents = '';
1269
+ if (!target || target === document.documentElement || target === document.body) {
1270
+ if (!this.multiSelectMode) {
1271
+ this.hideHighlight();
1272
+ }
1273
+ return;
1274
+ }
1275
+ if (this.shadow.contains(target))
1276
+ return;
1277
+ this.currentElement = target;
1278
+ this.showHighlight(target);
1279
+ }
1280
+ handleClick(e) {
1281
+ if (!this.inspectorEnabled)
1282
+ return;
1283
+ if (!this.currentElement)
1284
+ return;
1285
+ const path = e.composedPath();
1286
+ if (path.some((el) => el === this))
1287
+ return;
1288
+ e.preventDefault();
1289
+ e.stopPropagation();
1290
+ // In multi-select mode, add/toggle element in selection
1291
+ if (this.multiSelectMode) {
1292
+ this.toggleInSelection(this.currentElement);
1293
+ return;
1294
+ }
1295
+ // Normal single-select behavior
1296
+ if (this.frozen)
1297
+ return;
1298
+ this.freeze();
1299
+ }
1300
+ handleKeyDown(e) {
1301
+ if (e.key === 'Escape') {
1302
+ this.unfreeze();
1303
+ }
1304
+ }
1305
+ handleScroll() {
1306
+ if (!this.frozen)
1307
+ return;
1308
+ // Disable transitions during scroll for instant updates
1309
+ this.disableHighlightTransitions();
1310
+ // Update single highlight position
1311
+ if (this.currentElement && this.highlight && !this.multiSelectMode) {
1312
+ this.showHighlight(this.currentElement);
1313
+ }
1314
+ // Update multi-select highlights
1315
+ if (this.multiSelectMode && this.selectedElements.length > 0) {
1316
+ this.updateMultiSelectHighlightPositions();
1317
+ }
1318
+ // Re-enable transitions after scrolling stops
1319
+ if (this.scrollTimeout) {
1320
+ window.clearTimeout(this.scrollTimeout);
1321
+ }
1322
+ this.scrollTimeout = window.setTimeout(() => {
1323
+ this.enableHighlightTransitions();
1324
+ this.scrollTimeout = null;
1325
+ }, 150);
1326
+ }
1327
+ disableHighlightTransitions() {
1328
+ if (this.highlight) {
1329
+ this.highlight.classList.add('no-transition');
1330
+ }
1331
+ this.multiSelectHighlights.forEach(h => h.classList.add('no-transition'));
1332
+ }
1333
+ enableHighlightTransitions() {
1334
+ if (this.highlight) {
1335
+ this.highlight.classList.remove('no-transition');
1336
+ }
1337
+ this.multiSelectHighlights.forEach(h => h.classList.remove('no-transition'));
1338
+ }
1339
+ updateMultiSelectHighlightPositions() {
1340
+ const padding = 3;
1341
+ this.selectedElements.forEach((element, index) => {
1342
+ const highlight = this.multiSelectHighlights[index];
1343
+ if (!highlight)
1344
+ return;
1345
+ const rect = element.getBoundingClientRect();
1346
+ highlight.style.left = `${rect.left - padding}px`;
1347
+ highlight.style.top = `${rect.top - padding}px`;
1348
+ highlight.style.width = `${rect.width + padding * 2}px`;
1349
+ highlight.style.height = `${rect.height + padding * 2}px`;
1350
+ });
1351
+ }
1352
+ handlePanelDragStart(e) {
1353
+ // Don't drag if clicking on buttons
1354
+ if (e.target.closest('button'))
1355
+ return;
1356
+ this.isDragging = true;
1357
+ const panelRect = this.panel.getBoundingClientRect();
1358
+ this.dragOffset = {
1359
+ x: e.clientX - panelRect.left,
1360
+ y: e.clientY - panelRect.top,
1361
+ };
1362
+ document.addEventListener('mousemove', this.handlePanelDrag);
1363
+ document.addEventListener('mouseup', this.handlePanelDragEnd);
1364
+ }
1365
+ showHighlight(element) {
1366
+ if (!this.highlight)
1367
+ return;
1368
+ const rect = element.getBoundingClientRect();
1369
+ const padding = 3;
1370
+ this.highlight.style.display = 'block';
1371
+ this.highlight.style.left = `${rect.left - padding}px`;
1372
+ this.highlight.style.top = `${rect.top - padding}px`;
1373
+ this.highlight.style.width = `${rect.width + padding * 2}px`;
1374
+ this.highlight.style.height = `${rect.height + padding * 2}px`;
1375
+ }
1376
+ hideHighlight() {
1377
+ if (this.highlight) {
1378
+ this.highlight.style.display = 'none';
1379
+ }
1380
+ this.currentElement = null;
1381
+ }
1382
+ freeze() {
1383
+ if (!this.currentElement)
1384
+ return;
1385
+ this.frozen = true;
1386
+ this.currentSnapshot = captureSnapshot(this.currentElement);
1387
+ // Initialize selectedElements with the first element
1388
+ this.selectedElements = [this.currentElement];
1389
+ this.selectedSnapshots = [this.currentSnapshot];
1390
+ this.mode = 'input';
1391
+ this.activityEvents = [];
1392
+ this.currentStatus = 'idle';
1393
+ this.updateCursor();
1394
+ this.renderPanel();
1395
+ }
1396
+ enterMultiSelectMode() {
1397
+ if (!this.frozen || this.multiSelectMode)
1398
+ return;
1399
+ this.multiSelectMode = true;
1400
+ // Render highlight for the first selected element
1401
+ this.renderMultiSelectHighlights();
1402
+ this.updateCursor();
1403
+ this.renderPanel();
1404
+ }
1405
+ toggleInSelection(element) {
1406
+ if (!this.multiSelectMode)
1407
+ return;
1408
+ // Check if element is already selected (by reference)
1409
+ const existingIndex = this.selectedElements.indexOf(element);
1410
+ if (existingIndex >= 0) {
1411
+ // Remove from selection
1412
+ this.removeFromSelection(existingIndex);
1413
+ }
1414
+ else {
1415
+ // Add to selection (if under limit)
1416
+ if (this.selectedElements.length >= EyeglassInspector.MAX_SELECTION) {
1417
+ // Could show a toast/warning, for now just ignore
1418
+ return;
1419
+ }
1420
+ const snapshot = captureSnapshot(element);
1421
+ this.selectedElements.push(element);
1422
+ this.selectedSnapshots.push(snapshot);
1423
+ }
1424
+ this.renderMultiSelectHighlights();
1425
+ this.renderPanel();
1426
+ }
1427
+ removeFromSelection(index) {
1428
+ if (index < 0 || index >= this.selectedElements.length)
1429
+ return;
1430
+ // Don't allow removing the last element
1431
+ if (this.selectedElements.length === 1) {
1432
+ this.exitMultiSelectMode();
1433
+ return;
1434
+ }
1435
+ this.selectedElements.splice(index, 1);
1436
+ this.selectedSnapshots.splice(index, 1);
1437
+ this.renderMultiSelectHighlights();
1438
+ this.renderPanel();
1439
+ }
1440
+ exitMultiSelectMode() {
1441
+ this.multiSelectMode = false;
1442
+ // Keep the first selected element as the current single selection
1443
+ if (this.selectedElements.length > 0) {
1444
+ this.currentElement = this.selectedElements[0];
1445
+ this.currentSnapshot = this.selectedSnapshots[0];
1446
+ }
1447
+ this.selectedElements = this.currentElement ? [this.currentElement] : [];
1448
+ this.selectedSnapshots = this.currentSnapshot ? [this.currentSnapshot] : [];
1449
+ // Clear multi-select highlights
1450
+ this.clearMultiSelectHighlights();
1451
+ // Show single highlight for current element
1452
+ if (this.currentElement) {
1453
+ this.showHighlight(this.currentElement);
1454
+ }
1455
+ this.updateCursor();
1456
+ this.renderPanel();
1457
+ }
1458
+ renderMultiSelectHighlights() {
1459
+ // Clear existing multi-select highlights
1460
+ this.clearMultiSelectHighlights();
1461
+ const padding = 3;
1462
+ this.selectedElements.forEach((element, index) => {
1463
+ const rect = element.getBoundingClientRect();
1464
+ const highlight = document.createElement('div');
1465
+ highlight.className = 'highlight multi';
1466
+ highlight.style.display = 'block';
1467
+ highlight.style.left = `${rect.left - padding}px`;
1468
+ highlight.style.top = `${rect.top - padding}px`;
1469
+ highlight.style.width = `${rect.width + padding * 2}px`;
1470
+ highlight.style.height = `${rect.height + padding * 2}px`;
1471
+ // Add numbered badge
1472
+ const badge = document.createElement('div');
1473
+ badge.className = 'highlight-badge';
1474
+ badge.textContent = String(index + 1);
1475
+ highlight.appendChild(badge);
1476
+ this.shadow.appendChild(highlight);
1477
+ this.multiSelectHighlights.push(highlight);
1478
+ });
1479
+ // Hide the main single highlight when in multi-select mode
1480
+ if (this.highlight) {
1481
+ this.highlight.style.display = 'none';
1482
+ }
1483
+ }
1484
+ clearMultiSelectHighlights() {
1485
+ this.multiSelectHighlights.forEach((h) => h.remove());
1486
+ this.multiSelectHighlights = [];
1487
+ }
1488
+ unfreeze() {
1489
+ this.frozen = false;
1490
+ this.currentSnapshot = null;
1491
+ this.interactionId = null;
1492
+ this.mode = 'input';
1493
+ this.activityEvents = [];
1494
+ this.customPanelPosition = null;
1495
+ // Clear multi-select state
1496
+ this.multiSelectMode = false;
1497
+ this.selectedElements = [];
1498
+ this.selectedSnapshots = [];
1499
+ this.submittedSnapshots = [];
1500
+ this.clearMultiSelectHighlights();
1501
+ this.hidePanel();
1502
+ this.hideHighlight();
1503
+ this.updateCursor();
1504
+ // Clear persisted session
1505
+ try {
1506
+ sessionStorage.removeItem(STORAGE_KEY);
1507
+ }
1508
+ catch (e) {
1509
+ // Ignore
1510
+ }
1511
+ }
1512
+ renderPanel() {
1513
+ if (!this.currentSnapshot || !this.currentElement)
1514
+ return;
1515
+ const rect = this.currentElement.getBoundingClientRect();
1516
+ const { framework } = this.currentSnapshot;
1517
+ if (!this.panel) {
1518
+ this.panel = document.createElement('div');
1519
+ this.panel.className = 'glass-panel';
1520
+ this.shadow.appendChild(this.panel);
1521
+ }
1522
+ // Position panel - use custom position if user has dragged it
1523
+ if (this.customPanelPosition) {
1524
+ this.panel.style.left = `${this.customPanelPosition.x}px`;
1525
+ this.panel.style.top = `${this.customPanelPosition.y}px`;
1526
+ }
1527
+ else {
1528
+ const spaceBelow = window.innerHeight - rect.bottom;
1529
+ const panelHeight = this.mode === 'activity' ? 400 : 200;
1530
+ let top = rect.bottom + 12;
1531
+ if (spaceBelow < panelHeight && rect.top > panelHeight) {
1532
+ top = rect.top - panelHeight - 12;
1533
+ }
1534
+ let left = rect.left;
1535
+ if (left + 340 > window.innerWidth - 20) {
1536
+ left = window.innerWidth - 360;
1537
+ }
1538
+ if (left < 20)
1539
+ left = 20;
1540
+ this.panel.style.left = `${left}px`;
1541
+ this.panel.style.top = `${top}px`;
1542
+ }
1543
+ const componentName = framework.componentName || this.currentSnapshot.tagName;
1544
+ const filePath = framework.filePath
1545
+ ? framework.filePath.split('/').slice(-2).join('/')
1546
+ : null;
1547
+ if (this.mode === 'input') {
1548
+ this.renderInputMode(componentName, filePath);
1549
+ }
1550
+ else {
1551
+ this.renderActivityMode(componentName, filePath);
1552
+ }
1553
+ }
1554
+ renderInputMode(componentName, filePath) {
1555
+ if (!this.panel)
1556
+ return;
1557
+ const isMultiSelect = this.multiSelectMode;
1558
+ const multiSelectIconClass = isMultiSelect ? 'multi-select-icon active' : 'multi-select-icon';
1559
+ // Build selected list HTML for multi-select mode
1560
+ const selectedListHtml = isMultiSelect ? `
1561
+ <div class="selected-list">
1562
+ <div class="selected-list-header">
1563
+ <span class="selected-count">${this.selectedElements.length} element${this.selectedElements.length !== 1 ? 's' : ''} selected</span>
1564
+ </div>
1565
+ <div class="selected-chips">
1566
+ ${this.selectedSnapshots.map((snapshot, index) => {
1567
+ const name = snapshot.framework.componentName || snapshot.tagName;
1568
+ return `
1569
+ <div class="selected-chip" data-index="${index}">
1570
+ <span class="selected-chip-number">${index + 1}</span>
1571
+ <span>${this.escapeHtml(name)}</span>
1572
+ <button class="selected-chip-remove" data-index="${index}" title="Remove">&times;</button>
1573
+ </div>
1574
+ `;
1575
+ }).join('')}
1576
+ </div>
1577
+ </div>
1578
+ ` : '';
1579
+ const multiModeHint = isMultiSelect ? `
1580
+ <div class="multi-mode-hint">Click elements to add/remove from selection (max ${EyeglassInspector.MAX_SELECTION})</div>
1581
+ ` : '';
1582
+ this.panel.innerHTML = `
1583
+ <div class="panel-header">
1584
+ <span class="component-tag">&lt;${this.escapeHtml(componentName)} /&gt;</span>
1585
+ ${filePath ? `<span class="file-path">${this.escapeHtml(filePath)}</span>` : ''}
1586
+ <button class="${multiSelectIconClass}" title="${isMultiSelect ? 'Exit multi-select' : 'Select multiple elements'}">+</button>
1587
+ <button class="close-btn" title="Cancel (Esc)">&times;</button>
1588
+ </div>
1589
+ ${multiModeHint}
1590
+ ${selectedListHtml}
1591
+ <div class="input-area">
1592
+ <input
1593
+ type="text"
1594
+ class="input-field"
1595
+ placeholder="${isMultiSelect ? 'Describe what to change for these elements...' : 'What do you want to change?'}"
1596
+ autofocus
1597
+ />
1598
+ <div class="btn-row">
1599
+ <button class="btn btn-secondary">Cancel</button>
1600
+ <button class="btn btn-primary">Send</button>
1601
+ </div>
1602
+ </div>
1603
+ `;
1604
+ const input = this.panel.querySelector('.input-field');
1605
+ const closeBtn = this.panel.querySelector('.close-btn');
1606
+ const cancelBtn = this.panel.querySelector('.btn-secondary');
1607
+ const sendBtn = this.panel.querySelector('.btn-primary');
1608
+ const multiSelectBtn = this.panel.querySelector('.multi-select-icon');
1609
+ closeBtn.addEventListener('click', () => this.unfreeze());
1610
+ cancelBtn.addEventListener('click', () => this.unfreeze());
1611
+ sendBtn.addEventListener('click', () => this.submit(input.value));
1612
+ input.addEventListener('keydown', (e) => {
1613
+ if (e.key === 'Enter' && input.value.trim()) {
1614
+ this.submit(input.value);
1615
+ }
1616
+ });
1617
+ // Multi-select toggle button
1618
+ multiSelectBtn.addEventListener('click', () => {
1619
+ if (this.multiSelectMode) {
1620
+ this.exitMultiSelectMode();
1621
+ }
1622
+ else {
1623
+ this.enterMultiSelectMode();
1624
+ }
1625
+ });
1626
+ // Wire up chip remove buttons
1627
+ this.panel.querySelectorAll('.selected-chip-remove').forEach((btn) => {
1628
+ btn.addEventListener('click', (e) => {
1629
+ e.stopPropagation();
1630
+ const index = parseInt(e.currentTarget.dataset.index, 10);
1631
+ this.removeFromSelection(index);
1632
+ });
1633
+ });
1634
+ // Make panel draggable via header
1635
+ const header = this.panel.querySelector('.panel-header');
1636
+ header.addEventListener('mousedown', this.handlePanelDragStart);
1637
+ requestAnimationFrame(() => input.focus());
1638
+ }
1639
+ renderActivityMode(componentName, filePath) {
1640
+ if (!this.panel)
1641
+ return;
1642
+ const userNote = this.activityEvents.length > 0
1643
+ ? (this.panel.querySelector('.user-request-text')?.textContent || '')
1644
+ : '';
1645
+ const isDone = this.currentStatus === 'success' || this.currentStatus === 'failed';
1646
+ // Build header display based on submitted snapshots
1647
+ const snapshotCount = this.submittedSnapshots.length;
1648
+ const headerDisplay = snapshotCount > 1
1649
+ ? `${snapshotCount} elements`
1650
+ : `&lt;${this.escapeHtml(componentName)} /&gt;`;
1651
+ this.panel.innerHTML = `
1652
+ <div class="panel-header">
1653
+ <span class="component-tag">${headerDisplay}</span>
1654
+ ${snapshotCount <= 1 && filePath ? `<span class="file-path">${this.escapeHtml(filePath)}</span>` : ''}
1655
+ <button class="close-btn" title="Close">&times;</button>
1656
+ </div>
1657
+ <div class="user-request">
1658
+ <div class="user-request-label">Your request</div>
1659
+ <div class="user-request-text">${this.escapeHtml(this.getUserNote())}</div>
1660
+ </div>
1661
+ <div class="activity-feed">
1662
+ ${this.renderActivityFeed()}
1663
+ </div>
1664
+ <div class="panel-footer ${isDone ? 'done' : ''}">
1665
+ <div class="status-indicator ${this.currentStatus}"></div>
1666
+ <span class="status-text">${this.getStatusText()}</span>
1667
+ </div>
1668
+ `;
1669
+ const closeBtn = this.panel.querySelector('.close-btn');
1670
+ closeBtn.addEventListener('click', () => this.unfreeze());
1671
+ // Make panel draggable via header
1672
+ const header = this.panel.querySelector('.panel-header');
1673
+ header.addEventListener('mousedown', this.handlePanelDragStart);
1674
+ // Wire up question buttons if present
1675
+ this.panel.querySelectorAll('.question-option').forEach((btn) => {
1676
+ btn.addEventListener('click', (e) => {
1677
+ const target = e.target;
1678
+ const questionId = target.dataset.questionId;
1679
+ const answerId = target.dataset.answerId;
1680
+ const answerLabel = target.textContent;
1681
+ this.submitAnswer(questionId, answerId, answerLabel);
1682
+ });
1683
+ });
1684
+ // Scroll to bottom of activity feed
1685
+ const feed = this.panel.querySelector('.activity-feed');
1686
+ if (feed) {
1687
+ feed.scrollTop = feed.scrollHeight;
1688
+ }
1689
+ }
1690
+ renderActivityFeed() {
1691
+ return this.activityEvents.map((event) => {
1692
+ switch (event.type) {
1693
+ case 'status':
1694
+ // Skip pending/fixing - these are shown in the footer status bar
1695
+ if (event.status === 'pending' || event.status === 'fixing')
1696
+ return '';
1697
+ return this.renderStatusItem(event);
1698
+ case 'thought':
1699
+ return this.renderThoughtItem(event);
1700
+ case 'action':
1701
+ return this.renderActionItem(event);
1702
+ case 'question':
1703
+ return this.renderQuestionItem(event);
1704
+ default:
1705
+ return '';
1706
+ }
1707
+ }).join('');
1708
+ }
1709
+ renderStatusItem(event) {
1710
+ const iconClass = event.status === 'success' ? 'success' :
1711
+ event.status === 'failed' ? 'error' : 'status';
1712
+ const icon = event.status === 'success' ? '✓' :
1713
+ event.status === 'failed' ? '✕' : '●';
1714
+ return `
1715
+ <div class="activity-item">
1716
+ <div class="activity-icon ${iconClass}">${icon}</div>
1717
+ <div class="activity-content">
1718
+ <div class="activity-text">${this.escapeHtml(event.message || event.status)}</div>
1719
+ </div>
1720
+ </div>
1721
+ `;
1722
+ }
1723
+ renderThoughtItem(event) {
1724
+ return `
1725
+ <div class="activity-item">
1726
+ <div class="activity-icon thought">💭</div>
1727
+ <div class="activity-content">
1728
+ <div class="activity-text muted">${this.escapeHtml(event.content)}</div>
1729
+ </div>
1730
+ </div>
1731
+ `;
1732
+ }
1733
+ renderActionItem(event) {
1734
+ const icons = {
1735
+ reading: '📖',
1736
+ writing: '✏️',
1737
+ searching: '🔍',
1738
+ thinking: '🧠',
1739
+ };
1740
+ const verbs = {
1741
+ reading: 'Reading',
1742
+ writing: 'Writing',
1743
+ searching: 'Searching',
1744
+ thinking: 'Thinking about',
1745
+ };
1746
+ return `
1747
+ <div class="activity-item">
1748
+ <div class="activity-icon action">${icons[event.action] || '●'}</div>
1749
+ <div class="activity-content">
1750
+ <div class="activity-text">${verbs[event.action] || event.action}${event.complete ? ' ✓' : '...'}</div>
1751
+ <div class="activity-target">${this.escapeHtml(event.target)}</div>
1752
+ </div>
1753
+ </div>
1754
+ `;
1755
+ }
1756
+ renderQuestionItem(event) {
1757
+ // Check if this question was already answered
1758
+ const wasAnswered = this.activityEvents.some((e) => e.type === 'status' && e.timestamp > event.timestamp);
1759
+ if (wasAnswered) {
1760
+ return `
1761
+ <div class="activity-item">
1762
+ <div class="activity-icon question">?</div>
1763
+ <div class="activity-content">
1764
+ <div class="activity-text muted">${this.escapeHtml(event.question)}</div>
1765
+ </div>
1766
+ </div>
1767
+ `;
1768
+ }
1769
+ return `
1770
+ <div class="question-box">
1771
+ <div class="question-text">${this.escapeHtml(event.question)}</div>
1772
+ <div class="question-options">
1773
+ ${event.options.map((opt) => `
1774
+ <button
1775
+ class="question-option"
1776
+ data-question-id="${event.questionId}"
1777
+ data-answer-id="${opt.id}"
1778
+ >${this.escapeHtml(opt.label)}</button>
1779
+ `).join('')}
1780
+ </div>
1781
+ </div>
1782
+ `;
1783
+ }
1784
+ getUserNote() {
1785
+ // Find the original focus payload from the first status event or stored value
1786
+ const active = this.activityEvents.find((e) => e.type === 'status');
1787
+ // We need to store this separately
1788
+ return this._userNote || '';
1789
+ }
1790
+ getStatusText() {
1791
+ switch (this.currentStatus) {
1792
+ case 'idle': return 'Ready';
1793
+ case 'pending': return 'Waiting for agent...';
1794
+ case 'fixing': return 'Agent is working...';
1795
+ case 'success': return 'Done!';
1796
+ case 'failed': return 'Failed';
1797
+ default: return this.currentStatus;
1798
+ }
1799
+ }
1800
+ hidePanel() {
1801
+ if (this.panel) {
1802
+ this.panel.remove();
1803
+ this.panel = null;
1804
+ }
1805
+ }
1806
+ async submit(userNote) {
1807
+ if (!userNote.trim())
1808
+ return;
1809
+ if (this.selectedSnapshots.length === 0 && !this.currentSnapshot)
1810
+ return;
1811
+ this.interactionId = `eyeglass-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1812
+ this._userNote = userNote.trim();
1813
+ // Build payload - use snapshots array if multiple, otherwise single snapshot for backwards compat
1814
+ const snapshots = this.selectedSnapshots.length > 0 ? this.selectedSnapshots : (this.currentSnapshot ? [this.currentSnapshot] : []);
1815
+ // Store for activity mode display
1816
+ this.submittedSnapshots = [...snapshots];
1817
+ const payload = {
1818
+ interactionId: this.interactionId,
1819
+ userNote: userNote.trim(),
1820
+ ...(snapshots.length === 1
1821
+ ? { snapshot: snapshots[0] }
1822
+ : { snapshots }),
1823
+ };
1824
+ // Build component name for history (combine if multiple)
1825
+ const componentNames = snapshots.map(s => s.framework.componentName || s.tagName);
1826
+ const historyComponentName = snapshots.length === 1
1827
+ ? componentNames[0]
1828
+ : `${componentNames.length} elements`;
1829
+ // Add to history
1830
+ this.addToHistory({
1831
+ interactionId: this.interactionId,
1832
+ userNote: userNote.trim(),
1833
+ componentName: historyComponentName,
1834
+ filePath: snapshots[0]?.framework.filePath,
1835
+ status: 'pending',
1836
+ timestamp: Date.now(),
1837
+ });
1838
+ // Store multi-select state to restore on failure
1839
+ const wasMultiSelect = this.multiSelectMode;
1840
+ const savedElements = [...this.selectedElements];
1841
+ const savedSnapshots = [...this.selectedSnapshots];
1842
+ // Clear multi-select highlights before switching to activity mode
1843
+ this.clearMultiSelectHighlights();
1844
+ this.multiSelectMode = false;
1845
+ this.mode = 'activity';
1846
+ this.activityEvents = [];
1847
+ this.currentStatus = 'pending';
1848
+ this.renderPanel();
1849
+ try {
1850
+ const response = await fetch(`${BRIDGE_URL}/focus`, {
1851
+ method: 'POST',
1852
+ headers: { 'Content-Type': 'application/json' },
1853
+ body: JSON.stringify(payload),
1854
+ });
1855
+ if (!response.ok) {
1856
+ throw new Error(`HTTP ${response.status}`);
1857
+ }
1858
+ }
1859
+ catch (err) {
1860
+ this.currentStatus = 'failed';
1861
+ this.updateHistoryStatus(this.interactionId, 'failed');
1862
+ this.activityEvents.push({
1863
+ type: 'status',
1864
+ interactionId: this.interactionId,
1865
+ status: 'failed',
1866
+ message: 'Failed to connect to bridge',
1867
+ timestamp: Date.now(),
1868
+ });
1869
+ // Restore multi-select state on failure so user doesn't lose their selection
1870
+ if (wasMultiSelect && savedElements.length > 1) {
1871
+ this.multiSelectMode = true;
1872
+ this.selectedElements = savedElements;
1873
+ this.selectedSnapshots = savedSnapshots;
1874
+ this.mode = 'input';
1875
+ this.renderMultiSelectHighlights();
1876
+ }
1877
+ this.renderPanel();
1878
+ }
1879
+ }
1880
+ async submitAnswer(questionId, answerId, answerLabel) {
1881
+ if (!this.interactionId)
1882
+ return;
1883
+ const answer = {
1884
+ interactionId: this.interactionId,
1885
+ questionId,
1886
+ answerId,
1887
+ answerLabel,
1888
+ };
1889
+ try {
1890
+ await fetch(`${BRIDGE_URL}/answer`, {
1891
+ method: 'POST',
1892
+ headers: { 'Content-Type': 'application/json' },
1893
+ body: JSON.stringify(answer),
1894
+ });
1895
+ }
1896
+ catch (err) {
1897
+ // Silently fail
1898
+ }
1899
+ }
1900
+ escapeHtml(text) {
1901
+ const div = document.createElement('div');
1902
+ div.textContent = text;
1903
+ return div.innerHTML;
1904
+ }
1905
+ updateCursor() {
1906
+ // Show eye cursor when: enabled AND (not frozen OR in multi-select mode)
1907
+ const showEyeCursor = this.inspectorEnabled && (!this.frozen || this.multiSelectMode);
1908
+ if (showEyeCursor) {
1909
+ // Add eye cursor to document
1910
+ if (!this.cursorStyleElement) {
1911
+ this.cursorStyleElement = document.createElement('style');
1912
+ this.cursorStyleElement.id = 'eyeglass-cursor-style';
1913
+ document.head.appendChild(this.cursorStyleElement);
1914
+ }
1915
+ this.cursorStyleElement.textContent = `
1916
+ html, body, body * {
1917
+ cursor: ${EYE_CURSOR} !important;
1918
+ }
1919
+ `;
1920
+ }
1921
+ else {
1922
+ // Remove custom cursor
1923
+ if (this.cursorStyleElement) {
1924
+ this.cursorStyleElement.textContent = '';
1925
+ }
1926
+ }
1927
+ }
1928
+ }
1929
+ EyeglassInspector.MAX_SELECTION = 5;
1930
+ if (!customElements.get('eyeglass-inspector')) {
1931
+ customElements.define('eyeglass-inspector', EyeglassInspector);
1932
+ }