better-codex 0.1.4 → 0.2.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/apps/backend/README.md +43 -2
- package/apps/backend/src/core/app-server.ts +68 -2
- package/apps/backend/src/core/jsonrpc.ts +13 -0
- package/apps/backend/src/server.ts +156 -1
- package/apps/backend/src/services/codex-config.ts +561 -0
- package/apps/backend/src/thread-activity/service.ts +47 -0
- package/apps/web/README.md +18 -2
- package/apps/web/src/components/layout/codex-settings.tsx +1208 -0
- package/apps/web/src/components/layout/session-view.tsx +203 -8
- package/apps/web/src/components/layout/settings-dialog.tsx +9 -1
- package/apps/web/src/components/layout/virtualized-message-list.tsx +594 -90
- package/apps/web/src/components/session-view/rate-limit-banner.tsx +178 -0
- package/apps/web/src/components/session-view/session-dialogs.tsx +93 -2
- package/apps/web/src/components/session-view/session-header.tsx +21 -2
- package/apps/web/src/components/session-view/thread-account-switcher.tsx +191 -0
- package/apps/web/src/components/ui/icons.tsx +12 -0
- package/apps/web/src/hooks/use-hub-connection.ts +59 -19
- package/apps/web/src/hooks/use-thread-history.ts +94 -5
- package/apps/web/src/services/hub-client.ts +98 -1
- package/apps/web/src/store/index.ts +36 -1
- package/apps/web/src/types/index.ts +25 -0
- package/apps/web/src/utils/item-format.ts +55 -9
- package/apps/web/src/utils/slash-commands.ts +2 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState, useMemo } from 'react'
|
|
2
2
|
import { useAppStore } from '../../store'
|
|
3
3
|
import { type SelectOption } from '../ui'
|
|
4
4
|
import { VirtualizedMessageList } from './virtualized-message-list'
|
|
@@ -15,6 +15,7 @@ import { SessionAuthBanner } from '../session-view/session-auth-banner'
|
|
|
15
15
|
import { SessionComposer } from '../session-view/session-composer'
|
|
16
16
|
import { SessionDialogs } from '../session-view/session-dialogs'
|
|
17
17
|
import { SessionEmpty } from '../session-view/session-empty'
|
|
18
|
+
import { RateLimitBanner, isRateLimitError } from '../session-view/rate-limit-banner'
|
|
18
19
|
|
|
19
20
|
export function SessionView() {
|
|
20
21
|
const [inputValue, setInputValue] = useState('')
|
|
@@ -37,6 +38,8 @@ export function SessionView() {
|
|
|
37
38
|
const [pendingCwd, setPendingCwd] = useState('')
|
|
38
39
|
const [pendingApproval, setPendingApproval] = useState<ApprovalPolicy>('on-request')
|
|
39
40
|
const [showApiKeyPrompt, setShowApiKeyPrompt] = useState(false)
|
|
41
|
+
const [showAccountSwitchDialog, setShowAccountSwitchDialog] = useState(false)
|
|
42
|
+
const [rateLimitBannerDismissed, setRateLimitBannerDismissed] = useState(false)
|
|
40
43
|
// Attachments and file mentions state
|
|
41
44
|
const [attachments, setAttachments] = useState<Attachment[]>([])
|
|
42
45
|
const [fileMentions, setFileMentions] = useState<FileMention[]>([])
|
|
@@ -77,6 +80,7 @@ export function SessionView() {
|
|
|
77
80
|
threadTurnStartedAt,
|
|
78
81
|
threadLastTurnDuration,
|
|
79
82
|
threadTokenUsage,
|
|
83
|
+
threadPendingAccountSwitch,
|
|
80
84
|
setThreadModel,
|
|
81
85
|
setThreadEffort,
|
|
82
86
|
setThreadApproval,
|
|
@@ -93,6 +97,8 @@ export function SessionView() {
|
|
|
93
97
|
connectionStatus,
|
|
94
98
|
setMessagesForThread,
|
|
95
99
|
setAccountLoginId,
|
|
100
|
+
setThreadPendingAccountSwitch,
|
|
101
|
+
setBackendToUiThreadId,
|
|
96
102
|
} = useAppStore()
|
|
97
103
|
|
|
98
104
|
const selectedThread = threads.find((thread) => thread.id === selectedThreadId)
|
|
@@ -119,6 +125,9 @@ export function SessionView() {
|
|
|
119
125
|
const selectedCwd = selectedThreadId ? threadCwds[selectedThreadId] : undefined
|
|
120
126
|
const selectedUsage = selectedThreadId ? threadTokenUsage[selectedThreadId] : undefined
|
|
121
127
|
const webSearchEnabled = selectedThreadId ? threadWebSearch[selectedThreadId] ?? false : false
|
|
128
|
+
const selectedWebSearch = webSearchEnabled
|
|
129
|
+
// Get the effective backend thread ID (may differ from UI thread ID after account switch)
|
|
130
|
+
const effectiveBackendThreadId = selectedThread?.backendThreadId ?? selectedThreadId
|
|
122
131
|
const isAccountReady = account?.status === 'online'
|
|
123
132
|
const isAuthPending = account?.status === 'degraded'
|
|
124
133
|
const canInteract = connectionStatus === 'connected' && !isArchived && isAccountReady
|
|
@@ -143,6 +152,34 @@ export function SessionView() {
|
|
|
143
152
|
const mentionQuery = getMentionQuery(inputValue)
|
|
144
153
|
const mentionMenuOpen = mentionQuery !== null && !slashMenuOpen
|
|
145
154
|
const mentionMatches = mentionMenuOpen ? fileSearchResults : []
|
|
155
|
+
|
|
156
|
+
// Rate limit detection - check recent messages for rate limit errors
|
|
157
|
+
const rateLimitError = useMemo(() => {
|
|
158
|
+
if (!threadMessages.length) return null
|
|
159
|
+
// Check the last 5 messages for rate limit errors
|
|
160
|
+
const recentMessages = threadMessages.slice(-5)
|
|
161
|
+
for (const msg of recentMessages) {
|
|
162
|
+
if (msg.role === 'assistant' && msg.title === 'Error' && isRateLimitError(msg.content)) {
|
|
163
|
+
return msg.content
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}, [threadMessages])
|
|
168
|
+
|
|
169
|
+
// Get accounts available for switching (online accounts other than current)
|
|
170
|
+
const switchableAccounts = useMemo(() => {
|
|
171
|
+
return accounts.filter(
|
|
172
|
+
(a) => a.id !== threadAccountId && a.status === 'online'
|
|
173
|
+
)
|
|
174
|
+
}, [accounts, threadAccountId])
|
|
175
|
+
|
|
176
|
+
// Show rate limit banner when error detected and not dismissed
|
|
177
|
+
const showRateLimitBanner = !!rateLimitError && !rateLimitBannerDismissed && switchableAccounts.length > 0
|
|
178
|
+
|
|
179
|
+
// Reset banner dismissed state when thread changes
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
setRateLimitBannerDismissed(false)
|
|
182
|
+
}, [selectedThreadId])
|
|
146
183
|
|
|
147
184
|
const modelOptions = models.map((model): SelectOption => ({
|
|
148
185
|
value: model.id,
|
|
@@ -426,6 +463,69 @@ export function SessionView() {
|
|
|
426
463
|
try {
|
|
427
464
|
updateThread(selectedThreadId, { status: 'active' })
|
|
428
465
|
|
|
466
|
+
// Check if this thread has a pending account switch
|
|
467
|
+
// If so, we need to start a new thread on the new account
|
|
468
|
+
const pendingSwitch = threadPendingAccountSwitch[selectedThreadId]
|
|
469
|
+
let actualThreadId = selectedThreadId
|
|
470
|
+
|
|
471
|
+
if (pendingSwitch) {
|
|
472
|
+
// Start a new thread on the new account
|
|
473
|
+
const newAccountId = selectedThread.accountId
|
|
474
|
+
const accountModels = modelsByAccount[newAccountId] || []
|
|
475
|
+
const defaultThreadModel = accountModels.find((model) => model.isDefault) ?? accountModels[0]
|
|
476
|
+
const startParams: { model?: string; approvalPolicy?: ApprovalPolicy; config?: Record<string, unknown> } = {}
|
|
477
|
+
if (defaultThreadModel?.id) {
|
|
478
|
+
startParams.model = defaultThreadModel.id
|
|
479
|
+
}
|
|
480
|
+
if (selectedApproval) {
|
|
481
|
+
startParams.approvalPolicy = selectedApproval
|
|
482
|
+
}
|
|
483
|
+
if (selectedWebSearch) {
|
|
484
|
+
startParams.config = { 'features.web_search_request': true }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const result = (await hubClient.request(newAccountId, 'thread/start', startParams)) as {
|
|
488
|
+
thread?: {
|
|
489
|
+
id: string
|
|
490
|
+
preview?: string
|
|
491
|
+
modelProvider?: string
|
|
492
|
+
createdAt?: number
|
|
493
|
+
}
|
|
494
|
+
reasoningEffort?: ReasoningEffort | null
|
|
495
|
+
approvalPolicy?: ApprovalPolicy | null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!result.thread) {
|
|
499
|
+
throw new Error('Failed to start new thread on switched account')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Use the new backend thread ID for this turn
|
|
503
|
+
actualThreadId = result.thread.id
|
|
504
|
+
|
|
505
|
+
// Map the backend thread ID to the UI thread ID so events get routed correctly
|
|
506
|
+
setBackendToUiThreadId(actualThreadId, selectedThreadId)
|
|
507
|
+
|
|
508
|
+
// Update the local thread to use the new backend thread ID
|
|
509
|
+
// We keep the same local thread but update its properties
|
|
510
|
+
updateThread(selectedThreadId, {
|
|
511
|
+
backendThreadId: actualThreadId,
|
|
512
|
+
status: 'active',
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Clear the pending switch flag
|
|
516
|
+
setThreadPendingAccountSwitch(selectedThreadId, null)
|
|
517
|
+
|
|
518
|
+
// Add a system message noting the new thread was created
|
|
519
|
+
addMessage(selectedThreadId, {
|
|
520
|
+
id: `sys-new-thread-${Date.now()}`,
|
|
521
|
+
role: 'assistant',
|
|
522
|
+
content: `New conversation started on this account. Previous context from ${pendingSwitch.previousAccountId} is shown for reference but not available to the model.`,
|
|
523
|
+
kind: 'tool',
|
|
524
|
+
title: 'New Conversation',
|
|
525
|
+
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
429
529
|
// Build input array with text, images, and file references
|
|
430
530
|
const input: Array<{ type: string; text?: string; url?: string; path?: string }> = []
|
|
431
531
|
|
|
@@ -453,7 +553,7 @@ export function SessionView() {
|
|
|
453
553
|
cwd?: string
|
|
454
554
|
approvalPolicy?: ApprovalPolicy
|
|
455
555
|
} = {
|
|
456
|
-
threadId:
|
|
556
|
+
threadId: actualThreadId,
|
|
457
557
|
input,
|
|
458
558
|
}
|
|
459
559
|
if (effectiveModel) {
|
|
@@ -656,14 +756,14 @@ export function SessionView() {
|
|
|
656
756
|
}
|
|
657
757
|
|
|
658
758
|
const runReviewCommand = async (instructions?: string) => {
|
|
659
|
-
if (!selectedThreadId || !account) {
|
|
759
|
+
if (!selectedThreadId || !account || !effectiveBackendThreadId) {
|
|
660
760
|
return
|
|
661
761
|
}
|
|
662
762
|
const target = instructions
|
|
663
763
|
? { type: 'custom', instructions }
|
|
664
764
|
: { type: 'uncommittedChanges' }
|
|
665
765
|
await hubClient.request(account.id, 'review/start', {
|
|
666
|
-
threadId:
|
|
766
|
+
threadId: effectiveBackendThreadId,
|
|
667
767
|
target,
|
|
668
768
|
delivery: 'inline',
|
|
669
769
|
})
|
|
@@ -792,6 +892,36 @@ export function SessionView() {
|
|
|
792
892
|
setShowApprovalsDialog(true)
|
|
793
893
|
return
|
|
794
894
|
}
|
|
895
|
+
case 'switch': {
|
|
896
|
+
// If an account name is provided, try to switch directly
|
|
897
|
+
if (rest) {
|
|
898
|
+
const targetAccount = accounts.find(
|
|
899
|
+
(a) => a.name.toLowerCase() === rest.toLowerCase() ||
|
|
900
|
+
a.id === rest
|
|
901
|
+
)
|
|
902
|
+
if (targetAccount && targetAccount.status === 'online' && targetAccount.id !== threadAccountId) {
|
|
903
|
+
handleSwitchThreadAccount(targetAccount.id)
|
|
904
|
+
return
|
|
905
|
+
}
|
|
906
|
+
if (targetAccount && targetAccount.id === threadAccountId) {
|
|
907
|
+
addSystemMessage('tool', '/switch', `Already using account: ${targetAccount.name}`)
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
if (targetAccount && targetAccount.status !== 'online') {
|
|
911
|
+
addSystemMessage('tool', '/switch', `Account "${targetAccount.name}" is not authenticated.`)
|
|
912
|
+
return
|
|
913
|
+
}
|
|
914
|
+
addSystemMessage('tool', '/switch', `Account "${rest}" not found.`)
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
// Show dialog if no account specified
|
|
918
|
+
if (switchableAccounts.length === 0) {
|
|
919
|
+
addSystemMessage('tool', '/switch', 'No other authenticated accounts available to switch to.')
|
|
920
|
+
return
|
|
921
|
+
}
|
|
922
|
+
setShowAccountSwitchDialog(true)
|
|
923
|
+
return
|
|
924
|
+
}
|
|
795
925
|
case 'review': {
|
|
796
926
|
await runReviewCommand(rest || undefined)
|
|
797
927
|
return
|
|
@@ -966,12 +1096,12 @@ export function SessionView() {
|
|
|
966
1096
|
}
|
|
967
1097
|
|
|
968
1098
|
const handleArchive = async () => {
|
|
969
|
-
if (!selectedThreadId || !selectedThread || !canInteract) {
|
|
1099
|
+
if (!selectedThreadId || !selectedThread || !canInteract || !effectiveBackendThreadId) {
|
|
970
1100
|
return
|
|
971
1101
|
}
|
|
972
1102
|
try {
|
|
973
1103
|
await hubClient.request(selectedThread.accountId, 'thread/archive', {
|
|
974
|
-
threadId:
|
|
1104
|
+
threadId: effectiveBackendThreadId,
|
|
975
1105
|
})
|
|
976
1106
|
updateThread(selectedThreadId, { status: 'archived' })
|
|
977
1107
|
clearQueuedMessages(selectedThreadId)
|
|
@@ -1123,7 +1253,7 @@ export function SessionView() {
|
|
|
1123
1253
|
}
|
|
1124
1254
|
|
|
1125
1255
|
const handleInterruptTurn = async () => {
|
|
1126
|
-
if (!account || !selectedThreadId) {
|
|
1256
|
+
if (!account || !selectedThreadId || !effectiveBackendThreadId) {
|
|
1127
1257
|
return
|
|
1128
1258
|
}
|
|
1129
1259
|
const turnId = threadTurnIds[selectedThreadId]
|
|
@@ -1132,7 +1262,7 @@ export function SessionView() {
|
|
|
1132
1262
|
}
|
|
1133
1263
|
try {
|
|
1134
1264
|
await hubClient.request(account.id, 'turn/interrupt', {
|
|
1135
|
-
threadId:
|
|
1265
|
+
threadId: effectiveBackendThreadId,
|
|
1136
1266
|
turnId,
|
|
1137
1267
|
})
|
|
1138
1268
|
} catch {
|
|
@@ -1213,6 +1343,55 @@ export function SessionView() {
|
|
|
1213
1343
|
}
|
|
1214
1344
|
}
|
|
1215
1345
|
|
|
1346
|
+
const handleSwitchThreadAccount = (newAccountId: string) => {
|
|
1347
|
+
if (!selectedThreadId || !selectedThread) {
|
|
1348
|
+
return
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const newAccount = accounts.find((a) => a.id === newAccountId)
|
|
1352
|
+
if (!newAccount || newAccount.status !== 'online') {
|
|
1353
|
+
setAlertDialog({
|
|
1354
|
+
open: true,
|
|
1355
|
+
title: 'Account Unavailable',
|
|
1356
|
+
message: 'Selected account is not authenticated. Please sign in first.',
|
|
1357
|
+
variant: 'warning',
|
|
1358
|
+
})
|
|
1359
|
+
return
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const previousAccountId = selectedThread.accountId
|
|
1363
|
+
|
|
1364
|
+
// Mark this thread as having a pending account switch
|
|
1365
|
+
// The next message sent will start a fresh conversation on the new account
|
|
1366
|
+
setThreadPendingAccountSwitch(selectedThreadId, {
|
|
1367
|
+
originalThreadId: selectedThreadId,
|
|
1368
|
+
previousAccountId,
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
// Update thread's accountId
|
|
1372
|
+
updateThread(selectedThreadId, { accountId: newAccountId })
|
|
1373
|
+
|
|
1374
|
+
// Reset model selection to the new account's default model
|
|
1375
|
+
const newAccountModels = modelsByAccount[newAccountId] || []
|
|
1376
|
+
const newDefaultModel = newAccountModels.find((m) => m.isDefault) ?? newAccountModels[0]
|
|
1377
|
+
if (newDefaultModel) {
|
|
1378
|
+
setThreadModel(selectedThreadId, newDefaultModel.id)
|
|
1379
|
+
if (newDefaultModel.defaultReasoningEffort) {
|
|
1380
|
+
setThreadEffort(selectedThreadId, newDefaultModel.defaultReasoningEffort)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Add a system message to indicate the switch
|
|
1385
|
+
addMessage(selectedThreadId, {
|
|
1386
|
+
id: `sys-switch-${Date.now()}`,
|
|
1387
|
+
role: 'assistant',
|
|
1388
|
+
content: `Switched to account: ${newAccount.name}. Your next message will start a new conversation using this account.`,
|
|
1389
|
+
kind: 'tool',
|
|
1390
|
+
title: 'Account Switch',
|
|
1391
|
+
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
1392
|
+
})
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1216
1395
|
if (!selectedThread) {
|
|
1217
1396
|
return <SessionEmpty onNewSession={handleEmptyNewSession} />
|
|
1218
1397
|
}
|
|
@@ -1221,11 +1400,14 @@ export function SessionView() {
|
|
|
1221
1400
|
<main className="flex-1 flex flex-col h-full bg-bg-primary overflow-hidden">
|
|
1222
1401
|
<SessionHeader
|
|
1223
1402
|
title={selectedThread.title}
|
|
1403
|
+
accountId={selectedThread.accountId}
|
|
1224
1404
|
accountName={account?.name}
|
|
1405
|
+
accounts={accounts}
|
|
1225
1406
|
model={selectedThread.model}
|
|
1226
1407
|
status={selectedThread.status}
|
|
1227
1408
|
canInteract={canInteract}
|
|
1228
1409
|
onArchive={handleArchive}
|
|
1410
|
+
onSwitchAccount={handleSwitchThreadAccount}
|
|
1229
1411
|
/>
|
|
1230
1412
|
<SessionAuthBanner
|
|
1231
1413
|
visible={!isAccountReady}
|
|
@@ -1235,6 +1417,14 @@ export function SessionView() {
|
|
|
1235
1417
|
onCancel={account?.id && accountLoginIds[account.id] ? handleCancelAuth : undefined}
|
|
1236
1418
|
onRefresh={account ? () => void refreshAccountStatus(account.id) : undefined}
|
|
1237
1419
|
/>
|
|
1420
|
+
<RateLimitBanner
|
|
1421
|
+
visible={showRateLimitBanner}
|
|
1422
|
+
currentAccount={account}
|
|
1423
|
+
availableAccounts={switchableAccounts}
|
|
1424
|
+
errorMessage={rateLimitError ?? undefined}
|
|
1425
|
+
onSwitchAccount={handleSwitchThreadAccount}
|
|
1426
|
+
onDismiss={() => setRateLimitBannerDismissed(true)}
|
|
1427
|
+
/>
|
|
1238
1428
|
<VirtualizedMessageList
|
|
1239
1429
|
messages={threadMessages}
|
|
1240
1430
|
approvals={pendingApprovals}
|
|
@@ -1347,6 +1537,11 @@ export function SessionView() {
|
|
|
1347
1537
|
onCloseResumeDialog={() => setShowResumeDialog(false)}
|
|
1348
1538
|
resumeCandidates={resumeCandidates}
|
|
1349
1539
|
onResumeThread={handleResumeThread}
|
|
1540
|
+
showAccountSwitchDialog={showAccountSwitchDialog}
|
|
1541
|
+
onCloseAccountSwitchDialog={() => setShowAccountSwitchDialog(false)}
|
|
1542
|
+
currentAccount={account}
|
|
1543
|
+
switchableAccounts={switchableAccounts}
|
|
1544
|
+
onSwitchAccount={handleSwitchThreadAccount}
|
|
1350
1545
|
showFeedbackDialog={showFeedbackDialog}
|
|
1351
1546
|
onCloseFeedbackDialog={() => setShowFeedbackDialog(false)}
|
|
1352
1547
|
feedbackCategory={feedbackCategory}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Dialog, Button, Icons } from '../ui'
|
|
3
|
+
import { CodexSettings } from './codex-settings'
|
|
3
4
|
|
|
4
5
|
interface SettingsDialogProps {
|
|
5
6
|
open: boolean
|
|
6
7
|
onClose: () => void
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
type SettingsTab = 'general' | 'accounts' | 'appearance' | 'shortcuts'
|
|
10
|
+
type SettingsTab = 'general' | 'accounts' | 'codex' | 'appearance' | 'shortcuts'
|
|
10
11
|
|
|
11
12
|
export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
12
13
|
const [activeTab, setActiveTab] = useState<SettingsTab>('general')
|
|
@@ -29,6 +30,12 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
29
30
|
active={activeTab === 'accounts'}
|
|
30
31
|
onClick={() => setActiveTab('accounts')}
|
|
31
32
|
/>
|
|
33
|
+
<SettingsNavItem
|
|
34
|
+
icon={<Icons.Terminal className="w-4 h-4" />}
|
|
35
|
+
label="Codex"
|
|
36
|
+
active={activeTab === 'codex'}
|
|
37
|
+
onClick={() => setActiveTab('codex')}
|
|
38
|
+
/>
|
|
32
39
|
<SettingsNavItem
|
|
33
40
|
icon={<Icons.Bolt className="w-4 h-4" />}
|
|
34
41
|
label="Appearance"
|
|
@@ -52,6 +59,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
52
59
|
<div className="flex-1 overflow-y-auto">
|
|
53
60
|
{activeTab === 'general' && <GeneralSettings />}
|
|
54
61
|
{activeTab === 'accounts' && <AccountsSettings />}
|
|
62
|
+
{activeTab === 'codex' && <CodexSettings />}
|
|
55
63
|
{activeTab === 'appearance' && <AppearanceSettings />}
|
|
56
64
|
{activeTab === 'shortcuts' && <ShortcutsSettings />}
|
|
57
65
|
</div>
|