@eyeglass/inspector 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/inspector.js CHANGED
@@ -5,7 +5,32 @@ import { captureSnapshot } from './snapshot.js';
5
5
  const BRIDGE_URL = 'http://localhost:3300';
6
6
  const STORAGE_KEY = 'eyeglass_session';
7
7
  const HISTORY_KEY = 'eyeglass_history';
8
+ const ENABLED_KEY = 'eyeglass_enabled';
9
+ const AUTOCOMMIT_KEY = 'eyeglass_autocommit';
8
10
  const SESSION_TTL = 10000; // 10 seconds
11
+ // Fun rotating phrases for the "fixing" status
12
+ const WORKING_PHRASES = [
13
+ 'Ruminating...',
14
+ 'Percolating...',
15
+ 'Divining...',
16
+ 'Grokking...',
17
+ 'Communing...',
18
+ 'Concocting...',
19
+ 'Synthesizing...',
20
+ 'Distilling...',
21
+ 'Incubating...',
22
+ 'Forging...',
23
+ 'Scrutinizing...',
24
+ 'Triangulating...',
25
+ 'Unraveling...',
26
+ 'Traversing...',
27
+ 'Sifting...',
28
+ 'Marshaling...',
29
+ 'Hydrating...',
30
+ 'Harmonizing...',
31
+ 'Indexing...',
32
+ 'Entangling...',
33
+ ];
9
34
  // Eye cursor as base64-encoded SVG (16x16 eye icon, indigo color)
10
35
  const EYE_CURSOR = `url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM2MzY2ZjEiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xIDEyczQtOCAxMS04IDExIDggMTEgOC00IDgtMTEgOC0xMS04LTExLTh6Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iIzYzNjZmMSIvPjwvc3ZnPg==") 8 8, crosshair`;
11
36
  const STYLES = `
@@ -22,7 +47,7 @@ const STYLES = `
22
47
  font-size: 13px;
23
48
  line-height: 1.5;
24
49
  box-sizing: border-box;
25
- --glass-bg: rgba(255, 255, 255, 0.72);
50
+ --glass-bg: rgba(255, 255, 255, 0.88);
26
51
  --glass-border: rgba(0, 0, 0, 0.25);
27
52
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
28
53
  --divider: rgba(0, 0, 0, 0.18);
@@ -371,6 +396,37 @@ const STYLES = `
371
396
  color: var(--accent);
372
397
  }
373
398
 
399
+ /* Skeleton loader */
400
+ .skeleton-item {
401
+ padding: 8px 16px;
402
+ display: flex;
403
+ gap: 10px;
404
+ align-items: center;
405
+ }
406
+
407
+ .skeleton-icon {
408
+ width: 20px;
409
+ height: 20px;
410
+ border-radius: 50%;
411
+ background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
412
+ background-size: 200% 100%;
413
+ animation: shimmer 1.5s infinite;
414
+ }
415
+
416
+ .skeleton-line {
417
+ height: 14px;
418
+ border-radius: 4px;
419
+ background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
420
+ background-size: 200% 100%;
421
+ animation: shimmer 1.5s infinite;
422
+ width: 60%;
423
+ }
424
+
425
+ @keyframes shimmer {
426
+ 0% { background-position: 200% 0; }
427
+ 100% { background-position: -200% 0; }
428
+ }
429
+
374
430
  /* Status Footer */
375
431
  .panel-footer {
376
432
  padding: 10px 16px;
@@ -549,7 +605,7 @@ const STYLES = `
549
605
  }
550
606
  }
551
607
 
552
- .hub.disabled {
608
+ .hub.disabled.collapsed {
553
609
  opacity: 0.5;
554
610
  }
555
611
 
@@ -754,6 +810,107 @@ const STYLES = `
754
810
  color: var(--text-muted);
755
811
  }
756
812
 
813
+ .hub-settings-btn {
814
+ width: 20px;
815
+ height: 20px;
816
+ border: none;
817
+ background: transparent;
818
+ color: var(--text-muted);
819
+ cursor: pointer;
820
+ border-radius: 4px;
821
+ display: flex;
822
+ align-items: center;
823
+ justify-content: center;
824
+ transition: all 0.15s;
825
+ flex-shrink: 0;
826
+ }
827
+
828
+ .hub-settings-btn:hover {
829
+ background: rgba(0, 0, 0, 0.05);
830
+ color: var(--text-secondary);
831
+ }
832
+
833
+ .hub-settings-btn svg {
834
+ width: 12px;
835
+ height: 12px;
836
+ }
837
+
838
+ /* Settings Page */
839
+ .hub-settings-page {
840
+ padding: 8px 0;
841
+ }
842
+
843
+ .hub-settings-header {
844
+ display: flex;
845
+ align-items: center;
846
+ gap: 6px;
847
+ padding: 4px 8px 8px;
848
+ border-bottom: 1px solid var(--divider);
849
+ }
850
+
851
+ .hub-back-btn {
852
+ width: 20px;
853
+ height: 20px;
854
+ border: none;
855
+ background: transparent;
856
+ color: var(--text-secondary);
857
+ cursor: pointer;
858
+ border-radius: 4px;
859
+ display: flex;
860
+ align-items: center;
861
+ justify-content: center;
862
+ transition: all 0.15s;
863
+ }
864
+
865
+ .hub-back-btn:hover {
866
+ background: rgba(0, 0, 0, 0.05);
867
+ color: var(--text-primary);
868
+ }
869
+
870
+ .hub-settings-title {
871
+ font-size: 11px;
872
+ font-weight: 600;
873
+ color: var(--text-primary);
874
+ }
875
+
876
+ .hub-settings-list {
877
+ padding: 8px;
878
+ }
879
+
880
+ .hub-setting-row {
881
+ display: flex;
882
+ align-items: center;
883
+ justify-content: space-between;
884
+ gap: 8px;
885
+ padding: 6px 0;
886
+ }
887
+
888
+ .hub-setting-row:not(:last-child) {
889
+ border-bottom: 1px solid var(--divider);
890
+ padding-bottom: 10px;
891
+ margin-bottom: 4px;
892
+ }
893
+
894
+ .hub-setting-info {
895
+ flex: 1;
896
+ }
897
+
898
+ .hub-setting-label {
899
+ font-size: 11px;
900
+ font-weight: 500;
901
+ color: var(--text-primary);
902
+ }
903
+
904
+ .hub-setting-desc {
905
+ font-size: 9px;
906
+ color: var(--text-muted);
907
+ margin-top: 2px;
908
+ }
909
+
910
+ .hub-button-group {
911
+ display: flex;
912
+ }
913
+
757
914
  /* Collapsed hub (minimal) */
758
915
  .hub.collapsed .hub-title,
759
916
  .hub.collapsed .hub-toggle {
@@ -768,6 +925,156 @@ const STYLES = `
768
925
  gap: 4px;
769
926
  }
770
927
 
928
+ .toggle-switch {
929
+ position: relative;
930
+ width: 32px;
931
+ height: 18px;
932
+ background: #cbd5e1;
933
+ border-radius: 9px;
934
+ cursor: pointer;
935
+ transition: background 0.2s;
936
+ border: none;
937
+ padding: 0;
938
+ }
939
+
940
+ .toggle-switch.active {
941
+ background: var(--accent);
942
+ }
943
+
944
+ .toggle-switch::after {
945
+ content: '';
946
+ position: absolute;
947
+ top: 2px;
948
+ left: 2px;
949
+ width: 14px;
950
+ height: 14px;
951
+ background: white;
952
+ border-radius: 50%;
953
+ transition: transform 0.2s;
954
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
955
+ }
956
+
957
+ .toggle-switch.active::after {
958
+ transform: translateX(14px);
959
+ }
960
+
961
+ /* Success action buttons */
962
+ .success-actions {
963
+ display: flex;
964
+ gap: 6px;
965
+ margin-left: auto;
966
+ }
967
+
968
+ .action-btn {
969
+ padding: 4px 10px;
970
+ border: none;
971
+ border-radius: 6px;
972
+ font-size: 11px;
973
+ font-weight: 600;
974
+ font-family: inherit;
975
+ cursor: pointer;
976
+ transition: all 0.15s;
977
+ }
978
+
979
+ .action-btn-commit {
980
+ background: var(--success);
981
+ color: white;
982
+ }
983
+
984
+ .action-btn-commit:hover {
985
+ background: #059669;
986
+ }
987
+
988
+ .action-btn-undo {
989
+ background: rgba(239, 68, 68, 0.1);
990
+ color: var(--error);
991
+ }
992
+
993
+ .action-btn-undo:hover {
994
+ background: rgba(239, 68, 68, 0.2);
995
+ }
996
+
997
+ /* Follow-up input */
998
+ .followup-area {
999
+ padding: 12px;
1000
+ border-top: 1px solid var(--divider);
1001
+ background: rgba(16, 185, 129, 0.04);
1002
+ }
1003
+
1004
+ .followup-row {
1005
+ display: flex;
1006
+ gap: 8px;
1007
+ align-items: center;
1008
+ }
1009
+
1010
+ .followup-input {
1011
+ flex: 1;
1012
+ padding: 8px 12px;
1013
+ border: 1px solid rgba(0, 0, 0, 0.08);
1014
+ border-radius: var(--border-radius-sm);
1015
+ font-size: 12px;
1016
+ font-family: inherit;
1017
+ background: rgba(255, 255, 255, 0.8);
1018
+ color: var(--text-primary);
1019
+ outline: none;
1020
+ transition: all 0.15s;
1021
+ }
1022
+
1023
+ .followup-input::placeholder {
1024
+ color: var(--text-muted);
1025
+ }
1026
+
1027
+ .followup-input:focus {
1028
+ border-color: var(--accent);
1029
+ box-shadow: 0 0 0 2px var(--accent-soft);
1030
+ background: white;
1031
+ }
1032
+
1033
+ .followup-send {
1034
+ flex-shrink: 0;
1035
+ padding: 9px 16px;
1036
+ border: 1px solid transparent;
1037
+ border-radius: var(--border-radius-sm);
1038
+ background: var(--accent);
1039
+ color: white;
1040
+ font-size: 12px;
1041
+ font-weight: 600;
1042
+ font-family: inherit;
1043
+ cursor: pointer;
1044
+ transition: all 0.15s;
1045
+ }
1046
+
1047
+ .followup-send:hover {
1048
+ background: #4f46e5;
1049
+ }
1050
+
1051
+ .followup-send:disabled {
1052
+ opacity: 0.5;
1053
+ cursor: not-allowed;
1054
+ }
1055
+
1056
+ .followup-done {
1057
+ flex-shrink: 0;
1058
+ width: 32px;
1059
+ height: 32px;
1060
+ padding: 0;
1061
+ border: 1px solid var(--divider);
1062
+ border-radius: var(--border-radius-sm);
1063
+ background: transparent;
1064
+ color: var(--text-secondary);
1065
+ font-size: 14px;
1066
+ font-family: inherit;
1067
+ cursor: pointer;
1068
+ transition: all 0.15s;
1069
+ display: flex;
1070
+ align-items: center;
1071
+ justify-content: center;
1072
+ }
1073
+
1074
+ .followup-done:hover {
1075
+ background: rgba(0, 0, 0, 0.04);
1076
+ }
1077
+
771
1078
  /* Multi-select styles */
772
1079
  .highlight.multi {
773
1080
  border-style: dashed;
@@ -919,7 +1226,9 @@ export class EyeglassInspector extends HTMLElement {
919
1226
  this.activityEvents = [];
920
1227
  this.currentStatus = 'idle';
921
1228
  this.hubExpanded = false;
1229
+ this.hubPage = 'main';
922
1230
  this.inspectorEnabled = true;
1231
+ this.autoCommitEnabled = true;
923
1232
  this.history = [];
924
1233
  this.isDragging = false;
925
1234
  this.dragOffset = { x: 0, y: 0 };
@@ -934,6 +1243,9 @@ export class EyeglassInspector extends HTMLElement {
934
1243
  this.cursorStyleElement = null;
935
1244
  // Scroll handling
936
1245
  this.scrollTimeout = null;
1246
+ // Rotating status phrases
1247
+ this.phraseIndex = 0;
1248
+ this.phraseInterval = null;
937
1249
  this.handlePanelDrag = (e) => {
938
1250
  if (!this.isDragging || !this.panel)
939
1251
  return;
@@ -967,6 +1279,8 @@ export class EyeglassInspector extends HTMLElement {
967
1279
  document.addEventListener('click', this.handleClick, true);
968
1280
  document.addEventListener('keydown', this.handleKeyDown, true);
969
1281
  window.addEventListener('scroll', this.handleScroll, true);
1282
+ this.loadEnabledState();
1283
+ this.loadAutoCommitState();
970
1284
  this.loadHistory();
971
1285
  this.renderHub();
972
1286
  this.connectSSE();
@@ -1038,6 +1352,44 @@ export class EyeglassInspector extends HTMLElement {
1038
1352
  this.toast = null;
1039
1353
  }
1040
1354
  }
1355
+ loadEnabledState() {
1356
+ try {
1357
+ const stored = localStorage.getItem(ENABLED_KEY);
1358
+ if (stored !== null) {
1359
+ this.inspectorEnabled = stored === 'true';
1360
+ }
1361
+ }
1362
+ catch (e) {
1363
+ // Ignore storage errors
1364
+ }
1365
+ }
1366
+ saveEnabledState() {
1367
+ try {
1368
+ localStorage.setItem(ENABLED_KEY, String(this.inspectorEnabled));
1369
+ }
1370
+ catch (e) {
1371
+ // Ignore storage errors
1372
+ }
1373
+ }
1374
+ loadAutoCommitState() {
1375
+ try {
1376
+ const stored = localStorage.getItem(AUTOCOMMIT_KEY);
1377
+ if (stored !== null) {
1378
+ this.autoCommitEnabled = stored === 'true';
1379
+ }
1380
+ }
1381
+ catch (e) {
1382
+ // Ignore storage errors
1383
+ }
1384
+ }
1385
+ saveAutoCommitState() {
1386
+ try {
1387
+ localStorage.setItem(AUTOCOMMIT_KEY, String(this.autoCommitEnabled));
1388
+ }
1389
+ catch (e) {
1390
+ // Ignore storage errors
1391
+ }
1392
+ }
1041
1393
  loadHistory() {
1042
1394
  try {
1043
1395
  const stored = sessionStorage.getItem(HISTORY_KEY);
@@ -1087,6 +1439,16 @@ export class EyeglassInspector extends HTMLElement {
1087
1439
  this.hub.className = 'hub';
1088
1440
  this.shadow.appendChild(this.hub);
1089
1441
  }
1442
+ if (this.hubPage === 'settings') {
1443
+ this.renderHubSettingsPage();
1444
+ }
1445
+ else {
1446
+ this.renderHubMainPage();
1447
+ }
1448
+ }
1449
+ renderHubMainPage() {
1450
+ if (!this.hub)
1451
+ return;
1090
1452
  const collapsedClass = this.hubExpanded ? '' : 'collapsed';
1091
1453
  const disabledClass = this.inspectorEnabled ? '' : 'disabled';
1092
1454
  const expandedClass = this.hubExpanded ? 'expanded' : '';
@@ -1094,6 +1456,7 @@ export class EyeglassInspector extends HTMLElement {
1094
1456
  this.hub.className = `hub ${collapsedClass} ${disabledClass}`.trim();
1095
1457
  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
1458
  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>`;
1459
+ const gearSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`;
1097
1460
  this.hub.innerHTML = `
1098
1461
  <div class="hub-header">
1099
1462
  <div class="hub-header-left">
@@ -1102,9 +1465,12 @@ export class EyeglassInspector extends HTMLElement {
1102
1465
  ${activeCount > 0 ? `<span class="hub-badge">${activeCount}</span>` : ''}
1103
1466
  <button class="hub-toggle ${expandedClass}" title="Toggle history">▼</button>
1104
1467
  </div>
1468
+ <div class="hub-button-group">
1469
+ <button class="hub-settings-btn" title="Settings">${gearSvg}</button>
1105
1470
  <button class="hub-disable ${this.inspectorEnabled ? 'active' : ''}" title="${this.inspectorEnabled ? 'Disable' : 'Enable'} inspector">
1106
1471
  ${this.inspectorEnabled ? eyeOpenSvg : eyeClosedSvg}
1107
1472
  </button>
1473
+ </div>
1108
1474
  </div>
1109
1475
  <div class="hub-content ${expandedClass}">
1110
1476
  ${this.history.length > 0 ? `
@@ -1129,19 +1495,27 @@ export class EyeglassInspector extends HTMLElement {
1129
1495
  `;
1130
1496
  // Wire up event handlers
1131
1497
  const header = this.hub.querySelector('.hub-header');
1132
- const toggleBtn = this.hub.querySelector('.hub-toggle');
1133
1498
  const disableBtn = this.hub.querySelector('.hub-disable');
1134
- // Toggle expand/collapse on header click (except disable button)
1499
+ const settingsBtn = this.hub.querySelector('.hub-settings-btn');
1500
+ // Toggle expand/collapse on header click (except buttons)
1135
1501
  header.addEventListener('click', (e) => {
1136
- if (e.target === disableBtn)
1502
+ if (e.target === disableBtn || e.target === settingsBtn || e.target.closest('.hub-settings-btn'))
1137
1503
  return;
1138
1504
  this.hubExpanded = !this.hubExpanded;
1139
1505
  this.renderHub();
1140
1506
  });
1507
+ // Open settings page
1508
+ settingsBtn.addEventListener('click', (e) => {
1509
+ e.stopPropagation();
1510
+ this.hubPage = 'settings';
1511
+ this.hubExpanded = true; // Ensure expanded when viewing settings
1512
+ this.renderHub();
1513
+ });
1141
1514
  // Toggle inspector enabled state
1142
1515
  disableBtn.addEventListener('click', (e) => {
1143
1516
  e.stopPropagation();
1144
1517
  this.inspectorEnabled = !this.inspectorEnabled;
1518
+ this.saveEnabledState();
1145
1519
  if (!this.inspectorEnabled) {
1146
1520
  this.unfreeze();
1147
1521
  }
@@ -1157,6 +1531,53 @@ export class EyeglassInspector extends HTMLElement {
1157
1531
  });
1158
1532
  });
1159
1533
  }
1534
+ renderHubSettingsPage() {
1535
+ if (!this.hub)
1536
+ return;
1537
+ this.hub.className = 'hub';
1538
+ this.hub.innerHTML = `
1539
+ <div class="hub-header">
1540
+ <div class="hub-header-left">
1541
+ <div class="hub-logo">👁</div>
1542
+ <span class="hub-title">Eyeglass</span>
1543
+ </div>
1544
+ </div>
1545
+ <div class="hub-content expanded">
1546
+ <div class="hub-settings-page">
1547
+ <div class="hub-settings-header">
1548
+ <button class="hub-back-btn" title="Back">←</button>
1549
+ <span class="hub-settings-title">Settings</span>
1550
+ </div>
1551
+ <div class="hub-settings-list">
1552
+ <div class="hub-setting-row">
1553
+ <div class="hub-setting-info">
1554
+ <div class="hub-setting-label">Auto-commit</div>
1555
+ <div class="hub-setting-desc">Automatically commit changes on success</div>
1556
+ </div>
1557
+ <button class="toggle-switch ${this.autoCommitEnabled ? 'active' : ''}" data-setting="autoCommit"></button>
1558
+ </div>
1559
+ </div>
1560
+ </div>
1561
+ </div>
1562
+ `;
1563
+ // Wire up back button
1564
+ const backBtn = this.hub.querySelector('.hub-back-btn');
1565
+ backBtn.addEventListener('click', () => {
1566
+ this.hubPage = 'main';
1567
+ this.renderHub();
1568
+ });
1569
+ // Wire up toggle switches
1570
+ this.hub.querySelectorAll('.toggle-switch').forEach(btn => {
1571
+ btn.addEventListener('click', (e) => {
1572
+ const setting = e.currentTarget.dataset.setting;
1573
+ if (setting === 'autoCommit') {
1574
+ this.autoCommitEnabled = !this.autoCommitEnabled;
1575
+ this.saveAutoCommitState();
1576
+ this.renderHub();
1577
+ }
1578
+ });
1579
+ });
1580
+ }
1160
1581
  async requestUndo(interactionId) {
1161
1582
  const itemIndex = this.history.findIndex(h => h.interactionId === interactionId);
1162
1583
  if (itemIndex === -1)
@@ -1194,6 +1615,32 @@ export class EyeglassInspector extends HTMLElement {
1194
1615
  console.warn('Undo request failed:', err);
1195
1616
  }
1196
1617
  }
1618
+ async requestCommit(interactionId) {
1619
+ const itemIndex = this.history.findIndex(h => h.interactionId === interactionId);
1620
+ try {
1621
+ const response = await fetch(`${BRIDGE_URL}/commit`, {
1622
+ method: 'POST',
1623
+ headers: { 'Content-Type': 'application/json' },
1624
+ body: JSON.stringify({ interactionId }),
1625
+ });
1626
+ if (response.ok) {
1627
+ // Update status to show it's committed
1628
+ if (itemIndex >= 0) {
1629
+ this.history[itemIndex].status = 'success';
1630
+ this.saveHistory();
1631
+ this.renderHub();
1632
+ }
1633
+ // Close the panel after commit
1634
+ this.unfreeze();
1635
+ }
1636
+ else {
1637
+ console.warn('Commit request failed');
1638
+ }
1639
+ }
1640
+ catch (err) {
1641
+ console.warn('Commit request failed:', err);
1642
+ }
1643
+ }
1197
1644
  disconnectedCallback() {
1198
1645
  document.removeEventListener('mousemove', this.handleMouseMove, true);
1199
1646
  document.removeEventListener('click', this.handleClick, true);
@@ -1236,9 +1683,18 @@ export class EyeglassInspector extends HTMLElement {
1236
1683
  this.currentStatus = event.status;
1237
1684
  // Persist session so we can show result after page reload
1238
1685
  this.saveSession(event.message);
1239
- if (event.status === 'success' || event.status === 'failed') {
1686
+ // Manage phrase rotation based on status
1687
+ if (event.status === 'fixing') {
1688
+ this.startPhraseRotation();
1689
+ }
1690
+ else {
1691
+ this.stopPhraseRotation();
1692
+ }
1693
+ if (event.status === 'failed') {
1694
+ // Auto-close on failure after delay
1240
1695
  setTimeout(() => this.unfreeze(), 4000);
1241
1696
  }
1697
+ // Don't auto-close on success - show follow-up UI instead
1242
1698
  }
1243
1699
  this.renderPanel();
1244
1700
  }
@@ -1498,6 +1954,8 @@ export class EyeglassInspector extends HTMLElement {
1498
1954
  this.selectedSnapshots = [];
1499
1955
  this.submittedSnapshots = [];
1500
1956
  this.clearMultiSelectHighlights();
1957
+ // Stop phrase rotation
1958
+ this.stopPhraseRotation();
1501
1959
  this.hidePanel();
1502
1960
  this.hideHighlight();
1503
1961
  this.updateCursor();
@@ -1643,6 +2101,8 @@ export class EyeglassInspector extends HTMLElement {
1643
2101
  ? (this.panel.querySelector('.user-request-text')?.textContent || '')
1644
2102
  : '';
1645
2103
  const isDone = this.currentStatus === 'success' || this.currentStatus === 'failed';
2104
+ const showActionButtons = this.currentStatus === 'success' && !this.autoCommitEnabled;
2105
+ const showFollowUp = this.currentStatus === 'success';
1646
2106
  // Build header display based on submitted snapshots
1647
2107
  const snapshotCount = this.submittedSnapshots.length;
1648
2108
  const headerDisplay = snapshotCount > 1
@@ -1664,7 +2124,22 @@ export class EyeglassInspector extends HTMLElement {
1664
2124
  <div class="panel-footer ${isDone ? 'done' : ''}">
1665
2125
  <div class="status-indicator ${this.currentStatus}"></div>
1666
2126
  <span class="status-text">${this.getStatusText()}</span>
2127
+ ${showActionButtons ? `
2128
+ <div class="success-actions">
2129
+ <button class="action-btn action-btn-undo" title="Discard changes">Undo</button>
2130
+ <button class="action-btn action-btn-commit" title="Commit changes">Commit</button>
2131
+ </div>
2132
+ ` : ''}
1667
2133
  </div>
2134
+ ${showFollowUp ? `
2135
+ <div class="followup-area">
2136
+ <div class="followup-row">
2137
+ <input type="text" class="followup-input" placeholder="Anything else?" />
2138
+ <button class="followup-send">Send</button>
2139
+ <button class="followup-done">✕</button>
2140
+ </div>
2141
+ </div>
2142
+ ` : ''}
1668
2143
  `;
1669
2144
  const closeBtn = this.panel.querySelector('.close-btn');
1670
2145
  closeBtn.addEventListener('click', () => this.unfreeze());
@@ -1681,6 +2156,36 @@ export class EyeglassInspector extends HTMLElement {
1681
2156
  this.submitAnswer(questionId, answerId, answerLabel);
1682
2157
  });
1683
2158
  });
2159
+ // Wire up action buttons (commit/undo) if present
2160
+ const commitBtn = this.panel.querySelector('.action-btn-commit');
2161
+ const undoBtn = this.panel.querySelector('.action-btn-undo');
2162
+ if (commitBtn && this.interactionId) {
2163
+ commitBtn.addEventListener('click', () => this.requestCommit(this.interactionId));
2164
+ }
2165
+ if (undoBtn && this.interactionId) {
2166
+ undoBtn.addEventListener('click', () => this.requestUndo(this.interactionId));
2167
+ }
2168
+ // Wire up follow-up input if present
2169
+ const followupInput = this.panel.querySelector('.followup-input');
2170
+ const followupSend = this.panel.querySelector('.followup-send');
2171
+ const followupDone = this.panel.querySelector('.followup-done');
2172
+ if (followupInput && followupSend) {
2173
+ followupSend.addEventListener('click', () => {
2174
+ if (followupInput.value.trim()) {
2175
+ this.submitFollowUp(followupInput.value);
2176
+ }
2177
+ });
2178
+ followupInput.addEventListener('keydown', (e) => {
2179
+ if (e.key === 'Enter' && followupInput.value.trim()) {
2180
+ this.submitFollowUp(followupInput.value);
2181
+ }
2182
+ });
2183
+ // Focus the follow-up input
2184
+ requestAnimationFrame(() => followupInput.focus());
2185
+ }
2186
+ if (followupDone) {
2187
+ followupDone.addEventListener('click', () => this.unfreeze());
2188
+ }
1684
2189
  // Scroll to bottom of activity feed
1685
2190
  const feed = this.panel.querySelector('.activity-feed');
1686
2191
  if (feed) {
@@ -1688,12 +2193,17 @@ export class EyeglassInspector extends HTMLElement {
1688
2193
  }
1689
2194
  }
1690
2195
  renderActivityFeed() {
1691
- return this.activityEvents.map((event) => {
2196
+ const items = this.activityEvents.map((event) => {
1692
2197
  switch (event.type) {
1693
2198
  case 'status':
1694
- // Skip pending/fixing - these are shown in the footer status bar
1695
- if (event.status === 'pending' || event.status === 'fixing')
2199
+ // Skip pending - shown in footer. Show fixing only if it has a meaningful message
2200
+ if (event.status === 'pending')
1696
2201
  return '';
2202
+ if (event.status === 'fixing') {
2203
+ // Skip generic/missing messages - we have rotating phrases in the footer
2204
+ if (!event.message || event.message === 'Agent is working...')
2205
+ return '';
2206
+ }
1697
2207
  return this.renderStatusItem(event);
1698
2208
  case 'thought':
1699
2209
  return this.renderThoughtItem(event);
@@ -1704,7 +2214,17 @@ export class EyeglassInspector extends HTMLElement {
1704
2214
  default:
1705
2215
  return '';
1706
2216
  }
1707
- }).join('');
2217
+ }).filter(Boolean);
2218
+ // Show skeleton while waiting for first meaningful activity
2219
+ if (items.length === 0 && (this.currentStatus === 'pending' || this.currentStatus === 'fixing')) {
2220
+ return `
2221
+ <div class="skeleton-item">
2222
+ <div class="skeleton-icon"></div>
2223
+ <div class="skeleton-line"></div>
2224
+ </div>
2225
+ `;
2226
+ }
2227
+ return items.join('');
1708
2228
  }
1709
2229
  renderStatusItem(event) {
1710
2230
  const iconClass = event.status === 'success' ? 'success' :
@@ -1791,12 +2311,35 @@ export class EyeglassInspector extends HTMLElement {
1791
2311
  switch (this.currentStatus) {
1792
2312
  case 'idle': return 'Ready';
1793
2313
  case 'pending': return 'Waiting for agent...';
1794
- case 'fixing': return 'Agent is working...';
2314
+ case 'fixing': return WORKING_PHRASES[this.phraseIndex % WORKING_PHRASES.length];
1795
2315
  case 'success': return 'Done!';
1796
2316
  case 'failed': return 'Failed';
1797
2317
  default: return this.currentStatus;
1798
2318
  }
1799
2319
  }
2320
+ startPhraseRotation() {
2321
+ if (this.phraseInterval)
2322
+ return;
2323
+ this.phraseIndex = Math.floor(Math.random() * WORKING_PHRASES.length);
2324
+ this.phraseInterval = window.setInterval(() => {
2325
+ this.phraseIndex = (this.phraseIndex + 1) % WORKING_PHRASES.length;
2326
+ this.updateFooterText();
2327
+ }, 10000);
2328
+ }
2329
+ stopPhraseRotation() {
2330
+ if (this.phraseInterval) {
2331
+ window.clearInterval(this.phraseInterval);
2332
+ this.phraseInterval = null;
2333
+ }
2334
+ }
2335
+ updateFooterText() {
2336
+ if (!this.panel)
2337
+ return;
2338
+ const statusText = this.panel.querySelector('.status-text');
2339
+ if (statusText) {
2340
+ statusText.textContent = this.getStatusText();
2341
+ }
2342
+ }
1800
2343
  hidePanel() {
1801
2344
  if (this.panel) {
1802
2345
  this.panel.remove();
@@ -1817,6 +2360,7 @@ export class EyeglassInspector extends HTMLElement {
1817
2360
  const payload = {
1818
2361
  interactionId: this.interactionId,
1819
2362
  userNote: userNote.trim(),
2363
+ autoCommit: this.autoCommitEnabled,
1820
2364
  ...(snapshots.length === 1
1821
2365
  ? { snapshot: snapshots[0] }
1822
2366
  : { snapshots }),
@@ -1877,6 +2421,65 @@ export class EyeglassInspector extends HTMLElement {
1877
2421
  this.renderPanel();
1878
2422
  }
1879
2423
  }
2424
+ async submitFollowUp(userNote) {
2425
+ if (!userNote.trim())
2426
+ return;
2427
+ if (this.submittedSnapshots.length === 0)
2428
+ return;
2429
+ // Create new interaction ID for the follow-up
2430
+ this.interactionId = `eyeglass-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2431
+ this._userNote = userNote.trim();
2432
+ // Reuse the same snapshots from the previous request
2433
+ const snapshots = this.submittedSnapshots;
2434
+ const payload = {
2435
+ interactionId: this.interactionId,
2436
+ userNote: userNote.trim(),
2437
+ autoCommit: this.autoCommitEnabled,
2438
+ ...(snapshots.length === 1
2439
+ ? { snapshot: snapshots[0] }
2440
+ : { snapshots }),
2441
+ };
2442
+ // Build component name for history
2443
+ const componentNames = snapshots.map(s => s.framework.componentName || s.tagName);
2444
+ const historyComponentName = snapshots.length === 1
2445
+ ? componentNames[0]
2446
+ : `${componentNames.length} elements`;
2447
+ // Add to history
2448
+ this.addToHistory({
2449
+ interactionId: this.interactionId,
2450
+ userNote: userNote.trim(),
2451
+ componentName: historyComponentName,
2452
+ filePath: snapshots[0]?.framework.filePath,
2453
+ status: 'pending',
2454
+ timestamp: Date.now(),
2455
+ });
2456
+ // Reset activity state for new request
2457
+ this.activityEvents = [];
2458
+ this.currentStatus = 'pending';
2459
+ this.renderPanel();
2460
+ try {
2461
+ const response = await fetch(`${BRIDGE_URL}/focus`, {
2462
+ method: 'POST',
2463
+ headers: { 'Content-Type': 'application/json' },
2464
+ body: JSON.stringify(payload),
2465
+ });
2466
+ if (!response.ok) {
2467
+ throw new Error(`HTTP ${response.status}`);
2468
+ }
2469
+ }
2470
+ catch (err) {
2471
+ this.currentStatus = 'failed';
2472
+ this.updateHistoryStatus(this.interactionId, 'failed');
2473
+ this.activityEvents.push({
2474
+ type: 'status',
2475
+ interactionId: this.interactionId,
2476
+ status: 'failed',
2477
+ message: 'Failed to connect to bridge',
2478
+ timestamp: Date.now(),
2479
+ });
2480
+ this.renderPanel();
2481
+ }
2482
+ }
1880
2483
  async submitAnswer(questionId, answerId, answerLabel) {
1881
2484
  if (!this.interactionId)
1882
2485
  return;