total-recall-brain 3.1.2 → 3.2.2
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/extension/background.js +99 -0
- package/extension/content-script.css +16 -0
- package/extension/content-script.js +134 -0
- package/extension/icons/README.md +27 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/lib/brain-client.js +47 -0
- package/extension/manifest.json +39 -0
- package/extension/options/options.css +218 -0
- package/extension/options/options.html +72 -0
- package/extension/options/options.js +99 -0
- package/extension/popup/popup.css +221 -0
- package/extension/popup/popup.html +44 -0
- package/extension/popup/popup.js +107 -0
- package/extension/sidepanel/sidepanel.css +340 -0
- package/extension/sidepanel/sidepanel.html +38 -0
- package/extension/sidepanel/sidepanel.js +229 -0
- package/frontend/README.md +73 -0
- package/frontend/index.html +17 -0
- package/frontend/package.json +31 -0
- package/frontend/src/App.css +2 -0
- package/frontend/src/App.tsx +390 -0
- package/frontend/src/api.ts +688 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/BrainSelector.tsx +272 -0
- package/frontend/src/components/DaemonLogsTab.tsx +54 -0
- package/frontend/src/components/Graph3D.tsx +850 -0
- package/frontend/src/components/MarkdownUtils.tsx +99 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
- package/frontend/src/components/TaskQueueTab.tsx +185 -0
- package/frontend/src/index.css +1589 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/pages/ApiKeysPage.tsx +288 -0
- package/frontend/src/pages/AutomationsPage.tsx +354 -0
- package/frontend/src/pages/ChatPage.tsx +831 -0
- package/frontend/src/pages/DeploymentsPage.tsx +275 -0
- package/frontend/src/pages/FilesPage.tsx +384 -0
- package/frontend/src/pages/HealthPage.tsx +91 -0
- package/frontend/src/pages/IntegrationsPage.tsx +382 -0
- package/frontend/src/pages/LoginPage.tsx +237 -0
- package/frontend/src/pages/MemoryPage.tsx +1192 -0
- package/frontend/src/pages/SandboxPage.tsx +113 -0
- package/frontend/src/pages/SettingsPage.tsx +662 -0
- package/frontend/src/pages/SkillsPage.tsx +618 -0
- package/frontend/src/pages/TasksPage.tsx +392 -0
- package/frontend/src/pages/UsagePage.tsx +260 -0
- package/frontend/src/types.ts +163 -0
- package/frontend/tsconfig.app.json +25 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/tsconfig.node.json +24 -0
- package/frontend/vite.config.ts +24 -0
- package/package.json +8 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
- package/src/cli/backup.mjs +172 -1
- package/src/cli/setup.mjs +24 -0
- package/src/server/rest.mjs +56 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { fetchHealth } from '../api'
|
|
3
|
+
import type { HealthData } from '../types'
|
|
4
|
+
|
|
5
|
+
function formatUptime(seconds: number): string {
|
|
6
|
+
const d = Math.floor(seconds / 86400)
|
|
7
|
+
const h = Math.floor((seconds % 86400) / 3600)
|
|
8
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
9
|
+
if (d > 0) return `${d}d ${h}h ${m}m`
|
|
10
|
+
if (h > 0) return `${h}h ${m}m`
|
|
11
|
+
return `${m}m ${seconds % 60}s`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function HealthPage() {
|
|
15
|
+
const [health, setHealth] = useState<HealthData | null>(null)
|
|
16
|
+
const [error, setError] = useState('')
|
|
17
|
+
const [lastPoll, setLastPoll] = useState<Date | null>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let active = true
|
|
21
|
+
const poll = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const data = await fetchHealth()
|
|
24
|
+
if (active) {
|
|
25
|
+
setHealth(data)
|
|
26
|
+
setError('')
|
|
27
|
+
setLastPoll(new Date())
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (active) setError((e as Error).message)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
poll()
|
|
35
|
+
const interval = setInterval(poll, 5000)
|
|
36
|
+
return () => { active = false; clearInterval(interval) }
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
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>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
|
|
47
|
+
|
|
48
|
+
{health ? (
|
|
49
|
+
<>
|
|
50
|
+
<div className="health-grid">
|
|
51
|
+
<div className="stat-card healthy">
|
|
52
|
+
<div className="value" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
|
53
|
+
<span className="pulse" /> {health.status}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="label">Status</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="stat-card">
|
|
58
|
+
<div className="value" style={{ color: 'var(--accent)' }}>{health.version}</div>
|
|
59
|
+
<div className="label">Version</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="stat-card">
|
|
62
|
+
<div className="value" style={{ color: 'var(--text-primary)' }}>{formatUptime(health.uptime_seconds)}</div>
|
|
63
|
+
<div className="label">Uptime</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="stat-card">
|
|
66
|
+
<div className="value" style={{ color: 'var(--text-primary)', fontSize: 16 }}>{new Date(health.timestamp).toLocaleString()}</div>
|
|
67
|
+
<div className="label">Server Time</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="health-log">
|
|
72
|
+
<h3>System Info</h3>
|
|
73
|
+
<table>
|
|
74
|
+
<tbody>
|
|
75
|
+
<tr><th>Status</th><td>{health.status}</td></tr>
|
|
76
|
+
<tr><th>Version</th><td>{health.version}</td></tr>
|
|
77
|
+
<tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
|
|
78
|
+
<tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
|
|
79
|
+
<tr><th>Polling</th><td>Every 5 seconds</td></tr>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
) : !error ? (
|
|
85
|
+
<div className="health-grid">
|
|
86
|
+
{[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect } from 'react'
|
|
2
|
+
import { getApiBase, listApiKeys, connectClient, fetchActiveIntegrations } from '../api'
|
|
3
|
+
import type { ApiKey } from '../api'
|
|
4
|
+
|
|
5
|
+
type Preset = {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
surface: string
|
|
9
|
+
scopes: string
|
|
10
|
+
command: string
|
|
11
|
+
snippet: (baseUrl: string) => string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PRESETS: Preset[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'antigravity',
|
|
17
|
+
name: 'Antigravity',
|
|
18
|
+
surface: 'AGENTS.md',
|
|
19
|
+
scopes: 'ssss:read, memory:read',
|
|
20
|
+
command: 'npx total-recall connect antigravity',
|
|
21
|
+
snippet: baseUrl => `Read AGENTS.md from the repo root.\nDiscovery: ${baseUrl}/.well-known/total-recall.json`
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'cursor',
|
|
25
|
+
name: 'Cursor',
|
|
26
|
+
surface: '.cursor/rules/total-recall.mdc',
|
|
27
|
+
scopes: 'ssss:read, memory:read',
|
|
28
|
+
command: 'npx total-recall connect cursor',
|
|
29
|
+
snippet: baseUrl => `Discovery: ${baseUrl}/.well-known/total-recall.json`
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'claude-code',
|
|
33
|
+
name: 'Claude Code',
|
|
34
|
+
surface: 'CLAUDE.md',
|
|
35
|
+
scopes: 'ssss:read, memory:read',
|
|
36
|
+
command: 'npx total-recall connect claude-code',
|
|
37
|
+
snippet: () => `Read CLAUDE.md from the repo root.`
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'codex',
|
|
41
|
+
name: 'Codex',
|
|
42
|
+
surface: 'AGENTS.md',
|
|
43
|
+
scopes: 'ssss:read, memory:read',
|
|
44
|
+
command: 'npx total-recall connect codex',
|
|
45
|
+
snippet: baseUrl => `Read AGENTS.md from the repo root.\nDiscovery: ${baseUrl}/.well-known/total-recall.json`
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'ultrachat',
|
|
49
|
+
name: 'UltraChat',
|
|
50
|
+
surface: 'OpenAI-compatible provider',
|
|
51
|
+
scopes: 'models:read, chat:write, ssss:read',
|
|
52
|
+
command: 'npx total-recall connect ultrachat --brain <brain-url>',
|
|
53
|
+
snippet: baseUrl => `Base URL: ${baseUrl}/v1\nModel: total-recall/gemma4\nModels: ${baseUrl}/v1/models`
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'generic',
|
|
57
|
+
name: 'Generic Client',
|
|
58
|
+
surface: 'REST + OpenAI-compatible API',
|
|
59
|
+
scopes: 'models:read, chat:write, memory:read',
|
|
60
|
+
command: 'npx total-recall connect generic --brain <brain-url>',
|
|
61
|
+
snippet: baseUrl => `POST ${baseUrl}/v1/chat/completions\nGET ${baseUrl}/api/ssss\nAuthorization: Bearer <PAT>`
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
export default function IntegrationsPage() {
|
|
66
|
+
const [copied, setCopied] = useState<string | null>(null)
|
|
67
|
+
const [connecting, setConnecting] = useState<string | null>(null)
|
|
68
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
|
69
|
+
const [keys, setKeys] = useState<ApiKey[]>([])
|
|
70
|
+
const [activeIdes, setActiveIdes] = useState<string[]>([])
|
|
71
|
+
|
|
72
|
+
const baseUrl = useMemo(() => {
|
|
73
|
+
const configured = getApiBase()
|
|
74
|
+
if (configured) return configured.replace(/\/$/, '')
|
|
75
|
+
return window.location.origin
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let active = true
|
|
80
|
+
listApiKeys()
|
|
81
|
+
.then(res => {
|
|
82
|
+
if (active) setKeys(res.keys || [])
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {})
|
|
85
|
+
|
|
86
|
+
fetchActiveIntegrations()
|
|
87
|
+
.then(res => {
|
|
88
|
+
if (active && res.success && Array.isArray(res.active)) {
|
|
89
|
+
setActiveIdes(res.active)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch(() => {})
|
|
93
|
+
|
|
94
|
+
return () => { active = false }
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const filteredPresets = useMemo(() => {
|
|
98
|
+
if (activeIdes.length === 0) return PRESETS
|
|
99
|
+
return PRESETS.filter(preset => {
|
|
100
|
+
if (['ultrachat', 'generic'].includes(preset.id)) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
return activeIdes.includes(preset.id)
|
|
104
|
+
})
|
|
105
|
+
}, [activeIdes])
|
|
106
|
+
|
|
107
|
+
const copy = async (id: string, value: string) => {
|
|
108
|
+
await navigator.clipboard.writeText(value)
|
|
109
|
+
setCopied(id)
|
|
110
|
+
setTimeout(() => setCopied(null), 1800)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleConnect = async (presetId: string) => {
|
|
114
|
+
setConnecting(presetId)
|
|
115
|
+
setMessage(null)
|
|
116
|
+
try {
|
|
117
|
+
const result = await connectClient(presetId, baseUrl)
|
|
118
|
+
if (result.success) {
|
|
119
|
+
setMessage({ type: 'success', text: `Successfully connected ${presetId}! All config files have been automatically updated.` })
|
|
120
|
+
// Refresh keys to show registered/connected status badge
|
|
121
|
+
const res = await listApiKeys()
|
|
122
|
+
setKeys(res.keys || [])
|
|
123
|
+
} else {
|
|
124
|
+
setMessage({ type: 'error', text: `Failed to connect: ${result.message}` })
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
setMessage({ type: 'error', text: (e as Error).message || 'Failed to connect.' })
|
|
128
|
+
} finally {
|
|
129
|
+
setConnecting(null)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="page">
|
|
135
|
+
<div className="page-header">
|
|
136
|
+
<h1>Integrations</h1>
|
|
137
|
+
<p>Connect IDEs, UltraChat, and scripts to this Total Recall brain</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{message && (
|
|
141
|
+
<div style={{
|
|
142
|
+
padding: '12px 16px',
|
|
143
|
+
borderRadius: 8,
|
|
144
|
+
marginBottom: 16,
|
|
145
|
+
fontSize: 14,
|
|
146
|
+
fontWeight: 500,
|
|
147
|
+
background: message.type === 'success' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
|
148
|
+
border: message.type === 'success' ? '1px solid rgba(16, 185, 129, 0.3)' : '1px solid rgba(239, 68, 68, 0.3)',
|
|
149
|
+
color: message.type === 'success' ? '#34d399' : '#f87171',
|
|
150
|
+
display: 'flex',
|
|
151
|
+
justifyContent: 'space-between',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
backdropFilter: 'blur(8px)',
|
|
154
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
155
|
+
}}>
|
|
156
|
+
<span>{message.text}</span>
|
|
157
|
+
<button
|
|
158
|
+
onClick={() => setMessage(null)}
|
|
159
|
+
style={{
|
|
160
|
+
background: 'none',
|
|
161
|
+
border: 'none',
|
|
162
|
+
color: 'inherit',
|
|
163
|
+
cursor: 'pointer',
|
|
164
|
+
fontSize: 18,
|
|
165
|
+
padding: '0 4px',
|
|
166
|
+
lineHeight: '1',
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
×
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Explanatory Banner */}
|
|
175
|
+
<div style={{
|
|
176
|
+
background: 'rgba(30, 41, 59, 0.4)',
|
|
177
|
+
backdropFilter: 'blur(8px)',
|
|
178
|
+
border: '1px solid var(--border)',
|
|
179
|
+
borderRadius: 12,
|
|
180
|
+
padding: 20,
|
|
181
|
+
marginBottom: 22,
|
|
182
|
+
color: 'var(--text-secondary)',
|
|
183
|
+
fontSize: 14,
|
|
184
|
+
lineHeight: '1.5',
|
|
185
|
+
}}>
|
|
186
|
+
<strong style={{ color: 'var(--text-primary)', display: 'block', marginBottom: 6 }}>💡 How Integrations Work</strong>
|
|
187
|
+
Total Recall acts as a secure, local-first semantic memory hub. To give your development environments (Cursor, Claude Code, etc.) access to your memory vault and instruction layers, they communicate as API clients using secure tokens.
|
|
188
|
+
<br />
|
|
189
|
+
<span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>
|
|
190
|
+
The status badges below dynamically inspect whether your workspace has successfully registered its connection token and communicated with your active brain.
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div style={{
|
|
195
|
+
border: '1px solid var(--border)',
|
|
196
|
+
background: 'var(--bg-secondary)',
|
|
197
|
+
borderRadius: 12,
|
|
198
|
+
padding: 18,
|
|
199
|
+
marginBottom: 22,
|
|
200
|
+
display: 'grid',
|
|
201
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
|
202
|
+
gap: 12,
|
|
203
|
+
fontSize: 13,
|
|
204
|
+
}}>
|
|
205
|
+
<div>
|
|
206
|
+
<div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Discovery</div>
|
|
207
|
+
<code>{baseUrl}/.well-known/total-recall.json</code>
|
|
208
|
+
</div>
|
|
209
|
+
<div>
|
|
210
|
+
<div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Chat API</div>
|
|
211
|
+
<code>{baseUrl}/v1/chat/completions</code>
|
|
212
|
+
</div>
|
|
213
|
+
<div>
|
|
214
|
+
<div style={{ color: 'var(--text-tertiary)', marginBottom: 4 }}>Model</div>
|
|
215
|
+
<code>total-recall/gemma4</code>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div style={{
|
|
220
|
+
display: 'grid',
|
|
221
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
|
222
|
+
gap: 16,
|
|
223
|
+
}}>
|
|
224
|
+
{filteredPresets.map(preset => {
|
|
225
|
+
const snippet = preset.snippet(baseUrl)
|
|
226
|
+
|
|
227
|
+
// Match keys by name (e.g. "Cursor Link", "Claude Code Link", "Codex Link")
|
|
228
|
+
const matchingKey = keys.find(k => {
|
|
229
|
+
if (preset.id === 'cursor') return k.name.toLowerCase().includes('cursor') && !k.revoked;
|
|
230
|
+
if (preset.id === 'claude-code') return k.name.toLowerCase().includes('claude') && !k.revoked;
|
|
231
|
+
if (preset.id === 'codex') return (k.name.toLowerCase().includes('codex') || k.name.toLowerCase().includes('agents')) && !k.revoked;
|
|
232
|
+
if (preset.id === 'antigravity') return k.name.toLowerCase().includes('antigravity') && !k.revoked;
|
|
233
|
+
if (preset.id === 'ultrachat') return k.name.toLowerCase().includes('ultrachat') && !k.revoked;
|
|
234
|
+
return false;
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
let statusBadge = (
|
|
238
|
+
<span style={{
|
|
239
|
+
display: 'inline-flex',
|
|
240
|
+
alignItems: 'center',
|
|
241
|
+
gap: 6,
|
|
242
|
+
padding: '4px 8px',
|
|
243
|
+
borderRadius: 6,
|
|
244
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
245
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
246
|
+
fontSize: 12,
|
|
247
|
+
color: 'var(--text-tertiary)'
|
|
248
|
+
}}>
|
|
249
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#94a3b8' }} />
|
|
250
|
+
Not Connected
|
|
251
|
+
</span>
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (matchingKey) {
|
|
255
|
+
if (matchingKey.hit_count > 0) {
|
|
256
|
+
const lastUsedStr = matchingKey.last_used_at
|
|
257
|
+
? ` (Active ${new Date(matchingKey.last_used_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`
|
|
258
|
+
: ''
|
|
259
|
+
statusBadge = (
|
|
260
|
+
<span style={{
|
|
261
|
+
display: 'inline-flex',
|
|
262
|
+
alignItems: 'center',
|
|
263
|
+
gap: 6,
|
|
264
|
+
padding: '4px 8px',
|
|
265
|
+
borderRadius: 6,
|
|
266
|
+
background: 'rgba(16, 185, 129, 0.1)',
|
|
267
|
+
border: '1px solid rgba(16, 185, 129, 0.3)',
|
|
268
|
+
fontSize: 12,
|
|
269
|
+
color: '#34d399',
|
|
270
|
+
fontWeight: 500,
|
|
271
|
+
}}>
|
|
272
|
+
<span style={{
|
|
273
|
+
width: 8,
|
|
274
|
+
height: 8,
|
|
275
|
+
borderRadius: '50%',
|
|
276
|
+
background: '#10b981',
|
|
277
|
+
boxShadow: '0 0 8px #10b981',
|
|
278
|
+
}} />
|
|
279
|
+
Connected{lastUsedStr}
|
|
280
|
+
</span>
|
|
281
|
+
)
|
|
282
|
+
} else {
|
|
283
|
+
statusBadge = (
|
|
284
|
+
<span style={{
|
|
285
|
+
display: 'inline-flex',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
gap: 6,
|
|
288
|
+
padding: '4px 8px',
|
|
289
|
+
borderRadius: 6,
|
|
290
|
+
background: 'rgba(245, 158, 11, 0.1)',
|
|
291
|
+
border: '1px solid rgba(245, 158, 11, 0.3)',
|
|
292
|
+
fontSize: 12,
|
|
293
|
+
color: '#fbbf24',
|
|
294
|
+
fontWeight: 500,
|
|
295
|
+
}}>
|
|
296
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f59e0b' }} />
|
|
297
|
+
Registered (Pending Activity)
|
|
298
|
+
</span>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div key={preset.id} style={{
|
|
305
|
+
border: '1px solid var(--border)',
|
|
306
|
+
background: 'var(--bg-secondary)',
|
|
307
|
+
borderRadius: 12,
|
|
308
|
+
padding: 18,
|
|
309
|
+
display: 'flex',
|
|
310
|
+
flexDirection: 'column',
|
|
311
|
+
gap: 12,
|
|
312
|
+
position: 'relative',
|
|
313
|
+
}}>
|
|
314
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
|
315
|
+
<div>
|
|
316
|
+
<h3 style={{ margin: '0 0 4px' }}>{preset.name}</h3>
|
|
317
|
+
<div style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>{preset.surface}</div>
|
|
318
|
+
</div>
|
|
319
|
+
{statusBadge}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
323
|
+
Recommended scopes: <code>{preset.scopes}</code>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div style={{
|
|
327
|
+
background: 'var(--bg-primary)',
|
|
328
|
+
border: '1px solid var(--border)',
|
|
329
|
+
borderRadius: 8,
|
|
330
|
+
padding: 10,
|
|
331
|
+
fontFamily: 'monospace',
|
|
332
|
+
fontSize: 12,
|
|
333
|
+
whiteSpace: 'pre-wrap',
|
|
334
|
+
color: 'var(--text-secondary)',
|
|
335
|
+
}}>{preset.command}</div>
|
|
336
|
+
|
|
337
|
+
<div style={{
|
|
338
|
+
background: 'var(--bg-primary)',
|
|
339
|
+
border: '1px solid var(--border)',
|
|
340
|
+
borderRadius: 8,
|
|
341
|
+
padding: 10,
|
|
342
|
+
fontFamily: 'monospace',
|
|
343
|
+
fontSize: 12,
|
|
344
|
+
whiteSpace: 'pre-wrap',
|
|
345
|
+
color: 'var(--text-secondary)',
|
|
346
|
+
}}>{snippet}</div>
|
|
347
|
+
|
|
348
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
|
|
349
|
+
<button
|
|
350
|
+
className="btn btn-sm btn-primary"
|
|
351
|
+
onClick={() => handleConnect(preset.id)}
|
|
352
|
+
disabled={connecting !== null}
|
|
353
|
+
style={{
|
|
354
|
+
background: 'linear-gradient(135deg, #10b981 0%, #047857 100%)',
|
|
355
|
+
border: 'none',
|
|
356
|
+
boxShadow: '0 2px 4px rgba(16, 185, 129, 0.3)',
|
|
357
|
+
fontWeight: 600,
|
|
358
|
+
cursor: 'pointer',
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
{connecting === preset.id ? 'Connecting...' : 'Connect'}
|
|
362
|
+
</button>
|
|
363
|
+
<button
|
|
364
|
+
className="btn btn-sm"
|
|
365
|
+
onClick={() => copy(preset.id, `${preset.command}\n\n${snippet}`)}
|
|
366
|
+
style={{
|
|
367
|
+
background: 'rgba(255, 255, 255, 0.05)',
|
|
368
|
+
border: '1px solid var(--border)',
|
|
369
|
+
color: 'var(--text-secondary)',
|
|
370
|
+
cursor: 'pointer',
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
{copied === preset.id ? 'Copied!' : 'Copy Config'}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)
|
|
378
|
+
})}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)
|
|
382
|
+
}
|