cohvu 2.2.9 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/instructions.d.ts +2 -2
- package/dist/instructions.js +5 -3
- package/dist/instructions.js.map +1 -1
- package/dist/tui/App.js +506 -31
- 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/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/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 +3 -0
- package/dist/tui/tabs/KnowledgeTab.js.map +1 -1
- package/dist/tui/tabs/TeamTab.js +16 -3
- package/dist/tui/tabs/TeamTab.js.map +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -19,7 +19,7 @@ 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
24
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
@@ -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);
|
|
@@ -124,6 +130,14 @@ export default function App() {
|
|
|
124
130
|
}
|
|
125
131
|
});
|
|
126
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
|
+
}
|
|
127
141
|
},
|
|
128
142
|
onConnected: () => {
|
|
129
143
|
dispatch({ type: 'SET_SSE_CONNECTED', connected: true });
|
|
@@ -163,6 +177,8 @@ export default function App() {
|
|
|
163
177
|
]);
|
|
164
178
|
dispatch({ type: 'SET_MEMBERS', members });
|
|
165
179
|
dispatch({ type: 'SET_INVITE_LINKS', links });
|
|
180
|
+
const approvals = await api.listApprovals(activeTeam.team_id).catch(() => []);
|
|
181
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
166
182
|
}
|
|
167
183
|
else {
|
|
168
184
|
dispatch({ type: 'SET_MEMBERS', members: [] });
|
|
@@ -231,9 +247,7 @@ export default function App() {
|
|
|
231
247
|
if (cancelled)
|
|
232
248
|
return;
|
|
233
249
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
234
|
-
|
|
235
|
-
if (cancelled)
|
|
236
|
-
return;
|
|
250
|
+
// Detect platforms without re-running setup (setup already ran in enterDashboard)
|
|
237
251
|
const platforms = detectPlatformStatuses();
|
|
238
252
|
dispatch({ type: 'SET_PLATFORMS', platforms });
|
|
239
253
|
const flatProjects = deriveFlatProjectsFromMe(me);
|
|
@@ -268,6 +282,16 @@ export default function App() {
|
|
|
268
282
|
dispatch({ type: 'SET_BILLING', billing });
|
|
269
283
|
dispatch({ type: 'SET_INVITE_LINKS', links: inviteLinks });
|
|
270
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
|
+
}
|
|
271
295
|
if (notifications.length > 0)
|
|
272
296
|
api.markNotificationsSeen().catch(() => { });
|
|
273
297
|
connectFeed(projectId);
|
|
@@ -362,17 +386,48 @@ export default function App() {
|
|
|
362
386
|
await handleModalKey(input, key);
|
|
363
387
|
return;
|
|
364
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
|
+
}
|
|
365
420
|
// Tab switching
|
|
366
421
|
if (key.tab) {
|
|
367
422
|
dispatch({ type: 'NEXT_TAB' });
|
|
368
|
-
|
|
423
|
+
setTimeout(() => loadTabData(), 0);
|
|
369
424
|
return;
|
|
370
425
|
}
|
|
371
426
|
// Number keys switch tabs
|
|
372
427
|
const tabIdx = parseInt(input, 10);
|
|
373
428
|
if (tabIdx >= 1 && tabIdx <= 5 && !key.ctrl && !key.meta) {
|
|
374
429
|
dispatch({ type: 'SWITCH_TAB', tab: TABS[tabIdx - 1] });
|
|
375
|
-
|
|
430
|
+
setTimeout(() => loadTabData(), 0);
|
|
376
431
|
return;
|
|
377
432
|
}
|
|
378
433
|
// Global 'b' shortcut — subscribe/billing portal from banner
|
|
@@ -514,11 +569,12 @@ export default function App() {
|
|
|
514
569
|
return;
|
|
515
570
|
const memberCount = s.members.length;
|
|
516
571
|
const linkRoles = ['admin', 'member', 'viewer'];
|
|
517
|
-
|
|
518
|
-
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;
|
|
519
575
|
const sel = s.teamSelected;
|
|
520
|
-
const onLinkRow = s.userRole === 'admin' && sel >= memberCount;
|
|
521
576
|
const onMemberRow = sel < memberCount;
|
|
577
|
+
const onLinkRow = s.userRole === 'admin' && sel >= memberCount + 3;
|
|
522
578
|
const team = getActiveTeam(s);
|
|
523
579
|
if (key.upArrow) {
|
|
524
580
|
dispatch({ type: 'SET_TEAM_SELECTED', index: Math.max(0, sel - 1) });
|
|
@@ -528,8 +584,54 @@ export default function App() {
|
|
|
528
584
|
dispatch({ type: 'SET_TEAM_SELECTED', index: Math.min(totalRows - 1, sel + 1) });
|
|
529
585
|
return;
|
|
530
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
|
|
531
633
|
if (input === 'c' && s.userRole === 'admin' && onLinkRow) {
|
|
532
|
-
const linkIdx = sel - memberCount;
|
|
634
|
+
const linkIdx = sel - memberCount - 3;
|
|
533
635
|
const role = linkRoles[linkIdx];
|
|
534
636
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
535
637
|
if (link) {
|
|
@@ -541,12 +643,23 @@ export default function App() {
|
|
|
541
643
|
}
|
|
542
644
|
return;
|
|
543
645
|
}
|
|
544
|
-
|
|
545
|
-
|
|
646
|
+
// 'o' key on invite link row — open in browser
|
|
647
|
+
if (input === 'o' && s.userRole === 'admin' && onLinkRow) {
|
|
648
|
+
const linkIdx = sel - memberCount - 3;
|
|
546
649
|
const role = linkRoles[linkIdx];
|
|
547
|
-
|
|
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
|
+
}
|
|
548
660
|
return;
|
|
549
661
|
}
|
|
662
|
+
// 'e' key on member row — edit role
|
|
550
663
|
if (input === 'e' && s.userRole === 'admin' && onMemberRow) {
|
|
551
664
|
const target = s.members[sel];
|
|
552
665
|
if (target) {
|
|
@@ -565,6 +678,7 @@ export default function App() {
|
|
|
565
678
|
}
|
|
566
679
|
return;
|
|
567
680
|
}
|
|
681
|
+
// 'x' key — remove member or leave
|
|
568
682
|
if (input === 'x') {
|
|
569
683
|
if (s.userRole === 'admin' && onMemberRow && team) {
|
|
570
684
|
const target = s.members[sel];
|
|
@@ -637,11 +751,20 @@ export default function App() {
|
|
|
637
751
|
// ---- Project keys ----
|
|
638
752
|
async function handleProjectKey(input, _key) {
|
|
639
753
|
const s = stateRef.current;
|
|
754
|
+
if (input === 't') {
|
|
755
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-team', input: '' } });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
640
758
|
if (input === 'r' && s.userRole === 'admin') {
|
|
641
759
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'rename', input: '' } });
|
|
642
760
|
}
|
|
643
761
|
else if (input === 'n') {
|
|
644
|
-
|
|
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
|
+
}
|
|
645
768
|
}
|
|
646
769
|
else if (input === 'w' && s.projects.length > 1) {
|
|
647
770
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'switch-project', selected: 0 } });
|
|
@@ -756,8 +879,9 @@ export default function App() {
|
|
|
756
879
|
// Text input modals
|
|
757
880
|
if ('input' in modal) {
|
|
758
881
|
if (key.return) {
|
|
759
|
-
await confirmModal(modal);
|
|
760
|
-
|
|
882
|
+
const chained = await confirmModal(modal);
|
|
883
|
+
if (!chained)
|
|
884
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
761
885
|
}
|
|
762
886
|
else if (key.backspace) {
|
|
763
887
|
dispatch({ type: 'MODAL_BACKSPACE' });
|
|
@@ -781,19 +905,25 @@ export default function App() {
|
|
|
781
905
|
else if (key.return) {
|
|
782
906
|
if (modal.selected === s.projects.length) {
|
|
783
907
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
784
|
-
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '' } });
|
|
908
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-project', input: '', teamId: null } });
|
|
785
909
|
}
|
|
786
910
|
else {
|
|
787
911
|
const project = s.projects[modal.selected];
|
|
788
912
|
if (project) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
+
}
|
|
797
927
|
}
|
|
798
928
|
}
|
|
799
929
|
}
|
|
@@ -819,6 +949,23 @@ export default function App() {
|
|
|
819
949
|
return;
|
|
820
950
|
}
|
|
821
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
|
+
}
|
|
822
969
|
const changeTeam = getActiveTeam(s);
|
|
823
970
|
if (changeTeam) {
|
|
824
971
|
try {
|
|
@@ -835,6 +982,175 @@ export default function App() {
|
|
|
835
982
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
836
983
|
}
|
|
837
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
|
+
}
|
|
838
1154
|
}
|
|
839
1155
|
// ---- Modal confirmation ----
|
|
840
1156
|
async function confirmModal(modal) {
|
|
@@ -847,13 +1163,34 @@ export default function App() {
|
|
|
847
1163
|
unlinkSync(credentialsFile);
|
|
848
1164
|
}
|
|
849
1165
|
catch { }
|
|
850
|
-
|
|
851
|
-
|
|
1166
|
+
exit();
|
|
1167
|
+
return false;
|
|
852
1168
|
}
|
|
853
1169
|
const projectId = s.activeProjectId;
|
|
854
1170
|
if (!projectId)
|
|
855
|
-
return;
|
|
1171
|
+
return false;
|
|
856
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
|
+
}
|
|
857
1194
|
case 'confirm-forget':
|
|
858
1195
|
try {
|
|
859
1196
|
dispatch({ type: 'SET_OPERATION', operation: 'Removing memory' });
|
|
@@ -871,6 +1208,21 @@ export default function App() {
|
|
|
871
1208
|
case 'confirm-clear': {
|
|
872
1209
|
const project = getActiveProject(s);
|
|
873
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
|
+
}
|
|
874
1226
|
try {
|
|
875
1227
|
dispatch({ type: 'SET_OPERATION', operation: 'Clearing memories' });
|
|
876
1228
|
await api.clearMemories(projectId);
|
|
@@ -888,6 +1240,21 @@ export default function App() {
|
|
|
888
1240
|
case 'confirm-delete': {
|
|
889
1241
|
const project = getActiveProject(s);
|
|
890
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
|
+
}
|
|
891
1258
|
try {
|
|
892
1259
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting project' });
|
|
893
1260
|
await api.deleteProject(projectId);
|
|
@@ -984,7 +1351,9 @@ export default function App() {
|
|
|
984
1351
|
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
985
1352
|
try {
|
|
986
1353
|
dispatch({ type: 'SET_OPERATION', operation: 'Creating project' });
|
|
987
|
-
const project =
|
|
1354
|
+
const project = modal.teamId
|
|
1355
|
+
? await api.createTeamProject(modal.teamId, modal.input, slug)
|
|
1356
|
+
: await api.createProject(modal.input, slug);
|
|
988
1357
|
await api.switchProject(project.id);
|
|
989
1358
|
const me = await api.me();
|
|
990
1359
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
@@ -998,6 +1367,47 @@ export default function App() {
|
|
|
998
1367
|
}
|
|
999
1368
|
break;
|
|
1000
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
|
+
}
|
|
1001
1411
|
case 'confirm-regen-link': {
|
|
1002
1412
|
const teamForRegen = getActiveTeam(s);
|
|
1003
1413
|
if (teamForRegen) {
|
|
@@ -1016,7 +1426,72 @@ export default function App() {
|
|
|
1016
1426
|
}
|
|
1017
1427
|
break;
|
|
1018
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
|
+
}
|
|
1019
1493
|
}
|
|
1494
|
+
return false;
|
|
1020
1495
|
}
|
|
1021
1496
|
// ---- Small terminal guard ----
|
|
1022
1497
|
if (cols < 80 || rows < 20) {
|
|
@@ -1025,7 +1500,7 @@ export default function App() {
|
|
|
1025
1500
|
// ---- Determine content height ----
|
|
1026
1501
|
// Header + divider + (banner + divider?) + tabbar + divider = top
|
|
1027
1502
|
// toast? + operation? + divider + footer = bottom
|
|
1028
|
-
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());
|
|
1029
1504
|
const topLines = 2 + (hasBanners ? 2 : 0) + 2; // header+div, (banner+div), tabbar+div
|
|
1030
1505
|
const bottomLines = (state.toast ? 1 : 0) + (state.operationPending ? 1 : 0) + 2; // div + footer
|
|
1031
1506
|
const contentHeight = Math.max(1, rows - topLines - bottomLines);
|