cohvu 1.0.21 → 1.0.23

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.
@@ -24,6 +24,29 @@ async function launchDashboard() {
24
24
  let feedDisconnect = null;
25
25
  let copiedTimer = null;
26
26
  let inlineErrorTimer = null;
27
+ let toastTimer = null;
28
+ let lastSyncErrorAt = 0;
29
+ // Toast feedback
30
+ function showToast(message, type, durationMs = 3000) {
31
+ if (toastTimer)
32
+ clearTimeout(toastTimer);
33
+ dispatch({ type: 'SET_TOAST', toast: { message, type, expiresAt: Date.now() + durationMs } });
34
+ toastTimer = setTimeout(() => {
35
+ dispatch({ type: 'SET_TOAST', toast: null });
36
+ }, durationMs);
37
+ }
38
+ // Loading indicator for async operations
39
+ async function withOperation(description, fn) {
40
+ dispatch({ type: 'SET_OPERATION', operation: description });
41
+ doRender();
42
+ try {
43
+ return await fn();
44
+ }
45
+ finally {
46
+ dispatch({ type: 'SET_OPERATION', operation: null });
47
+ doRender();
48
+ }
49
+ }
27
50
  // Load first-login state
28
51
  const savedState = loadState();
29
52
  state.firstLogin = !savedState.hasOpenedDashboard;
@@ -84,13 +107,16 @@ async function launchDashboard() {
84
107
  // Load knowledge, members, billing, invite links, notifications in parallel
85
108
  const projectId = state.activeProjectId;
86
109
  if (projectId) {
110
+ let loadError = false;
87
111
  const [memResult, members, billing, inviteLinks, notifications] = await Promise.all([
88
- api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).catch(() => null),
89
- api.listMembers(projectId).catch(() => []),
90
- api.getBilling(projectId).catch(() => null),
112
+ api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).catch(() => { loadError = true; return null; }),
113
+ api.listMembers(projectId).catch(() => { loadError = true; return []; }),
114
+ api.getBilling(projectId).catch(() => { loadError = true; return null; }),
91
115
  api.listInviteLinks(projectId).catch(() => []),
92
116
  api.listNotifications().catch(() => []),
93
117
  ]);
118
+ if (loadError)
119
+ showToast('Some data failed to load', 'error');
94
120
  if (memResult) {
95
121
  dispatch({ type: 'SET_MEMORIES', memories: memResult.memories, total: memResult.total });
96
122
  }
@@ -157,19 +183,25 @@ async function launchDashboard() {
157
183
  const sub = state.billing?.subscription;
158
184
  if (!sub || sub.status !== 'active') {
159
185
  try {
186
+ showToast('Opening checkout...', 'info');
160
187
  const checkout = await api.createCheckout(project.project_id);
161
188
  if (checkout.checkout_url)
162
189
  openBrowser(checkout.checkout_url);
163
190
  }
164
- catch { }
191
+ catch {
192
+ showToast('Failed to open checkout', 'error');
193
+ }
165
194
  }
166
195
  else {
167
196
  try {
197
+ showToast('Opening billing portal...', 'info');
168
198
  const portal = await api.getPortalUrl(project.project_id);
169
199
  if (portal.url)
170
200
  openBrowser(portal.url);
171
201
  }
172
- catch { }
202
+ catch {
203
+ showToast('Failed to open billing portal', 'error');
204
+ }
173
205
  }
174
206
  }
175
207
  return;
@@ -238,14 +270,24 @@ async function launchDashboard() {
238
270
  // Confirmed — delete
239
271
  const projectId = state.activeProjectId;
240
272
  if (projectId) {
273
+ let failures = 0;
274
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing memories' });
275
+ doRender();
241
276
  await Promise.all([...state.forgetSelected].map(async (id) => {
242
277
  try {
243
278
  await api.deleteMemory(projectId, id);
244
279
  dispatch({ type: 'REMOVE_MEMORY', id });
245
280
  }
246
- catch { }
281
+ catch {
282
+ failures++;
283
+ }
247
284
  }));
285
+ dispatch({ type: 'SET_OPERATION', operation: null });
248
286
  dispatch({ type: 'EXIT_FORGET' });
287
+ if (failures > 0)
288
+ showToast(`${failures} failed to remove`, 'error');
289
+ else
290
+ showToast('Removed', 'success');
249
291
  }
250
292
  }
251
293
  else if (state.forgetConfirming && (key.name === 'escape' || (key.name === 'char' && key.char === 'n'))) {
@@ -399,8 +441,11 @@ async function launchDashboard() {
399
441
  await api.cancelApproval(projectId, demoteApproval.id);
400
442
  const approvals = await api.listApprovals(projectId);
401
443
  dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
444
+ showToast('Approval cancelled', 'success');
445
+ }
446
+ catch {
447
+ showToast('Failed to cancel approval', 'error');
402
448
  }
403
- catch { }
404
449
  }
405
450
  return;
406
451
  }
@@ -439,22 +484,28 @@ async function launchDashboard() {
439
484
  const projectId = state.activeProjectId;
440
485
  if (projectId) {
441
486
  try {
487
+ showToast('Opening checkout...', 'info');
442
488
  const checkout = await api.createCheckout(projectId);
443
489
  if (checkout.checkout_url)
444
490
  openBrowser(checkout.checkout_url);
445
491
  }
446
- catch { }
492
+ catch {
493
+ showToast('Failed to open checkout', 'error');
494
+ }
447
495
  }
448
496
  }
449
497
  else if (key.name === 'char' && key.char === 'p') {
450
498
  const projectId = state.activeProjectId;
451
499
  if (projectId) {
452
500
  try {
501
+ showToast('Opening billing portal...', 'info');
453
502
  const portal = await api.getPortalUrl(projectId);
454
503
  if (portal.url)
455
504
  openBrowser(portal.url);
456
505
  }
457
- catch { }
506
+ catch {
507
+ showToast('Failed to open billing portal', 'error');
508
+ }
458
509
  }
459
510
  }
460
511
  }
@@ -484,8 +535,11 @@ async function launchDashboard() {
484
535
  await api.cancelApproval(projectId, projectApproval.id);
485
536
  const approvals = await api.listApprovals(projectId);
486
537
  dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
538
+ showToast('Approval cancelled', 'success');
539
+ }
540
+ catch {
541
+ showToast('Failed to cancel approval', 'error');
487
542
  }
488
- catch { }
489
543
  }
490
544
  return;
491
545
  }
@@ -679,8 +733,11 @@ async function launchDashboard() {
679
733
  await api.changeRole(projectId, modal.targetUserId, newRole);
680
734
  const members = await api.listMembers(projectId);
681
735
  dispatch({ type: 'SET_MEMBERS', members });
736
+ showToast('Role updated', 'success');
737
+ }
738
+ catch {
739
+ showToast('Failed to update role', 'error');
682
740
  }
683
- catch { }
684
741
  }
685
742
  }
686
743
  dispatch({ type: 'CLOSE_MODAL' });
@@ -694,20 +751,32 @@ async function launchDashboard() {
694
751
  switch (modal.kind) {
695
752
  case 'confirm-forget':
696
753
  try {
697
- await api.deleteMemory(projectId, modal.memoryId);
698
- dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
754
+ await withOperation('Removing memory', async () => {
755
+ await api.deleteMemory(projectId, modal.memoryId);
756
+ dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
757
+ });
758
+ showToast('Memory removed', 'success');
759
+ }
760
+ catch {
761
+ showToast('Failed to remove memory', 'error');
762
+ return;
699
763
  }
700
- catch { }
701
764
  break;
702
765
  case 'confirm-forget-all':
703
766
  case 'confirm-clear': {
704
767
  const project = (0, state_1.getActiveProject)(state);
705
768
  if (project && modal.input === project.slug) {
706
769
  try {
707
- await api.clearMemories(projectId);
708
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
770
+ await withOperation('Clearing memories', async () => {
771
+ await api.clearMemories(projectId);
772
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
773
+ });
774
+ showToast('All memories cleared', 'success');
775
+ }
776
+ catch {
777
+ showToast('Failed to clear memories', 'error');
778
+ return;
709
779
  }
710
- catch { }
711
780
  }
712
781
  break;
713
782
  }
@@ -715,59 +784,72 @@ async function launchDashboard() {
715
784
  const project = (0, state_1.getActiveProject)(state);
716
785
  if (project && modal.input === project.slug) {
717
786
  try {
718
- await api.deleteProject(projectId);
719
- const me = await api.me();
720
- dispatch({ type: 'SET_USER_DATA', me });
721
- dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
722
- if (state.activeProjectId) {
723
- connectFeed(state.activeProjectId);
724
- await loadTabData();
725
- }
726
- else {
727
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
728
- dispatch({ type: 'SET_MEMBERS', members: [] });
729
- }
787
+ await withOperation('Deleting project', async () => {
788
+ await api.deleteProject(projectId);
789
+ const me = await api.me();
790
+ dispatch({ type: 'SET_USER_DATA', me });
791
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
792
+ if (state.activeProjectId) {
793
+ connectFeed(state.activeProjectId);
794
+ await loadTabData();
795
+ }
796
+ else {
797
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
798
+ dispatch({ type: 'SET_MEMBERS', members: [] });
799
+ }
800
+ });
801
+ showToast('Project deleted', 'success');
802
+ }
803
+ catch {
804
+ showToast('Failed to delete project', 'error');
805
+ return;
730
806
  }
731
- catch { }
732
807
  }
733
808
  break;
734
809
  }
735
810
  case 'confirm-remove-member':
736
811
  try {
737
- await api.removeMember(projectId, modal.userId);
738
- const members = await api.listMembers(projectId);
739
- dispatch({ type: 'SET_MEMBERS', members });
812
+ await withOperation('Removing member', async () => {
813
+ await api.removeMember(projectId, modal.userId);
814
+ const members = await api.listMembers(projectId);
815
+ dispatch({ type: 'SET_MEMBERS', members });
816
+ });
817
+ showToast('Member removed', 'success');
818
+ }
819
+ catch {
820
+ showToast('Failed to remove member', 'error');
821
+ return;
740
822
  }
741
- catch { }
742
823
  break;
743
824
  case 'confirm-leave':
744
825
  try {
745
- // If admin who initiated a pending approval is leaving, cancel it
746
- const leavingUserId = state.user.id;
747
- for (const approval of state.pendingApprovals) {
748
- // Check if this user initiated the approval (email match)
749
- const leavingEmail = state.user.email;
750
- if (approval.initiator_email === leavingEmail) {
751
- try {
752
- await api.cancelApproval(projectId, approval.id);
826
+ await withOperation('Leaving project', async () => {
827
+ const leavingUserId = state.user.id;
828
+ for (const approval of state.pendingApprovals) {
829
+ const leavingEmail = state.user.email;
830
+ if (approval.initiator_email === leavingEmail) {
831
+ await api.cancelApproval(projectId, approval.id).catch(() => { });
753
832
  }
754
- catch { }
755
833
  }
756
- }
757
- await api.removeMember(projectId, leavingUserId);
758
- const me = await api.me();
759
- dispatch({ type: 'SET_USER_DATA', me });
760
- dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
761
- if (state.activeProjectId) {
762
- connectFeed(state.activeProjectId);
763
- await loadTabData();
764
- }
765
- else {
766
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
767
- dispatch({ type: 'SET_MEMBERS', members: [] });
768
- }
834
+ await api.removeMember(projectId, leavingUserId);
835
+ const me = await api.me();
836
+ dispatch({ type: 'SET_USER_DATA', me });
837
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
838
+ if (state.activeProjectId) {
839
+ connectFeed(state.activeProjectId);
840
+ await loadTabData();
841
+ }
842
+ else {
843
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
844
+ dispatch({ type: 'SET_MEMBERS', members: [] });
845
+ }
846
+ });
847
+ showToast('Left project', 'success');
848
+ }
849
+ catch {
850
+ showToast('Failed to leave project', 'error');
851
+ return;
769
852
  }
770
- catch { }
771
853
  break;
772
854
  case 'confirm-logout':
773
855
  try {
@@ -778,6 +860,8 @@ async function launchDashboard() {
778
860
  }
779
861
  }
780
862
  catch { }
863
+ // Brief delay for in-flight proxy requests
864
+ await new Promise((r) => setTimeout(r, 500));
781
865
  cleanExit();
782
866
  break;
783
867
  case 'rename': {
@@ -785,11 +869,17 @@ async function launchDashboard() {
785
869
  break;
786
870
  const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
787
871
  try {
788
- await api.renameProject(projectId, modal.input, slug);
789
- const me = await api.me();
790
- dispatch({ type: 'SET_USER_DATA', me });
872
+ await withOperation('Renaming', async () => {
873
+ await api.renameProject(projectId, modal.input, slug);
874
+ const me = await api.me();
875
+ dispatch({ type: 'SET_USER_DATA', me });
876
+ });
877
+ showToast('Project renamed', 'success');
878
+ }
879
+ catch {
880
+ showToast('Failed to rename', 'error');
881
+ return;
791
882
  }
792
- catch { }
793
883
  break;
794
884
  }
795
885
  case 'create-project': {
@@ -797,40 +887,64 @@ async function launchDashboard() {
797
887
  break;
798
888
  const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
799
889
  try {
800
- const project = await api.createProject(modal.input, slug);
801
- await api.switchProject(project.id);
802
- const me = await api.me();
803
- dispatch({ type: 'SET_USER_DATA', me });
804
- await loadTabData();
890
+ await withOperation('Creating project', async () => {
891
+ const project = await api.createProject(modal.input, slug);
892
+ await api.switchProject(project.id);
893
+ const me = await api.me();
894
+ dispatch({ type: 'SET_USER_DATA', me });
895
+ await loadTabData();
896
+ });
897
+ showToast('Project created', 'success');
898
+ }
899
+ catch {
900
+ showToast('Failed to create project', 'error');
901
+ return;
805
902
  }
806
- catch { }
807
903
  break;
808
904
  }
809
905
  case 'initiate-consensus': {
810
906
  try {
811
- await api.initiateApproval(projectId, modal.action, modal.targetUserId);
812
- const approvals = await api.listApprovals(projectId);
813
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
907
+ await withOperation('Initiating approval', async () => {
908
+ await api.initiateApproval(projectId, modal.action, modal.targetUserId);
909
+ const approvals = await api.listApprovals(projectId);
910
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
911
+ });
912
+ showToast('Approval initiated', 'success');
913
+ }
914
+ catch {
915
+ showToast('Failed to initiate approval', 'error');
916
+ return;
814
917
  }
815
- catch { }
816
918
  break;
817
919
  }
818
920
  case 'approve-action': {
819
921
  try {
820
- await api.approveAction(projectId, modal.approvalId);
821
- const approvals = await api.listApprovals(projectId);
822
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
922
+ await withOperation('Approving', async () => {
923
+ await api.approveAction(projectId, modal.approvalId);
924
+ const approvals = await api.listApprovals(projectId);
925
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
926
+ });
927
+ showToast('Approved', 'success');
928
+ }
929
+ catch {
930
+ showToast('Failed to approve', 'error');
931
+ return;
823
932
  }
824
- catch { }
825
933
  break;
826
934
  }
827
935
  case 'confirm-regen-link': {
828
936
  try {
829
- await api.regenerateInviteLink(projectId, modal.role);
830
- const links = await api.listInviteLinks(projectId);
831
- dispatch({ type: 'SET_INVITE_LINKS', links });
937
+ await withOperation('Regenerating link', async () => {
938
+ await api.regenerateInviteLink(projectId, modal.role);
939
+ const links = await api.listInviteLinks(projectId);
940
+ dispatch({ type: 'SET_INVITE_LINKS', links });
941
+ });
942
+ showToast('Link regenerated', 'success');
943
+ }
944
+ catch {
945
+ showToast('Failed to regenerate link', 'error');
946
+ return;
832
947
  }
833
- catch { }
834
948
  break;
835
949
  }
836
950
  }
@@ -869,7 +983,9 @@ async function launchDashboard() {
869
983
  }
870
984
  }
871
985
  }
872
- catch { }
986
+ catch {
987
+ showToast('Failed to load data', 'error');
988
+ }
873
989
  }
874
990
  async function loadMoreMemories() {
875
991
  const projectId = state.activeProjectId;
@@ -880,7 +996,10 @@ async function launchDashboard() {
880
996
  const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: state.memories.length });
881
997
  dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total, append: true });
882
998
  }
883
- catch { }
999
+ catch {
1000
+ showToast('Failed to load more', 'error');
1001
+ dispatch({ type: 'SET_LOADING', loading: false });
1002
+ }
884
1003
  }
885
1004
  async function executeSearch() {
886
1005
  const projectId = state.activeProjectId;
@@ -891,63 +1010,88 @@ async function launchDashboard() {
891
1010
  const result = await api.searchMemories(projectId, state.searchQuery);
892
1011
  dispatch({ type: 'SET_SEARCH_RESULTS', results: result.memories });
893
1012
  }
894
- catch { }
1013
+ catch {
1014
+ showToast('Search failed', 'error');
1015
+ }
895
1016
  dispatch({ type: 'SET_SEARCHING', searching: false });
896
1017
  }
897
1018
  // ------ SSE feed ------
898
1019
  function connectFeed(projectId) {
899
1020
  if (feedDisconnect)
900
1021
  feedDisconnect();
901
- const conn = api.connectFeed(projectId, (eventType, data) => {
902
- if (eventType === 'memory') {
903
- const event = data;
904
- if (event.operation === 'create') {
905
- dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
906
- dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
907
- if (liveDotTimer)
908
- clearTimeout(liveDotTimer);
909
- liveDotTimer = setTimeout(() => {
910
- dispatch({ type: 'CLEAR_LIVE_DOT' });
911
- }, 10000);
1022
+ const conn = api.connectFeed(projectId, {
1023
+ onEvent: (eventType, data) => {
1024
+ if (eventType === 'memory') {
1025
+ const event = data;
1026
+ if (event.operation === 'create') {
1027
+ dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
1028
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
1029
+ if (liveDotTimer)
1030
+ clearTimeout(liveDotTimer);
1031
+ liveDotTimer = setTimeout(() => {
1032
+ dispatch({ type: 'CLEAR_LIVE_DOT' });
1033
+ }, 10000);
1034
+ }
1035
+ else if (event.operation === 'update') {
1036
+ const updated = { id: event.id, body: event.body, updated_at: event.updated_at };
1037
+ state = {
1038
+ ...state,
1039
+ memories: state.memories.map(m => m.id === event.id ? updated : m),
1040
+ searchResults: state.searchResults?.map(m => m.id === event.id ? updated : m) ?? null,
1041
+ };
1042
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
1043
+ if (liveDotTimer)
1044
+ clearTimeout(liveDotTimer);
1045
+ liveDotTimer = setTimeout(() => {
1046
+ dispatch({ type: 'CLEAR_LIVE_DOT' });
1047
+ }, 10000);
1048
+ }
912
1049
  }
913
- else if (event.operation === 'update') {
914
- // Update the memory in place
915
- const updated = { id: event.id, body: event.body, updated_at: event.updated_at };
916
- state = {
917
- ...state,
918
- memories: state.memories.map(m => m.id === event.id ? updated : m),
919
- searchResults: state.searchResults?.map(m => m.id === event.id ? updated : m) ?? null,
920
- };
921
- dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
922
- if (liveDotTimer)
923
- clearTimeout(liveDotTimer);
924
- liveDotTimer = setTimeout(() => {
925
- dispatch({ type: 'CLEAR_LIVE_DOT' });
926
- }, 10000);
1050
+ else if (eventType === 'role_change') {
1051
+ api.me().then(me => {
1052
+ dispatch({ type: 'SET_USER_DATA', me });
1053
+ }).catch(() => {
1054
+ if (Date.now() - lastSyncErrorAt > 30000) {
1055
+ lastSyncErrorAt = Date.now();
1056
+ showToast('Sync error', 'error');
1057
+ }
1058
+ });
927
1059
  }
928
- }
929
- else if (eventType === 'role_change') {
930
- // Role changed for the current user — refresh user data
931
- api.me().then(me => {
932
- dispatch({ type: 'SET_USER_DATA', me });
933
- }).catch(() => { });
934
- }
935
- else if (eventType === 'approval') {
936
- // Approval state changed — refresh approvals
937
- api.listApprovals(projectId).then(approvals => {
938
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
939
- }).catch(() => { });
940
- }
941
- else if (eventType === 'notification') {
942
- // New notification — refresh notifications
943
- api.listNotifications().then(notifications => {
944
- dispatch({ type: 'SET_NOTIFICATIONS', notifications });
945
- api.markNotificationsSeen().catch(() => { });
1060
+ else if (eventType === 'approval') {
1061
+ api.listApprovals(projectId).then(approvals => {
1062
+ dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
1063
+ }).catch(() => {
1064
+ if (Date.now() - lastSyncErrorAt > 30000) {
1065
+ lastSyncErrorAt = Date.now();
1066
+ showToast('Sync error', 'error');
1067
+ }
1068
+ });
1069
+ }
1070
+ else if (eventType === 'notification') {
1071
+ api.listNotifications().then(notifications => {
1072
+ dispatch({ type: 'SET_NOTIFICATIONS', notifications });
1073
+ api.markNotificationsSeen().catch(() => { });
1074
+ }).catch(() => {
1075
+ if (Date.now() - lastSyncErrorAt > 30000) {
1076
+ lastSyncErrorAt = Date.now();
1077
+ showToast('Sync error', 'error');
1078
+ }
1079
+ });
1080
+ }
1081
+ },
1082
+ onConnected: () => {
1083
+ dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
1084
+ // Refresh memories on reconnect to catch anything missed
1085
+ api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).then(result => {
1086
+ if (result)
1087
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
946
1088
  }).catch(() => { });
947
- }
1089
+ },
1090
+ onDisconnected: () => {
1091
+ dispatch({ type: 'SET_SSE_CONNECTED', connected: false });
1092
+ },
948
1093
  });
949
1094
  feedDisconnect = conn.disconnect;
950
- dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
951
1095
  }
952
1096
  // ------ File watchers for MCP config changes ------
953
1097
  function setupFileWatchers() {