cohvu 2.16.0 → 2.17.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 +65 -17
- package/dist/api.js +23 -8
- package/dist/api.js.map +1 -1
- package/dist/tui/App.js +309 -144
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/LockCard.d.ts +17 -0
- package/dist/tui/components/LockCard.js +37 -0
- package/dist/tui/components/LockCard.js.map +1 -0
- package/dist/tui/components/Modal.js +31 -23
- package/dist/tui/components/Modal.js.map +1 -1
- package/dist/tui/state.d.ts +38 -11
- package/dist/tui/state.js +46 -3
- package/dist/tui/state.js.map +1 -1
- package/dist/tui/tabs/KeysTab.js +50 -11
- package/dist/tui/tabs/KeysTab.js.map +1 -1
- package/dist/tui/tabs/KnowledgeTab.js +10 -0
- package/dist/tui/tabs/KnowledgeTab.js.map +1 -1
- package/dist/tui/tabs/TeamTab.js +10 -3
- package/dist/tui/tabs/TeamTab.js.map +1 -1
- package/package.json +1 -1
package/dist/tui/App.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Direct translation of dashboard.ts into React hooks.
|
|
4
4
|
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
|
5
5
|
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
6
|
-
import { reduce, initialState, TABS, getActiveProject, getActiveTeam } from './state.js';
|
|
6
|
+
import { reduce, initialState, TABS, getActiveProject, getActiveTeam, isTeamLocked, filterToSameEntity } from './state.js';
|
|
7
7
|
import { ApiClient } from '../api.js';
|
|
8
8
|
import { runSetup } from '../setup.js';
|
|
9
9
|
import { detectPlatformStatuses } from './platform-detect.js';
|
|
@@ -20,7 +20,7 @@ import { BillingTab } from './tabs/BillingTab.js';
|
|
|
20
20
|
import { ProjectTab } from './tabs/ProjectTab.js';
|
|
21
21
|
import { KeysTab } from './tabs/KeysTab.js';
|
|
22
22
|
import { YouTab } from './tabs/YouTab.js';
|
|
23
|
-
import {
|
|
23
|
+
import { ALL_KEY_ACTIONS } from './state.js';
|
|
24
24
|
import { timeUntil } from './utils.js';
|
|
25
25
|
import { exec, execFile } from 'child_process';
|
|
26
26
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
|
|
@@ -76,12 +76,6 @@ export default function App() {
|
|
|
76
76
|
execFile(cmd, [url], () => { });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
function shouldRequireConsensus(s) {
|
|
80
|
-
if (!s.requireConsensus)
|
|
81
|
-
return false;
|
|
82
|
-
const admins = s.members.filter(m => m.role === 'admin');
|
|
83
|
-
return admins.length >= 2;
|
|
84
|
-
}
|
|
85
79
|
function copyToClipboard(text) {
|
|
86
80
|
const cmd = process.platform === 'darwin' ? 'pbcopy'
|
|
87
81
|
: process.platform === 'win32' ? 'clip' : 'xclip -selection clipboard';
|
|
@@ -278,6 +272,23 @@ export default function App() {
|
|
|
278
272
|
}
|
|
279
273
|
catch { }
|
|
280
274
|
}, 30_000);
|
|
275
|
+
// Team overview powers the lock card when a team sub is canceled. It's
|
|
276
|
+
// cheap and always reflects the current team's shape — fetch on every
|
|
277
|
+
// project switch into a team project, clear when switching to personal.
|
|
278
|
+
const project = stateRef.current.projects.find(p => p.project_id === projectId);
|
|
279
|
+
if (project?.owner.kind === 'team') {
|
|
280
|
+
const teamId = project.owner.teamId;
|
|
281
|
+
api.getTeamOverview(teamId)
|
|
282
|
+
.then(overview => {
|
|
283
|
+
if (stateRef.current.activeProjectId === projectId) {
|
|
284
|
+
dispatch({ type: 'SET_TEAM_OVERVIEW', overview });
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
.catch(() => { });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
dispatch({ type: 'SET_TEAM_OVERVIEW', overview: null });
|
|
291
|
+
}
|
|
281
292
|
setTimeout(() => loadTabData(), 0);
|
|
282
293
|
}, [api, connectFeed, dispatch, loadTabData]);
|
|
283
294
|
const executeSearch = useCallback(async () => {
|
|
@@ -482,6 +493,43 @@ export default function App() {
|
|
|
482
493
|
await handleModalKey(input, key);
|
|
483
494
|
return;
|
|
484
495
|
}
|
|
496
|
+
// Team sub canceled — admins can hit `r` from any team tab to start
|
|
497
|
+
// resubscribe checkout. Members just see the contact list in the lock
|
|
498
|
+
// card; `r` does nothing for them.
|
|
499
|
+
if (isTeamLocked(s) && input === 'r' && s.userRole === 'admin') {
|
|
500
|
+
const team = getActiveTeam(s);
|
|
501
|
+
if (team) {
|
|
502
|
+
try {
|
|
503
|
+
showToast('Opening checkout...', 'info');
|
|
504
|
+
const c = await api.createTeamCheckout(team.team_id);
|
|
505
|
+
if (c.checkout_url)
|
|
506
|
+
openBrowser(c.checkout_url);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
showToast('Failed to open checkout', 'error');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Pro canceled with locked personal project in view — `u` opens Pro
|
|
515
|
+
// checkout. One checkout unlocks every personal project at once, no
|
|
516
|
+
// per-project choice. Integration keys stay disabled and need manual
|
|
517
|
+
// restore after.
|
|
518
|
+
if (input === 'u' && !s.modal) {
|
|
519
|
+
const project = getActiveProject(s);
|
|
520
|
+
if (project?.owner.kind === 'personal' && project.locked) {
|
|
521
|
+
try {
|
|
522
|
+
showToast('Opening checkout...', 'info');
|
|
523
|
+
const c = await api.createIndividualCheckout();
|
|
524
|
+
if (c.checkout_url)
|
|
525
|
+
openBrowser(c.checkout_url);
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
showToast('Failed to open checkout', 'error');
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
485
533
|
// Approval keys (when approvals exist, on team/project tab, team project)
|
|
486
534
|
if ((input === 'a' || input === 'x') && !s.modal && s.pendingApprovals.length > 0 && (s.tab === 'team' || s.tab === 'project')) {
|
|
487
535
|
const activeProject = getActiveProject(s);
|
|
@@ -492,7 +540,7 @@ export default function App() {
|
|
|
492
540
|
kind: 'approve-action',
|
|
493
541
|
approvalId: approval.id,
|
|
494
542
|
description: approval.description,
|
|
495
|
-
initiator: approval.initiator_email,
|
|
543
|
+
initiator: approval.initiator_email ?? approval.initiator_name ?? 'unknown',
|
|
496
544
|
expiresIn: timeUntil(approval.expires_at),
|
|
497
545
|
} });
|
|
498
546
|
}
|
|
@@ -567,21 +615,22 @@ export default function App() {
|
|
|
567
615
|
}
|
|
568
616
|
if (input === 'n' && s.userRole === 'admin') {
|
|
569
617
|
if (!hasPaidPlan(s)) {
|
|
570
|
-
showToast('
|
|
618
|
+
showToast('Keys require a paid plan', 'error');
|
|
571
619
|
return;
|
|
572
620
|
}
|
|
573
|
-
const adminProjects = adminScopableProjects(s);
|
|
574
621
|
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
575
622
|
kind: 'create-integration-key',
|
|
576
623
|
step: 'name',
|
|
577
624
|
input: '',
|
|
578
|
-
projectId: adminProjects[0]?.project_id ?? '',
|
|
579
|
-
projectSelectedIdx: 0,
|
|
580
|
-
role: 'member',
|
|
581
|
-
roleSelectedIdx: 1,
|
|
582
|
-
allowedTools: [],
|
|
583
|
-
toolsSelectedIdx: 0,
|
|
584
625
|
agentName: '',
|
|
626
|
+
projectSelectedIdx: 0,
|
|
627
|
+
selectedProjectIds: new Set(),
|
|
628
|
+
projectScopes: new Map(),
|
|
629
|
+
permProjectIdx: 0,
|
|
630
|
+
permActionIdx: 0,
|
|
631
|
+
limitsField: 'expiry',
|
|
632
|
+
expiresInDays: '',
|
|
633
|
+
opsLimit: '',
|
|
585
634
|
} });
|
|
586
635
|
return;
|
|
587
636
|
}
|
|
@@ -592,6 +641,37 @@ export default function App() {
|
|
|
592
641
|
}
|
|
593
642
|
return;
|
|
594
643
|
}
|
|
644
|
+
// Restore a disabled integration key. Only valid when the selected key
|
|
645
|
+
// is disabled — otherwise `r` is a no-op here. Server rotates the
|
|
646
|
+
// secret and returns it once; TUI shows the "new secret" modal so the
|
|
647
|
+
// admin can copy it into their external systems.
|
|
648
|
+
if (input === 'r' && s.userRole === 'admin' && s.apiKeys.length > 0) {
|
|
649
|
+
const target = s.apiKeys[s.apiKeysSelected];
|
|
650
|
+
if (target && target.disabled_at) {
|
|
651
|
+
try {
|
|
652
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Restoring key' });
|
|
653
|
+
const { api_key, key } = await api.restoreApiKey(target.id);
|
|
654
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
655
|
+
dispatch({ type: 'OPEN_MODAL', modal: {
|
|
656
|
+
kind: 'key-created',
|
|
657
|
+
keyId: api_key.id,
|
|
658
|
+
keyValue: key,
|
|
659
|
+
keyName: api_key.name,
|
|
660
|
+
kind2: 'integration',
|
|
661
|
+
} });
|
|
662
|
+
// Refresh the list so the restored key moves out of the disabled section.
|
|
663
|
+
if (s.activeProjectId) {
|
|
664
|
+
const keys = await api.listIntegrationKeys(s.activeProjectId);
|
|
665
|
+
dispatch({ type: 'SET_API_KEYS', keys });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
670
|
+
showToast('Failed to restore key', 'error');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
595
675
|
}
|
|
596
676
|
// ---- Knowledge keys ----
|
|
597
677
|
async function handleKnowledgeKey(input, key) {
|
|
@@ -746,10 +826,22 @@ export default function App() {
|
|
|
746
826
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-rename-team', input: '' } });
|
|
747
827
|
return;
|
|
748
828
|
}
|
|
749
|
-
// 'r' key on invite link row — regen
|
|
829
|
+
// 'r' key on invite link row — regen (member/viewer) or mint-new (admin).
|
|
830
|
+
// Admin regen is semantically the same as mint since each admin link is
|
|
831
|
+
// single-use; we just mint and surface instead of asking for confirmation.
|
|
750
832
|
if (input === 'r' && s.userRole === 'admin' && onLinkRow) {
|
|
751
833
|
const linkIdx = sel - memberCount - 3;
|
|
752
834
|
const role = linkRoles[linkIdx];
|
|
835
|
+
if (role === 'admin' && team) {
|
|
836
|
+
try {
|
|
837
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
838
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role: 'admin', url: link.url } });
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
showToast('Failed to mint admin invite', 'error');
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
753
845
|
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-regen-link', role } });
|
|
754
846
|
return;
|
|
755
847
|
}
|
|
@@ -781,10 +873,13 @@ export default function App() {
|
|
|
781
873
|
}
|
|
782
874
|
return;
|
|
783
875
|
}
|
|
784
|
-
// 'c' key on invite link row — copy link
|
|
876
|
+
// 'c' key on invite link row — copy link. Admin row has no persistent
|
|
877
|
+
// URL (admin invites are minted on demand) so 'c' is a no-op there.
|
|
785
878
|
if (input === 'c' && s.userRole === 'admin' && onLinkRow) {
|
|
786
879
|
const linkIdx = sel - memberCount - 3;
|
|
787
880
|
const role = linkRoles[linkIdx];
|
|
881
|
+
if (role === 'admin')
|
|
882
|
+
return;
|
|
788
883
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
789
884
|
if (link) {
|
|
790
885
|
copyToClipboard(link.url);
|
|
@@ -795,15 +890,34 @@ export default function App() {
|
|
|
795
890
|
}
|
|
796
891
|
return;
|
|
797
892
|
}
|
|
798
|
-
// 'o' key on invite link row — open in browser
|
|
893
|
+
// 'o' key on invite link row — open in browser. Same as 'c', admin row
|
|
894
|
+
// has nothing to open since the link doesn't exist until minted.
|
|
799
895
|
if (input === 'o' && s.userRole === 'admin' && onLinkRow) {
|
|
800
896
|
const linkIdx = sel - memberCount - 3;
|
|
801
897
|
const role = linkRoles[linkIdx];
|
|
898
|
+
if (role === 'admin')
|
|
899
|
+
return;
|
|
802
900
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
803
901
|
if (link)
|
|
804
902
|
openBrowser(link.url);
|
|
805
903
|
return;
|
|
806
904
|
}
|
|
905
|
+
// Enter on the admin invite row — mint a fresh short-lived single-use
|
|
906
|
+
// link and surface it via the invite-link modal (copy/open affordances).
|
|
907
|
+
if (key.return && s.userRole === 'admin' && onLinkRow) {
|
|
908
|
+
const linkIdx = sel - memberCount - 3;
|
|
909
|
+
const role = linkRoles[linkIdx];
|
|
910
|
+
if (role === 'admin' && team) {
|
|
911
|
+
try {
|
|
912
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
913
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role: 'admin', url: link.url } });
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
showToast('Failed to mint admin invite', 'error');
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
807
921
|
// 'd' key (admin) — delete team
|
|
808
922
|
if (input === 'd' && s.userRole === 'admin') {
|
|
809
923
|
if (team) {
|
|
@@ -1032,14 +1146,13 @@ export default function App() {
|
|
|
1032
1146
|
}
|
|
1033
1147
|
// Integration key creation wizard
|
|
1034
1148
|
if (modal.kind === 'create-integration-key') {
|
|
1035
|
-
|
|
1149
|
+
// Silo rule: once one project is selected, only same-entity projects
|
|
1150
|
+
// are reachable. Filter here so index arithmetic in the picker stays
|
|
1151
|
+
// consistent with what the Modal renders.
|
|
1152
|
+
const adminProjects = filterToSameEntity(adminScopableProjects(s), modal.selectedProjectIds, s.user?.id ?? null);
|
|
1036
1153
|
if (modal.step === 'name') {
|
|
1037
1154
|
if (key.return && modal.input.trim().length > 0) {
|
|
1038
|
-
|
|
1039
|
-
showInlineError('no projects you can scope an integration key to', 3000);
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'project' } });
|
|
1155
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'agent' } });
|
|
1043
1156
|
}
|
|
1044
1157
|
else if (key.backspace || key.delete) {
|
|
1045
1158
|
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input.slice(0, -1) } });
|
|
@@ -1052,75 +1165,131 @@ export default function App() {
|
|
|
1052
1165
|
}
|
|
1053
1166
|
return;
|
|
1054
1167
|
}
|
|
1055
|
-
if (modal.step === '
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1168
|
+
if (modal.step === 'agent') {
|
|
1169
|
+
if (key.return) {
|
|
1170
|
+
if (adminProjects.length === 0) {
|
|
1171
|
+
showInlineError('no projects you can scope a key to', 3000);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'projects' } });
|
|
1061
1175
|
}
|
|
1062
|
-
else if (key.
|
|
1063
|
-
|
|
1064
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: i, projectId: adminProjects[i].project_id } });
|
|
1176
|
+
else if (key.backspace || key.delete) {
|
|
1177
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName.slice(0, -1) } });
|
|
1065
1178
|
}
|
|
1066
|
-
else if (
|
|
1067
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal,
|
|
1179
|
+
else if (input === ' ') {
|
|
1180
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + ' ' } });
|
|
1181
|
+
}
|
|
1182
|
+
else if (input.length === 1 && !key.ctrl && !key.meta) {
|
|
1183
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + input } });
|
|
1068
1184
|
}
|
|
1069
1185
|
return;
|
|
1070
1186
|
}
|
|
1071
|
-
if (modal.step === '
|
|
1072
|
-
|
|
1187
|
+
if (modal.step === 'projects') {
|
|
1188
|
+
if (adminProjects.length === 0)
|
|
1189
|
+
return;
|
|
1073
1190
|
if (key.upArrow) {
|
|
1074
|
-
|
|
1075
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
|
|
1191
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: Math.max(0, modal.projectSelectedIdx - 1) } });
|
|
1076
1192
|
}
|
|
1077
1193
|
else if (key.downArrow) {
|
|
1078
|
-
|
|
1079
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
|
|
1194
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: Math.min(adminProjects.length - 1, modal.projectSelectedIdx + 1) } });
|
|
1080
1195
|
}
|
|
1081
|
-
else if (
|
|
1082
|
-
|
|
1196
|
+
else if (input === ' ') {
|
|
1197
|
+
const p = adminProjects[modal.projectSelectedIdx];
|
|
1198
|
+
if (p) {
|
|
1199
|
+
const next = new Set(modal.selectedProjectIds);
|
|
1200
|
+
if (next.has(p.project_id))
|
|
1201
|
+
next.delete(p.project_id);
|
|
1202
|
+
else
|
|
1203
|
+
next.add(p.project_id);
|
|
1204
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selectedProjectIds: next } });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else if (key.return && modal.selectedProjectIds.size > 0) {
|
|
1208
|
+
// Initialize default scopes for new projects (all actions on)
|
|
1209
|
+
const scopes = new Map(modal.projectScopes);
|
|
1210
|
+
for (const pid of modal.selectedProjectIds) {
|
|
1211
|
+
if (!scopes.has(pid)) {
|
|
1212
|
+
scopes.set(pid, new Set([...ALL_KEY_ACTIONS]));
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Remove scopes for deselected projects
|
|
1216
|
+
for (const pid of scopes.keys()) {
|
|
1217
|
+
if (!modal.selectedProjectIds.has(pid))
|
|
1218
|
+
scopes.delete(pid);
|
|
1219
|
+
}
|
|
1220
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'permissions', projectScopes: scopes, permProjectIdx: 0, permActionIdx: 0 } });
|
|
1083
1221
|
}
|
|
1084
1222
|
return;
|
|
1085
1223
|
}
|
|
1086
|
-
if (modal.step === '
|
|
1087
|
-
|
|
1088
|
-
|
|
1224
|
+
if (modal.step === 'permissions') {
|
|
1225
|
+
const selectedProjects = adminProjects.filter(p => modal.selectedProjectIds.has(p.project_id));
|
|
1226
|
+
const projectCount = selectedProjects.length;
|
|
1227
|
+
if (projectCount === 0)
|
|
1228
|
+
return;
|
|
1229
|
+
if (key.tab) {
|
|
1230
|
+
// tab cycles through projects
|
|
1231
|
+
const next = (modal.permProjectIdx + 1) % projectCount;
|
|
1232
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, permProjectIdx: next, permActionIdx: 0 } });
|
|
1233
|
+
}
|
|
1234
|
+
else if (key.upArrow) {
|
|
1235
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, permProjectIdx: Math.max(0, modal.permProjectIdx - 1), permActionIdx: 0 } });
|
|
1089
1236
|
}
|
|
1090
1237
|
else if (key.downArrow) {
|
|
1091
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal,
|
|
1238
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, permProjectIdx: Math.min(projectCount - 1, modal.permProjectIdx + 1), permActionIdx: 0 } });
|
|
1239
|
+
}
|
|
1240
|
+
else if (key.leftArrow) {
|
|
1241
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, permActionIdx: Math.max(0, modal.permActionIdx - 1) } });
|
|
1242
|
+
}
|
|
1243
|
+
else if (key.rightArrow) {
|
|
1244
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, permActionIdx: Math.min(ALL_KEY_ACTIONS.length - 1, modal.permActionIdx + 1) } });
|
|
1092
1245
|
}
|
|
1093
1246
|
else if (input === ' ') {
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1247
|
+
const p = selectedProjects[modal.permProjectIdx];
|
|
1248
|
+
if (p) {
|
|
1249
|
+
const action = ALL_KEY_ACTIONS[modal.permActionIdx];
|
|
1250
|
+
const scopes = new Map(modal.projectScopes);
|
|
1251
|
+
const actions = new Set(scopes.get(p.project_id) ?? []);
|
|
1252
|
+
if (actions.has(action))
|
|
1253
|
+
actions.delete(action);
|
|
1254
|
+
else
|
|
1255
|
+
actions.add(action);
|
|
1256
|
+
scopes.set(p.project_id, actions);
|
|
1257
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectScopes: scopes } });
|
|
1258
|
+
}
|
|
1099
1259
|
}
|
|
1100
1260
|
else if (key.return) {
|
|
1101
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: '
|
|
1261
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'limits' } });
|
|
1102
1262
|
}
|
|
1103
1263
|
return;
|
|
1104
1264
|
}
|
|
1105
|
-
if (modal.step === '
|
|
1265
|
+
if (modal.step === 'limits') {
|
|
1106
1266
|
if (key.return) {
|
|
1107
1267
|
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'confirm' } });
|
|
1108
1268
|
}
|
|
1109
|
-
else if (key.
|
|
1110
|
-
dispatch({ type: 'OPEN_MODAL', modal: { ...modal,
|
|
1269
|
+
else if (key.tab) {
|
|
1270
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, limitsField: modal.limitsField === 'expiry' ? 'ops' : 'expiry' } });
|
|
1111
1271
|
}
|
|
1112
|
-
else if (
|
|
1113
|
-
|
|
1272
|
+
else if (key.backspace || key.delete) {
|
|
1273
|
+
if (modal.limitsField === 'expiry') {
|
|
1274
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, expiresInDays: modal.expiresInDays.slice(0, -1) } });
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, opsLimit: modal.opsLimit.slice(0, -1) } });
|
|
1278
|
+
}
|
|
1114
1279
|
}
|
|
1115
|
-
else if (input
|
|
1116
|
-
|
|
1280
|
+
else if (/^[0-9]$/.test(input)) {
|
|
1281
|
+
if (modal.limitsField === 'expiry') {
|
|
1282
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, expiresInDays: modal.expiresInDays + input } });
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
dispatch({ type: 'OPEN_MODAL', modal: { ...modal, opsLimit: modal.opsLimit + input } });
|
|
1286
|
+
}
|
|
1117
1287
|
}
|
|
1118
1288
|
return;
|
|
1119
1289
|
}
|
|
1120
1290
|
if (modal.step === 'confirm') {
|
|
1121
1291
|
if (input === 'y') {
|
|
1122
1292
|
await confirmModal(modal);
|
|
1123
|
-
// confirmModal handles the modal transition (closes on error, opens key-created on success)
|
|
1124
1293
|
}
|
|
1125
1294
|
else if (input === 'n') {
|
|
1126
1295
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
@@ -1212,30 +1381,23 @@ export default function App() {
|
|
|
1212
1381
|
return;
|
|
1213
1382
|
}
|
|
1214
1383
|
}
|
|
1215
|
-
if (shouldRequireConsensus(stateRef.current) && modal.currentRole === 'admin' && newRole !== 'admin') {
|
|
1216
|
-
const team = getActiveTeam(stateRef.current);
|
|
1217
|
-
if (team) {
|
|
1218
|
-
try {
|
|
1219
|
-
await api.initiateApproval(team.team_id, 'demote_admin', `demote ${modal.targetEmail} from admin`, modal.targetUserId);
|
|
1220
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1221
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1222
|
-
dispatch({ type: 'CLOSE_MODAL' });
|
|
1223
|
-
showToast('Approval requested', 'info');
|
|
1224
|
-
}
|
|
1225
|
-
catch {
|
|
1226
|
-
showToast('Failed to request approval', 'error');
|
|
1227
|
-
dispatch({ type: 'CLOSE_MODAL' });
|
|
1228
|
-
}
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
1384
|
const changeTeam = getActiveTeam(s);
|
|
1233
1385
|
if (changeTeam) {
|
|
1234
1386
|
try {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1387
|
+
// Server decides whether consensus applies — if so, it returns
|
|
1388
|
+
// 202 with a pending_approval object; otherwise the role change
|
|
1389
|
+
// applies immediately. We don't branch client-side.
|
|
1390
|
+
const resp = await api.changeTeamRole(changeTeam.team_id, modal.targetUserId, newRole);
|
|
1391
|
+
if (resp.pending_approval) {
|
|
1392
|
+
const approvals = await api.listApprovals(changeTeam.team_id);
|
|
1393
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1394
|
+
showToast('Role change proposed — waiting for majority', 'info');
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
const members = await api.listTeamMembers(changeTeam.team_id);
|
|
1398
|
+
dispatch({ type: 'SET_MEMBERS', members });
|
|
1399
|
+
showToast('Role updated', 'success');
|
|
1400
|
+
}
|
|
1239
1401
|
}
|
|
1240
1402
|
catch {
|
|
1241
1403
|
showToast('Failed to update role', 'error');
|
|
@@ -1283,6 +1445,26 @@ export default function App() {
|
|
|
1283
1445
|
}
|
|
1284
1446
|
else if (key.return) {
|
|
1285
1447
|
const role = roles[modal.selected];
|
|
1448
|
+
const team = getActiveTeam(s);
|
|
1449
|
+
if (!team) {
|
|
1450
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1451
|
+
showToast('No team selected', 'error');
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (role === 'admin') {
|
|
1455
|
+
// Admin invites are minted on the fly — short-lived, single-use.
|
|
1456
|
+
// No persistent link in state.inviteLinks; each invite is fresh.
|
|
1457
|
+
try {
|
|
1458
|
+
const link = await api.mintAdminInvite(team.team_id);
|
|
1459
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1460
|
+
dispatch({ type: 'OPEN_MODAL', modal: { kind: 'invite-link', role, url: link.url } });
|
|
1461
|
+
}
|
|
1462
|
+
catch {
|
|
1463
|
+
dispatch({ type: 'CLOSE_MODAL' });
|
|
1464
|
+
showToast('Failed to mint admin invite', 'error');
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1286
1468
|
const link = s.inviteLinks.find(l => l.role === role);
|
|
1287
1469
|
if (link) {
|
|
1288
1470
|
dispatch({ type: 'CLOSE_MODAL' });
|
|
@@ -1476,16 +1658,19 @@ export default function App() {
|
|
|
1476
1658
|
return false;
|
|
1477
1659
|
const slug = modal.input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1478
1660
|
try {
|
|
1479
|
-
dispatch({ type: 'SET_OPERATION', operation: '
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1661
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Opening team checkout' });
|
|
1662
|
+
// Team creation is gated on a team subscription — we hand the user
|
|
1663
|
+
// to Stripe checkout; the webhook creates the team + sub atomically
|
|
1664
|
+
// on payment. The new team appears on the next me() refresh.
|
|
1665
|
+
const { checkout_url } = await api.createTeam(modal.input, slug);
|
|
1666
|
+
if (checkout_url)
|
|
1667
|
+
openBrowser(checkout_url);
|
|
1483
1668
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1484
|
-
|
|
1669
|
+
showToast('Complete checkout in your browser — team will appear after payment', 'info');
|
|
1485
1670
|
}
|
|
1486
1671
|
catch {
|
|
1487
1672
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1488
|
-
showToast('Failed to
|
|
1673
|
+
showToast('Failed to start team checkout', 'error');
|
|
1489
1674
|
}
|
|
1490
1675
|
return false;
|
|
1491
1676
|
}
|
|
@@ -1512,19 +1697,23 @@ export default function App() {
|
|
|
1512
1697
|
return false;
|
|
1513
1698
|
}
|
|
1514
1699
|
if (modal.kind === 'create-integration-key') {
|
|
1515
|
-
// Only the confirm step submits — earlier steps just advance via OPEN_MODAL.
|
|
1516
1700
|
if (modal.step !== 'confirm')
|
|
1517
1701
|
return false;
|
|
1518
|
-
if (!modal.input.trim() ||
|
|
1702
|
+
if (!modal.input.trim() || modal.selectedProjectIds.size === 0)
|
|
1519
1703
|
return false;
|
|
1520
1704
|
try {
|
|
1521
1705
|
dispatch({ type: 'SET_OPERATION', operation: 'Creating key' });
|
|
1706
|
+
const projects = [...modal.selectedProjectIds].map(pid => ({
|
|
1707
|
+
project_id: pid,
|
|
1708
|
+
allowed_actions: [...(modal.projectScopes.get(pid) ?? [])],
|
|
1709
|
+
}));
|
|
1522
1710
|
const result = await api.createApiKey({
|
|
1523
1711
|
kind: 'integration',
|
|
1524
1712
|
name: modal.input.trim(),
|
|
1525
|
-
projects
|
|
1526
|
-
...(modal.allowedTools.length > 0 ? { allowed_tools: modal.allowedTools } : {}),
|
|
1713
|
+
projects,
|
|
1527
1714
|
...(modal.agentName.trim() ? { default_agent_name: modal.agentName.trim() } : {}),
|
|
1715
|
+
...(modal.expiresInDays ? { expires_in_days: parseInt(modal.expiresInDays, 10) } : {}),
|
|
1716
|
+
...(modal.opsLimit ? { ops_limit: parseInt(modal.opsLimit, 10) } : {}),
|
|
1528
1717
|
});
|
|
1529
1718
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1530
1719
|
const integrationKeys = stateRef.current.userRole === 'admin' && stateRef.current.activeProjectId
|
|
@@ -1602,27 +1791,25 @@ export default function App() {
|
|
|
1602
1791
|
case 'confirm-clear': {
|
|
1603
1792
|
const project = getActiveProject(s);
|
|
1604
1793
|
if (project && modal.input === project.slug) {
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1794
|
+
try {
|
|
1795
|
+
dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
|
|
1796
|
+
// Server gates this behind consensus for team projects. If the
|
|
1797
|
+
// response includes pending_approval, the action is queued.
|
|
1798
|
+
const resp = await api.clearMemories(projectId);
|
|
1799
|
+
if (resp.pending_approval) {
|
|
1800
|
+
const team = getActiveTeam(s);
|
|
1801
|
+
if (team) {
|
|
1610
1802
|
const approvals = await api.listApprovals(team.team_id);
|
|
1611
1803
|
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1612
|
-
showToast('Approval requested', 'info');
|
|
1613
|
-
}
|
|
1614
|
-
catch {
|
|
1615
|
-
showToast('Failed to request approval', 'error');
|
|
1616
1804
|
}
|
|
1617
|
-
|
|
1805
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1806
|
+
showToast('Clear proposed — waiting for majority', 'info');
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
|
|
1810
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1811
|
+
showToast('All contributions cleared', 'success');
|
|
1618
1812
|
}
|
|
1619
|
-
}
|
|
1620
|
-
try {
|
|
1621
|
-
dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
|
|
1622
|
-
await api.clearMemories(projectId);
|
|
1623
|
-
dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
|
|
1624
|
-
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1625
|
-
showToast('All contributions cleared', 'success');
|
|
1626
1813
|
}
|
|
1627
1814
|
catch {
|
|
1628
1815
|
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
@@ -1634,21 +1821,6 @@ export default function App() {
|
|
|
1634
1821
|
case 'confirm-delete': {
|
|
1635
1822
|
const project = getActiveProject(s);
|
|
1636
1823
|
if (project && modal.input === project.slug) {
|
|
1637
|
-
if (shouldRequireConsensus(s)) {
|
|
1638
|
-
const team = getActiveTeam(s);
|
|
1639
|
-
if (team) {
|
|
1640
|
-
try {
|
|
1641
|
-
await api.initiateApproval(team.team_id, 'delete_project', `delete project "${project.slug}"`, projectId);
|
|
1642
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1643
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1644
|
-
showToast('Approval requested', 'info');
|
|
1645
|
-
}
|
|
1646
|
-
catch {
|
|
1647
|
-
showToast('Failed to request approval', 'error');
|
|
1648
|
-
}
|
|
1649
|
-
break;
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
1824
|
try {
|
|
1653
1825
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting project' });
|
|
1654
1826
|
await api.deleteProject(projectId);
|
|
@@ -1791,24 +1963,16 @@ export default function App() {
|
|
|
1791
1963
|
case 'confirm-delete-team': {
|
|
1792
1964
|
const teamForDelete = getActiveTeam(s);
|
|
1793
1965
|
if (teamForDelete && modal.input === modal.slug) {
|
|
1794
|
-
if (shouldRequireConsensus(s)) {
|
|
1795
|
-
const team = getActiveTeam(s);
|
|
1796
|
-
if (team) {
|
|
1797
|
-
try {
|
|
1798
|
-
await api.initiateApproval(team.team_id, 'delete_team', `delete team "${team.name}"`);
|
|
1799
|
-
const approvals = await api.listApprovals(team.team_id);
|
|
1800
|
-
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1801
|
-
showToast('Approval requested', 'info');
|
|
1802
|
-
}
|
|
1803
|
-
catch {
|
|
1804
|
-
showToast('Failed to request approval', 'error');
|
|
1805
|
-
}
|
|
1806
|
-
break;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
1966
|
try {
|
|
1810
1967
|
dispatch({ type: 'SET_OPERATION', operation: 'Deleting team' });
|
|
1811
|
-
await api.deleteTeam(teamForDelete.team_id);
|
|
1968
|
+
const resp = await api.deleteTeam(teamForDelete.team_id);
|
|
1969
|
+
if (resp.pending_approval) {
|
|
1970
|
+
const approvals = await api.listApprovals(teamForDelete.team_id);
|
|
1971
|
+
dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
|
|
1972
|
+
dispatch({ type: 'SET_OPERATION', operation: null });
|
|
1973
|
+
showToast('Team deletion proposed — waiting for majority', 'info');
|
|
1974
|
+
break;
|
|
1975
|
+
}
|
|
1812
1976
|
const me = await api.me();
|
|
1813
1977
|
dispatch({ type: 'SET_USER_DATA', me });
|
|
1814
1978
|
dispatch({ type: 'SWITCH_TAB', tab: 'knowledge' });
|
|
@@ -1939,11 +2103,12 @@ function hasBillingBanner(state) {
|
|
|
1939
2103
|
function deriveFlatProjectsFromMe(me) {
|
|
1940
2104
|
const list = [];
|
|
1941
2105
|
for (const p of me.personal_projects) {
|
|
1942
|
-
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'personal' } });
|
|
2106
|
+
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, locked: p.locked ?? false, owner: { kind: 'personal' } });
|
|
1943
2107
|
}
|
|
1944
2108
|
for (const team of me.teams) {
|
|
2109
|
+
const teamCanceled = team.subscription?.status === 'canceled';
|
|
1945
2110
|
for (const p of team.projects) {
|
|
1946
|
-
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, owner: { kind: 'team', teamId: team.team_id, teamName: team.name, teamSlug: team.slug } });
|
|
2111
|
+
list.push({ project_id: p.project_id, slug: p.slug, name: p.name, created_at: p.created_at, locked: teamCanceled, owner: { kind: 'team', teamId: team.team_id, teamName: team.name, teamSlug: team.slug } });
|
|
1947
2112
|
}
|
|
1948
2113
|
}
|
|
1949
2114
|
return list;
|