total-recall-brain 3.4.0 → 3.6.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.
@@ -0,0 +1,62 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { listMemory, listResearch, fetchChatThreads } from '../api'
3
+ import type { MemoryNode, ResearchItem } from '../types'
4
+ import type { ChatThread } from '../api'
5
+ import Graph3D from '../components/Graph3D'
6
+
7
+ export default function GraphPage({ activeBrainId }: { activeBrainId?: string }) {
8
+ const [memoryNodes, setMemoryNodes] = useState<MemoryNode[]>([])
9
+ const [researchItems, setResearchItems] = useState<ResearchItem[]>([])
10
+ const [threads, setThreads] = useState<ChatThread[]>([])
11
+ const [loading, setLoading] = useState(true)
12
+ const [selectedGroundingNodes, setSelectedGroundingNodes] = useState<string[]>([])
13
+
14
+ useEffect(() => {
15
+ async function loadData() {
16
+ setLoading(true)
17
+ try {
18
+ const [nodes, researchRes, threadsRes] = await Promise.all([
19
+ listMemory(activeBrainId),
20
+ listResearch(activeBrainId).catch(() => ({ items: [] })),
21
+ fetchChatThreads().catch(() => [])
22
+ ])
23
+ setMemoryNodes(nodes)
24
+ setResearchItems(researchRes.items || [])
25
+ setThreads(threadsRes)
26
+ } catch (err) {
27
+ console.error('Failed to load graph data', err)
28
+ } finally {
29
+ setLoading(false)
30
+ }
31
+ }
32
+ loadData()
33
+ }, [activeBrainId])
34
+
35
+ return (
36
+ <div className="page" style={{ padding: 0, height: 'calc(100vh - 1px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
37
+ <div style={{ padding: '16px 24px', background: 'rgba(18, 18, 26, 0.45)', backdropFilter: 'blur(16px)', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
38
+ <h1 style={{ fontSize: '18px', fontWeight: 700 }}>Sovereign Memory Network</h1>
39
+ <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px' }}>Interactive 3D constellation layout of your knowledge vault, sessions, and active thoughts.</p>
40
+ </div>
41
+
42
+ <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
43
+ {loading ? (
44
+ <div className="skeleton" style={{ width: '100%', height: '100%' }} />
45
+ ) : (
46
+ <Graph3D
47
+ threads={threads}
48
+ memoryNodes={memoryNodes}
49
+ researchItems={researchItems}
50
+ onOpenThread={() => {}}
51
+ onGroundMemoryNode={(slug) => {
52
+ setSelectedGroundingNodes(prev =>
53
+ prev.includes(slug) ? prev.filter(s => s !== slug) : [...prev, slug]
54
+ )
55
+ }}
56
+ selectedGroundingNodes={selectedGroundingNodes}
57
+ />
58
+ )}
59
+ </div>
60
+ </div>
61
+ )
62
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { fetchHealth } from '../api'
3
- import type { HealthData } from '../types'
2
+ import { fetchHealth, checkUpdate, runUpdate } from '../api'
3
+ import type { HealthData, UpdateCheckResult } from '../types'
4
4
 
5
5
  function formatUptime(seconds: number): string {
6
6
  const d = Math.floor(seconds / 86400)
@@ -15,6 +15,12 @@ export default function HealthPage() {
15
15
  const [health, setHealth] = useState<HealthData | null>(null)
16
16
  const [error, setError] = useState('')
17
17
  const [lastPoll, setLastPoll] = useState<Date | null>(null)
18
+
19
+ // Auto-Update States
20
+ const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null)
21
+ const [isUpdating, setIsUpdating] = useState(false)
22
+ const [showConfirmModal, setShowConfirmModal] = useState(false)
23
+ const [updateError, setUpdateError] = useState('')
18
24
 
19
25
  useEffect(() => {
20
26
  let active = true
@@ -33,17 +39,171 @@ export default function HealthPage() {
33
39
 
34
40
  poll()
35
41
  const interval = setInterval(poll, 5000)
36
- return () => { active = false; clearInterval(interval) }
42
+
43
+ // Check for updates
44
+ const checkVersion = async () => {
45
+ try {
46
+ const data = await checkUpdate()
47
+ if (active) setUpdateInfo(data)
48
+ } catch (e) {
49
+ console.error('Failed to check updates', e)
50
+ }
51
+ }
52
+ checkVersion()
53
+ const updateInterval = setInterval(checkVersion, 3600000)
54
+
55
+ return () => {
56
+ active = false;
57
+ clearInterval(interval);
58
+ clearInterval(updateInterval);
59
+ }
37
60
  }, [])
38
61
 
62
+ const handleStartUpdate = async () => {
63
+ setShowConfirmModal(false)
64
+ setIsUpdating(true)
65
+ setUpdateError('')
66
+
67
+ try {
68
+ await runUpdate()
69
+
70
+ // Keep polling health to check when it goes down and recovers
71
+ let serverDown = false
72
+ const pollInterval = setInterval(async () => {
73
+ try {
74
+ const res = await fetchHealth()
75
+ if (serverDown && res.status === 'healthy') {
76
+ clearInterval(pollInterval)
77
+ window.location.reload()
78
+ }
79
+ } catch {
80
+ // Connection failed = server is offline / restarting
81
+ serverDown = true
82
+ }
83
+ }, 2000)
84
+
85
+ } catch (e) {
86
+ setUpdateError((e as Error).message)
87
+ setIsUpdating(false)
88
+ }
89
+ }
90
+
39
91
  return (
40
- <div className="page">
41
- <div className="page-header">
42
- <h1>System Health</h1>
43
- <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
92
+ <div className="page" style={{ position: 'relative' }}>
93
+ {isUpdating && (
94
+ <div style={{
95
+ position: 'fixed',
96
+ inset: 0,
97
+ background: 'rgba(13, 17, 23, 0.9)',
98
+ zIndex: 1000,
99
+ display: 'flex',
100
+ flexDirection: 'column',
101
+ alignItems: 'center',
102
+ justifyContent: 'center',
103
+ backdropFilter: 'blur(8px)',
104
+ animation: 'fadeIn 0.3s ease'
105
+ }}>
106
+ <div className="spinner" style={{
107
+ width: 50,
108
+ height: 50,
109
+ border: '4px solid var(--border)',
110
+ borderTopColor: 'var(--accent)',
111
+ borderRadius: '50%',
112
+ animation: 'spin 1s linear infinite',
113
+ marginBottom: 20
114
+ }} />
115
+ <h2 style={{ color: 'var(--text-primary)', marginBottom: 8 }}>Updating Total Recall...</h2>
116
+ <p style={{ color: 'var(--text-muted)', fontSize: 14 }}>Applying git changes, installing npm modules, and restarting daemon.</p>
117
+ <p style={{ color: 'var(--accent)', fontSize: 13, marginTop: 10 }}>Please do not close this window.</p>
118
+ </div>
119
+ )}
120
+
121
+ {showConfirmModal && (
122
+ <div style={{
123
+ position: 'fixed',
124
+ inset: 0,
125
+ background: 'rgba(0, 0, 0, 0.7)',
126
+ zIndex: 900,
127
+ display: 'flex',
128
+ alignItems: 'center',
129
+ justifyContent: 'center',
130
+ backdropFilter: 'blur(4px)',
131
+ padding: 20
132
+ }}>
133
+ <div className="card" style={{
134
+ maxWidth: 450,
135
+ width: '100%',
136
+ backgroundColor: 'var(--bg-surface)',
137
+ border: '1px solid var(--border)',
138
+ borderRadius: 12,
139
+ padding: 24,
140
+ boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)'
141
+ }}>
142
+ <h3 style={{ marginBottom: 12, color: 'var(--text-primary)' }}>Confirm Core Self-Update</h3>
143
+ <p style={{ color: 'var(--text-muted)', marginBottom: 20, fontSize: 14, lineHeight: 1.5 }}>
144
+ This will pull the latest version of Total Recall ({updateInfo?.latestVersion}) from the git repository, reinstall dependencies, rebuild frontend bundles, and reboot the system kernel.
145
+ </p>
146
+ <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
147
+ <button
148
+ className="btn btn-secondary"
149
+ onClick={() => setShowConfirmModal(false)}
150
+ style={{ padding: '8px 16px', borderRadius: 8 }}
151
+ >
152
+ Cancel
153
+ </button>
154
+ <button
155
+ className="btn btn-primary"
156
+ onClick={handleStartUpdate}
157
+ style={{ padding: '8px 16px', borderRadius: 8, background: 'var(--accent)' }}
158
+ >
159
+ Update Now
160
+ </button>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
167
+ <div>
168
+ <h1>System Health</h1>
169
+ <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
170
+ </div>
171
+ {updateInfo?.updateAvailable && (
172
+ <div className="badge badge-success" style={{
173
+ display: 'flex',
174
+ alignItems: 'center',
175
+ gap: 12,
176
+ padding: '12px 18px',
177
+ borderRadius: 12,
178
+ background: 'rgba(63, 185, 80, 0.1)',
179
+ border: '1px solid rgba(63, 185, 80, 0.3)',
180
+ animation: 'pulse 2s infinite'
181
+ }}>
182
+ <span style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 600 }}>
183
+ New version available: {updateInfo.latestVersion}
184
+ </span>
185
+ <button
186
+ className="btn btn-primary"
187
+ onClick={() => setShowConfirmModal(true)}
188
+ style={{
189
+ fontSize: 12,
190
+ padding: '6px 12px',
191
+ borderRadius: 6,
192
+ backgroundColor: '#3fb950',
193
+ color: '#fff',
194
+ border: 'none',
195
+ cursor: 'pointer',
196
+ fontWeight: 600
197
+ }}
198
+ >
199
+ Update Now
200
+ </button>
201
+ </div>
202
+ )}
44
203
  </div>
45
204
 
46
205
  {error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
206
+ {updateError && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ Update failed: {updateError}</div>}
47
207
 
48
208
  {health ? (
49
209
  <>
@@ -55,7 +215,9 @@ export default function HealthPage() {
55
215
  <div className="label">Status</div>
56
216
  </div>
57
217
  <div className="stat-card">
58
- <div className="value" style={{ color: 'var(--accent)' }}>{health.version}</div>
218
+ <div className="value" style={{ color: 'var(--accent)' }}>
219
+ {health.version} {updateInfo && !updateInfo.updateAvailable && <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>(Latest)</span>}
220
+ </div>
59
221
  <div className="label">Version</div>
60
222
  </div>
61
223
  <div className="stat-card">
@@ -76,6 +238,10 @@ export default function HealthPage() {
76
238
  <tr><th>Version</th><td>{health.version}</td></tr>
77
239
  <tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
78
240
  <tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
241
+ <tr><th>Daemon Status</th><td>{health.daemon || 'unknown'}</td></tr>
242
+ {health.cli_agents && (
243
+ <tr><th>CLI Agents</th><td>{health.cli_agents.join(', ')}</td></tr>
244
+ )}
79
245
  <tr><th>Polling</th><td>Every 5 seconds</td></tr>
80
246
  </tbody>
81
247
  </table>
@@ -86,6 +252,17 @@ export default function HealthPage() {
86
252
  {[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
87
253
  </div>
88
254
  ) : null}
255
+
256
+ <style>{`
257
+ @keyframes spin {
258
+ 0% { transform: rotate(0deg); }
259
+ 100% { transform: rotate(360deg); }
260
+ }
261
+ @keyframes fadeIn {
262
+ from { opacity: 0; }
263
+ to { opacity: 1; }
264
+ }
265
+ `}</style>
89
266
  </div>
90
267
  )
91
268
  }
@@ -0,0 +1,141 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { fetchHelpTopics, fetchHelpContent } from '../api'
3
+ import type { HelpTopic } from '../api'
4
+
5
+ function renderMarkdown(md: string) {
6
+ if (!md) return ''
7
+ let html = md
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+
12
+ // Headings
13
+ html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>')
14
+ html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>')
15
+ html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>')
16
+
17
+ // Bold & Italic
18
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
19
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
20
+
21
+ // Code blocks
22
+ html = html.replace(
23
+ /```([\s\S]*?)```/g,
24
+ '<pre style="background:rgba(0,0,0,0.3);padding:12px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);font-family:monospace;overflow-x:auto;margin:12px 0;"><code>$1</code></pre>'
25
+ )
26
+ html = html.replace(
27
+ /`(.*?)`/g,
28
+ '<code style="background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-family:monospace;">$1</code>'
29
+ )
30
+
31
+ // Blockquotes
32
+ html = html.replace(
33
+ /^> (.*$)/gim,
34
+ '<blockquote style="border-left:4px solid var(--purple-primary);padding-left:14px;color:var(--text-secondary);margin:16px 0;font-style:italic;">$1</blockquote>'
35
+ )
36
+
37
+ // Lists
38
+ html = html.replace(/^- (.*$)/gim, '<li style="margin-left:20px;list-style-type:disc;">$1</li>')
39
+
40
+ // Linebreaks
41
+ html = html.replace(/\n/g, '<br />')
42
+
43
+ return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />
44
+ }
45
+
46
+ export default function HelpPage() {
47
+ const [topics, setTopics] = useState<HelpTopic[]>([])
48
+ const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
49
+ const [content, setContent] = useState<string>('')
50
+ const [loading, setLoading] = useState<boolean>(false)
51
+ const [error, setError] = useState<string>('')
52
+
53
+ useEffect(() => {
54
+ async function loadTopics() {
55
+ try {
56
+ const data = await fetchHelpTopics()
57
+ setTopics(data.topics)
58
+ if (data.topics.length > 0) {
59
+ setSelectedTopic(data.topics[0].id)
60
+ }
61
+ } catch (err: unknown) {
62
+ setError((err as Error).message || 'Failed to load help topics')
63
+ }
64
+ }
65
+ loadTopics()
66
+ }, [])
67
+
68
+ useEffect(() => {
69
+ if (!selectedTopic) return
70
+ async function loadContent() {
71
+ setLoading(true)
72
+ try {
73
+ const data = await fetchHelpContent(selectedTopic!)
74
+ setContent(data.content)
75
+ } catch (err: unknown) {
76
+ setError((err as Error).message || 'Failed to load help details')
77
+ } finally {
78
+ setLoading(false)
79
+ }
80
+ }
81
+ loadContent()
82
+ }, [selectedTopic])
83
+
84
+ return (
85
+ <div className="page" style={{ padding: '24px', maxWidth: '1100px', margin: '0 auto' }}>
86
+ <div className="page-header" style={{ marginBottom: '24px' }}>
87
+ <h1>Sovereign Help System</h1>
88
+ <p>Offline interactive documentation, CLI command references, and SSSS VFS specifications.</p>
89
+ </div>
90
+
91
+ {error && <div className="badge badge-error" style={{ marginBottom: '16px' }}>⚠️ {error}</div>}
92
+
93
+ <div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: '30px', minHeight: '500px' }}>
94
+ {/* Sidebar */}
95
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
96
+ {topics.map((t) => (
97
+ <button
98
+ key={t.id}
99
+ onClick={() => setSelectedTopic(t.id)}
100
+ style={{
101
+ textAlign: 'left',
102
+ padding: '12px 16px',
103
+ borderRadius: '8px',
104
+ background: selectedTopic === t.id ? 'var(--purple-primary)' : 'rgba(255, 255, 255, 0.03)',
105
+ color: '#fff',
106
+ border: selectedTopic === t.id ? '1px solid var(--purple-border)' : '1px solid transparent',
107
+ cursor: 'pointer',
108
+ transition: 'all 0.2s',
109
+ }}
110
+ >
111
+ <div style={{ fontWeight: 'bold', fontSize: '14px' }}>{t.title}</div>
112
+ <div style={{ fontSize: '11px', color: selectedTopic === t.id ? 'rgba(255, 255, 255, 0.8)' : 'var(--text-secondary)', marginTop: '4px' }}>
113
+ {t.description}
114
+ </div>
115
+ </button>
116
+ ))}
117
+ </div>
118
+
119
+ {/* Content Pane */}
120
+ <div
121
+ style={{
122
+ background: 'var(--surface-bg)',
123
+ border: '1px solid var(--border)',
124
+ borderRadius: '12px',
125
+ padding: '30px',
126
+ overflowY: 'auto',
127
+ maxHeight: '75vh',
128
+ }}
129
+ >
130
+ {loading ? (
131
+ <div className="skeleton" style={{ height: '400px' }} />
132
+ ) : (
133
+ <div className="markdown-body" style={{ color: 'var(--text-primary)', lineHeight: '1.7', fontSize: '14px' }}>
134
+ {renderMarkdown(content)}
135
+ </div>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
@@ -35,6 +35,7 @@ export default function SettingsPage() {
35
35
  if (!data.budget.budget) data.budget.budget = { daily_cap_usd: 5.0, weekly_cap_usd: 25.0, enabled: true };
36
36
  if (!data.brain) data.brain = {};
37
37
  if (!data.brain.preferred_agent) data.brain.preferred_agent = 'auto';
38
+ if (!data.secrets) data.secrets = {};
38
39
 
39
40
  setConfigData(data);
40
41
  } catch (err: unknown) {
@@ -152,6 +153,17 @@ export default function SettingsPage() {
152
153
  });
153
154
  };
154
155
 
156
+ const updateSecretsProp = (prop: string, value: string) => {
157
+ if (!configData) return;
158
+ setConfigData({
159
+ ...configData,
160
+ secrets: {
161
+ ...configData.secrets,
162
+ [prop]: value
163
+ }
164
+ });
165
+ };
166
+
155
167
  return (
156
168
  <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
157
169
  <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
@@ -209,7 +221,7 @@ export default function SettingsPage() {
209
221
  </div>
210
222
  ) : viewMode === 'visual' && configData ? (
211
223
  /* ==================== VISUAL CONTROL PANEL UI ==================== */
212
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
224
+ <form onSubmit={(e) => { e.preventDefault(); handleSaveVisual(); }} style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
213
225
 
214
226
  {/* Card 1: AI Reasoning & Autonomy */}
215
227
  <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
@@ -537,7 +549,190 @@ export default function SettingsPage() {
537
549
  </div>
538
550
  </div>
539
551
 
540
- </div>
552
+ {/* Card 5: API Keys & Integrations */}
553
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
554
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
555
+ <span style={{ fontSize: 20 }}>🔑</span>
556
+ <div>
557
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>API Keys & Integrations</h3>
558
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Manage access keys and tokens for AI models and search providers</p>
559
+ </div>
560
+ </div>
561
+
562
+ {/* Google Gemini API Key */}
563
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
564
+ <label htmlFor="google_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Google Gemini API Key</label>
565
+ <input
566
+ id="google_api_key"
567
+ type="password"
568
+ placeholder="AIzaSy..."
569
+ value={configData.secrets?.google_api_key || ''}
570
+ onChange={(e) => updateSecretsProp('google_api_key', e.target.value)}
571
+ style={{
572
+ background: 'var(--bg-tertiary)',
573
+ color: 'var(--text-primary)',
574
+ border: '1px solid var(--border)',
575
+ padding: '8px 12px',
576
+ borderRadius: 6,
577
+ outline: 'none',
578
+ fontSize: 13
579
+ }}
580
+ />
581
+ </div>
582
+
583
+ {/* Anthropic API Key */}
584
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
585
+ <label htmlFor="anthropic_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Anthropic API Key (Claude Code)</label>
586
+ <input
587
+ id="anthropic_api_key"
588
+ type="password"
589
+ placeholder="sk-ant-..."
590
+ value={configData.secrets?.anthropic_api_key || ''}
591
+ onChange={(e) => updateSecretsProp('anthropic_api_key', e.target.value)}
592
+ style={{
593
+ background: 'var(--bg-tertiary)',
594
+ color: 'var(--text-primary)',
595
+ border: '1px solid var(--border)',
596
+ padding: '8px 12px',
597
+ borderRadius: 6,
598
+ outline: 'none',
599
+ fontSize: 13
600
+ }}
601
+ />
602
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Configuring this allows Claude Code CLI to run without prompting for OAuth login.</span>
603
+ </div>
604
+
605
+ {/* OpenAI API Key */}
606
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
607
+ <label htmlFor="openai_api_key" style={{ fontSize: 13, fontWeight: 500 }}>OpenAI API Key (Codex)</label>
608
+ <input
609
+ id="openai_api_key"
610
+ type="password"
611
+ placeholder="sk-proj-..."
612
+ value={configData.secrets?.openai_api_key || ''}
613
+ onChange={(e) => updateSecretsProp('openai_api_key', e.target.value)}
614
+ style={{
615
+ background: 'var(--bg-tertiary)',
616
+ color: 'var(--text-primary)',
617
+ border: '1px solid var(--border)',
618
+ padding: '8px 12px',
619
+ borderRadius: 6,
620
+ outline: 'none',
621
+ fontSize: 13
622
+ }}
623
+ />
624
+ </div>
625
+
626
+ {/* Tavily API Key */}
627
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
628
+ <label htmlFor="tavily_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Tavily Search API Key</label>
629
+ <input
630
+ id="tavily_api_key"
631
+ type="password"
632
+ placeholder="tvly-..."
633
+ value={configData.secrets?.tavily_api_key || ''}
634
+ onChange={(e) => updateSecretsProp('tavily_api_key', e.target.value)}
635
+ style={{
636
+ background: 'var(--bg-tertiary)',
637
+ color: 'var(--text-primary)',
638
+ border: '1px solid var(--border)',
639
+ padding: '8px 12px',
640
+ borderRadius: 6,
641
+ outline: 'none',
642
+ fontSize: 13
643
+ }}
644
+ />
645
+ </div>
646
+
647
+ {/* Other Search Keys (Brave, Exa, Serper) */}
648
+ <details style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
649
+ <summary style={{ cursor: 'pointer', fontWeight: 500, outline: 'none', padding: '4px 0' }}>More Provider Keys (Brave, Exa, Serper, GitHub)</summary>
650
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12, paddingLeft: 8, borderLeft: '2px solid var(--border)' }}>
651
+ {/* Brave Search Key */}
652
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
653
+ <label htmlFor="brave_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Brave Search API Key</label>
654
+ <input
655
+ id="brave_api_key"
656
+ type="password"
657
+ value={configData.secrets?.brave_api_key || ''}
658
+ onChange={(e) => updateSecretsProp('brave_api_key', e.target.value)}
659
+ style={{
660
+ background: 'var(--bg-tertiary)',
661
+ color: 'var(--text-primary)',
662
+ border: '1px solid var(--border)',
663
+ padding: '8px 12px',
664
+ borderRadius: 6,
665
+ outline: 'none',
666
+ fontSize: 13
667
+ }}
668
+ />
669
+ </div>
670
+
671
+ {/* Exa Search Key */}
672
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
673
+ <label htmlFor="exa_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Exa API Key</label>
674
+ <input
675
+ id="exa_api_key"
676
+ type="password"
677
+ value={configData.secrets?.exa_api_key || ''}
678
+ onChange={(e) => updateSecretsProp('exa_api_key', e.target.value)}
679
+ style={{
680
+ background: 'var(--bg-tertiary)',
681
+ color: 'var(--text-primary)',
682
+ border: '1px solid var(--border)',
683
+ padding: '8px 12px',
684
+ borderRadius: 6,
685
+ outline: 'none',
686
+ fontSize: 13
687
+ }}
688
+ />
689
+ </div>
690
+
691
+ {/* Serper API Key */}
692
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
693
+ <label htmlFor="serper_api_key" style={{ fontSize: 12, fontWeight: 500 }}>Serper (Google Search API) Key</label>
694
+ <input
695
+ id="serper_api_key"
696
+ type="password"
697
+ value={configData.secrets?.serper_api_key || ''}
698
+ onChange={(e) => updateSecretsProp('serper_api_key', e.target.value)}
699
+ style={{
700
+ background: 'var(--bg-tertiary)',
701
+ color: 'var(--text-primary)',
702
+ border: '1px solid var(--border)',
703
+ padding: '8px 12px',
704
+ borderRadius: 6,
705
+ outline: 'none',
706
+ fontSize: 13
707
+ }}
708
+ />
709
+ </div>
710
+
711
+ {/* GitHub Token */}
712
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
713
+ <label htmlFor="github_token" style={{ fontSize: 12, fontWeight: 500 }}>GitHub Personal Access Token</label>
714
+ <input
715
+ id="github_token"
716
+ type="password"
717
+ placeholder="ghp_..."
718
+ value={configData.secrets?.github_token || ''}
719
+ onChange={(e) => updateSecretsProp('github_token', e.target.value)}
720
+ style={{
721
+ background: 'var(--bg-tertiary)',
722
+ color: 'var(--text-primary)',
723
+ border: '1px solid var(--border)',
724
+ padding: '8px 12px',
725
+ borderRadius: 6,
726
+ outline: 'none',
727
+ fontSize: 13
728
+ }}
729
+ />
730
+ </div>
731
+ </div>
732
+ </details>
733
+ </div>
734
+
735
+ </form>
541
736
  ) : (
542
737
  /* ==================== RAW YAML EDITOR UI ==================== */
543
738
  <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>