cohvu 2.12.9 → 2.14.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/tui/App.js CHANGED
@@ -18,7 +18,9 @@ import { KnowledgeTab } from './tabs/KnowledgeTab.js';
18
18
  import { TeamTab } from './tabs/TeamTab.js';
19
19
  import { BillingTab } from './tabs/BillingTab.js';
20
20
  import { ProjectTab } from './tabs/ProjectTab.js';
21
+ import { KeysTab } from './tabs/KeysTab.js';
21
22
  import { YouTab } from './tabs/YouTab.js';
23
+ import { KNOWN_TOOL_NAMES } from './state.js';
22
24
  import { daysUntil, timeUntil } from './utils.js';
23
25
  import { exec, execFile } from 'child_process';
24
26
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, watch } from 'fs';
@@ -219,6 +221,29 @@ export default function App() {
219
221
  }
220
222
  break;
221
223
  }
224
+ case 'project': {
225
+ // Linked siblings (explicit + same-team computed at query time)
226
+ const links = await api.listProjectLinks(projectId).catch(() => []);
227
+ if (stateRef.current.activeProjectId !== projectId)
228
+ break;
229
+ dispatch({ type: 'SET_PROJECT_LINKS', links });
230
+ break;
231
+ }
232
+ case 'keys': {
233
+ dispatch({ type: 'SET_API_KEYS_LOADING', loading: true });
234
+ // Session keys are user-scoped (no project filter). Integration keys
235
+ // require admin on the active project — skip the call if not, since
236
+ // the API will 403 anyway and we'd rather not show a toast for that.
237
+ const sessionPromise = api.listSessionKeys().catch(() => []);
238
+ const integrationPromise = (s.userRole === 'admin')
239
+ ? api.listIntegrationKeys(projectId).catch(() => [])
240
+ : Promise.resolve([]);
241
+ const [sessionKeys, integrationKeys] = await Promise.all([sessionPromise, integrationPromise]);
242
+ if (stateRef.current.activeProjectId !== projectId)
243
+ break;
244
+ dispatch({ type: 'SET_API_KEYS', keys: [...sessionKeys, ...integrationKeys] });
245
+ break;
246
+ }
222
247
  }
223
248
  }
224
249
  catch {
@@ -501,7 +526,7 @@ export default function App() {
501
526
  }
502
527
  // Number keys switch tabs (but not when typing in search mode)
503
528
  const tabIdx = parseInt(input, 10);
504
- if (tabIdx >= 1 && tabIdx <= 5 && !key.ctrl && !key.meta && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
529
+ if (tabIdx >= 1 && tabIdx <= TABS.length && !key.ctrl && !key.meta && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
505
530
  dispatch({ type: 'SWITCH_TAB', tab: TABS[tabIdx - 1] });
506
531
  setTimeout(() => loadTabData(), 0);
507
532
  return;
@@ -525,11 +550,38 @@ export default function App() {
525
550
  case 'project':
526
551
  await handleProjectKey(input, key);
527
552
  break;
553
+ case 'keys':
554
+ await handleKeysKey(input, key);
555
+ break;
528
556
  case 'you':
529
557
  await handleYouKey(input, key);
530
558
  break;
531
559
  }
532
560
  }
561
+ // ---- Keys tab keys ----
562
+ async function handleKeysKey(input, key) {
563
+ const s = stateRef.current;
564
+ if (key.upArrow) {
565
+ dispatch({ type: 'SET_API_KEYS_SELECTED', index: Math.max(0, s.apiKeysSelected - 1) });
566
+ return;
567
+ }
568
+ if (key.downArrow) {
569
+ const max = Math.max(0, s.apiKeys.length - 1);
570
+ dispatch({ type: 'SET_API_KEYS_SELECTED', index: Math.min(max, s.apiKeysSelected + 1) });
571
+ return;
572
+ }
573
+ if (input === 'n') {
574
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'select-key-kind', selected: 0 } });
575
+ return;
576
+ }
577
+ if (input === 'x' && s.apiKeys.length > 0) {
578
+ const target = s.apiKeys[s.apiKeysSelected];
579
+ if (target) {
580
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-revoke-key', keyId: target.id, keyName: target.name } });
581
+ }
582
+ return;
583
+ }
584
+ }
533
585
  // ---- Knowledge keys ----
534
586
  async function handleKnowledgeKey(input, key) {
535
587
  const s = stateRef.current;
@@ -579,7 +631,7 @@ export default function App() {
579
631
  const projectId = s.activeProjectId;
580
632
  if (projectId) {
581
633
  let failures = 0;
582
- dispatch({ type: 'SET_OPERATION', operation: 'Removing memories' });
634
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing contributions' });
583
635
  await Promise.all([...s.forgetSelected].map(async (id) => {
584
636
  try {
585
637
  await api.deleteMemory(projectId, id);
@@ -838,7 +890,7 @@ export default function App() {
838
890
  }
839
891
  }
840
892
  // ---- Project keys ----
841
- async function handleProjectKey(input, _key) {
893
+ async function handleProjectKey(input, key) {
842
894
  const s = stateRef.current;
843
895
  if (input === 't') {
844
896
  dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-team', input: '' } });
@@ -863,6 +915,30 @@ export default function App() {
863
915
  if (project)
864
916
  dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-delete', slug: project.slug, memoryCount: s.memoryTotal, input: '' } });
865
917
  }
918
+ else if (input === 'L' && s.userRole === 'admin') {
919
+ // Open link picker — only meaningful if there are other admin projects
920
+ const candidates = adminScopableProjects(s).filter(p => p.project_id !== s.activeProjectId);
921
+ if (candidates.length === 0) {
922
+ showInlineError('no other projects you admin', 2000);
923
+ return;
924
+ }
925
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'link-project', selected: 0 } });
926
+ }
927
+ else if (input === 'U' && s.userRole === 'admin') {
928
+ const link = s.projectLinks[s.projectLinkSelected];
929
+ if (link && link.source === 'explicit') {
930
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-unlink-project', otherProjectId: link.project_id, otherSlug: link.slug } });
931
+ }
932
+ else if (link && link.source === 'team') {
933
+ showInlineError('team siblings are auto-linked — cannot unlink', 2000);
934
+ }
935
+ }
936
+ else if (key.upArrow && s.projectLinks.length > 0) {
937
+ dispatch({ type: 'SET_PROJECT_LINK_SELECTED', index: Math.max(0, s.projectLinkSelected - 1) });
938
+ }
939
+ else if (key.downArrow && s.projectLinks.length > 0) {
940
+ dispatch({ type: 'SET_PROJECT_LINK_SELECTED', index: Math.min(s.projectLinks.length - 1, s.projectLinkSelected + 1) });
941
+ }
866
942
  }
867
943
  // ---- You keys ----
868
944
  async function handleYouKey(input, _key) {
@@ -955,7 +1031,8 @@ export default function App() {
955
1031
  if (modal.kind === 'confirm-forget' || modal.kind === 'confirm-remove-member' ||
956
1032
  modal.kind === 'confirm-leave' || modal.kind === 'confirm-logout' ||
957
1033
  modal.kind === 'confirm-regen-link' || modal.kind === 'initiate-consensus' ||
958
- modal.kind === 'approve-action') {
1034
+ modal.kind === 'approve-action' || modal.kind === 'confirm-revoke-key' ||
1035
+ modal.kind === 'confirm-unlink-project') {
959
1036
  if (input === 'y') {
960
1037
  const willExit = modal.kind === 'confirm-logout';
961
1038
  await confirmModal(modal);
@@ -967,6 +1044,179 @@ export default function App() {
967
1044
  }
968
1045
  return;
969
1046
  }
1047
+ // Select key kind (session vs integration)
1048
+ if (modal.kind === 'select-key-kind') {
1049
+ if (key.upArrow) {
1050
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.max(0, modal.selected - 1) } });
1051
+ }
1052
+ else if (key.downArrow) {
1053
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.min(1, modal.selected + 1) } });
1054
+ }
1055
+ else if (key.return) {
1056
+ if (modal.selected === 0) {
1057
+ dispatch({ type: 'OPEN_MODAL', modal: { kind: 'create-session-key', input: '' } });
1058
+ }
1059
+ else {
1060
+ // Pre-select the active project for convenience
1061
+ const adminProjects = adminScopableProjects(s);
1062
+ const activeIdx = Math.max(0, adminProjects.findIndex(p => p.project_id === s.activeProjectId));
1063
+ const project = adminProjects[activeIdx];
1064
+ dispatch({ type: 'OPEN_MODAL', modal: {
1065
+ kind: 'create-integration-key',
1066
+ step: 'name',
1067
+ input: '',
1068
+ projectId: project?.project_id ?? null,
1069
+ projectSelectedIdx: activeIdx,
1070
+ role: 'member',
1071
+ roleSelectedIdx: 1,
1072
+ allowedTools: [],
1073
+ toolsSelectedIdx: 0,
1074
+ agentName: '',
1075
+ } });
1076
+ }
1077
+ }
1078
+ return;
1079
+ }
1080
+ // Integration key creation wizard
1081
+ if (modal.kind === 'create-integration-key') {
1082
+ const adminProjects = adminScopableProjects(s);
1083
+ if (modal.step === 'name') {
1084
+ if (key.return && modal.input.trim().length > 0) {
1085
+ if (adminProjects.length === 0) {
1086
+ showInlineError('no projects you can scope an integration key to', 3000);
1087
+ return;
1088
+ }
1089
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'project' } });
1090
+ }
1091
+ else if (key.backspace || key.delete) {
1092
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input.slice(0, -1) } });
1093
+ }
1094
+ else if (input === ' ') {
1095
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input + ' ' } });
1096
+ }
1097
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
1098
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, input: modal.input + input } });
1099
+ }
1100
+ return;
1101
+ }
1102
+ if (modal.step === 'project') {
1103
+ if (adminProjects.length === 0)
1104
+ return;
1105
+ if (key.upArrow) {
1106
+ const i = Math.max(0, modal.projectSelectedIdx - 1);
1107
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: i, projectId: adminProjects[i].project_id } });
1108
+ }
1109
+ else if (key.downArrow) {
1110
+ const i = Math.min(adminProjects.length - 1, modal.projectSelectedIdx + 1);
1111
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, projectSelectedIdx: i, projectId: adminProjects[i].project_id } });
1112
+ }
1113
+ else if (key.return && modal.projectId) {
1114
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'role' } });
1115
+ }
1116
+ return;
1117
+ }
1118
+ if (modal.step === 'role') {
1119
+ const roles = ['admin', 'member', 'viewer'];
1120
+ if (key.upArrow) {
1121
+ const i = Math.max(0, modal.roleSelectedIdx - 1);
1122
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
1123
+ }
1124
+ else if (key.downArrow) {
1125
+ const i = Math.min(2, modal.roleSelectedIdx + 1);
1126
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, roleSelectedIdx: i, role: roles[i] } });
1127
+ }
1128
+ else if (key.return) {
1129
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'tools' } });
1130
+ }
1131
+ return;
1132
+ }
1133
+ if (modal.step === 'tools') {
1134
+ if (key.upArrow) {
1135
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, toolsSelectedIdx: Math.max(0, modal.toolsSelectedIdx - 1) } });
1136
+ }
1137
+ else if (key.downArrow) {
1138
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, toolsSelectedIdx: Math.min(KNOWN_TOOL_NAMES.length - 1, modal.toolsSelectedIdx + 1) } });
1139
+ }
1140
+ else if (input === ' ') {
1141
+ const t = KNOWN_TOOL_NAMES[modal.toolsSelectedIdx];
1142
+ const next = modal.allowedTools.includes(t)
1143
+ ? modal.allowedTools.filter(x => x !== t)
1144
+ : [...modal.allowedTools, t];
1145
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, allowedTools: next } });
1146
+ }
1147
+ else if (key.return) {
1148
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'agent' } });
1149
+ }
1150
+ return;
1151
+ }
1152
+ if (modal.step === 'agent') {
1153
+ if (key.return) {
1154
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, step: 'confirm' } });
1155
+ }
1156
+ else if (key.backspace || key.delete) {
1157
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName.slice(0, -1) } });
1158
+ }
1159
+ else if (input === ' ') {
1160
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + ' ' } });
1161
+ }
1162
+ else if (input.length === 1 && !key.ctrl && !key.meta) {
1163
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, agentName: modal.agentName + input } });
1164
+ }
1165
+ return;
1166
+ }
1167
+ if (modal.step === 'confirm') {
1168
+ if (input === 'y') {
1169
+ await confirmModal(modal);
1170
+ // confirmModal handles the modal transition (closes on error, opens key-created on success)
1171
+ }
1172
+ else if (input === 'n') {
1173
+ dispatch({ type: 'CLOSE_MODAL' });
1174
+ }
1175
+ return;
1176
+ }
1177
+ return;
1178
+ }
1179
+ // Link to another project — pick from candidates
1180
+ if (modal.kind === 'link-project') {
1181
+ const candidates = adminScopableProjects(s).filter(p => p.project_id !== s.activeProjectId);
1182
+ if (key.upArrow) {
1183
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.max(0, modal.selected - 1) } });
1184
+ }
1185
+ else if (key.downArrow) {
1186
+ dispatch({ type: 'OPEN_MODAL', modal: { ...modal, selected: Math.min(candidates.length - 1, modal.selected + 1) } });
1187
+ }
1188
+ else if (key.return) {
1189
+ const target = candidates[modal.selected];
1190
+ if (target && s.activeProjectId) {
1191
+ try {
1192
+ dispatch({ type: 'SET_OPERATION', operation: 'Linking project' });
1193
+ await api.linkProjects(s.activeProjectId, target.project_id);
1194
+ const links = await api.listProjectLinks(s.activeProjectId).catch(() => stateRef.current.projectLinks);
1195
+ dispatch({ type: 'SET_PROJECT_LINKS', links });
1196
+ dispatch({ type: 'SET_OPERATION', operation: null });
1197
+ dispatch({ type: 'CLOSE_MODAL' });
1198
+ showToast('Linked', 'success');
1199
+ }
1200
+ catch {
1201
+ dispatch({ type: 'SET_OPERATION', operation: null });
1202
+ dispatch({ type: 'CLOSE_MODAL' });
1203
+ showToast('Failed to link', 'error');
1204
+ }
1205
+ }
1206
+ }
1207
+ return;
1208
+ }
1209
+ // Key created — copy or dismiss (esc handled at top)
1210
+ if (modal.kind === 'key-created') {
1211
+ if (input === 'c') {
1212
+ copyToClipboard(modal.keyValue);
1213
+ dispatch({ type: 'SET_COPIED_FEEDBACK', active: true });
1214
+ if (copiedTimerRef.current)
1215
+ clearTimeout(copiedTimerRef.current);
1216
+ copiedTimerRef.current = setTimeout(() => dispatch({ type: 'SET_COPIED_FEEDBACK', active: false }), 1500);
1217
+ }
1218
+ return;
1219
+ }
970
1220
  // Text input modals
971
1221
  if ('input' in modal) {
972
1222
  if (key.return) {
@@ -1338,6 +1588,101 @@ export default function App() {
1338
1588
  }
1339
1589
  return false;
1340
1590
  }
1591
+ if (modal.kind === 'create-session-key') {
1592
+ if (!modal.input.trim())
1593
+ return false;
1594
+ try {
1595
+ dispatch({ type: 'SET_OPERATION', operation: 'Creating key' });
1596
+ const result = await api.createApiKey({ kind: 'session', name: modal.input.trim() });
1597
+ dispatch({ type: 'SET_OPERATION', operation: null });
1598
+ // Refresh list, then transition modal so the secret is shown once.
1599
+ const sessionKeys = await api.listSessionKeys().catch(() => []);
1600
+ const integrationKeys = stateRef.current.userRole === 'admin' && stateRef.current.activeProjectId
1601
+ ? await api.listIntegrationKeys(stateRef.current.activeProjectId).catch(() => [])
1602
+ : [];
1603
+ dispatch({ type: 'SET_API_KEYS', keys: [...sessionKeys, ...integrationKeys] });
1604
+ dispatch({ type: 'OPEN_MODAL', modal: {
1605
+ kind: 'key-created',
1606
+ keyId: result.api_key.id,
1607
+ keyValue: result.key,
1608
+ keyName: result.api_key.name,
1609
+ kind2: 'session',
1610
+ } });
1611
+ return true; // chained — caller must not CLOSE_MODAL
1612
+ }
1613
+ catch {
1614
+ dispatch({ type: 'SET_OPERATION', operation: null });
1615
+ showToast('Failed to create key', 'error');
1616
+ }
1617
+ return false;
1618
+ }
1619
+ if (modal.kind === 'create-integration-key') {
1620
+ // Only the confirm step submits — earlier steps just advance via OPEN_MODAL.
1621
+ if (modal.step !== 'confirm')
1622
+ return false;
1623
+ if (!modal.input.trim() || !modal.projectId)
1624
+ return false;
1625
+ try {
1626
+ dispatch({ type: 'SET_OPERATION', operation: 'Creating key' });
1627
+ const result = await api.createApiKey({
1628
+ kind: 'integration',
1629
+ name: modal.input.trim(),
1630
+ projects: [{ project_id: modal.projectId, role: modal.role }],
1631
+ ...(modal.allowedTools.length > 0 ? { allowed_tools: modal.allowedTools } : {}),
1632
+ ...(modal.agentName.trim() ? { default_agent_name: modal.agentName.trim() } : {}),
1633
+ });
1634
+ dispatch({ type: 'SET_OPERATION', operation: null });
1635
+ const sessionKeys = await api.listSessionKeys().catch(() => []);
1636
+ const integrationKeys = stateRef.current.userRole === 'admin' && stateRef.current.activeProjectId
1637
+ ? await api.listIntegrationKeys(stateRef.current.activeProjectId).catch(() => [])
1638
+ : [];
1639
+ dispatch({ type: 'SET_API_KEYS', keys: [...sessionKeys, ...integrationKeys] });
1640
+ dispatch({ type: 'OPEN_MODAL', modal: {
1641
+ kind: 'key-created',
1642
+ keyId: result.api_key.id,
1643
+ keyValue: result.key,
1644
+ keyName: result.api_key.name,
1645
+ kind2: 'integration',
1646
+ } });
1647
+ return true;
1648
+ }
1649
+ catch {
1650
+ dispatch({ type: 'SET_OPERATION', operation: null });
1651
+ showToast('Failed to create key', 'error');
1652
+ }
1653
+ return false;
1654
+ }
1655
+ if (modal.kind === 'confirm-revoke-key') {
1656
+ try {
1657
+ dispatch({ type: 'SET_OPERATION', operation: 'Revoking key' });
1658
+ await api.deleteApiKey(modal.keyId);
1659
+ dispatch({ type: 'REMOVE_API_KEY', id: modal.keyId });
1660
+ dispatch({ type: 'SET_OPERATION', operation: null });
1661
+ showToast('Key revoked', 'success');
1662
+ }
1663
+ catch {
1664
+ dispatch({ type: 'SET_OPERATION', operation: null });
1665
+ showToast('Failed to revoke key', 'error');
1666
+ }
1667
+ return false;
1668
+ }
1669
+ if (modal.kind === 'confirm-unlink-project') {
1670
+ const projectId = stateRef.current.activeProjectId;
1671
+ if (!projectId)
1672
+ return false;
1673
+ try {
1674
+ dispatch({ type: 'SET_OPERATION', operation: 'Unlinking' });
1675
+ await api.unlinkProjects(projectId, modal.otherProjectId);
1676
+ dispatch({ type: 'REMOVE_PROJECT_LINK', otherProjectId: modal.otherProjectId });
1677
+ dispatch({ type: 'SET_OPERATION', operation: null });
1678
+ showToast('Unlinked', 'success');
1679
+ }
1680
+ catch {
1681
+ dispatch({ type: 'SET_OPERATION', operation: null });
1682
+ showToast('Failed to unlink', 'error');
1683
+ }
1684
+ return false;
1685
+ }
1341
1686
  const projectId = s.activeProjectId;
1342
1687
  if (!projectId)
1343
1688
  return false;
@@ -1365,15 +1710,15 @@ export default function App() {
1365
1710
  }
1366
1711
  case 'confirm-forget':
1367
1712
  try {
1368
- dispatch({ type: 'SET_OPERATION', operation: 'Removing memory' });
1713
+ dispatch({ type: 'SET_OPERATION', operation: 'Removing contribution' });
1369
1714
  await api.deleteMemory(projectId, modal.memoryId);
1370
1715
  dispatch({ type: 'REMOVE_MEMORY', id: modal.memoryId });
1371
1716
  dispatch({ type: 'SET_OPERATION', operation: null });
1372
- showToast('Memory removed', 'success');
1717
+ showToast('Contribution removed', 'success');
1373
1718
  }
1374
1719
  catch {
1375
1720
  dispatch({ type: 'SET_OPERATION', operation: null });
1376
- showToast('Failed to remove memory', 'error');
1721
+ showToast('Failed to remove contribution', 'error');
1377
1722
  }
1378
1723
  break;
1379
1724
  case 'confirm-forget-all':
@@ -1384,7 +1729,7 @@ export default function App() {
1384
1729
  const team = getActiveTeam(s);
1385
1730
  if (team) {
1386
1731
  try {
1387
- await api.initiateApproval(team.team_id, 'clear_memories', `clear all memories from "${project.slug}"`, projectId);
1732
+ await api.initiateApproval(team.team_id, 'clear_memories', `clear all contributions from "${project.slug}"`, projectId);
1388
1733
  const approvals = await api.listApprovals(team.team_id);
1389
1734
  dispatch({ type: 'SET_PENDING_APPROVALS', approvals });
1390
1735
  showToast('Approval requested', 'info');
@@ -1396,15 +1741,15 @@ export default function App() {
1396
1741
  }
1397
1742
  }
1398
1743
  try {
1399
- dispatch({ type: 'SET_OPERATION', operation: 'Clearing memories' });
1744
+ dispatch({ type: 'SET_OPERATION', operation: 'Clearing contributions' });
1400
1745
  await api.clearMemories(projectId);
1401
1746
  dispatch({ type: 'SET_MEMORIES', memories: [], total: 0 });
1402
1747
  dispatch({ type: 'SET_OPERATION', operation: null });
1403
- showToast('All memories cleared', 'success');
1748
+ showToast('All contributions cleared', 'success');
1404
1749
  }
1405
1750
  catch {
1406
1751
  dispatch({ type: 'SET_OPERATION', operation: null });
1407
- showToast('Failed to clear memories', 'error');
1752
+ showToast('Failed to clear contributions', 'error');
1408
1753
  }
1409
1754
  }
1410
1755
  break;
@@ -1641,19 +1986,31 @@ function renderTab(state, height) {
1641
1986
  case 'team': return _jsx(TeamTab, { state: state, height: height });
1642
1987
  case 'billing': return _jsx(BillingTab, { state: state, height: height });
1643
1988
  case 'project': return _jsx(ProjectTab, { state: state, height: height });
1989
+ case 'keys': return _jsx(KeysTab, { state: state, height: height });
1644
1990
  case 'you': return _jsx(YouTab, { state: state, height: height });
1645
1991
  }
1646
1992
  }
1993
+ // Projects the caller can scope an integration key to. Personal projects are
1994
+ // always scopable by their owner; team projects only by team admins.
1995
+ function adminScopableProjects(s) {
1996
+ return s.projects.filter(p => {
1997
+ if (p.owner.kind === 'personal')
1998
+ return true; // user owns it
1999
+ const teamId = p.owner.teamId;
2000
+ const team = s.teams.find(t => t.team_id === teamId);
2001
+ return team?.role === 'admin';
2002
+ });
2003
+ }
1647
2004
  function HelpOverlay({ state }) {
1648
2005
  const lines = [
1649
- ['tab / 1-5', 'switch tabs'],
2006
+ ['tab / 1-6', 'switch tabs'],
1650
2007
  ['?', 'toggle this help'],
1651
2008
  ['q', 'quit'],
1652
2009
  ['', ''],
1653
2010
  ];
1654
2011
  switch (state.tab) {
1655
2012
  case 'knowledge':
1656
- lines.push(['/', 'search'], ['enter', 'expand memory'], ['↑↓', 'navigate'], ['space', 'load more'], ['d', 'forget mode'], ['D', 'forget all (admin)']);
2013
+ lines.push(['/', 'search'], ['enter', 'expand contribution'], ['↑↓', 'navigate'], ['space', 'load more'], ['d', 'forget mode'], ['D', 'forget all (admin)']);
1657
2014
  break;
1658
2015
  case 'team':
1659
2016
  lines.push(['↑↓', 'navigate'], ['i', 'invite'], ['e', 'edit role (admin)'], ['x', 'remove member'], ['c', 'copy invite link'], ['r', 'regenerate link']);
@@ -1662,7 +2019,10 @@ function HelpOverlay({ state }) {
1662
2019
  lines.push(['s', 'subscribe'], ['p', 'billing portal']);
1663
2020
  break;
1664
2021
  case 'project':
1665
- lines.push(['w', 'switch project'], ['n', 'new project'], ['t', 'new team'], ['r', 'rename'], ['c', 'clear knowledge'], ['d', 'delete project']);
2022
+ lines.push(['↑↓', 'navigate links'], ['w', 'switch project'], ['n', 'new project'], ['t', 'new team'], ['r', 'rename'], ['L', 'link to another project (admin)'], ['U', 'unlink selected (admin, explicit only)'], ['c', 'clear knowledge'], ['d', 'delete project']);
2023
+ break;
2024
+ case 'keys':
2025
+ lines.push(['↑↓', 'navigate'], ['n', 'new key'], ['x', 'revoke selected']);
1666
2026
  break;
1667
2027
  case 'you':
1668
2028
  lines.push(['p', 'pause / resume'], ['r', 're-run setup'], ['l', 'logout']);