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.
- package/dist/api.d.ts +44 -26
- package/dist/api.js +43 -59
- package/dist/api.js.map +1 -1
- package/dist/auth.d.ts +2 -13
- package/dist/auth.js +41 -105
- package/dist/auth.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.js +59 -103
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +2 -2
- package/dist/instructions.js +22 -26
- package/dist/instructions.js.map +1 -1
- package/dist/platforms.js +22 -25
- package/dist/platforms.js.map +1 -1
- package/dist/proxy.js +26 -14
- package/dist/proxy.js.map +1 -1
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +43 -75
- package/dist/setup.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1091 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/components/Banner.d.ts +4 -0
- package/dist/tui/components/Banner.js +78 -0
- package/dist/tui/components/Banner.js.map +1 -0
- package/dist/tui/components/Divider.d.ts +1 -0
- package/dist/tui/components/Divider.js +7 -0
- package/dist/tui/components/Divider.js.map +1 -0
- package/dist/tui/components/Footer.d.ts +4 -0
- package/dist/tui/components/Footer.js +143 -0
- package/dist/tui/components/Footer.js.map +1 -0
- package/dist/tui/components/Header.d.ts +4 -0
- package/dist/tui/components/Header.js +53 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/Modal.d.ts +5 -0
- package/dist/tui/components/Modal.js +99 -0
- package/dist/tui/components/Modal.js.map +1 -0
- package/dist/tui/components/TabBar.d.ts +4 -0
- package/dist/tui/components/TabBar.js +16 -0
- package/dist/tui/components/TabBar.js.map +1 -0
- package/dist/tui/components/Toast.d.ts +4 -0
- package/dist/tui/components/Toast.js +9 -0
- package/dist/tui/components/Toast.js.map +1 -0
- package/dist/tui/index.js +19 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/platform-detect.d.ts +1 -1
- package/dist/tui/platform-detect.js +19 -22
- package/dist/tui/platform-detect.js.map +1 -1
- package/dist/tui/state.d.ts +33 -6
- package/dist/tui/state.js +65 -17
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/tabs/BillingTab.d.ts +4 -0
- package/dist/tui/tabs/BillingTab.js +68 -0
- package/dist/tui/tabs/BillingTab.js.map +1 -0
- package/dist/tui/tabs/KnowledgeTab.d.ts +5 -0
- package/dist/tui/tabs/KnowledgeTab.js +83 -0
- package/dist/tui/tabs/KnowledgeTab.js.map +1 -0
- package/dist/tui/tabs/ProjectTab.d.ts +4 -0
- package/dist/tui/tabs/ProjectTab.js +31 -0
- package/dist/tui/tabs/ProjectTab.js.map +1 -0
- package/dist/tui/tabs/TeamTab.d.ts +4 -0
- package/dist/tui/tabs/TeamTab.js +39 -0
- package/dist/tui/tabs/TeamTab.js.map +1 -0
- package/dist/tui/tabs/YouTab.d.ts +4 -0
- package/dist/tui/tabs/YouTab.js +9 -0
- package/dist/tui/tabs/YouTab.js.map +1 -0
- package/dist/tui/utils.d.ts +6 -0
- package/dist/tui/utils.js +58 -0
- package/dist/tui/utils.js.map +1 -0
- package/package.json +9 -2
- package/dist/tui/ansi.d.ts +0 -41
- package/dist/tui/ansi.js +0 -117
- package/dist/tui/ansi.js.map +0 -1
- package/dist/tui/dashboard.js +0 -1250
- package/dist/tui/dashboard.js.map +0 -1
- package/dist/tui/keys.d.ts +0 -31
- package/dist/tui/keys.js +0 -56
- package/dist/tui/keys.js.map +0 -1
- package/dist/tui/render.d.ts +0 -5
- package/dist/tui/render.js +0 -158
- package/dist/tui/render.js.map +0 -1
- package/dist/tui/views/billing.d.ts +0 -3
- package/dist/tui/views/billing.js +0 -127
- package/dist/tui/views/billing.js.map +0 -1
- package/dist/tui/views/knowledge.d.ts +0 -3
- package/dist/tui/views/knowledge.js +0 -202
- package/dist/tui/views/knowledge.js.map +0 -1
- package/dist/tui/views/modals.d.ts +0 -3
- package/dist/tui/views/modals.js +0 -198
- package/dist/tui/views/modals.js.map +0 -1
- package/dist/tui/views/project.d.ts +0 -3
- package/dist/tui/views/project.js +0 -76
- package/dist/tui/views/project.js.map +0 -1
- package/dist/tui/views/team.d.ts +0 -3
- package/dist/tui/views/team.js +0 -106
- package/dist/tui/views/team.js.map +0 -1
- package/dist/tui/views/you.d.ts +0 -3
- package/dist/tui/views/you.js +0 -40
- package/dist/tui/views/you.js.map +0 -1
- /package/dist/tui/{dashboard.d.ts → index.d.ts} +0 -0
package/dist/tui/dashboard.js
DELETED
|
@@ -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
|