cohvu 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/api.d.ts +42 -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 +1090 -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 +20 -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 +42 -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 -203
  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
@@ -0,0 +1,1090 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Main TUI component — state, data loading, SSE, key handling, and layout.
3
+ // Direct translation of dashboard.ts into React hooks.
4
+ import { useReducer, useRef, useEffect, useCallback } from 'react';
5
+ import { Box, Text, useInput, useApp, useStdout } from 'ink';
6
+ import { reduce, initialState, TABS, getActiveProject, getActiveTeam } from './state.js';
7
+ import { ApiClient } from '../api.js';
8
+ import { runSetup } from '../setup.js';
9
+ import { detectPlatformStatuses } from './platform-detect.js';
10
+ import { Header } from './components/Header.js';
11
+ import { TabBar } from './components/TabBar.js';
12
+ import { Banner } from './components/Banner.js';
13
+ import { Footer } from './components/Footer.js';
14
+ import { Toast } from './components/Toast.js';
15
+ import { Divider } from './components/Divider.js';
16
+ import { ModalView } from './components/Modal.js';
17
+ import { KnowledgeTab } from './tabs/KnowledgeTab.js';
18
+ import { TeamTab } from './tabs/TeamTab.js';
19
+ import { BillingTab } from './tabs/BillingTab.js';
20
+ import { ProjectTab } from './tabs/ProjectTab.js';
21
+ import { YouTab } from './tabs/YouTab.js';
22
+ import { daysUntil } from './utils.js';
23
+ import { exec, execFile } from 'child_process';
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
25
+ import { join } from 'path';
26
+ import { homedir } from 'os';
27
+ const STATE_FILE = join(homedir(), '.cohvu', 'state.json');
28
+ const PAGE_SIZE = 20;
29
+ export default function App() {
30
+ const { exit } = useApp();
31
+ const { stdout } = useStdout();
32
+ const rows = stdout.rows;
33
+ const cols = stdout.columns;
34
+ // ---- State ----
35
+ const [state, rawDispatch] = useReducer(reduce, undefined, initialState);
36
+ const stateRef = useRef(state);
37
+ stateRef.current = state;
38
+ const api = useRef(new ApiClient()).current;
39
+ // Timers
40
+ const toastTimerRef = useRef(null);
41
+ const liveDotTimerRef = useRef(null);
42
+ const copiedTimerRef = useRef(null);
43
+ const inlineErrorTimerRef = useRef(null);
44
+ const feedDisconnectRef = useRef(null);
45
+ const pollIntervalRef = useRef(null);
46
+ const watchersRef = useRef([]);
47
+ const lastSyncErrorAt = useRef(0);
48
+ const dispatch = rawDispatch;
49
+ // ---- Helpers ----
50
+ const showToast = useCallback((message, type, durationMs = 3000) => {
51
+ if (toastTimerRef.current)
52
+ clearTimeout(toastTimerRef.current);
53
+ dispatch({ type: 'SET_TOAST', toast: { message, type, expiresAt: Date.now() + durationMs } });
54
+ toastTimerRef.current = setTimeout(() => {
55
+ dispatch({ type: 'SET_TOAST', toast: null });
56
+ }, durationMs);
57
+ }, [dispatch]);
58
+ const showInlineError = useCallback((message, durationMs) => {
59
+ dispatch({ type: 'SET_INLINE_ERROR', message });
60
+ if (inlineErrorTimerRef.current)
61
+ clearTimeout(inlineErrorTimerRef.current);
62
+ inlineErrorTimerRef.current = setTimeout(() => {
63
+ dispatch({ type: 'SET_INLINE_ERROR', message: null });
64
+ }, durationMs);
65
+ }, [dispatch]);
66
+ function openBrowser(url) {
67
+ if (process.platform === 'win32') {
68
+ exec(`start "" "${url}"`);
69
+ }
70
+ else {
71
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
72
+ execFile(cmd, [url], () => { });
73
+ }
74
+ }
75
+ function copyToClipboard(text) {
76
+ const cmd = process.platform === 'darwin' ? 'pbcopy'
77
+ : process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
78
+ const child = exec(cmd);
79
+ child.stdin?.write(text);
80
+ child.stdin?.end();
81
+ }
82
+ // ---- SSE feed ----
83
+ const connectFeed = useCallback((projectId) => {
84
+ if (feedDisconnectRef.current)
85
+ feedDisconnectRef.current();
86
+ const conn = api.connectFeed(projectId, {
87
+ onEvent: (eventType, data) => {
88
+ if (eventType === 'memory') {
89
+ const event = data;
90
+ if (event.operation === 'create') {
91
+ dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
92
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
93
+ if (liveDotTimerRef.current)
94
+ clearTimeout(liveDotTimerRef.current);
95
+ liveDotTimerRef.current = setTimeout(() => dispatch({ type: 'CLEAR_LIVE_DOT' }), 10000);
96
+ }
97
+ else if (event.operation === 'update') {
98
+ dispatch({ type: 'UPDATE_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
99
+ dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
100
+ if (liveDotTimerRef.current)
101
+ clearTimeout(liveDotTimerRef.current);
102
+ liveDotTimerRef.current = setTimeout(() => dispatch({ type: 'CLEAR_LIVE_DOT' }), 10000);
103
+ }
104
+ }
105
+ else if (eventType === 'role_change') {
106
+ api.me().then(me => dispatch({ type: 'SET_USER_DATA', me })).catch(() => {
107
+ if (Date.now() - lastSyncErrorAt.current > 30000) {
108
+ lastSyncErrorAt.current = Date.now();
109
+ showToast('Sync error', 'error');
110
+ }
111
+ });
112
+ }
113
+ else if (eventType === 'notification') {
114
+ api.listNotifications().then(notifications => {
115
+ dispatch({ type: 'SET_NOTIFICATIONS', notifications });
116
+ api.markNotificationsSeen().catch(() => { });
117
+ }).catch(() => {
118
+ if (Date.now() - lastSyncErrorAt.current > 30000) {
119
+ lastSyncErrorAt.current = Date.now();
120
+ showToast('Sync error', 'error');
121
+ }
122
+ });
123
+ }
124
+ },
125
+ onConnected: () => {
126
+ dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
127
+ api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).then(result => {
128
+ if (result)
129
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
130
+ }).catch(() => { });
131
+ },
132
+ onDisconnected: () => {
133
+ dispatch({ type: 'SET_SSE_CONNECTED', connected: false });
134
+ },
135
+ });
136
+ feedDisconnectRef.current = conn.disconnect;
137
+ }, [api, dispatch, showToast]);
138
+ // ---- Data loading ----
139
+ const loadTabData = useCallback(async () => {
140
+ const s = stateRef.current;
141
+ const projectId = s.activeProjectId;
142
+ if (!projectId)
143
+ return;
144
+ const activeProject = getActiveProject(s);
145
+ const activeTeam = getActiveTeam(s);
146
+ const isTeamProject = activeProject?.owner.kind === 'team';
147
+ try {
148
+ switch (s.tab) {
149
+ case 'knowledge': {
150
+ dispatch({ type: 'SET_LOADING', loading: true });
151
+ const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 });
152
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
153
+ break;
154
+ }
155
+ case 'team': {
156
+ if (isTeamProject && activeTeam) {
157
+ const [members, links] = await Promise.all([
158
+ api.listTeamMembers(activeTeam.team_id),
159
+ api.listTeamInviteLinks(activeTeam.team_id).catch(() => []),
160
+ ]);
161
+ dispatch({ type: 'SET_MEMBERS', members });
162
+ dispatch({ type: 'SET_INVITE_LINKS', links });
163
+ }
164
+ else {
165
+ dispatch({ type: 'SET_MEMBERS', members: [] });
166
+ dispatch({ type: 'SET_INVITE_LINKS', links: [] });
167
+ }
168
+ break;
169
+ }
170
+ case 'billing': {
171
+ if (isTeamProject && activeTeam) {
172
+ const billing = await api.getTeamBilling(activeTeam.team_id);
173
+ dispatch({ type: 'SET_BILLING', billing });
174
+ }
175
+ else {
176
+ const billing = await api.getIndividualBilling();
177
+ dispatch({ type: 'SET_BILLING', billing });
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ catch {
184
+ showToast('Failed to load data', 'error');
185
+ }
186
+ }, [api, dispatch, showToast]);
187
+ const loadMoreMemories = useCallback(async () => {
188
+ const s = stateRef.current;
189
+ const projectId = s.activeProjectId;
190
+ if (!projectId || !s.memoryHasMore)
191
+ return;
192
+ dispatch({ type: 'SET_LOADING', loading: true });
193
+ try {
194
+ const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: s.memories.length });
195
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total, append: true });
196
+ }
197
+ catch {
198
+ showToast('Failed to load more', 'error');
199
+ dispatch({ type: 'SET_LOADING', loading: false });
200
+ }
201
+ }, [api, dispatch, showToast]);
202
+ const executeSearch = useCallback(async () => {
203
+ const s = stateRef.current;
204
+ const projectId = s.activeProjectId;
205
+ if (!projectId || s.searchQuery.length === 0)
206
+ return;
207
+ dispatch({ type: 'SET_SEARCHING', searching: true });
208
+ try {
209
+ const result = await api.searchMemories(projectId, s.searchQuery);
210
+ dispatch({ type: 'SET_SEARCH_RESULTS', results: result.memories });
211
+ }
212
+ catch {
213
+ showToast('Search failed', 'error');
214
+ }
215
+ dispatch({ type: 'SET_SEARCHING', searching: false });
216
+ }, [api, dispatch, showToast]);
217
+ // ---- Initial data load ----
218
+ useEffect(() => {
219
+ let cancelled = false;
220
+ async function load() {
221
+ // First-login state
222
+ const saved = loadPersistedState();
223
+ if (!saved.hasOpenedDashboard) {
224
+ dispatch({ type: 'SET_FIRST_LOGIN', firstLogin: true });
225
+ }
226
+ try {
227
+ const me = await api.me();
228
+ if (cancelled)
229
+ return;
230
+ dispatch({ type: 'SET_USER_DATA', me });
231
+ await runSetup();
232
+ if (cancelled)
233
+ return;
234
+ const platforms = detectPlatformStatuses();
235
+ dispatch({ type: 'SET_PLATFORMS', platforms });
236
+ const projectId = me.user.active_project_id;
237
+ const flatProjects = deriveFlatProjectsFromMe(me);
238
+ const activeProject = flatProjects.find(p => p.project_id === projectId);
239
+ const isTeamProject = activeProject?.owner.kind === 'team';
240
+ const activeTeamId = isTeamProject && activeProject.owner.kind === 'team' ? activeProject.owner.teamId : null;
241
+ const activeTeam = activeTeamId ? me.teams.find(t => t.team_id === activeTeamId) : null;
242
+ if (projectId) {
243
+ let loadError = false;
244
+ const [memResult, members, billing, inviteLinks, notifications] = await Promise.all([
245
+ api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 }).catch(() => { loadError = true; return null; }),
246
+ isTeamProject && activeTeam
247
+ ? api.listTeamMembers(activeTeam.team_id).catch(() => { loadError = true; return []; })
248
+ : Promise.resolve([]),
249
+ isTeamProject && activeTeam
250
+ ? api.getTeamBilling(activeTeam.team_id).catch(() => { loadError = true; return null; })
251
+ : api.getIndividualBilling().catch(() => { loadError = true; return null; }),
252
+ isTeamProject && activeTeam
253
+ ? api.listTeamInviteLinks(activeTeam.team_id).catch(() => [])
254
+ : Promise.resolve([]),
255
+ api.listNotifications().catch(() => []),
256
+ ]);
257
+ if (cancelled)
258
+ return;
259
+ if (loadError)
260
+ showToast('Some data failed to load', 'error');
261
+ if (memResult)
262
+ dispatch({ type: 'SET_MEMORIES', memories: memResult.memories, total: memResult.total });
263
+ dispatch({ type: 'SET_MEMBERS', members });
264
+ if (billing)
265
+ dispatch({ type: 'SET_BILLING', billing });
266
+ dispatch({ type: 'SET_INVITE_LINKS', links: inviteLinks });
267
+ dispatch({ type: 'SET_NOTIFICATIONS', notifications });
268
+ if (notifications.length > 0)
269
+ api.markNotificationsSeen().catch(() => { });
270
+ connectFeed(projectId);
271
+ // Background poll
272
+ pollIntervalRef.current = setInterval(async () => {
273
+ const s = stateRef.current;
274
+ if (s.tab !== 'knowledge')
275
+ return;
276
+ try {
277
+ const result = await api.listMemories(projectId, { limit: PAGE_SIZE, offset: 0 });
278
+ if (result && result.total !== stateRef.current.memoryTotal) {
279
+ dispatch({ type: 'SET_MEMORIES', memories: result.memories, total: result.total });
280
+ }
281
+ }
282
+ catch { }
283
+ }, 30_000);
284
+ }
285
+ if (!saved.hasOpenedDashboard) {
286
+ savePersistedState({ hasOpenedDashboard: true });
287
+ }
288
+ }
289
+ catch {
290
+ if (!cancelled) {
291
+ dispatch({ type: 'SET_OFFLINE', offline: true });
292
+ dispatch({ type: 'SET_ERROR', error: "can't reach cohvu" });
293
+ }
294
+ }
295
+ }
296
+ load();
297
+ return () => {
298
+ cancelled = true;
299
+ if (feedDisconnectRef.current)
300
+ feedDisconnectRef.current();
301
+ if (pollIntervalRef.current)
302
+ clearInterval(pollIntervalRef.current);
303
+ };
304
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
305
+ // ---- File watchers ----
306
+ useEffect(() => {
307
+ const configPaths = [
308
+ join(homedir(), '.claude.json'),
309
+ join(homedir(), '.cursor', 'mcp.json'),
310
+ ];
311
+ for (const configPath of configPaths) {
312
+ if (existsSync(configPath)) {
313
+ try {
314
+ const w = watch(configPath, () => {
315
+ const platforms = detectPlatformStatuses();
316
+ dispatch({ type: 'SET_PLATFORMS', platforms });
317
+ });
318
+ watchersRef.current.push(w);
319
+ }
320
+ catch { }
321
+ }
322
+ }
323
+ return () => {
324
+ watchersRef.current.forEach(w => w.close());
325
+ watchersRef.current = [];
326
+ };
327
+ }, [dispatch]);
328
+ // ---- Cleanup timers on unmount ----
329
+ useEffect(() => {
330
+ return () => {
331
+ if (toastTimerRef.current)
332
+ clearTimeout(toastTimerRef.current);
333
+ if (liveDotTimerRef.current)
334
+ clearTimeout(liveDotTimerRef.current);
335
+ if (copiedTimerRef.current)
336
+ clearTimeout(copiedTimerRef.current);
337
+ if (inlineErrorTimerRef.current)
338
+ clearTimeout(inlineErrorTimerRef.current);
339
+ };
340
+ }, []);
341
+ // ---- Key handling ----
342
+ useInput((input, key) => {
343
+ void handleKey(input, key);
344
+ });
345
+ async function handleKey(input, key) {
346
+ const s = stateRef.current;
347
+ // Global: ctrl+c always exits
348
+ if (input === 'c' && key.ctrl) {
349
+ exit();
350
+ return;
351
+ }
352
+ // Global: q exits (unless modal open or in search mode)
353
+ if (input === 'q' && !s.modal && s.knowledgeMode !== 'search') {
354
+ exit();
355
+ return;
356
+ }
357
+ // Modal keys
358
+ if (s.modal) {
359
+ await handleModalKey(input, key);
360
+ return;
361
+ }
362
+ // Tab switching
363
+ if (key.tab) {
364
+ dispatch({ type: 'NEXT_TAB' });
365
+ await loadTabData();
366
+ return;
367
+ }
368
+ // Number keys switch tabs
369
+ const tabIdx = parseInt(input, 10);
370
+ if (tabIdx >= 1 && tabIdx <= 5 && !key.ctrl && !key.meta) {
371
+ dispatch({ type: 'SWITCH_TAB', tab: TABS[tabIdx - 1] });
372
+ await loadTabData();
373
+ return;
374
+ }
375
+ // Global 'b' shortcut — subscribe/billing portal from banner
376
+ if (input === 'b' && s.userRole === 'admin' && s.tab !== 'billing') {
377
+ await handleBillingShortcut();
378
+ return;
379
+ }
380
+ // Tab-specific keys
381
+ switch (s.tab) {
382
+ case 'knowledge':
383
+ await handleKnowledgeKey(input, key);
384
+ break;
385
+ case 'team':
386
+ await handleTeamKey(input, key);
387
+ break;
388
+ case 'billing':
389
+ await handleBillingKey(input, key);
390
+ break;
391
+ case 'project':
392
+ await handleProjectKey(input, key);
393
+ break;
394
+ case 'you':
395
+ await handleYouKey(input, key);
396
+ break;
397
+ }
398
+ }
399
+ // ---- Knowledge keys ----
400
+ async function handleKnowledgeKey(input, key) {
401
+ const s = stateRef.current;
402
+ // alt+d or ∂ enters forget mode
403
+ if (((key.meta && input === 'd') || input === '∂') && s.userRole !== 'viewer') {
404
+ const list = s.searchResults ?? s.memories;
405
+ if (list.length > 0)
406
+ dispatch({ type: 'ENTER_FORGET' });
407
+ return;
408
+ }
409
+ if (s.knowledgeMode === 'search') {
410
+ if (key.escape) {
411
+ dispatch({ type: 'EXIT_SEARCH' });
412
+ }
413
+ else if (key.return && s.searchQuery.length > 0) {
414
+ await executeSearch();
415
+ }
416
+ else if (key.backspace) {
417
+ dispatch({ type: 'SEARCH_BACKSPACE' });
418
+ }
419
+ else if (input === ' ') {
420
+ dispatch({ type: 'SEARCH_INPUT', char: ' ' });
421
+ }
422
+ else if (key.upArrow) {
423
+ dispatch({ type: 'SCROLL_UP' });
424
+ }
425
+ else if (key.downArrow) {
426
+ dispatch({ type: 'SCROLL_DOWN' });
427
+ }
428
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
429
+ dispatch({ type: 'SEARCH_INPUT', char: input });
430
+ }
431
+ return;
432
+ }
433
+ if (s.knowledgeMode === 'forget') {
434
+ const forgetList = s.searchResults ?? s.memories;
435
+ const filtered = s.userRole === 'admin'
436
+ ? forgetList
437
+ : forgetList.filter(m => m.contributed_by?.user_id === s.user?.id);
438
+ if (s.forgetConfirming && input === 'y') {
439
+ const projectId = s.activeProjectId;
440
+ if (projectId) {
441
+ let failures = 0;
442
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing memories' });
443
+ await Promise.all([...s.forgetSelected].map(async (id) => {
444
+ try {
445
+ await api.deleteMemory(projectId, id);
446
+ dispatch({ type: 'REMOVE_MEMORY', id });
447
+ }
448
+ catch {
449
+ failures++;
450
+ }
451
+ }));
452
+ dispatch({ type: 'SET_OPERATION', operation: null });
453
+ dispatch({ type: 'EXIT_FORGET' });
454
+ if (failures > 0)
455
+ showToast(`${failures} failed to remove`, 'error');
456
+ else
457
+ showToast('Removed', 'success');
458
+ }
459
+ }
460
+ else if (s.forgetConfirming && (key.escape || input === 'n')) {
461
+ dispatch({ type: 'SET_FORGET_CONFIRMING', confirming: false });
462
+ }
463
+ else if (key.escape) {
464
+ dispatch({ type: 'EXIT_FORGET' });
465
+ }
466
+ else if (input === ' ') {
467
+ const mem = filtered[s.memorySelected];
468
+ if (mem)
469
+ dispatch({ type: 'TOGGLE_FORGET', memoryId: mem.id });
470
+ }
471
+ else if (key.upArrow) {
472
+ if (s.memorySelected > 0)
473
+ dispatch({ type: 'SCROLL_UP' });
474
+ }
475
+ else if (key.downArrow) {
476
+ if (s.memorySelected < filtered.length - 1)
477
+ dispatch({ type: 'SCROLL_DOWN' });
478
+ }
479
+ else if (key.return && s.forgetSelected.size > 0 && !s.forgetConfirming) {
480
+ dispatch({ type: 'SET_FORGET_CONFIRMING', confirming: true });
481
+ }
482
+ return;
483
+ }
484
+ // Browse mode
485
+ if (input === '/') {
486
+ dispatch({ type: 'ENTER_SEARCH' });
487
+ }
488
+ else if (input === 'd' && s.userRole !== 'viewer' && s.memories.length > 0) {
489
+ dispatch({ type: 'ENTER_FORGET' });
490
+ }
491
+ else if (input === 'D' && s.userRole === 'admin') {
492
+ const project = getActiveProject(s);
493
+ if (project)
494
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-forget-all', slug: project.slug, memoryCount: s.memoryTotal, input: '' } });
495
+ }
496
+ else if (key.upArrow) {
497
+ dispatch({ type: 'SCROLL_UP' });
498
+ }
499
+ else if (key.downArrow) {
500
+ dispatch({ type: 'SCROLL_DOWN' });
501
+ }
502
+ else if (input === ' ') {
503
+ await loadMoreMemories();
504
+ }
505
+ }
506
+ // ---- Team keys ----
507
+ async function handleTeamKey(input, key) {
508
+ const s = stateRef.current;
509
+ const activeProject = getActiveProject(s);
510
+ if (!activeProject || activeProject.owner.kind === 'personal')
511
+ return;
512
+ const memberCount = s.members.length;
513
+ const linkRoles = ['admin', 'member', 'viewer'];
514
+ const linkCount = s.userRole === 'admin' ? linkRoles.length : 0;
515
+ const totalRows = memberCount + linkCount;
516
+ const sel = s.teamSelected;
517
+ const onLinkRow = s.userRole === 'admin' && sel >= memberCount;
518
+ const onMemberRow = sel < memberCount;
519
+ const team = getActiveTeam(s);
520
+ if (key.upArrow) {
521
+ dispatch({ type: 'SET_TEAM_SELECTED', index: Math.max(0, sel - 1) });
522
+ return;
523
+ }
524
+ if (key.downArrow) {
525
+ dispatch({ type: 'SET_TEAM_SELECTED', index: Math.min(totalRows - 1, sel + 1) });
526
+ return;
527
+ }
528
+ if (input === 'c' && s.userRole === 'admin' && onLinkRow) {
529
+ const linkIdx = sel - memberCount;
530
+ const role = linkRoles[linkIdx];
531
+ const link = s.inviteLinks.find(l => l.role === role);
532
+ if (link) {
533
+ copyToClipboard(link.url);
534
+ dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
535
+ if (copiedTimerRef.current)
536
+ clearTimeout(copiedTimerRef.current);
537
+ copiedTimerRef.current = setTimeout(() => dispatch({ type: 'SET_COPIED_FEEDBACK', active: false }), 1500);
538
+ }
539
+ return;
540
+ }
541
+ if (input === 'r' && s.userRole === 'admin' && onLinkRow) {
542
+ const linkIdx = sel - memberCount;
543
+ const role = linkRoles[linkIdx];
544
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
545
+ return;
546
+ }
547
+ if (input === 'e' && s.userRole === 'admin' && onMemberRow) {
548
+ const target = s.members[sel];
549
+ if (target) {
550
+ if (target.user_id === s.user?.id) {
551
+ showInlineError('you cannot change your own role', 2000);
552
+ return;
553
+ }
554
+ const roleIdx = ['admin', 'member', 'viewer'].indexOf(target.role ?? 'member');
555
+ dispatch({ type: 'OPEN_MODAL', modal: {
556
+ kind: 'edit-role',
557
+ targetEmail: target.email ?? target.user_id,
558
+ targetUserId: target.user_id,
559
+ currentRole: target.role ?? 'member',
560
+ selected: roleIdx >= 0 ? roleIdx : 1,
561
+ } });
562
+ }
563
+ return;
564
+ }
565
+ if (input === 'x') {
566
+ if (s.userRole === 'admin' && onMemberRow && team) {
567
+ const target = s.members[sel];
568
+ if (target) {
569
+ if (target.user_id === s.user?.id) {
570
+ const adminCount = s.members.filter(m => m.role === 'admin').length;
571
+ if (adminCount <= 1) {
572
+ showInlineError('you are the only admin · promote another member before leaving', 3000);
573
+ return;
574
+ }
575
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
576
+ }
577
+ else {
578
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-remove-member', email: target.email ?? target.user_id, userId: target.user_id } });
579
+ }
580
+ }
581
+ }
582
+ else if (s.userRole !== 'admin') {
583
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-leave' } });
584
+ }
585
+ }
586
+ }
587
+ // ---- Billing keys ----
588
+ async function handleBillingKey(input, _key) {
589
+ const s = stateRef.current;
590
+ if (s.userRole !== 'admin')
591
+ return;
592
+ const project = getActiveProject(s);
593
+ if (!project)
594
+ return;
595
+ const isTeam = project.owner.kind === 'team';
596
+ const team = getActiveTeam(s);
597
+ if (input === 's') {
598
+ try {
599
+ showToast('Opening checkout...', 'info');
600
+ if (isTeam && team) {
601
+ const checkout = await api.createTeamCheckout(team.team_id);
602
+ if (checkout.checkout_url)
603
+ openBrowser(checkout.checkout_url);
604
+ }
605
+ else {
606
+ const checkout = await api.createIndividualCheckout();
607
+ if (checkout.checkout_url)
608
+ openBrowser(checkout.checkout_url);
609
+ }
610
+ }
611
+ catch {
612
+ showToast('Failed to open checkout', 'error');
613
+ }
614
+ }
615
+ else if (input === 'p') {
616
+ try {
617
+ showToast('Opening billing portal...', 'info');
618
+ if (isTeam && team) {
619
+ const portal = await api.getTeamPortalUrl(team.team_id);
620
+ if (portal.url)
621
+ openBrowser(portal.url);
622
+ }
623
+ else {
624
+ const portal = await api.getIndividualPortalUrl();
625
+ if (portal.url)
626
+ openBrowser(portal.url);
627
+ }
628
+ }
629
+ catch {
630
+ showToast('Failed to open billing portal', 'error');
631
+ }
632
+ }
633
+ }
634
+ // ---- Project keys ----
635
+ async function handleProjectKey(input, _key) {
636
+ const s = stateRef.current;
637
+ if (input === 'r' && s.userRole === 'admin') {
638
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'rename', input: '' } });
639
+ }
640
+ else if (input === 'n') {
641
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
642
+ }
643
+ else if (input === 'w' && s.projects.length > 1) {
644
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: 0 } });
645
+ }
646
+ else if (input === 'c' && s.userRole === 'admin') {
647
+ const project = getActiveProject(s);
648
+ if (project)
649
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-clear', slug: project.slug, memoryCount: s.memoryTotal, input: '' } });
650
+ }
651
+ else if (input === 'd' && s.userRole === 'admin') {
652
+ const project = getActiveProject(s);
653
+ if (project)
654
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-delete', slug: project.slug, memoryCount: s.memoryTotal, input: '' } });
655
+ }
656
+ }
657
+ // ---- You keys ----
658
+ async function handleYouKey(input, _key) {
659
+ if (input === 'r') {
660
+ await runSetup();
661
+ const platforms = detectPlatformStatuses();
662
+ dispatch({ type: 'SET_PLATFORMS', platforms });
663
+ }
664
+ else if (input === 'l') {
665
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-logout' } });
666
+ }
667
+ }
668
+ // ---- Billing shortcut (from banner) ----
669
+ async function handleBillingShortcut() {
670
+ const s = stateRef.current;
671
+ const project = getActiveProject(s);
672
+ if (!project)
673
+ return;
674
+ const isTeam = project.owner.kind === 'team';
675
+ const team = getActiveTeam(s);
676
+ if (isTeam && team) {
677
+ const sub = team.subscription;
678
+ if (!sub || sub.status !== 'active') {
679
+ try {
680
+ showToast('Opening checkout...', 'info');
681
+ const c = await api.createTeamCheckout(team.team_id);
682
+ if (c.checkout_url)
683
+ openBrowser(c.checkout_url);
684
+ }
685
+ catch {
686
+ showToast('Failed to open checkout', 'error');
687
+ }
688
+ }
689
+ else {
690
+ try {
691
+ showToast('Opening billing portal...', 'info');
692
+ const p = await api.getTeamPortalUrl(team.team_id);
693
+ if (p.url)
694
+ openBrowser(p.url);
695
+ }
696
+ catch {
697
+ showToast('Failed to open billing portal', 'error');
698
+ }
699
+ }
700
+ }
701
+ else {
702
+ const sub = s.individualSubscription;
703
+ if (!sub || sub.status !== 'active') {
704
+ try {
705
+ showToast('Opening checkout...', 'info');
706
+ const c = await api.createIndividualCheckout();
707
+ if (c.checkout_url)
708
+ openBrowser(c.checkout_url);
709
+ }
710
+ catch {
711
+ showToast('Failed to open checkout', 'error');
712
+ }
713
+ }
714
+ else {
715
+ try {
716
+ showToast('Opening billing portal...', 'info');
717
+ const p = await api.getIndividualPortalUrl();
718
+ if (p.url)
719
+ openBrowser(p.url);
720
+ }
721
+ catch {
722
+ showToast('Failed to open billing portal', 'error');
723
+ }
724
+ }
725
+ }
726
+ }
727
+ // ---- Modal keys ----
728
+ async function handleModalKey(input, key) {
729
+ const s = stateRef.current;
730
+ if (!s.modal)
731
+ return;
732
+ if (key.escape) {
733
+ dispatch({ type: 'CLOSE_MODAL' });
734
+ return;
735
+ }
736
+ const modal = s.modal;
737
+ // y/n modals
738
+ if (modal.kind === 'confirm-forget' || modal.kind === 'confirm-remove-member' ||
739
+ modal.kind === 'confirm-leave' || modal.kind === 'confirm-logout' ||
740
+ modal.kind === 'confirm-regen-link' || modal.kind === 'initiate-consensus' ||
741
+ modal.kind === 'approve-action') {
742
+ if (input === 'y') {
743
+ const willExit = modal.kind === 'confirm-logout';
744
+ await confirmModal(modal);
745
+ if (!willExit)
746
+ dispatch({ type: 'CLOSE_MODAL' });
747
+ }
748
+ else if (input === 'n') {
749
+ dispatch({ type: 'CLOSE_MODAL' });
750
+ }
751
+ return;
752
+ }
753
+ // Text input modals
754
+ if ('input' in modal) {
755
+ if (key.return) {
756
+ await confirmModal(modal);
757
+ dispatch({ type: 'CLOSE_MODAL' });
758
+ }
759
+ else if (key.backspace) {
760
+ dispatch({ type: 'MODAL_BACKSPACE' });
761
+ }
762
+ else if (input === ' ') {
763
+ dispatch({ type: 'MODAL_INPUT', char: ' ' });
764
+ }
765
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
766
+ dispatch({ type: 'MODAL_INPUT', char: input });
767
+ }
768
+ return;
769
+ }
770
+ // Switch project modal
771
+ if (modal.kind === 'switch-project') {
772
+ if (key.upArrow) {
773
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: Math.max(0, modal.selected - 1) } });
774
+ }
775
+ else if (key.downArrow) {
776
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: Math.min(s.projects.length, modal.selected + 1) } });
777
+ }
778
+ else if (key.return) {
779
+ if (modal.selected === s.projects.length) {
780
+ dispatch({ type: 'CLOSE_MODAL' });
781
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
782
+ }
783
+ else {
784
+ const project = s.projects[modal.selected];
785
+ if (project) {
786
+ await api.switchProject(project.project_id);
787
+ const me = await api.me();
788
+ dispatch({ type: 'SET_USER_DATA', me });
789
+ dispatch({ type: 'CLOSE_MODAL' });
790
+ const newProjectId = me.user.active_project_id;
791
+ if (newProjectId)
792
+ connectFeed(newProjectId);
793
+ await loadTabData();
794
+ }
795
+ }
796
+ }
797
+ return;
798
+ }
799
+ // Edit role modal
800
+ if (modal.kind === 'edit-role') {
801
+ const roles = ['admin', 'member', 'viewer'];
802
+ if (key.upArrow) {
803
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.max(0, modal.selected - 1) } });
804
+ }
805
+ else if (key.downArrow) {
806
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.min(roles.length - 1, modal.selected + 1) } });
807
+ }
808
+ else if (key.return) {
809
+ const newRole = roles[modal.selected];
810
+ if (newRole !== modal.currentRole) {
811
+ if (modal.currentRole === 'admin' && newRole !== 'admin') {
812
+ const adminCount = s.members.filter(m => m.role === 'admin').length;
813
+ if (adminCount <= 1) {
814
+ showInlineError('cannot demote the last admin · promote another member first', 2000);
815
+ dispatch({ type: 'CLOSE_MODAL' });
816
+ return;
817
+ }
818
+ }
819
+ const changeTeam = getActiveTeam(s);
820
+ if (changeTeam) {
821
+ try {
822
+ await api.changeTeamRole(changeTeam.team_id, modal.targetUserId, newRole);
823
+ const members = await api.listTeamMembers(changeTeam.team_id);
824
+ dispatch({ type: 'SET_MEMBERS', members });
825
+ showToast('Role updated', 'success');
826
+ }
827
+ catch {
828
+ showToast('Failed to update role', 'error');
829
+ }
830
+ }
831
+ }
832
+ dispatch({ type: 'CLOSE_MODAL' });
833
+ }
834
+ }
835
+ }
836
+ // ---- Modal confirmation ----
837
+ async function confirmModal(modal) {
838
+ const s = stateRef.current;
839
+ const projectId = s.activeProjectId;
840
+ if (!projectId)
841
+ return;
842
+ switch (modal.kind) {
843
+ case 'confirm-forget':
844
+ try {
845
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing memory' });
846
+ await api.deleteMemory(projectId, modal.memoryId);
847
+ dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
848
+ dispatch({ type: 'SET_OPERATION', operation: null });
849
+ showToast('Memory removed', 'success');
850
+ }
851
+ catch {
852
+ dispatch({ type: 'SET_OPERATION', operation: null });
853
+ showToast('Failed to remove memory', 'error');
854
+ }
855
+ break;
856
+ case 'confirm-forget-all':
857
+ case 'confirm-clear': {
858
+ const project = getActiveProject(s);
859
+ if (project && modal.input === project.slug) {
860
+ try {
861
+ dispatch({ type: 'SET_OPERATION', operation: 'Clearing memories' });
862
+ await api.clearMemories(projectId);
863
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
864
+ dispatch({ type: 'SET_OPERATION', operation: null });
865
+ showToast('All memories cleared', 'success');
866
+ }
867
+ catch {
868
+ dispatch({ type: 'SET_OPERATION', operation: null });
869
+ showToast('Failed to clear memories', 'error');
870
+ }
871
+ }
872
+ break;
873
+ }
874
+ case 'confirm-delete': {
875
+ const project = getActiveProject(s);
876
+ if (project && modal.input === project.slug) {
877
+ try {
878
+ dispatch({ type: 'SET_OPERATION', operation: 'Deleting project' });
879
+ await api.deleteProject(projectId);
880
+ const me = await api.me();
881
+ dispatch({ type: 'SET_USER_DATA', me });
882
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
883
+ dispatch({ type: 'SET_OPERATION', operation: null });
884
+ const newProjectId = me.user.active_project_id;
885
+ if (newProjectId) {
886
+ connectFeed(newProjectId);
887
+ await loadTabData();
888
+ }
889
+ else {
890
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
891
+ dispatch({ type: 'SET_MEMBERS', members: [] });
892
+ }
893
+ showToast('Project deleted', 'success');
894
+ }
895
+ catch {
896
+ dispatch({ type: 'SET_OPERATION', operation: null });
897
+ showToast('Failed to delete project', 'error');
898
+ }
899
+ }
900
+ break;
901
+ }
902
+ case 'confirm-remove-member': {
903
+ const teamForRemove = getActiveTeam(s);
904
+ if (teamForRemove) {
905
+ try {
906
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing member' });
907
+ await api.removeTeamMember(teamForRemove.team_id, modal.userId);
908
+ const members = await api.listTeamMembers(teamForRemove.team_id);
909
+ dispatch({ type: 'SET_MEMBERS', members });
910
+ dispatch({ type: 'SET_OPERATION', operation: null });
911
+ showToast('Member removed', 'success');
912
+ }
913
+ catch {
914
+ dispatch({ type: 'SET_OPERATION', operation: null });
915
+ showToast('Failed to remove member', 'error');
916
+ }
917
+ }
918
+ break;
919
+ }
920
+ case 'confirm-leave': {
921
+ const teamForLeave = getActiveTeam(s);
922
+ if (teamForLeave) {
923
+ try {
924
+ dispatch({ type: 'SET_OPERATION', operation: 'Leaving' });
925
+ await api.removeTeamMember(teamForLeave.team_id, s.user.id);
926
+ const me = await api.me();
927
+ dispatch({ type: 'SET_USER_DATA', me });
928
+ dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
929
+ dispatch({ type: 'SET_OPERATION', operation: null });
930
+ const newProjectId = me.user.active_project_id;
931
+ if (newProjectId) {
932
+ connectFeed(newProjectId);
933
+ await loadTabData();
934
+ }
935
+ else {
936
+ dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
937
+ dispatch({ type: 'SET_MEMBERS', members: [] });
938
+ }
939
+ showToast('Left', 'success');
940
+ }
941
+ catch {
942
+ dispatch({ type: 'SET_OPERATION', operation: null });
943
+ showToast('Failed to leave', 'error');
944
+ }
945
+ }
946
+ break;
947
+ }
948
+ case 'confirm-logout':
949
+ try {
950
+ const credentialsFile = join(homedir(), '.cohvu', 'credentials');
951
+ if (existsSync(credentialsFile))
952
+ unlinkSync(credentialsFile);
953
+ }
954
+ catch { }
955
+ exit();
956
+ break;
957
+ case 'rename': {
958
+ if (!modal.input)
959
+ break;
960
+ const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
961
+ try {
962
+ dispatch({ type: 'SET_OPERATION', operation: 'Renaming' });
963
+ await api.renameProject(projectId, modal.input, slug);
964
+ const me = await api.me();
965
+ dispatch({ type: 'SET_USER_DATA', me });
966
+ dispatch({ type: 'SET_OPERATION', operation: null });
967
+ showToast('Project renamed', 'success');
968
+ }
969
+ catch {
970
+ dispatch({ type: 'SET_OPERATION', operation: null });
971
+ showToast('Failed to rename', 'error');
972
+ }
973
+ break;
974
+ }
975
+ case 'create-project': {
976
+ if (!modal.input)
977
+ break;
978
+ const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
979
+ try {
980
+ dispatch({ type: 'SET_OPERATION', operation: 'Creating project' });
981
+ const project = await api.createProject(modal.input, slug);
982
+ await api.switchProject(project.id);
983
+ const me = await api.me();
984
+ dispatch({ type: 'SET_USER_DATA', me });
985
+ dispatch({ type: 'SET_OPERATION', operation: null });
986
+ await loadTabData();
987
+ showToast('Project created', 'success');
988
+ }
989
+ catch {
990
+ dispatch({ type: 'SET_OPERATION', operation: null });
991
+ showToast('Failed to create project', 'error');
992
+ }
993
+ break;
994
+ }
995
+ case 'confirm-regen-link': {
996
+ const teamForRegen = getActiveTeam(s);
997
+ if (teamForRegen) {
998
+ try {
999
+ dispatch({ type: 'SET_OPERATION', operation: 'Regenerating link' });
1000
+ await api.regenerateTeamInviteLink(teamForRegen.team_id, modal.role);
1001
+ const links = await api.listTeamInviteLinks(teamForRegen.team_id);
1002
+ dispatch({ type: 'SET_INVITE_LINKS', links });
1003
+ dispatch({ type: 'SET_OPERATION', operation: null });
1004
+ showToast('Link regenerated', 'success');
1005
+ }
1006
+ catch {
1007
+ dispatch({ type: 'SET_OPERATION', operation: null });
1008
+ showToast('Failed to regenerate link', 'error');
1009
+ }
1010
+ }
1011
+ break;
1012
+ }
1013
+ }
1014
+ }
1015
+ // ---- Small terminal guard ----
1016
+ if (cols < 60 || rows < 20) {
1017
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Box, { height: 1 }), _jsx(Text, { color: "gray", children: " terminal too small" }), _jsx(Text, { color: "gray", dimColor: true, children: " resize to continue" })] }));
1018
+ }
1019
+ // ---- Determine content height ----
1020
+ // Header + divider + (banner + divider?) + tabbar + divider = top
1021
+ // toast? + operation? + divider + footer = bottom
1022
+ const hasBanners = state.notifications.some(n => !n.seen) || hasBillingBanner(state) || state.firstLogin || !!state.joinedProjectName;
1023
+ const topLines = 2 + (hasBanners ? 2 : 0) + 2; // header+div, (banner+div), tabbar+div
1024
+ const bottomLines = (state.toast ? 1 : 0) + (state.operationPending ? 1 : 0) + 2; // div + footer
1025
+ const contentHeight = Math.max(1, rows - topLines - bottomLines);
1026
+ // ---- Render ----
1027
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { state: state }), _jsx(Divider, {}), _jsx(Banner, { state: state }), hasBanners && _jsx(Divider, {}), _jsx(TabBar, { active: state.tab }), _jsx(Divider, {}), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: state.modal
1028
+ ? _jsx(ModalView, { state: state, height: contentHeight })
1029
+ : renderTab(state, contentHeight) }), state.toast && _jsx(Toast, { toast: state.toast }), state.operationPending && _jsxs(Text, { color: "gray", dimColor: true, children: [" ", state.operationPending, "..."] }), _jsx(Divider, {}), _jsx(Footer, { state: state })] }));
1030
+ }
1031
+ function renderTab(state, height) {
1032
+ switch (state.tab) {
1033
+ case 'knowledge': return _jsx(KnowledgeTab, { state: state, height: height });
1034
+ case 'team': return _jsx(TeamTab, { state: state });
1035
+ case 'billing': return _jsx(BillingTab, { state: state });
1036
+ case 'project': return _jsx(ProjectTab, { state: state });
1037
+ case 'you': return _jsx(YouTab, { state: state });
1038
+ }
1039
+ }
1040
+ function hasBillingBanner(state) {
1041
+ const project = getActiveProject(state);
1042
+ if (!project)
1043
+ return false;
1044
+ if (project.owner.kind === 'team') {
1045
+ const team = getActiveTeam(state);
1046
+ const sub = team?.subscription;
1047
+ return !sub || (sub.status !== 'active' && sub.status !== 'past_due') || sub.status === 'past_due';
1048
+ }
1049
+ const user = state.user;
1050
+ const trialDays = user?.trial_ends_at ? daysUntil(user.trial_ends_at) : null;
1051
+ const sub = state.individualSubscription;
1052
+ if (trialDays !== null && trialDays <= 0 && sub?.status !== 'active')
1053
+ return true;
1054
+ if (sub?.status === 'past_due')
1055
+ return true;
1056
+ if (trialDays !== null && trialDays > 0 && trialDays <= 3)
1057
+ return true;
1058
+ return false;
1059
+ }
1060
+ // Helper: derive flat projects from MeResponse (same logic as state.ts)
1061
+ function deriveFlatProjectsFromMe(me) {
1062
+ const list = [];
1063
+ for (const p of me.personal_projects) {
1064
+ list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'personal' } });
1065
+ }
1066
+ for (const team of me.teams) {
1067
+ for (const p of team.projects) {
1068
+ list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'team', teamId: team.team_id, teamName: team.name, teamSlug: team.slug } });
1069
+ }
1070
+ }
1071
+ return list;
1072
+ }
1073
+ function loadPersistedState() {
1074
+ try {
1075
+ if (existsSync(STATE_FILE))
1076
+ return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
1077
+ }
1078
+ catch { }
1079
+ return { hasOpenedDashboard: false };
1080
+ }
1081
+ function savePersistedState(data) {
1082
+ try {
1083
+ const dir = join(homedir(), '.cohvu');
1084
+ if (!existsSync(dir))
1085
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
1086
+ writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
1087
+ }
1088
+ catch { }
1089
+ }
1090
+ //# sourceMappingURL=App.js.map