cohvu 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/api.d.ts +44 -26
  2. package/dist/api.js +43 -59
  3. package/dist/api.js.map +1 -1
  4. package/dist/auth.d.ts +2 -13
  5. package/dist/auth.js +41 -105
  6. package/dist/auth.js.map +1 -1
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/index.js +59 -103
  11. package/dist/index.js.map +1 -1
  12. package/dist/instructions.d.ts +2 -2
  13. package/dist/instructions.js +22 -26
  14. package/dist/instructions.js.map +1 -1
  15. package/dist/platforms.js +22 -25
  16. package/dist/platforms.js.map +1 -1
  17. package/dist/proxy.js +26 -14
  18. package/dist/proxy.js.map +1 -1
  19. package/dist/setup.d.ts +0 -1
  20. package/dist/setup.js +43 -75
  21. package/dist/setup.js.map +1 -1
  22. package/dist/tui/App.d.ts +1 -0
  23. package/dist/tui/App.js +1091 -0
  24. package/dist/tui/App.js.map +1 -0
  25. package/dist/tui/components/Banner.d.ts +4 -0
  26. package/dist/tui/components/Banner.js +78 -0
  27. package/dist/tui/components/Banner.js.map +1 -0
  28. package/dist/tui/components/Divider.d.ts +1 -0
  29. package/dist/tui/components/Divider.js +7 -0
  30. package/dist/tui/components/Divider.js.map +1 -0
  31. package/dist/tui/components/Footer.d.ts +4 -0
  32. package/dist/tui/components/Footer.js +143 -0
  33. package/dist/tui/components/Footer.js.map +1 -0
  34. package/dist/tui/components/Header.d.ts +4 -0
  35. package/dist/tui/components/Header.js +53 -0
  36. package/dist/tui/components/Header.js.map +1 -0
  37. package/dist/tui/components/Modal.d.ts +5 -0
  38. package/dist/tui/components/Modal.js +99 -0
  39. package/dist/tui/components/Modal.js.map +1 -0
  40. package/dist/tui/components/TabBar.d.ts +4 -0
  41. package/dist/tui/components/TabBar.js +16 -0
  42. package/dist/tui/components/TabBar.js.map +1 -0
  43. package/dist/tui/components/Toast.d.ts +4 -0
  44. package/dist/tui/components/Toast.js +9 -0
  45. package/dist/tui/components/Toast.js.map +1 -0
  46. package/dist/tui/index.js +19 -0
  47. package/dist/tui/index.js.map +1 -0
  48. package/dist/tui/platform-detect.d.ts +1 -1
  49. package/dist/tui/platform-detect.js +19 -22
  50. package/dist/tui/platform-detect.js.map +1 -1
  51. package/dist/tui/state.d.ts +33 -6
  52. package/dist/tui/state.js +65 -17
  53. package/dist/tui/state.js.map +1 -1
  54. package/dist/tui/tabs/BillingTab.d.ts +4 -0
  55. package/dist/tui/tabs/BillingTab.js +68 -0
  56. package/dist/tui/tabs/BillingTab.js.map +1 -0
  57. package/dist/tui/tabs/KnowledgeTab.d.ts +5 -0
  58. package/dist/tui/tabs/KnowledgeTab.js +83 -0
  59. package/dist/tui/tabs/KnowledgeTab.js.map +1 -0
  60. package/dist/tui/tabs/ProjectTab.d.ts +4 -0
  61. package/dist/tui/tabs/ProjectTab.js +31 -0
  62. package/dist/tui/tabs/ProjectTab.js.map +1 -0
  63. package/dist/tui/tabs/TeamTab.d.ts +4 -0
  64. package/dist/tui/tabs/TeamTab.js +39 -0
  65. package/dist/tui/tabs/TeamTab.js.map +1 -0
  66. package/dist/tui/tabs/YouTab.d.ts +4 -0
  67. package/dist/tui/tabs/YouTab.js +9 -0
  68. package/dist/tui/tabs/YouTab.js.map +1 -0
  69. package/dist/tui/utils.d.ts +6 -0
  70. package/dist/tui/utils.js +58 -0
  71. package/dist/tui/utils.js.map +1 -0
  72. package/package.json +9 -2
  73. package/dist/tui/ansi.d.ts +0 -41
  74. package/dist/tui/ansi.js +0 -117
  75. package/dist/tui/ansi.js.map +0 -1
  76. package/dist/tui/dashboard.js +0 -1250
  77. package/dist/tui/dashboard.js.map +0 -1
  78. package/dist/tui/keys.d.ts +0 -31
  79. package/dist/tui/keys.js +0 -56
  80. package/dist/tui/keys.js.map +0 -1
  81. package/dist/tui/render.d.ts +0 -5
  82. package/dist/tui/render.js +0 -158
  83. package/dist/tui/render.js.map +0 -1
  84. package/dist/tui/views/billing.d.ts +0 -3
  85. package/dist/tui/views/billing.js +0 -127
  86. package/dist/tui/views/billing.js.map +0 -1
  87. package/dist/tui/views/knowledge.d.ts +0 -3
  88. package/dist/tui/views/knowledge.js +0 -202
  89. package/dist/tui/views/knowledge.js.map +0 -1
  90. package/dist/tui/views/modals.d.ts +0 -3
  91. package/dist/tui/views/modals.js +0 -198
  92. package/dist/tui/views/modals.js.map +0 -1
  93. package/dist/tui/views/project.d.ts +0 -3
  94. package/dist/tui/views/project.js +0 -76
  95. package/dist/tui/views/project.js.map +0 -1
  96. package/dist/tui/views/team.d.ts +0 -3
  97. package/dist/tui/views/team.js +0 -106
  98. package/dist/tui/views/team.js.map +0 -1
  99. package/dist/tui/views/you.d.ts +0 -3
  100. package/dist/tui/views/you.js +0 -40
  101. package/dist/tui/views/you.js.map +0 -1
  102. /package/dist/tui/{dashboard.d.ts → index.d.ts} +0 -0
@@ -1,1250 +0,0 @@
1
- "use strict";
2
- // Dashboard main loop — raw mode, keypress dispatch, render, SSE, fs.watch.
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.launchDashboard = launchDashboard;
5
- const keys_1 = require("./keys");
6
- const state_1 = require("./state");
7
- const render_1 = require("./render");
8
- const ansi_1 = require("./ansi");
9
- const api_1 = require("../api");
10
- const setup_1 = require("../setup");
11
- const platform_detect_1 = require("./platform-detect");
12
- const child_process_1 = require("child_process");
13
- const fs_1 = require("fs");
14
- const path_1 = require("path");
15
- const os_1 = require("os");
16
- const STATE_FILE = (0, path_1.join)((0, os_1.homedir)(), ".cohvu", "state.json");
17
- const PAGE_SIZE = 20;
18
- async function launchDashboard() {
19
- const api = new api_1.ApiClient();
20
- let state = (0, state_1.initialState)();
21
- const watchers = [];
22
- let searchTimer = null;
23
- let liveDotTimer = null;
24
- let feedDisconnect = null;
25
- let copiedTimer = null;
26
- let inlineErrorTimer = null;
27
- let toastTimer = null;
28
- let lastSyncErrorAt = 0;
29
- let pollInterval = null;
30
- // Toast feedback
31
- function showToast(message, type, durationMs = 3000) {
32
- if (toastTimer)
33
- clearTimeout(toastTimer);
34
- dispatch({ type: 'SET_TOAST', toast: { message, type, expiresAt: Date.now() + durationMs } });
35
- toastTimer = setTimeout(() => {
36
- dispatch({ type: 'SET_TOAST', toast: null });
37
- }, durationMs);
38
- }
39
- // Loading indicator for async operations
40
- async function withOperation(description, fn) {
41
- dispatch({ type: 'SET_OPERATION', operation: description });
42
- doRender();
43
- try {
44
- return await fn();
45
- }
46
- finally {
47
- dispatch({ type: 'SET_OPERATION', operation: null });
48
- doRender();
49
- }
50
- }
51
- // Load first-login state
52
- const savedState = loadState();
53
- state.firstLogin = !savedState.hasOpenedDashboard;
54
- // Dispatch helper
55
- function dispatch(action) {
56
- state = (0, state_1.reduce)(state, action);
57
- // Regenerate banners on relevant state changes
58
- if (['SET_USER_DATA', 'SET_PLATFORMS', 'SET_BILLING', 'SET_PENDING_APPROVALS', 'SET_NOTIFICATIONS'].includes(action.type)) {
59
- state = { ...state, bannerLines: generateBanners(state) };
60
- }
61
- doRender();
62
- }
63
- function doRender() {
64
- const dims = { rows: process.stdout.rows, cols: process.stdout.columns };
65
- (0, render_1.render)(state, dims);
66
- }
67
- // Clean exit
68
- function cleanup() {
69
- if (feedDisconnect)
70
- feedDisconnect();
71
- if (pollInterval)
72
- clearInterval(pollInterval);
73
- watchers.forEach(w => w.close());
74
- if (searchTimer)
75
- clearTimeout(searchTimer);
76
- if (liveDotTimer)
77
- clearTimeout(liveDotTimer);
78
- if (inlineErrorTimer)
79
- clearTimeout(inlineErrorTimer);
80
- process.stdin.setRawMode?.(false);
81
- (0, ansi_1.exitScreen)();
82
- }
83
- function cleanExit() {
84
- cleanup();
85
- process.exit(0);
86
- }
87
- process.on('SIGTERM', cleanExit);
88
- process.on('SIGINT', cleanExit);
89
- process.on('uncaughtException', (err) => {
90
- cleanup();
91
- process.stderr.write(`Error: ${err.message}\n`);
92
- process.exit(1);
93
- });
94
- // Enter TUI
95
- (0, ansi_1.enterScreen)();
96
- process.stdin.setRawMode?.(true);
97
- process.stdin.resume();
98
- // Resize handler
99
- process.stdout.on('resize', doRender);
100
- // Initial render (loading state)
101
- doRender();
102
- // Load data
103
- try {
104
- const me = await api.me();
105
- dispatch({ type: 'SET_USER_DATA', me });
106
- // Run setup (idempotent — skips if already configured) then detect status
107
- await (0, setup_1.runSetup)();
108
- const platforms = (0, platform_detect_1.detectPlatformStatuses)();
109
- dispatch({ type: 'SET_PLATFORMS', platforms });
110
- // Load knowledge, members, billing, invite links, notifications in parallel
111
- const projectId = state.activeProjectId;
112
- if (projectId) {
113
- let loadError = false;
114
- const [memResult, members, billing, inviteLinks, notifications] = await Promise.all([
115
- api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).catch(() => { loadError = true; return null; }),
116
- api.listMembers(projectId).catch(() => { loadError = true; return []; }),
117
- api.getBilling(projectId).catch(() => { loadError = true; return null; }),
118
- api.listInviteLinks(projectId).catch(() => []),
119
- api.listNotifications().catch(() => []),
120
- ]);
121
- if (loadError)
122
- showToast('Some data failed to load', 'error');
123
- if (memResult) {
124
- dispatch({ type: 'SET_MEMORIES', memories: memResult.memories, total: memResult.total });
125
- }
126
- dispatch({ type: 'SET_MEMBERS', members });
127
- if (billing) {
128
- dispatch({ type: 'SET_BILLING', billing });
129
- }
130
- dispatch({ type: 'SET_INVITE_LINKS', links: inviteLinks });
131
- dispatch({ type: 'SET_NOTIFICATIONS', notifications });
132
- // Mark notifications as seen
133
- if (notifications.length > 0) {
134
- api.markNotificationsSeen().catch(() => { });
135
- }
136
- // Connect SSE feed (real-time acceleration)
137
- connectFeed(projectId);
138
- // Background poll (foundation — ensures data is never more than 30s stale)
139
- startPolling(projectId);
140
- }
141
- // Mark first login seen
142
- if (state.firstLogin) {
143
- saveState({ hasOpenedDashboard: true });
144
- }
145
- }
146
- catch (err) {
147
- dispatch({ type: 'SET_OFFLINE', offline: true });
148
- dispatch({ type: 'SET_ERROR', error: "can't reach cohvu" });
149
- }
150
- // Watch MCP config files for live detection updates
151
- setupFileWatchers();
152
- // Keypress loop
153
- process.stdin.on('data', async (data) => {
154
- const key = (0, keys_1.parseKey)(data);
155
- await handleKey(key);
156
- doRender();
157
- });
158
- // ------ Key handler ------
159
- async function handleKey(key) {
160
- // Global keys
161
- if (key.name === 'ctrl-c' || (key.name === 'char' && key.char === 'q' && !state.modal && state.knowledgeMode !== 'search')) {
162
- cleanExit();
163
- return;
164
- }
165
- // Modal keys
166
- if (state.modal) {
167
- await handleModalKey(key);
168
- return;
169
- }
170
- // Tab switching
171
- if (key.name === 'tab') {
172
- dispatch({ type: 'NEXT_TAB' });
173
- await loadTabData();
174
- return;
175
- }
176
- if (key.name === 'char') {
177
- const tabIdx = parseInt(key.char, 10);
178
- if (tabIdx >= 1 && tabIdx <= 5) {
179
- dispatch({ type: 'SWITCH_TAB', tab: state_1.TABS[tabIdx - 1] });
180
- await loadTabData();
181
- return;
182
- }
183
- }
184
- // Global 'b' shortcut — subscribe/billing portal from banner
185
- if (key.name === 'char' && key.char === 'b' && state.userRole === 'admin' && state.tab !== 'billing') {
186
- const project = (0, state_1.getActiveProject)(state);
187
- if (project) {
188
- const sub = state.billing?.subscription;
189
- if (!sub || sub.status !== 'active') {
190
- try {
191
- showToast('Opening checkout...', 'info');
192
- const checkout = await api.createCheckout(project.project_id);
193
- if (checkout.checkout_url)
194
- openBrowser(checkout.checkout_url);
195
- }
196
- catch {
197
- showToast('Failed to open checkout', 'error');
198
- }
199
- }
200
- else {
201
- try {
202
- showToast('Opening billing portal...', 'info');
203
- const portal = await api.getPortalUrl(project.project_id);
204
- if (portal.url)
205
- openBrowser(portal.url);
206
- }
207
- catch {
208
- showToast('Failed to open billing portal', 'error');
209
- }
210
- }
211
- }
212
- return;
213
- }
214
- // Tab-specific keys
215
- switch (state.tab) {
216
- case 'knowledge':
217
- await handleKnowledgeKey(key);
218
- break;
219
- case 'team':
220
- await handleTeamKey(key);
221
- break;
222
- case 'billing':
223
- await handleBillingKey(key);
224
- break;
225
- case 'project':
226
- await handleProjectKey(key);
227
- break;
228
- case 'you':
229
- await handleYouKey(key);
230
- break;
231
- }
232
- }
233
- // ------ Knowledge keys ------
234
- async function handleKnowledgeKey(key) {
235
- // alt+d enters forget mode from any knowledge mode
236
- if (key.name === 'alt-d' && state.userRole !== 'viewer') {
237
- const list = state.searchResults ?? state.memories;
238
- if (list.length > 0) {
239
- dispatch({ type: 'ENTER_FORGET' });
240
- }
241
- return;
242
- }
243
- if (state.knowledgeMode === 'search') {
244
- if (key.name === 'escape') {
245
- dispatch({ type: 'EXIT_SEARCH' });
246
- }
247
- else if (key.name === 'enter' && state.searchQuery.length > 0) {
248
- // Enter triggers the search
249
- await executeSearch();
250
- }
251
- else if (key.name === 'backspace') {
252
- dispatch({ type: 'SEARCH_BACKSPACE' });
253
- }
254
- else if (key.name === 'space') {
255
- dispatch({ type: 'SEARCH_INPUT', char: ' ' });
256
- }
257
- else if (key.name === 'char') {
258
- dispatch({ type: 'SEARCH_INPUT', char: key.char });
259
- }
260
- else if (key.name === 'up') {
261
- dispatch({ type: 'SCROLL_UP' });
262
- }
263
- else if (key.name === 'down') {
264
- dispatch({ type: 'SCROLL_DOWN' });
265
- }
266
- return;
267
- }
268
- if (state.knowledgeMode === 'forget') {
269
- // Build the same filtered list the view renders
270
- const forgetList = state.searchResults ?? state.memories;
271
- const filtered = state.userRole === 'admin'
272
- ? forgetList
273
- : forgetList.filter(m => m.contributed_by?.user_id === state.user?.id);
274
- if (state.forgetConfirming && key.name === 'char' && key.char === 'y') {
275
- // Confirmed — delete
276
- const projectId = state.activeProjectId;
277
- if (projectId) {
278
- let failures = 0;
279
- dispatch({ type: 'SET_OPERATION', operation: 'Removing memories' });
280
- doRender();
281
- await Promise.all([...state.forgetSelected].map(async (id) => {
282
- try {
283
- await api.deleteMemory(projectId, id);
284
- dispatch({ type: 'REMOVE_MEMORY', id });
285
- }
286
- catch {
287
- failures++;
288
- }
289
- }));
290
- dispatch({ type: 'SET_OPERATION', operation: null });
291
- dispatch({ type: 'EXIT_FORGET' });
292
- if (failures > 0)
293
- showToast(`${failures} failed to remove`, 'error');
294
- else
295
- showToast('Removed', 'success');
296
- }
297
- }
298
- else if (state.forgetConfirming && (key.name === 'escape' || (key.name === 'char' && key.char === 'n'))) {
299
- // Cancel confirmation
300
- state = { ...state, forgetConfirming: false };
301
- doRender();
302
- }
303
- else if (key.name === 'escape') {
304
- dispatch({ type: 'EXIT_FORGET' });
305
- }
306
- else if (key.name === 'space') {
307
- const mem = filtered[state.memorySelected];
308
- if (mem) {
309
- dispatch({ type: 'TOGGLE_FORGET', memoryId: mem.id });
310
- }
311
- }
312
- else if (key.name === 'up') {
313
- const newIdx = Math.max(0, state.memorySelected - 1);
314
- state = { ...state, memorySelected: newIdx };
315
- doRender();
316
- }
317
- else if (key.name === 'down') {
318
- const newIdx = Math.min(filtered.length - 1, state.memorySelected + 1);
319
- state = { ...state, memorySelected: newIdx };
320
- doRender();
321
- }
322
- else if (key.name === 'enter' && state.forgetSelected.size > 0 && !state.forgetConfirming) {
323
- // Show confirmation
324
- state = { ...state, forgetConfirming: true };
325
- doRender();
326
- }
327
- return;
328
- }
329
- // Browse mode
330
- if (key.name === 'char' && key.char === '/') {
331
- dispatch({ type: 'ENTER_SEARCH' });
332
- }
333
- else if (key.name === 'char' && key.char === 'd' && state.userRole !== 'viewer') {
334
- if (state.memories.length > 0) {
335
- dispatch({ type: 'ENTER_FORGET' });
336
- }
337
- }
338
- else if (key.name === 'char' && key.char === 'D' && state.userRole === 'admin') {
339
- const project = (0, state_1.getActiveProject)(state);
340
- if (project) {
341
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-forget-all', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
342
- }
343
- }
344
- else if (key.name === 'up') {
345
- dispatch({ type: 'SCROLL_UP' });
346
- }
347
- else if (key.name === 'down') {
348
- dispatch({ type: 'SCROLL_DOWN' });
349
- }
350
- else if (key.name === 'space') {
351
- await loadMoreMemories();
352
- }
353
- }
354
- // ------ Team keys ------
355
- function showInlineError(message, durationMs) {
356
- dispatch({ type: 'SET_INLINE_ERROR', message });
357
- if (inlineErrorTimer)
358
- clearTimeout(inlineErrorTimer);
359
- inlineErrorTimer = setTimeout(() => {
360
- dispatch({ type: 'SET_INLINE_ERROR', message: null });
361
- }, durationMs);
362
- }
363
- async function handleTeamKey(key) {
364
- const memberCount = state.members.length;
365
- const linkRoles = ['admin', 'member', 'viewer'];
366
- const linkCount = state.userRole === 'admin' ? linkRoles.length : 0;
367
- const totalRows = memberCount + linkCount;
368
- const sel = state.teamSelected;
369
- const onLinkRow = state.userRole === 'admin' && sel >= memberCount;
370
- const onMemberRow = sel < memberCount;
371
- // Up/down navigation
372
- if (key.name === 'up') {
373
- const newIdx = Math.max(0, sel - 1);
374
- dispatch({ type: 'SET_TEAM_SELECTED', index: newIdx });
375
- return;
376
- }
377
- if (key.name === 'down') {
378
- const newIdx = Math.min(totalRows - 1, sel + 1);
379
- dispatch({ type: 'SET_TEAM_SELECTED', index: newIdx });
380
- return;
381
- }
382
- // 'c' — copy invite link (only when cursor is on a link row, admin only)
383
- if (key.name === 'char' && key.char === 'c' && state.userRole === 'admin' && onLinkRow) {
384
- const linkIdx = sel - memberCount;
385
- const role = linkRoles[linkIdx];
386
- const link = state.inviteLinks.find(l => l.role === role);
387
- if (link) {
388
- copyToClipboard(link.url);
389
- dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
390
- if (copiedTimer)
391
- clearTimeout(copiedTimer);
392
- copiedTimer = setTimeout(() => {
393
- dispatch({ type: 'SET_COPIED_FEEDBACK', active: false });
394
- }, 1500);
395
- }
396
- return;
397
- }
398
- // 'r' — regen invite link (only when cursor is on a link row, admin only)
399
- if (key.name === 'char' && key.char === 'r' && state.userRole === 'admin' && onLinkRow) {
400
- const linkIdx = sel - memberCount;
401
- const role = linkRoles[linkIdx];
402
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
403
- return;
404
- }
405
- // 'e' — edit role (only when cursor is on a member row, admin only)
406
- if (key.name === 'char' && key.char === 'e' && state.userRole === 'admin' && onMemberRow) {
407
- const target = state.members[sel];
408
- if (target) {
409
- // Cannot edit own role
410
- if (target.user_id === state.user?.id) {
411
- showInlineError('you cannot change your own role', 2000);
412
- return;
413
- }
414
- const roleIdx = ['admin', 'member', 'viewer'].indexOf(target.role ?? 'member');
415
- dispatch({ type: 'OPEN_MODAL', modal: {
416
- kind: 'edit-role',
417
- targetEmail: target.email ?? target.user_id,
418
- targetUserId: target.user_id,
419
- currentRole: target.role ?? 'member',
420
- selected: roleIdx >= 0 ? roleIdx : 1,
421
- } });
422
- }
423
- return;
424
- }
425
- // 'a' — approve pending approval (admin only)
426
- if (key.name === 'char' && key.char === 'a' && state.userRole === 'admin') {
427
- const demoteApproval = state.pendingApprovals.find(a => a.action === 'demote_admin');
428
- if (demoteApproval) {
429
- dispatch({ type: 'OPEN_MODAL', modal: {
430
- kind: 'approve-action',
431
- approvalId: demoteApproval.id,
432
- description: demoteApproval.description,
433
- initiator: demoteApproval.initiator_email,
434
- expiresIn: '',
435
- } });
436
- }
437
- return;
438
- }
439
- // 'x' — cancel pending approval (admin, when approval exists) or remove member or leave
440
- if (key.name === 'char' && key.char === 'x' && state.userRole === 'admin' && state.pendingApprovals.length > 0) {
441
- const demoteApproval = state.pendingApprovals.find(a => a.action === 'demote_admin');
442
- if (demoteApproval) {
443
- const projectId = state.activeProjectId;
444
- if (projectId) {
445
- try {
446
- await api.cancelApproval(projectId, demoteApproval.id);
447
- const approvals = await api.listApprovals(projectId);
448
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
449
- showToast('Approval cancelled', 'success');
450
- }
451
- catch {
452
- showToast('Failed to cancel approval', 'error');
453
- }
454
- }
455
- return;
456
- }
457
- }
458
- // 'x' — remove member (admin on member row) or leave (member/viewer)
459
- if (key.name === 'char' && key.char === 'x') {
460
- if (state.userRole === 'admin' && onMemberRow) {
461
- const target = state.members[sel];
462
- if (target) {
463
- if (target.user_id === state.user?.id) {
464
- // Admin trying to leave — check if last admin
465
- const adminCount = state.members.filter(m => m.role === 'admin').length;
466
- if (adminCount <= 1) {
467
- showInlineError('you are the only admin \u00b7 promote another member before leaving', 3000);
468
- return;
469
- }
470
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
471
- }
472
- else {
473
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-remove-member', email: target.email ?? target.user_id, userId: target.user_id } });
474
- }
475
- }
476
- }
477
- else if (state.userRole !== 'admin') {
478
- // Member/viewer can leave
479
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
480
- }
481
- return;
482
- }
483
- }
484
- // ------ Billing keys ------
485
- async function handleBillingKey(key) {
486
- if (state.userRole !== 'admin')
487
- return;
488
- if (key.name === 'char' && key.char === 's') {
489
- const projectId = state.activeProjectId;
490
- if (projectId) {
491
- try {
492
- showToast('Opening checkout...', 'info');
493
- const checkout = await api.createCheckout(projectId);
494
- if (checkout.checkout_url)
495
- openBrowser(checkout.checkout_url);
496
- }
497
- catch {
498
- showToast('Failed to open checkout', 'error');
499
- }
500
- }
501
- }
502
- else if (key.name === 'char' && key.char === 'p') {
503
- const projectId = state.activeProjectId;
504
- if (projectId) {
505
- try {
506
- showToast('Opening billing portal...', 'info');
507
- const portal = await api.getPortalUrl(projectId);
508
- if (portal.url)
509
- openBrowser(portal.url);
510
- }
511
- catch {
512
- showToast('Failed to open billing portal', 'error');
513
- }
514
- }
515
- }
516
- }
517
- // ------ Project keys ------
518
- async function handleProjectKey(key) {
519
- // 'a' — approve pending approval (admin only)
520
- if (key.name === 'char' && key.char === 'a' && state.userRole === 'admin') {
521
- const projectApproval = state.pendingApprovals.find(a => a.action === 'delete_project' || a.action === 'clear_memories');
522
- if (projectApproval) {
523
- dispatch({ type: 'OPEN_MODAL', modal: {
524
- kind: 'approve-action',
525
- approvalId: projectApproval.id,
526
- description: projectApproval.description,
527
- initiator: projectApproval.initiator_email,
528
- expiresIn: '',
529
- } });
530
- }
531
- return;
532
- }
533
- // 'x' — cancel pending approval (admin only)
534
- if (key.name === 'char' && key.char === 'x' && state.userRole === 'admin') {
535
- const projectApproval = state.pendingApprovals.find(a => a.action === 'delete_project' || a.action === 'clear_memories');
536
- if (projectApproval) {
537
- const projectId = state.activeProjectId;
538
- if (projectId) {
539
- try {
540
- await api.cancelApproval(projectId, projectApproval.id);
541
- const approvals = await api.listApprovals(projectId);
542
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
543
- showToast('Approval cancelled', 'success');
544
- }
545
- catch {
546
- showToast('Failed to cancel approval', 'error');
547
- }
548
- }
549
- return;
550
- }
551
- }
552
- if (key.name === 'char' && key.char === 'r' && state.userRole === 'admin') {
553
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'rename', input: '' } });
554
- }
555
- else if (key.name === 'char' && key.char === 'n') {
556
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
557
- }
558
- else if (key.name === 'char' && key.char === 'w' && state.projects.length > 1) {
559
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: 0 } });
560
- }
561
- else if (key.name === 'char' && key.char === 'c' && state.userRole === 'admin') {
562
- const project = (0, state_1.getActiveProject)(state);
563
- if (project) {
564
- const adminCount = state.members.filter(m => m.role === 'admin').length;
565
- if (adminCount > 1) {
566
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'initiate-consensus', action: 'clear_memories', description: 'clear all memories' } });
567
- }
568
- else {
569
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-clear', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
570
- }
571
- }
572
- }
573
- else if (key.name === 'char' && key.char === 'd' && state.userRole === 'admin') {
574
- const project = (0, state_1.getActiveProject)(state);
575
- if (project) {
576
- const adminCount = state.members.filter(m => m.role === 'admin').length;
577
- if (adminCount > 1) {
578
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'initiate-consensus', action: 'delete_project', description: 'delete ' + project.slug } });
579
- }
580
- else {
581
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-delete', slug: project.slug, memoryCount: state.memoryTotal, input: '' } });
582
- }
583
- }
584
- }
585
- }
586
- // ------ You keys ------
587
- async function handleYouKey(key) {
588
- if (key.name === 'char' && key.char === 'r') {
589
- // Re-run setup — writes MCP configs + instructions, then re-detect
590
- await (0, setup_1.runSetup)();
591
- const platforms = (0, platform_detect_1.detectPlatformStatuses)();
592
- dispatch({ type: 'SET_PLATFORMS', platforms });
593
- }
594
- else if (key.name === 'char' && key.char === 'l') {
595
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-logout' } });
596
- }
597
- }
598
- // ------ Modal keys ------
599
- async function handleModalKey(key) {
600
- if (!state.modal)
601
- return;
602
- if (key.name === 'escape') {
603
- dispatch({ type: 'CLOSE_MODAL' });
604
- return;
605
- }
606
- const modal = state.modal;
607
- // y/n modals
608
- if (modal.kind === 'confirm-forget' || modal.kind === 'confirm-remove-member' || modal.kind === 'confirm-leave' || modal.kind === 'confirm-logout') {
609
- if (key.name === 'char' && key.char === 'y') {
610
- await confirmModal(modal);
611
- dispatch({ type: 'CLOSE_MODAL' });
612
- }
613
- else if (key.name === 'char' && key.char === 'n') {
614
- dispatch({ type: 'CLOSE_MODAL' });
615
- }
616
- return;
617
- }
618
- // Initiate consensus modal (y/n)
619
- if (modal.kind === 'initiate-consensus') {
620
- if (key.name === 'char' && key.char === 'y') {
621
- await confirmModal(modal);
622
- dispatch({ type: 'CLOSE_MODAL' });
623
- }
624
- else if (key.name === 'char' && key.char === 'n') {
625
- dispatch({ type: 'CLOSE_MODAL' });
626
- }
627
- return;
628
- }
629
- // Approve action modal (y/n)
630
- if (modal.kind === 'approve-action') {
631
- if (key.name === 'char' && key.char === 'y') {
632
- await confirmModal(modal);
633
- dispatch({ type: 'CLOSE_MODAL' });
634
- }
635
- else if (key.name === 'char' && key.char === 'n') {
636
- dispatch({ type: 'CLOSE_MODAL' });
637
- }
638
- return;
639
- }
640
- // Confirm regen link modal (y/n)
641
- if (modal.kind === 'confirm-regen-link') {
642
- if (key.name === 'char' && key.char === 'y') {
643
- await confirmModal(modal);
644
- dispatch({ type: 'CLOSE_MODAL' });
645
- }
646
- else if (key.name === 'char' && key.char === 'n') {
647
- dispatch({ type: 'CLOSE_MODAL' });
648
- }
649
- return;
650
- }
651
- // Text input modals
652
- if ('input' in modal) {
653
- if (key.name === 'enter') {
654
- await confirmModal(modal);
655
- dispatch({ type: 'CLOSE_MODAL' });
656
- }
657
- else if (key.name === 'backspace') {
658
- dispatch({ type: 'MODAL_BACKSPACE' });
659
- }
660
- else if (key.name === 'space') {
661
- dispatch({ type: 'MODAL_INPUT', char: ' ' });
662
- }
663
- else if (key.name === 'char') {
664
- dispatch({ type: 'MODAL_INPUT', char: key.char });
665
- }
666
- return;
667
- }
668
- // Switch project modal
669
- if (modal.kind === 'switch-project') {
670
- if (key.name === 'up') {
671
- const newIdx = Math.max(0, modal.selected - 1);
672
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: newIdx } });
673
- }
674
- else if (key.name === 'down') {
675
- // +1 for the "new project" row at the end
676
- const newIdx = Math.min(state.projects.length, modal.selected + 1);
677
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: newIdx } });
678
- }
679
- else if (key.name === 'enter') {
680
- if (modal.selected === state.projects.length) {
681
- // "New project" row selected
682
- dispatch({ type: 'CLOSE_MODAL' });
683
- dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
684
- }
685
- else {
686
- const project = state.projects[modal.selected];
687
- if (project) {
688
- await api.switchProject(project.project_id);
689
- const me = await api.me();
690
- dispatch({ type: 'SET_USER_DATA', me });
691
- dispatch({ type: 'CLOSE_MODAL' });
692
- if (state.activeProjectId)
693
- connectFeed(state.activeProjectId);
694
- await loadTabData();
695
- }
696
- }
697
- }
698
- }
699
- // Edit role modal
700
- if (modal.kind === 'edit-role') {
701
- const roles = ['admin', 'member', 'viewer'];
702
- if (key.name === 'up') {
703
- const newIdx = Math.max(0, modal.selected - 1);
704
- dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: newIdx } });
705
- }
706
- else if (key.name === 'down') {
707
- const newIdx = Math.min(roles.length - 1, modal.selected + 1);
708
- dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: newIdx } });
709
- }
710
- else if (key.name === 'enter') {
711
- const newRole = roles[modal.selected];
712
- if (newRole !== modal.currentRole) {
713
- // Check if demoting an admin
714
- if (modal.currentRole === 'admin' && newRole !== 'admin') {
715
- const adminCount = state.members.filter(m => m.role === 'admin').length;
716
- // Last admin protection — cannot demote the only admin
717
- if (adminCount <= 1) {
718
- showInlineError('cannot demote the last admin \u00b7 promote another member first', 2000);
719
- dispatch({ type: 'CLOSE_MODAL' });
720
- return;
721
- }
722
- if (adminCount > 1) {
723
- // Initiate consensus for demotion
724
- dispatch({ type: 'CLOSE_MODAL' });
725
- dispatch({ type: 'OPEN_MODAL', modal: {
726
- kind: 'initiate-consensus',
727
- action: 'demote_admin',
728
- description: 'demoting ' + modal.targetEmail,
729
- targetUserId: modal.targetUserId,
730
- } });
731
- return;
732
- }
733
- }
734
- // Direct role change
735
- const projectId = state.activeProjectId;
736
- if (projectId) {
737
- try {
738
- await api.changeRole(projectId, modal.targetUserId, newRole);
739
- const members = await api.listMembers(projectId);
740
- dispatch({ type: 'SET_MEMBERS', members });
741
- showToast('Role updated', 'success');
742
- }
743
- catch {
744
- showToast('Failed to update role', 'error');
745
- }
746
- }
747
- }
748
- dispatch({ type: 'CLOSE_MODAL' });
749
- }
750
- }
751
- }
752
- async function confirmModal(modal) {
753
- const projectId = state.activeProjectId;
754
- if (!projectId)
755
- return;
756
- switch (modal.kind) {
757
- case 'confirm-forget':
758
- try {
759
- await withOperation('Removing memory', async () => {
760
- await api.deleteMemory(projectId, modal.memoryId);
761
- dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
762
- });
763
- showToast('Memory removed', 'success');
764
- }
765
- catch {
766
- showToast('Failed to remove memory', 'error');
767
- return;
768
- }
769
- break;
770
- case 'confirm-forget-all':
771
- case 'confirm-clear': {
772
- const project = (0, state_1.getActiveProject)(state);
773
- if (project && modal.input === project.slug) {
774
- try {
775
- await withOperation('Clearing memories', async () => {
776
- await api.clearMemories(projectId);
777
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
778
- });
779
- showToast('All memories cleared', 'success');
780
- }
781
- catch {
782
- showToast('Failed to clear memories', 'error');
783
- return;
784
- }
785
- }
786
- break;
787
- }
788
- case 'confirm-delete': {
789
- const project = (0, state_1.getActiveProject)(state);
790
- if (project && modal.input === project.slug) {
791
- try {
792
- await withOperation('Deleting project', async () => {
793
- await api.deleteProject(projectId);
794
- const me = await api.me();
795
- dispatch({ type: 'SET_USER_DATA', me });
796
- dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
797
- if (state.activeProjectId) {
798
- connectFeed(state.activeProjectId);
799
- await loadTabData();
800
- }
801
- else {
802
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
803
- dispatch({ type: 'SET_MEMBERS', members: [] });
804
- }
805
- });
806
- showToast('Project deleted', 'success');
807
- }
808
- catch {
809
- showToast('Failed to delete project', 'error');
810
- return;
811
- }
812
- }
813
- break;
814
- }
815
- case 'confirm-remove-member':
816
- try {
817
- await withOperation('Removing member', async () => {
818
- await api.removeMember(projectId, modal.userId);
819
- const members = await api.listMembers(projectId);
820
- dispatch({ type: 'SET_MEMBERS', members });
821
- });
822
- showToast('Member removed', 'success');
823
- }
824
- catch {
825
- showToast('Failed to remove member', 'error');
826
- return;
827
- }
828
- break;
829
- case 'confirm-leave':
830
- try {
831
- await withOperation('Leaving project', async () => {
832
- const leavingUserId = state.user.id;
833
- for (const approval of state.pendingApprovals) {
834
- const leavingEmail = state.user.email;
835
- if (approval.initiator_email === leavingEmail) {
836
- await api.cancelApproval(projectId, approval.id).catch(() => { });
837
- }
838
- }
839
- await api.removeMember(projectId, leavingUserId);
840
- const me = await api.me();
841
- dispatch({ type: 'SET_USER_DATA', me });
842
- dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
843
- if (state.activeProjectId) {
844
- connectFeed(state.activeProjectId);
845
- await loadTabData();
846
- }
847
- else {
848
- dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
849
- dispatch({ type: 'SET_MEMBERS', members: [] });
850
- }
851
- });
852
- showToast('Left project', 'success');
853
- }
854
- catch {
855
- showToast('Failed to leave project', 'error');
856
- return;
857
- }
858
- break;
859
- case 'confirm-logout':
860
- try {
861
- const tokensFile = (0, path_1.join)((0, os_1.homedir)(), ".cohvu", "tokens.json");
862
- if ((0, fs_1.existsSync)(tokensFile)) {
863
- const { unlinkSync } = require("fs");
864
- unlinkSync(tokensFile);
865
- }
866
- }
867
- catch { }
868
- // Brief delay for in-flight proxy requests
869
- await new Promise((r) => setTimeout(r, 500));
870
- cleanExit();
871
- break;
872
- case 'rename': {
873
- if (!modal.input)
874
- break;
875
- const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
876
- try {
877
- await withOperation('Renaming', async () => {
878
- await api.renameProject(projectId, modal.input, slug);
879
- const me = await api.me();
880
- dispatch({ type: 'SET_USER_DATA', me });
881
- });
882
- showToast('Project renamed', 'success');
883
- }
884
- catch {
885
- showToast('Failed to rename', 'error');
886
- return;
887
- }
888
- break;
889
- }
890
- case 'create-project': {
891
- if (!modal.input)
892
- break;
893
- const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
894
- try {
895
- await withOperation('Creating project', async () => {
896
- const project = await api.createProject(modal.input, slug);
897
- await api.switchProject(project.id);
898
- const me = await api.me();
899
- dispatch({ type: 'SET_USER_DATA', me });
900
- await loadTabData();
901
- });
902
- showToast('Project created', 'success');
903
- }
904
- catch {
905
- showToast('Failed to create project', 'error');
906
- return;
907
- }
908
- break;
909
- }
910
- case 'initiate-consensus': {
911
- try {
912
- await withOperation('Initiating approval', async () => {
913
- await api.initiateApproval(projectId, modal.action, modal.targetUserId);
914
- const approvals = await api.listApprovals(projectId);
915
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
916
- });
917
- showToast('Approval initiated', 'success');
918
- }
919
- catch {
920
- showToast('Failed to initiate approval', 'error');
921
- return;
922
- }
923
- break;
924
- }
925
- case 'approve-action': {
926
- try {
927
- await withOperation('Approving', async () => {
928
- await api.approveAction(projectId, modal.approvalId);
929
- const approvals = await api.listApprovals(projectId);
930
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
931
- });
932
- showToast('Approved', 'success');
933
- }
934
- catch {
935
- showToast('Failed to approve', 'error');
936
- return;
937
- }
938
- break;
939
- }
940
- case 'confirm-regen-link': {
941
- try {
942
- await withOperation('Regenerating link', async () => {
943
- await api.regenerateInviteLink(projectId, modal.role);
944
- const links = await api.listInviteLinks(projectId);
945
- dispatch({ type: 'SET_INVITE_LINKS', links });
946
- });
947
- showToast('Link regenerated', 'success');
948
- }
949
- catch {
950
- showToast('Failed to regenerate link', 'error');
951
- return;
952
- }
953
- break;
954
- }
955
- }
956
- }
957
- // ------ Data loading ------
958
- async function loadTabData() {
959
- const projectId = state.activeProjectId;
960
- if (!projectId)
961
- return;
962
- try {
963
- switch (state.tab) {
964
- case 'knowledge': {
965
- dispatch({ type: 'SET_LOADING', loading: true });
966
- const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 });
967
- dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
968
- break;
969
- }
970
- case 'team': {
971
- const [members, links] = await Promise.all([
972
- api.listMembers(projectId),
973
- api.listInviteLinks(projectId).catch(() => []),
974
- ]);
975
- dispatch({ type: 'SET_MEMBERS', members });
976
- dispatch({ type: 'SET_INVITE_LINKS', links });
977
- break;
978
- }
979
- case 'billing': {
980
- const billing = await api.getBilling(projectId);
981
- dispatch({ type: 'SET_BILLING', billing });
982
- break;
983
- }
984
- case 'project': {
985
- const approvals = await api.listApprovals(projectId).catch(() => []);
986
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
987
- break;
988
- }
989
- }
990
- }
991
- catch {
992
- showToast('Failed to load data', 'error');
993
- }
994
- }
995
- async function loadMoreMemories() {
996
- const projectId = state.activeProjectId;
997
- if (!projectId || !state.memoryHasMore)
998
- return;
999
- dispatch({ type: 'SET_LOADING', loading: true });
1000
- try {
1001
- const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: state.memories.length });
1002
- dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total, append: true });
1003
- }
1004
- catch {
1005
- showToast('Failed to load more', 'error');
1006
- dispatch({ type: 'SET_LOADING', loading: false });
1007
- }
1008
- }
1009
- async function executeSearch() {
1010
- const projectId = state.activeProjectId;
1011
- if (!projectId || state.searchQuery.length === 0)
1012
- return;
1013
- dispatch({ type: 'SET_SEARCHING', searching: true });
1014
- try {
1015
- const result = await api.searchMemories(projectId, state.searchQuery);
1016
- dispatch({ type: 'SET_SEARCH_RESULTS', results: result.memories });
1017
- }
1018
- catch {
1019
- showToast('Search failed', 'error');
1020
- }
1021
- dispatch({ type: 'SET_SEARCHING', searching: false });
1022
- }
1023
- // ------ SSE feed ------
1024
- function connectFeed(projectId) {
1025
- if (feedDisconnect)
1026
- feedDisconnect();
1027
- const conn = api.connectFeed(projectId, {
1028
- onEvent: (eventType, data) => {
1029
- if (eventType === 'memory') {
1030
- const event = data;
1031
- if (event.operation === 'create') {
1032
- dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
1033
- dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
1034
- if (liveDotTimer)
1035
- clearTimeout(liveDotTimer);
1036
- liveDotTimer = setTimeout(() => {
1037
- dispatch({ type: 'CLEAR_LIVE_DOT' });
1038
- }, 10000);
1039
- }
1040
- else if (event.operation === 'update') {
1041
- const updated = { id: event.id, body: event.body, updated_at: event.updated_at };
1042
- state = {
1043
- ...state,
1044
- memories: state.memories.map(m => m.id === event.id ? updated : m),
1045
- searchResults: state.searchResults?.map(m => m.id === event.id ? updated : m) ?? null,
1046
- };
1047
- dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
1048
- if (liveDotTimer)
1049
- clearTimeout(liveDotTimer);
1050
- liveDotTimer = setTimeout(() => {
1051
- dispatch({ type: 'CLEAR_LIVE_DOT' });
1052
- }, 10000);
1053
- }
1054
- }
1055
- else if (eventType === 'role_change') {
1056
- api.me().then(me => {
1057
- dispatch({ type: 'SET_USER_DATA', me });
1058
- }).catch(() => {
1059
- if (Date.now() - lastSyncErrorAt > 30000) {
1060
- lastSyncErrorAt = Date.now();
1061
- showToast('Sync error', 'error');
1062
- }
1063
- });
1064
- }
1065
- else if (eventType === 'approval') {
1066
- api.listApprovals(projectId).then(approvals => {
1067
- dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
1068
- }).catch(() => {
1069
- if (Date.now() - lastSyncErrorAt > 30000) {
1070
- lastSyncErrorAt = Date.now();
1071
- showToast('Sync error', 'error');
1072
- }
1073
- });
1074
- }
1075
- else if (eventType === 'notification') {
1076
- api.listNotifications().then(notifications => {
1077
- dispatch({ type: 'SET_NOTIFICATIONS', notifications });
1078
- api.markNotificationsSeen().catch(() => { });
1079
- }).catch(() => {
1080
- if (Date.now() - lastSyncErrorAt > 30000) {
1081
- lastSyncErrorAt = Date.now();
1082
- showToast('Sync error', 'error');
1083
- }
1084
- });
1085
- }
1086
- },
1087
- onConnected: () => {
1088
- dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
1089
- // Refresh memories on reconnect to catch anything missed
1090
- api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).then(result => {
1091
- if (result)
1092
- dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
1093
- }).catch(() => { });
1094
- },
1095
- onDisconnected: () => {
1096
- dispatch({ type: 'SET_SSE_CONNECTED', connected: false });
1097
- },
1098
- });
1099
- feedDisconnect = conn.disconnect;
1100
- }
1101
- // ------ Background poll — foundation for data freshness ------
1102
- function startPolling(projectId) {
1103
- if (pollInterval)
1104
- clearInterval(pollInterval);
1105
- pollInterval = setInterval(async () => {
1106
- if (state.tab !== 'knowledge')
1107
- return; // only poll active tab
1108
- try {
1109
- const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 });
1110
- if (result && result.total !== state.memoryTotal) {
1111
- dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
1112
- }
1113
- }
1114
- catch { } // silent — SSE or next poll will catch up
1115
- }, 30_000);
1116
- }
1117
- // ------ File watchers for MCP config changes ------
1118
- function setupFileWatchers() {
1119
- const configPaths = [
1120
- (0, path_1.join)((0, os_1.homedir)(), '.claude.json'),
1121
- (0, path_1.join)((0, os_1.homedir)(), '.cursor', 'mcp.json'),
1122
- ];
1123
- for (const configPath of configPaths) {
1124
- if ((0, fs_1.existsSync)(configPath)) {
1125
- try {
1126
- const w = (0, fs_1.watch)(configPath, () => {
1127
- const platforms = (0, platform_detect_1.detectPlatformStatuses)();
1128
- dispatch({ type: 'SET_PLATFORMS', platforms });
1129
- });
1130
- watchers.push(w);
1131
- }
1132
- catch { }
1133
- }
1134
- }
1135
- }
1136
- // ------ Helpers ------
1137
- function openBrowser(url) {
1138
- const cmd = process.platform === "darwin" ? "open"
1139
- : process.platform === "win32" ? "start" : "xdg-open";
1140
- (0, child_process_1.execFile)(cmd, [url], () => { });
1141
- }
1142
- function copyToClipboard(text) {
1143
- const cmd = process.platform === "darwin" ? "pbcopy"
1144
- : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
1145
- const child = (0, child_process_1.exec)(cmd);
1146
- child.stdin?.write(text);
1147
- child.stdin?.end();
1148
- }
1149
- }
1150
- function generateBanners(state) {
1151
- const lines = [];
1152
- const project = (0, state_1.getActiveProject)(state);
1153
- // Priority: pending approvals > notifications > locked > past due > trial ending > plugin nudge > joined
1154
- // 1. Pending approvals (admin only)
1155
- if (state.userRole === 'admin') {
1156
- for (const approval of state.pendingApprovals) {
1157
- const approved = approval.approved_by.length;
1158
- const required = approval.required_count;
1159
- const action = approval.action === 'delete_project' ? 'delete ' + (project?.slug ?? 'project')
1160
- : approval.action === 'clear_memories' ? 'clear memories'
1161
- : approval.action === 'demote_admin' ? 'demote ' + (approval.initiator_email ?? 'admin')
1162
- : approval.description;
1163
- lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'approval pending: ' + action + ansi_1.C.reset +
1164
- ansi_1.C.muted + ' \u00b7 ' + approved + ' of ' + required + ' admins approved \u00b7 press p to review' + ansi_1.C.reset);
1165
- }
1166
- }
1167
- // 2. Notifications (unseen)
1168
- for (const notif of state.notifications) {
1169
- if (!notif.seen) {
1170
- if (notif.type === 'role_change' && notif.message.includes('admin')) {
1171
- lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
1172
- }
1173
- else if (notif.type === 'role_change') {
1174
- lines.push(' ' + ansi_1.C.yellow + '! ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
1175
- }
1176
- else if (notif.type === 'approval_completed') {
1177
- lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + notif.message + ansi_1.C.reset);
1178
- }
1179
- else if (notif.type === 'approval_expired' || notif.type === 'approval_canceled') {
1180
- lines.push(' ' + ansi_1.C.muted + ' ' + notif.message + ansi_1.C.reset);
1181
- }
1182
- else {
1183
- lines.push(' ' + ansi_1.C.muted + ' ' + notif.message + ansi_1.C.reset);
1184
- }
1185
- }
1186
- }
1187
- // Billing banners (suppress plugin nudges when billing issues exist)
1188
- let hasBillingBanner = false;
1189
- if (project) {
1190
- const trialDays = project.trial_ends_at ? (0, ansi_1.daysUntil)(project.trial_ends_at) : null;
1191
- // 3. Locked (trial ended, no subscription)
1192
- if (trialDays !== null && trialDays <= 0 && project.billing_status !== 'active') {
1193
- lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'trial ended \u00b7 your agents have stopped working' + ansi_1.C.reset +
1194
- ansi_1.C.muted + ' \u00b7 press b to subscribe' + ansi_1.C.reset);
1195
- hasBillingBanner = true;
1196
- }
1197
- // 4. Past due
1198
- else if (project.billing_status === 'past_due') {
1199
- lines.push(' ' + ansi_1.C.red + '! ' + ansi_1.C.secondary + 'payment failed' + ansi_1.C.reset +
1200
- ansi_1.C.muted + ' \u00b7 update your payment method \u00b7 press b' + ansi_1.C.reset);
1201
- hasBillingBanner = true;
1202
- }
1203
- // 5. Trial ending soon (<= 3 days)
1204
- else if (trialDays !== null && trialDays > 0 && trialDays <= 3) {
1205
- const dayWord = trialDays === 1 ? 'tomorrow' : `in ${trialDays} days`;
1206
- lines.push(' ' + ansi_1.C.yellow + '! ' + ansi_1.C.secondary + 'trial ends ' + dayWord + ansi_1.C.reset +
1207
- ansi_1.C.muted + ' \u00b7 press b to subscribe' + ansi_1.C.reset);
1208
- hasBillingBanner = true;
1209
- }
1210
- }
1211
- if (!hasBillingBanner) {
1212
- // First-login: show all configured platforms
1213
- if (state.firstLogin) {
1214
- const configured = state.platforms
1215
- .filter(p => p.state === 'configured')
1216
- .map(p => p.name);
1217
- if (configured.length > 0) {
1218
- lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + configured.join(', ') + ansi_1.C.reset +
1219
- ansi_1.C.muted + ' ready' + ansi_1.C.reset);
1220
- }
1221
- }
1222
- // Joined banner
1223
- if (state.joinedProjectName) {
1224
- lines.push(' ' + ansi_1.C.green + ' ' + ansi_1.C.secondary + 'you joined ' + state.joinedProjectName + ansi_1.C.reset +
1225
- ansi_1.C.muted + ' \u00b7 your agent is ready' + ansi_1.C.reset);
1226
- }
1227
- }
1228
- return lines;
1229
- }
1230
- function loadState() {
1231
- try {
1232
- if ((0, fs_1.existsSync)(STATE_FILE)) {
1233
- return JSON.parse((0, fs_1.readFileSync)(STATE_FILE, 'utf8'));
1234
- }
1235
- }
1236
- catch { }
1237
- return { hasOpenedDashboard: false };
1238
- }
1239
- function saveState(data) {
1240
- try {
1241
- const dir = (0, path_1.join)((0, os_1.homedir)(), ".cohvu");
1242
- if (!(0, fs_1.existsSync)(dir)) {
1243
- const { mkdirSync } = require("fs");
1244
- mkdirSync(dir, { recursive: true, mode: 0o700 });
1245
- }
1246
- (0, fs_1.writeFileSync)(STATE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
1247
- }
1248
- catch { }
1249
- }
1250
- //# sourceMappingURL=dashboard.js.map