cohvu 2.2.8 → 2.3.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 +37 -0
- package/dist/api.js +46 -0
- package/dist/api.js.map +1 -1
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/dist/tui/App.js +528 -48
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/Banner.js +24 -3
- package/dist/tui/components/Banner.js.map +1 -1
- package/dist/tui/components/Divider.js +2 -1
- package/dist/tui/components/Divider.js.map +1 -1
- package/dist/tui/components/Footer.js +34 -4
- package/dist/tui/components/Footer.js.map +1 -1
- package/dist/tui/components/Header.js +10 -1
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/Modal.js +39 -1
- package/dist/tui/components/Modal.js.map +1 -1
- package/dist/tui/index.js +1 -0
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/state.d.ts +52 -0
- package/dist/tui/state.js +10 -2
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/tabs/BillingTab.js +8 -0
- package/dist/tui/tabs/BillingTab.js.map +1 -1
- package/dist/tui/tabs/KnowledgeTab.js +6 -3
- package/dist/tui/tabs/KnowledgeTab.js.map +1 -1
- package/dist/tui/tabs/ProjectTab.js +1 -1
- package/dist/tui/tabs/ProjectTab.js.map +1 -1
- package/dist/tui/tabs/TeamTab.js +19 -8
- package/dist/tui/tabs/TeamTab.js.map +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -19,9 +19,9 @@ import { TeamTab } from './tabs/TeamTab.js';
|
|
|
19
19
|
import { BillingTab } from './tabs/BillingTab.js';
|
|
20
20
|
import { ProjectTab } from './tabs/ProjectTab.js';
|
|
21
21
|
import { YouTab } from './tabs/YouTab.js';
|
|
22
|
-
import { daysUntil } from './utils.js';
|
|
22
|
+
import { daysUntil, timeUntil } from './utils.js';
|
|
23
23
|
import { exec, execFile } from 'child_process';
|
|
24
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, watch } from 'fs';
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { homedir } from 'os';
|
|
27
27
|
const STATE_FILE = join(homedir(), '.cohvu', 'state.json');
|
|
@@ -72,6 +72,12 @@ export default function App() {
|
|
|
72
72
|
execFile(cmd, [url], () => { });
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
function shouldRequireConsensus(s) {
|
|
76
|
+
if (!s.requireConsensus)
|
|
77
|
+
return false;
|
|
78
|
+
const admins = s.members.filter(m => m.role === 'admin');
|
|
79
|
+
return admins.length >= 2;
|
|
80
|
+
}
|
|
75
81
|
function copyToClipboard(text) {
|
|
76
82
|
const cmd = process.platform === 'darwin' ? 'pbcopy'
|
|
77
83
|
: process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
|
|
@@ -88,7 +94,7 @@ export default function App() {
|
|
|
88
94
|
if (eventType === 'memory') {
|
|
89
95
|
const event = data;
|
|
90
96
|
if (event.operation === 'create') {
|
|
91
|
-
dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at } });
|
|
97
|
+
dispatch({ type: 'ADD_MEMORY', memory: { id: event.id, body: event.body, updated_at: event.updated_at, contributed_by: event.contributed_by, memory_type: event.memory_type } });
|
|
92
98
|
dispatch({ type: 'SET_LIVE_DOT', memoryId: event.id });
|
|
93
99
|
if (liveDotTimerRef.current)
|
|
94
100
|
clearTimeout(liveDotTimerRef.current);
|
|
@@ -101,6 +107,9 @@ export default function App() {
|
|
|
101
107
|
clearTimeout(liveDotTimerRef.current);
|
|
102
108
|
liveDotTimerRef.current = setTimeout(() => dispatch({ type: 'CLEAR_LIVE_DOT' }), 10000);
|
|
103
109
|
}
|
|
110
|
+
else if (event.operation === 'delete') {
|
|
111
|
+
dispatch({ type: 'REMOVE_MEMORY', id: event.id });
|
|
112
|
+
}
|
|
104
113
|
}
|
|
105
114
|
else if (eventType === 'role_change') {
|
|
106
115
|
api.me().then(me => dispatch({ type: 'SET_USER_DATA', me })).catch(() => {
|
|
@@ -121,6 +130,14 @@ export default function App() {
|
|
|
121
130
|
}
|
|
122
131
|
});
|
|
123
132
|
}
|
|
133
|
+
else if (eventType === 'approval') {
|
|
134
|
+
const team = getActiveTeam(stateRef.current);
|
|
135
|
+
if (team) {
|
|
136
|
+
api.listApprovals(team.team_id).then(approvals => {
|
|
137
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
138
|
+
}).catch(() => { });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
124
141
|
},
|
|
125
142
|
onConnected: () => {
|
|
126
143
|
dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
|
|
@@ -160,6 +177,8 @@ export default function App() {
|
|
|
160
177
|
]);
|
|
161
178
|
dispatch({ type: 'SET_MEMBERS', members });
|
|
162
179
|
dispatch({ type: 'SET_INVITE_LINKS', links });
|
|
180
|
+
const approvals = await api.listApprovals(activeTeam.team_id).catch(() => []);
|
|
181
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
163
182
|
}
|
|
164
183
|
else {
|
|
165
184
|
dispatch({ type: 'SET_MEMBERS', members: [] });
|
|
@@ -228,14 +247,12 @@ export default function App() {
|
|
|
228
247
|
if (cancelled)
|
|
229
248
|
return;
|
|
230
249
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
231
|
-
|
|
232
|
-
if (cancelled)
|
|
233
|
-
return;
|
|
250
|
+
// Detect platforms without re-running setup (setup already ran in enterDashboard)
|
|
234
251
|
const platforms = detectPlatformStatuses();
|
|
235
252
|
dispatch({ type: 'SET_PLATFORMS', platforms });
|
|
236
|
-
const projectId = me.user.active_project_id;
|
|
237
253
|
const flatProjects = deriveFlatProjectsFromMe(me);
|
|
238
|
-
const activeProject = flatProjects.find(p => p.project_id ===
|
|
254
|
+
const activeProject = flatProjects.find(p => p.project_id === me.user.active_project_id) ?? flatProjects[0] ?? null;
|
|
255
|
+
const projectId = activeProject?.project_id ?? null;
|
|
239
256
|
const isTeamProject = activeProject?.owner.kind === 'team';
|
|
240
257
|
const activeTeamId = isTeamProject && activeProject.owner.kind === 'team' ? activeProject.owner.teamId : null;
|
|
241
258
|
const activeTeam = activeTeamId ? me.teams.find(t => t.team_id === activeTeamId) : null;
|
|
@@ -265,6 +282,16 @@ export default function App() {
|
|
|
265
282
|
dispatch({ type: 'SET_BILLING', billing });
|
|
266
283
|
dispatch({ type: 'SET_INVITE_LINKS', links: inviteLinks });
|
|
267
284
|
dispatch({ type: 'SET_NOTIFICATIONS', notifications });
|
|
285
|
+
if (isTeamProject && activeTeam) {
|
|
286
|
+
dispatch({ type: 'SET_REQUIRE_CONSENSUS', value: activeTeam.require_consensus ?? false });
|
|
287
|
+
const ssoConfig = await api.getSso(activeTeam.team_id).catch(() => null);
|
|
288
|
+
if (ssoConfig)
|
|
289
|
+
dispatch({ type: 'SET_SSO_CONFIG', config: ssoConfig });
|
|
290
|
+
}
|
|
291
|
+
if (isTeamProject && activeTeam) {
|
|
292
|
+
const approvals = await api.listApprovals(activeTeam.team_id).catch(() => []);
|
|
293
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
294
|
+
}
|
|
268
295
|
if (notifications.length > 0)
|
|
269
296
|
api.markNotificationsSeen().catch(() => { });
|
|
270
297
|
connectFeed(projectId);
|
|
@@ -349,8 +376,8 @@ export default function App() {
|
|
|
349
376
|
exit();
|
|
350
377
|
return;
|
|
351
378
|
}
|
|
352
|
-
// Global: q exits (unless modal open or in search mode)
|
|
353
|
-
if (input === 'q' && !s.modal && s.knowledgeMode !== '
|
|
379
|
+
// Global: q exits (unless modal open or in knowledge search/forget mode)
|
|
380
|
+
if (input === 'q' && !s.modal && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
|
|
354
381
|
exit();
|
|
355
382
|
return;
|
|
356
383
|
}
|
|
@@ -359,17 +386,48 @@ export default function App() {
|
|
|
359
386
|
await handleModalKey(input, key);
|
|
360
387
|
return;
|
|
361
388
|
}
|
|
389
|
+
// Approval keys (when approvals exist, on team/project tab, team project)
|
|
390
|
+
if ((input === 'a' || input === 'x') && !s.modal && s.pendingApprovals.length > 0 && (s.tab === 'team' || s.tab === 'project')) {
|
|
391
|
+
const activeProject = getActiveProject(s);
|
|
392
|
+
if (activeProject?.owner.kind === 'team') {
|
|
393
|
+
const approval = s.pendingApprovals[0];
|
|
394
|
+
if (input === 'a') {
|
|
395
|
+
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
396
|
+
kind: 'approve-action',
|
|
397
|
+
approvalId: approval.id,
|
|
398
|
+
description: approval.description,
|
|
399
|
+
initiator: approval.initiator_email,
|
|
400
|
+
expiresIn: timeUntil(approval.expires_at),
|
|
401
|
+
} });
|
|
402
|
+
}
|
|
403
|
+
else if (input === 'x') {
|
|
404
|
+
const team = getActiveTeam(s);
|
|
405
|
+
if (team) {
|
|
406
|
+
try {
|
|
407
|
+
await api.cancelApproval(team.team_id, approval.id);
|
|
408
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
409
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
410
|
+
showToast('Canceled', 'success');
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
showToast('Failed to cancel', 'error');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
362
420
|
// Tab switching
|
|
363
421
|
if (key.tab) {
|
|
364
422
|
dispatch({ type: 'NEXT_TAB' });
|
|
365
|
-
|
|
423
|
+
setTimeout(() => loadTabData(), 0);
|
|
366
424
|
return;
|
|
367
425
|
}
|
|
368
426
|
// Number keys switch tabs
|
|
369
427
|
const tabIdx = parseInt(input, 10);
|
|
370
428
|
if (tabIdx >= 1 && tabIdx <= 5 && !key.ctrl && !key.meta) {
|
|
371
429
|
dispatch({ type: 'SWITCH_TAB', tab: TABS[tabIdx - 1] });
|
|
372
|
-
|
|
430
|
+
setTimeout(() => loadTabData(), 0);
|
|
373
431
|
return;
|
|
374
432
|
}
|
|
375
433
|
// Global 'b' shortcut — subscribe/billing portal from banner
|
|
@@ -511,11 +569,12 @@ export default function App() {
|
|
|
511
569
|
return;
|
|
512
570
|
const memberCount = s.members.length;
|
|
513
571
|
const linkRoles = ['admin', 'member', 'viewer'];
|
|
514
|
-
|
|
515
|
-
const
|
|
572
|
+
// Settings rows (admin only): name, consensus, sso, then 3 invite link rows
|
|
573
|
+
const settingsCount = s.userRole === 'admin' ? 6 : 0; // 3 settings + 3 links
|
|
574
|
+
const totalRows = memberCount + settingsCount;
|
|
516
575
|
const sel = s.teamSelected;
|
|
517
|
-
const onLinkRow = s.userRole === 'admin' && sel >= memberCount;
|
|
518
576
|
const onMemberRow = sel < memberCount;
|
|
577
|
+
const onLinkRow = s.userRole === 'admin' && sel >= memberCount + 3;
|
|
519
578
|
const team = getActiveTeam(s);
|
|
520
579
|
if (key.upArrow) {
|
|
521
580
|
dispatch({ type: 'SET_TEAM_SELECTED', index: Math.max(0, sel - 1) });
|
|
@@ -525,8 +584,54 @@ export default function App() {
|
|
|
525
584
|
dispatch({ type: 'SET_TEAM_SELECTED', index: Math.min(totalRows - 1, sel + 1) });
|
|
526
585
|
return;
|
|
527
586
|
}
|
|
587
|
+
// 'i' key (admin) — open invite modal
|
|
588
|
+
if (input === 'i' && s.userRole === 'admin') {
|
|
589
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite', selected: 0 } });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// 'r' key on name row — rename team
|
|
593
|
+
if (input === 'r' && s.userRole === 'admin' && sel === memberCount + 0) {
|
|
594
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-rename-team', input: '' } });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// 'r' key on invite link row — regen link
|
|
598
|
+
if (input === 'r' && s.userRole === 'admin' && onLinkRow) {
|
|
599
|
+
const linkIdx = sel - memberCount - 3;
|
|
600
|
+
const role = linkRoles[linkIdx];
|
|
601
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// Enter on consensus row — toggle
|
|
605
|
+
if (key.return && s.userRole === 'admin' && sel === memberCount + 1) {
|
|
606
|
+
if (team) {
|
|
607
|
+
try {
|
|
608
|
+
const newValue = !s.requireConsensus;
|
|
609
|
+
await api.updateTeamSettings(team.team_id, { require_consensus: newValue });
|
|
610
|
+
dispatch({ type: 'SET_REQUIRE_CONSENSUS', value: newValue });
|
|
611
|
+
showToast(newValue ? 'Consensus required' : 'Consensus off', 'success');
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
showToast('Failed to update', 'error');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// 's' key on SSO row — configure or manage SSO
|
|
620
|
+
if (input === 's' && s.userRole === 'admin' && sel === memberCount + 2) {
|
|
621
|
+
if (s.ssoConfig) {
|
|
622
|
+
// SSO already exists — offer edit/delete
|
|
623
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'manage-sso' } });
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
627
|
+
kind: 'configure-sso', step: 1, issuer: '', clientId: '', clientSecret: '', domains: '', defaultRole: 0, requireSso: false,
|
|
628
|
+
} });
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
// 'c' key on invite link row — copy link
|
|
528
633
|
if (input === 'c' && s.userRole === 'admin' && onLinkRow) {
|
|
529
|
-
const linkIdx = sel - memberCount;
|
|
634
|
+
const linkIdx = sel - memberCount - 3;
|
|
530
635
|
const role = linkRoles[linkIdx];
|
|
531
636
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
532
637
|
if (link) {
|
|
@@ -538,12 +643,23 @@ export default function App() {
|
|
|
538
643
|
}
|
|
539
644
|
return;
|
|
540
645
|
}
|
|
541
|
-
|
|
542
|
-
|
|
646
|
+
// 'o' key on invite link row — open in browser
|
|
647
|
+
if (input === 'o' && s.userRole === 'admin' && onLinkRow) {
|
|
648
|
+
const linkIdx = sel - memberCount - 3;
|
|
543
649
|
const role = linkRoles[linkIdx];
|
|
544
|
-
|
|
650
|
+
const link = s.inviteLinks.find(l => l.role === role);
|
|
651
|
+
if (link)
|
|
652
|
+
openBrowser(link.url);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// 'd' key (admin) — delete team
|
|
656
|
+
if (input === 'd' && s.userRole === 'admin') {
|
|
657
|
+
if (team) {
|
|
658
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-delete-team', slug: team.slug, teamName: team.name, input: '' } });
|
|
659
|
+
}
|
|
545
660
|
return;
|
|
546
661
|
}
|
|
662
|
+
// 'e' key on member row — edit role
|
|
547
663
|
if (input === 'e' && s.userRole === 'admin' && onMemberRow) {
|
|
548
664
|
const target = s.members[sel];
|
|
549
665
|
if (target) {
|
|
@@ -562,6 +678,7 @@ export default function App() {
|
|
|
562
678
|
}
|
|
563
679
|
return;
|
|
564
680
|
}
|
|
681
|
+
// 'x' key — remove member or leave
|
|
565
682
|
if (input === 'x') {
|
|
566
683
|
if (s.userRole === 'admin' && onMemberRow && team) {
|
|
567
684
|
const target = s.members[sel];
|
|
@@ -634,11 +751,20 @@ export default function App() {
|
|
|
634
751
|
// ---- Project keys ----
|
|
635
752
|
async function handleProjectKey(input, _key) {
|
|
636
753
|
const s = stateRef.current;
|
|
754
|
+
if (input === 't') {
|
|
755
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-team', input: '' } });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
637
758
|
if (input === 'r' && s.userRole === 'admin') {
|
|
638
759
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'rename', input: '' } });
|
|
639
760
|
}
|
|
640
761
|
else if (input === 'n') {
|
|
641
|
-
|
|
762
|
+
if (s.teams.length > 0) {
|
|
763
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'select-owner', selected: 0 } });
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '', teamId: null } });
|
|
767
|
+
}
|
|
642
768
|
}
|
|
643
769
|
else if (input === 'w' && s.projects.length > 1) {
|
|
644
770
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: 0 } });
|
|
@@ -740,8 +866,10 @@ export default function App() {
|
|
|
740
866
|
modal.kind === 'confirm-regen-link' || modal.kind === 'initiate-consensus' ||
|
|
741
867
|
modal.kind === 'approve-action') {
|
|
742
868
|
if (input === 'y') {
|
|
869
|
+
const willExit = modal.kind === 'confirm-logout';
|
|
743
870
|
await confirmModal(modal);
|
|
744
|
-
|
|
871
|
+
if (!willExit)
|
|
872
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
745
873
|
}
|
|
746
874
|
else if (input === 'n') {
|
|
747
875
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
@@ -751,8 +879,9 @@ export default function App() {
|
|
|
751
879
|
// Text input modals
|
|
752
880
|
if ('input' in modal) {
|
|
753
881
|
if (key.return) {
|
|
754
|
-
await confirmModal(modal);
|
|
755
|
-
|
|
882
|
+
const chained = await confirmModal(modal);
|
|
883
|
+
if (!chained)
|
|
884
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
756
885
|
}
|
|
757
886
|
else if (key.backspace) {
|
|
758
887
|
dispatch({ type: 'MODAL_BACKSPACE' });
|
|
@@ -776,19 +905,25 @@ export default function App() {
|
|
|
776
905
|
else if (key.return) {
|
|
777
906
|
if (modal.selected === s.projects.length) {
|
|
778
907
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
779
|
-
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
|
|
908
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '', teamId: null } });
|
|
780
909
|
}
|
|
781
910
|
else {
|
|
782
911
|
const project = s.projects[modal.selected];
|
|
783
912
|
if (project) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
913
|
+
try {
|
|
914
|
+
await api.switchProject(project.project_id);
|
|
915
|
+
const me = await api.me();
|
|
916
|
+
dispatch({ type: 'SET_USER_DATA', me });
|
|
917
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
918
|
+
const newProjectId = me.user.active_project_id;
|
|
919
|
+
if (newProjectId)
|
|
920
|
+
connectFeed(newProjectId);
|
|
921
|
+
setTimeout(() => loadTabData(), 0);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
925
|
+
showToast('Failed to switch project', 'error');
|
|
926
|
+
}
|
|
792
927
|
}
|
|
793
928
|
}
|
|
794
929
|
}
|
|
@@ -814,6 +949,23 @@ export default function App() {
|
|
|
814
949
|
return;
|
|
815
950
|
}
|
|
816
951
|
}
|
|
952
|
+
if (shouldRequireConsensus(stateRef.current) && modal.currentRole === 'admin' && newRole !== 'admin') {
|
|
953
|
+
const team = getActiveTeam(stateRef.current);
|
|
954
|
+
if (team) {
|
|
955
|
+
try {
|
|
956
|
+
await api.initiateApproval(team.team_id, 'demote_admin', `demote ${modal.targetEmail} from admin`, modal.targetUserId);
|
|
957
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
958
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
959
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
960
|
+
showToast('Approval requested', 'info');
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
showToast('Failed to request approval', 'error');
|
|
964
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
817
969
|
const changeTeam = getActiveTeam(s);
|
|
818
970
|
if (changeTeam) {
|
|
819
971
|
try {
|
|
@@ -830,14 +982,215 @@ export default function App() {
|
|
|
830
982
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
831
983
|
}
|
|
832
984
|
}
|
|
985
|
+
// Select owner modal
|
|
986
|
+
if (modal.kind === 'select-owner') {
|
|
987
|
+
const itemCount = 1 + s.teams.length; // "personal" + each team
|
|
988
|
+
if (key.upArrow) {
|
|
989
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.max(0, modal.selected - 1) } });
|
|
990
|
+
}
|
|
991
|
+
else if (key.downArrow) {
|
|
992
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.min(itemCount - 1, modal.selected + 1) } });
|
|
993
|
+
}
|
|
994
|
+
else if (key.return) {
|
|
995
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
996
|
+
if (modal.selected === 0) {
|
|
997
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '', teamId: null } });
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
const team = s.teams[modal.selected - 1];
|
|
1001
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '', teamId: team.team_id } });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
// Invite modal — select role
|
|
1007
|
+
if (modal.kind === 'invite') {
|
|
1008
|
+
const roles = ['admin', 'member', 'viewer'];
|
|
1009
|
+
if (key.upArrow) {
|
|
1010
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.max(0, modal.selected - 1) } });
|
|
1011
|
+
}
|
|
1012
|
+
else if (key.downArrow) {
|
|
1013
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.min(2, modal.selected + 1) } });
|
|
1014
|
+
}
|
|
1015
|
+
else if (key.return) {
|
|
1016
|
+
const role = roles[modal.selected];
|
|
1017
|
+
const link = s.inviteLinks.find(l => l.role === role);
|
|
1018
|
+
if (link) {
|
|
1019
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1020
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role, url: link.url } });
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1024
|
+
showToast('Invite link not available', 'error');
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
// Invite link modal — copy or open
|
|
1030
|
+
if (modal.kind === 'invite-link') {
|
|
1031
|
+
if (input === 'c') {
|
|
1032
|
+
copyToClipboard(modal.url);
|
|
1033
|
+
dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
|
|
1034
|
+
if (copiedTimerRef.current)
|
|
1035
|
+
clearTimeout(copiedTimerRef.current);
|
|
1036
|
+
copiedTimerRef.current = setTimeout(() => dispatch({ type: 'SET_COPIED_FEEDBACK', active: false }), 1500);
|
|
1037
|
+
}
|
|
1038
|
+
else if (input === 'o') {
|
|
1039
|
+
openBrowser(modal.url);
|
|
1040
|
+
showToast('Opened in browser', 'info');
|
|
1041
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
// Manage existing SSO — edit or delete
|
|
1045
|
+
if (modal.kind === 'manage-sso') {
|
|
1046
|
+
if (input === 'e') {
|
|
1047
|
+
// Edit — open wizard pre-filled with current values
|
|
1048
|
+
const sso = s.ssoConfig;
|
|
1049
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1050
|
+
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
1051
|
+
kind: 'configure-sso', step: 1,
|
|
1052
|
+
issuer: sso?.issuer ?? '', clientId: '', clientSecret: '',
|
|
1053
|
+
domains: sso?.allowed_domains.join(', ') ?? '', defaultRole: ['member', 'viewer', 'admin'].indexOf(sso?.default_role ?? 'member'),
|
|
1054
|
+
requireSso: sso?.require_sso ?? false,
|
|
1055
|
+
} });
|
|
1056
|
+
}
|
|
1057
|
+
else if (input === 'd') {
|
|
1058
|
+
const team = getActiveTeam(s);
|
|
1059
|
+
if (team) {
|
|
1060
|
+
try {
|
|
1061
|
+
await api.deleteSso(team.team_id);
|
|
1062
|
+
dispatch({ type: 'SET_SSO_CONFIG', config: null });
|
|
1063
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1064
|
+
showToast('SSO removed', 'success');
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
showToast('Failed to remove SSO', 'error');
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
// Configure SSO wizard
|
|
1074
|
+
if (modal.kind === 'configure-sso') {
|
|
1075
|
+
if (key.escape) {
|
|
1076
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const fieldMap = { 1: 'issuer', 2: 'clientId', 3: 'clientSecret', 4: 'domains' };
|
|
1080
|
+
if (modal.step >= 1 && modal.step <= 4) {
|
|
1081
|
+
const field = fieldMap[modal.step];
|
|
1082
|
+
const current = modal[field];
|
|
1083
|
+
if (key.return && current.length > 0) {
|
|
1084
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: modal.step + 1 } });
|
|
1085
|
+
}
|
|
1086
|
+
else if (key.backspace) {
|
|
1087
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, [field]: current.slice(0, -1) } });
|
|
1088
|
+
}
|
|
1089
|
+
else if (input === ' ') {
|
|
1090
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, [field]: current + ' ' } });
|
|
1091
|
+
}
|
|
1092
|
+
else if (input.length === 1 && !key.ctrl && !key.meta) {
|
|
1093
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, [field]: current + input } });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
else if (modal.step === 5) {
|
|
1097
|
+
// Role selection (member=0, viewer=1, admin=2)
|
|
1098
|
+
if (key.upArrow) {
|
|
1099
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, defaultRole: Math.max(0, modal.defaultRole - 1) } });
|
|
1100
|
+
}
|
|
1101
|
+
else if (key.downArrow) {
|
|
1102
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, defaultRole: Math.min(2, modal.defaultRole + 1) } });
|
|
1103
|
+
}
|
|
1104
|
+
else if (key.return) {
|
|
1105
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 6 } });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
else if (modal.step === 6) {
|
|
1109
|
+
// Require SSO toggle
|
|
1110
|
+
if (input === 'y') {
|
|
1111
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, requireSso: true, step: 7 } });
|
|
1112
|
+
}
|
|
1113
|
+
else if (input === 'n') {
|
|
1114
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, requireSso: false, step: 7 } });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
else if (modal.step === 7) {
|
|
1118
|
+
// Confirm
|
|
1119
|
+
if (key.return) {
|
|
1120
|
+
const team = getActiveTeam(s);
|
|
1121
|
+
if (team) {
|
|
1122
|
+
const roles = ['member', 'viewer', 'admin'];
|
|
1123
|
+
try {
|
|
1124
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Configuring SSO' });
|
|
1125
|
+
const ssoPayload = {
|
|
1126
|
+
issuer: modal.issuer,
|
|
1127
|
+
client_id: modal.clientId,
|
|
1128
|
+
client_secret: modal.clientSecret,
|
|
1129
|
+
allowed_domains: modal.domains.split(',').map((d) => d.trim()).filter(Boolean),
|
|
1130
|
+
default_role: roles[modal.defaultRole],
|
|
1131
|
+
require_sso: modal.requireSso,
|
|
1132
|
+
};
|
|
1133
|
+
if (s.ssoConfig) {
|
|
1134
|
+
await api.updateSso(team.team_id, ssoPayload);
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
await api.configureSso(team.team_id, ssoPayload);
|
|
1138
|
+
}
|
|
1139
|
+
const ssoConfig = await api.getSso(team.team_id);
|
|
1140
|
+
dispatch({ type: 'SET_SSO_CONFIG', config: ssoConfig });
|
|
1141
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1142
|
+
showToast('SSO configured', 'success');
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1146
|
+
showToast('Failed to configure SSO', 'error');
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
833
1154
|
}
|
|
834
1155
|
// ---- Modal confirmation ----
|
|
835
1156
|
async function confirmModal(modal) {
|
|
836
1157
|
const s = stateRef.current;
|
|
1158
|
+
// Logout doesn't need a project
|
|
1159
|
+
if (modal.kind === 'confirm-logout') {
|
|
1160
|
+
try {
|
|
1161
|
+
const credentialsFile = join(homedir(), '.cohvu', 'credentials');
|
|
1162
|
+
if (existsSync(credentialsFile))
|
|
1163
|
+
unlinkSync(credentialsFile);
|
|
1164
|
+
}
|
|
1165
|
+
catch { }
|
|
1166
|
+
exit();
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
837
1169
|
const projectId = s.activeProjectId;
|
|
838
1170
|
if (!projectId)
|
|
839
|
-
return;
|
|
1171
|
+
return false;
|
|
840
1172
|
switch (modal.kind) {
|
|
1173
|
+
case 'approve-action': {
|
|
1174
|
+
const team = getActiveTeam(s);
|
|
1175
|
+
if (team && 'approvalId' in modal) {
|
|
1176
|
+
try {
|
|
1177
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Approving' });
|
|
1178
|
+
await api.approveAction(team.team_id, modal.approvalId);
|
|
1179
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
1180
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1181
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1182
|
+
const me = await api.me();
|
|
1183
|
+
dispatch({ type: 'SET_USER_DATA', me });
|
|
1184
|
+
await loadTabData();
|
|
1185
|
+
showToast('Approved', 'success');
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1189
|
+
showToast('Failed to approve', 'error');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
841
1194
|
case 'confirm-forget':
|
|
842
1195
|
try {
|
|
843
1196
|
dispatch({ type: 'SET_OPERATION', operation: 'Removing memory' });
|
|
@@ -855,6 +1208,21 @@ export default function App() {
|
|
|
855
1208
|
case 'confirm-clear': {
|
|
856
1209
|
const project = getActiveProject(s);
|
|
857
1210
|
if (project && modal.input === project.slug) {
|
|
1211
|
+
if (shouldRequireConsensus(s)) {
|
|
1212
|
+
const team = getActiveTeam(s);
|
|
1213
|
+
if (team) {
|
|
1214
|
+
try {
|
|
1215
|
+
await api.initiateApproval(team.team_id, 'clear_memories', `clear all memories from "${project.slug}"`, projectId);
|
|
1216
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
1217
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1218
|
+
showToast('Approval requested', 'info');
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
showToast('Failed to request approval', 'error');
|
|
1222
|
+
}
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
858
1226
|
try {
|
|
859
1227
|
dispatch({ type: 'SET_OPERATION', operation: 'Clearing memories' });
|
|
860
1228
|
await api.clearMemories(projectId);
|
|
@@ -872,6 +1240,21 @@ export default function App() {
|
|
|
872
1240
|
case 'confirm-delete': {
|
|
873
1241
|
const project = getActiveProject(s);
|
|
874
1242
|
if (project && modal.input === project.slug) {
|
|
1243
|
+
if (shouldRequireConsensus(s)) {
|
|
1244
|
+
const team = getActiveTeam(s);
|
|
1245
|
+
if (team) {
|
|
1246
|
+
try {
|
|
1247
|
+
await api.initiateApproval(team.team_id, 'delete_project', `delete project "${project.slug}"`, projectId);
|
|
1248
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
1249
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1250
|
+
showToast('Approval requested', 'info');
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
showToast('Failed to request approval', 'error');
|
|
1254
|
+
}
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
875
1258
|
try {
|
|
876
1259
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting project' });
|
|
877
1260
|
await api.deleteProject(projectId);
|
|
@@ -943,18 +1326,7 @@ export default function App() {
|
|
|
943
1326
|
}
|
|
944
1327
|
break;
|
|
945
1328
|
}
|
|
946
|
-
|
|
947
|
-
try {
|
|
948
|
-
const credentialsFile = join(homedir(), '.cohvu', 'credentials');
|
|
949
|
-
if (existsSync(credentialsFile)) {
|
|
950
|
-
const { unlinkSync } = await import('fs');
|
|
951
|
-
unlinkSync(credentialsFile);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
catch { }
|
|
955
|
-
await new Promise(r => setTimeout(r, 500));
|
|
956
|
-
exit();
|
|
957
|
-
break;
|
|
1329
|
+
// confirm-logout handled above (before projectId check)
|
|
958
1330
|
case 'rename': {
|
|
959
1331
|
if (!modal.input)
|
|
960
1332
|
break;
|
|
@@ -979,7 +1351,9 @@ export default function App() {
|
|
|
979
1351
|
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
980
1352
|
try {
|
|
981
1353
|
dispatch({ type: 'SET_OPERATION', operation: 'Creating project' });
|
|
982
|
-
const project =
|
|
1354
|
+
const project = modal.teamId
|
|
1355
|
+
? await api.createTeamProject(modal.teamId, modal.input, slug)
|
|
1356
|
+
: await api.createProject(modal.input, slug);
|
|
983
1357
|
await api.switchProject(project.id);
|
|
984
1358
|
const me = await api.me();
|
|
985
1359
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
@@ -993,6 +1367,47 @@ export default function App() {
|
|
|
993
1367
|
}
|
|
994
1368
|
break;
|
|
995
1369
|
}
|
|
1370
|
+
case 'create-team': {
|
|
1371
|
+
if (!modal.input)
|
|
1372
|
+
break;
|
|
1373
|
+
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1374
|
+
try {
|
|
1375
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Creating team' });
|
|
1376
|
+
const team = await api.createTeam(modal.input, slug);
|
|
1377
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1378
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-team-project', teamId: team.id, teamName: modal.input, input: '' } });
|
|
1379
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1380
|
+
return true; // chained to create-team-project — caller must not CLOSE_MODAL
|
|
1381
|
+
}
|
|
1382
|
+
catch {
|
|
1383
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1384
|
+
showToast('Failed to create team', 'error');
|
|
1385
|
+
}
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
case 'create-team-project': {
|
|
1389
|
+
if (!modal.input)
|
|
1390
|
+
break;
|
|
1391
|
+
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1392
|
+
try {
|
|
1393
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Creating project' });
|
|
1394
|
+
const project = await api.createTeamProject(modal.teamId, modal.input, slug);
|
|
1395
|
+
await api.switchProject(project.id);
|
|
1396
|
+
const me = await api.me();
|
|
1397
|
+
dispatch({ type: 'SET_USER_DATA', me });
|
|
1398
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1399
|
+
const newProjectId = me.user.active_project_id;
|
|
1400
|
+
if (newProjectId)
|
|
1401
|
+
connectFeed(newProjectId);
|
|
1402
|
+
await loadTabData();
|
|
1403
|
+
showToast('Team created', 'success');
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1407
|
+
showToast('Failed to create project', 'error');
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
996
1411
|
case 'confirm-regen-link': {
|
|
997
1412
|
const teamForRegen = getActiveTeam(s);
|
|
998
1413
|
if (teamForRegen) {
|
|
@@ -1011,16 +1426,81 @@ export default function App() {
|
|
|
1011
1426
|
}
|
|
1012
1427
|
break;
|
|
1013
1428
|
}
|
|
1429
|
+
case 'confirm-rename-team': {
|
|
1430
|
+
if (!modal.input)
|
|
1431
|
+
break;
|
|
1432
|
+
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1433
|
+
const teamForRename = getActiveTeam(s);
|
|
1434
|
+
if (teamForRename) {
|
|
1435
|
+
try {
|
|
1436
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Renaming team' });
|
|
1437
|
+
await api.renameTeam(teamForRename.team_id, modal.input, slug);
|
|
1438
|
+
const me = await api.me();
|
|
1439
|
+
dispatch({ type: 'SET_USER_DATA', me });
|
|
1440
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1441
|
+
showToast('Team renamed', 'success');
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1445
|
+
showToast('Failed to rename team', 'error');
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
case 'confirm-delete-team': {
|
|
1451
|
+
const teamForDelete = getActiveTeam(s);
|
|
1452
|
+
if (teamForDelete && modal.input === modal.slug) {
|
|
1453
|
+
if (shouldRequireConsensus(s)) {
|
|
1454
|
+
const team = getActiveTeam(s);
|
|
1455
|
+
if (team) {
|
|
1456
|
+
try {
|
|
1457
|
+
await api.initiateApproval(team.team_id, 'delete_team', `delete team "${team.name}"`);
|
|
1458
|
+
const approvals = await api.listApprovals(team.team_id);
|
|
1459
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1460
|
+
showToast('Approval requested', 'info');
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
showToast('Failed to request approval', 'error');
|
|
1464
|
+
}
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Deleting team' });
|
|
1470
|
+
await api.deleteTeam(teamForDelete.team_id);
|
|
1471
|
+
const me = await api.me();
|
|
1472
|
+
dispatch({ type: 'SET_USER_DATA', me });
|
|
1473
|
+
dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
|
|
1474
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1475
|
+
const newProjectId = me.user.active_project_id;
|
|
1476
|
+
if (newProjectId) {
|
|
1477
|
+
connectFeed(newProjectId);
|
|
1478
|
+
await loadTabData();
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
|
|
1482
|
+
dispatch({ type: 'SET_MEMBERS', members: [] });
|
|
1483
|
+
}
|
|
1484
|
+
showToast('Team deleted', 'success');
|
|
1485
|
+
}
|
|
1486
|
+
catch {
|
|
1487
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1488
|
+
showToast('Failed to delete team', 'error');
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1014
1493
|
}
|
|
1494
|
+
return false;
|
|
1015
1495
|
}
|
|
1016
1496
|
// ---- Small terminal guard ----
|
|
1017
|
-
if (cols <
|
|
1497
|
+
if (cols < 80 || rows < 20) {
|
|
1018
1498
|
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" })] }));
|
|
1019
1499
|
}
|
|
1020
1500
|
// ---- Determine content height ----
|
|
1021
1501
|
// Header + divider + (banner + divider?) + tabbar + divider = top
|
|
1022
1502
|
// toast? + operation? + divider + footer = bottom
|
|
1023
|
-
const hasBanners = state.notifications.some(n => !n.seen) || hasBillingBanner(state) || state.firstLogin || !!state.joinedProjectName;
|
|
1503
|
+
const hasBanners = state.notifications.some(n => !n.seen) || hasBillingBanner(state) || state.firstLogin || !!state.joinedProjectName || state.pendingApprovals.some(a => new Date(a.expires_at) > new Date());
|
|
1024
1504
|
const topLines = 2 + (hasBanners ? 2 : 0) + 2; // header+div, (banner+div), tabbar+div
|
|
1025
1505
|
const bottomLines = (state.toast ? 1 : 0) + (state.operationPending ? 1 : 0) + 2; // div + footer
|
|
1026
1506
|
const contentHeight = Math.max(1, rows - topLines - bottomLines);
|