groove-dev 0.19.6 → 0.19.8
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/CHANGELOG.md +14 -0
- package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +1 -1
- package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
- package/node_modules/@groove-dev/gui/.groove/timeline.json +112 -0
- package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-CJUQO4Kb.js → index-CF0k082p.js} +114 -114
- package/node_modules/@groove-dev/gui/dist/assets/index-DqtVdTZe.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +278 -276
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +1 -1
- package/packages/gui/dist/assets/{index-CJUQO4Kb.js → index-CF0k082p.js} +114 -114
- package/packages/gui/dist/assets/index-DqtVdTZe.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/views/settings.jsx +278 -276
- package/node_modules/@groove-dev/gui/dist/assets/index-saapU4LM.css +0 -1
- package/packages/gui/dist/assets/index-saapU4LM.css +0 -1
|
@@ -5,47 +5,17 @@ import { Button } from '../components/ui/button';
|
|
|
5
5
|
import { Badge } from '../components/ui/badge';
|
|
6
6
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
7
7
|
import { Skeleton } from '../components/ui/skeleton';
|
|
8
|
+
import { StatusDot } from '../components/ui/status-dot';
|
|
8
9
|
import { OllamaSetup } from '../components/agents/ollama-setup';
|
|
9
10
|
import { api } from '../lib/api';
|
|
10
11
|
import { cn } from '../lib/cn';
|
|
12
|
+
import { fmtUptime } from '../lib/format';
|
|
11
13
|
import {
|
|
12
|
-
Key, Eye, EyeOff, Check,
|
|
13
|
-
FolderOpen, RotateCw, Users, Gauge, Zap,
|
|
14
|
-
LogIn, LogOut, User, ShieldCheck,
|
|
15
|
-
Newspaper, Layers, Activity,
|
|
14
|
+
Key, Eye, EyeOff, Check, Cpu, ChevronDown,
|
|
15
|
+
FolderOpen, RotateCw, Users, Gauge, Zap,
|
|
16
|
+
LogIn, LogOut, User, ShieldCheck, Newspaper,
|
|
16
17
|
} from 'lucide-react';
|
|
17
18
|
|
|
18
|
-
/* ── Section Header ────────────────────────────────────────── */
|
|
19
|
-
|
|
20
|
-
function SectionHeader({ icon: Icon, title, description }) {
|
|
21
|
-
return (
|
|
22
|
-
<div className="mb-4">
|
|
23
|
-
<div className="flex items-center gap-2.5 mb-1">
|
|
24
|
-
<div className="w-7 h-7 rounded-md bg-accent/10 flex items-center justify-center flex-shrink-0">
|
|
25
|
-
<Icon size={14} className="text-accent" />
|
|
26
|
-
</div>
|
|
27
|
-
<h3 className="text-sm font-bold text-text-0 font-sans tracking-tight">{title}</h3>
|
|
28
|
-
</div>
|
|
29
|
-
{description && <p className="text-xs text-text-3 font-sans ml-[38px]">{description}</p>}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* ── Config Row ────────────────────────────────────────────── */
|
|
35
|
-
|
|
36
|
-
function ConfigRow({ icon: Icon, label, description, children }) {
|
|
37
|
-
return (
|
|
38
|
-
<div className="flex items-center gap-3.5 py-3 border-b border-border-subtle last:border-b-0">
|
|
39
|
-
<Icon size={15} className="text-text-4 flex-shrink-0" />
|
|
40
|
-
<div className="flex-1 min-w-0">
|
|
41
|
-
<div className="text-[13px] font-medium text-text-0 font-sans">{label}</div>
|
|
42
|
-
{description && <div className="text-2xs text-text-4 font-sans mt-0.5 leading-relaxed">{description}</div>}
|
|
43
|
-
</div>
|
|
44
|
-
<div className="flex-shrink-0">{children}</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
19
|
/* ── Toggle ────────────────────────────────────────────────── */
|
|
50
20
|
|
|
51
21
|
function Toggle({ value, onChange }) {
|
|
@@ -53,42 +23,25 @@ function Toggle({ value, onChange }) {
|
|
|
53
23
|
<button
|
|
54
24
|
onClick={() => onChange(!value)}
|
|
55
25
|
className={cn(
|
|
56
|
-
'w-
|
|
26
|
+
'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer',
|
|
57
27
|
value ? 'bg-accent' : 'bg-surface-5',
|
|
58
28
|
)}
|
|
59
29
|
>
|
|
60
30
|
<div className={cn(
|
|
61
|
-
'w-
|
|
62
|
-
value ? 'translate-x-
|
|
31
|
+
'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
|
|
32
|
+
value ? 'translate-x-4' : 'translate-x-0',
|
|
63
33
|
)} />
|
|
64
34
|
</button>
|
|
65
35
|
);
|
|
66
36
|
}
|
|
67
37
|
|
|
68
|
-
/* ──
|
|
69
|
-
|
|
70
|
-
function NumberInput({ value, onChange, min, max, step, suffix }) {
|
|
71
|
-
return (
|
|
72
|
-
<div className="flex items-center gap-1.5">
|
|
73
|
-
<input
|
|
74
|
-
type="number"
|
|
75
|
-
value={value}
|
|
76
|
-
onChange={(e) => onChange(parseInt(e.target.value, 10) || min || 0)}
|
|
77
|
-
className="w-20 h-8 px-2.5 text-xs text-center bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent"
|
|
78
|
-
min={min} max={max} step={step}
|
|
79
|
-
/>
|
|
80
|
-
{suffix && <span className="text-2xs text-text-4 font-sans">{suffix}</span>}
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* ── Provider Card ─────────────────────────────────────────── */
|
|
38
|
+
/* ── Provider Card (always visible, no expand) ─────────────── */
|
|
86
39
|
|
|
87
40
|
function ProviderCard({ provider, onKeyChange }) {
|
|
88
|
-
const [
|
|
41
|
+
const [settingKey, setSettingKey] = useState(false);
|
|
89
42
|
const [keyInput, setKeyInput] = useState('');
|
|
90
43
|
const [showKey, setShowKey] = useState(false);
|
|
91
|
-
const [
|
|
44
|
+
const [ollamaOpen, setOllamaOpen] = useState(false);
|
|
92
45
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
93
46
|
|
|
94
47
|
const available = provider.installed || provider.hasKey;
|
|
@@ -110,90 +63,150 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
110
63
|
async function handleDeleteKey() {
|
|
111
64
|
try {
|
|
112
65
|
await api.delete(`/credentials/${provider.id}`);
|
|
113
|
-
addToast('info', `
|
|
66
|
+
addToast('info', `Removed ${provider.name} key`);
|
|
114
67
|
if (onKeyChange) onKeyChange();
|
|
115
68
|
} catch (err) {
|
|
116
|
-
addToast('error', '
|
|
69
|
+
addToast('error', 'Remove failed', err.message);
|
|
117
70
|
}
|
|
118
71
|
}
|
|
119
72
|
|
|
73
|
+
// Ollama gets its own tall card with setup inline
|
|
74
|
+
if (isLocal) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
|
|
77
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-border-subtle">
|
|
78
|
+
<StatusDot status={available ? 'running' : 'crashed'} size="sm" />
|
|
79
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">{provider.name}</span>
|
|
80
|
+
<div className="flex-1" />
|
|
81
|
+
{available ? (
|
|
82
|
+
<Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
|
|
83
|
+
) : (
|
|
84
|
+
<Badge variant="default" className="text-2xs">Not installed</Badge>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex-1">
|
|
88
|
+
{ollamaOpen ? (
|
|
89
|
+
<OllamaSetup isInstalled={available} onModelChange={onKeyChange} />
|
|
90
|
+
) : (
|
|
91
|
+
<div className="px-4 py-3 space-y-2">
|
|
92
|
+
<div className="text-xs text-text-3 font-sans">
|
|
93
|
+
{available ? `${provider.models?.length || 0} models available` : 'Local AI models — free, private, no API key'}
|
|
94
|
+
</div>
|
|
95
|
+
<Button
|
|
96
|
+
variant={available ? 'secondary' : 'primary'}
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={() => setOllamaOpen(true)}
|
|
99
|
+
className="w-full h-8 text-2xs gap-1.5"
|
|
100
|
+
>
|
|
101
|
+
<Cpu size={11} />
|
|
102
|
+
{available ? 'Manage Models' : 'Set Up Ollama'}
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
120
111
|
return (
|
|
121
|
-
<div className=
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
className="w-full flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-surface-4/30 transition-colors"
|
|
128
|
-
>
|
|
129
|
-
<div className={cn('w-2 h-2 rounded-full flex-shrink-0', available ? 'bg-success' : 'bg-text-4/40')} />
|
|
130
|
-
<span className="text-[13px] font-semibold text-text-0 font-sans flex-1 text-left">{provider.name}</span>
|
|
112
|
+
<div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
|
|
113
|
+
{/* Header */}
|
|
114
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-border-subtle">
|
|
115
|
+
<StatusDot status={available ? 'running' : 'crashed'} size="sm" />
|
|
116
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">{provider.name}</span>
|
|
117
|
+
<div className="flex-1" />
|
|
131
118
|
{available ? (
|
|
132
|
-
<Badge variant="success" className="text-2xs gap-1"><Check size={
|
|
119
|
+
<Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
|
|
133
120
|
) : (
|
|
134
|
-
<
|
|
121
|
+
<Badge variant="default" className="text-2xs">No key</Badge>
|
|
135
122
|
)}
|
|
136
|
-
|
|
137
|
-
</button>
|
|
123
|
+
</div>
|
|
138
124
|
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<button onClick={() => setShowKey(!showKey)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
170
|
-
{showKey ? <EyeOff size={11} /> : <Eye size={11} />}
|
|
171
|
-
</button>
|
|
172
|
-
</div>
|
|
173
|
-
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="h-8 px-2.5 text-2xs">Save</Button>
|
|
174
|
-
<Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-8 px-2.5 text-2xs">Cancel</Button>
|
|
175
|
-
</div>
|
|
176
|
-
) : (
|
|
177
|
-
<div className="flex items-center gap-2">
|
|
178
|
-
<code className="flex-1 h-8 px-3 flex items-center bg-surface-0 border border-border-subtle rounded-md text-2xs font-mono text-text-3 truncate">
|
|
179
|
-
{provider.hasKey ? '••••••••••••••••••••' : 'No API key configured'}
|
|
180
|
-
</code>
|
|
181
|
-
<Button variant="secondary" size="sm" onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="h-8 px-2.5 text-2xs gap-1">
|
|
182
|
-
<Key size={10} /> {provider.hasKey ? 'Update' : 'Add Key'}
|
|
183
|
-
</Button>
|
|
184
|
-
{provider.hasKey && (
|
|
185
|
-
<Button variant="danger" size="sm" onClick={handleDeleteKey} className="h-8 px-2.5 text-2xs">Remove</Button>
|
|
186
|
-
)}
|
|
187
|
-
</div>
|
|
188
|
-
)}
|
|
189
|
-
|
|
190
|
-
<div className="text-2xs text-text-4 font-sans">
|
|
191
|
-
{provider.authType === 'subscription' ? 'Uses your Claude subscription — no API key needed' : `Requires a ${provider.name} API key`}
|
|
125
|
+
{/* Body */}
|
|
126
|
+
<div className="flex-1 px-4 py-3 space-y-2.5">
|
|
127
|
+
{/* Models */}
|
|
128
|
+
{provider.models?.length > 0 && (
|
|
129
|
+
<div className="flex flex-wrap gap-1">
|
|
130
|
+
{provider.models.map((m) => (
|
|
131
|
+
<span key={m.id} className="px-1.5 py-0.5 rounded bg-surface-4 text-2xs font-mono text-text-3">
|
|
132
|
+
{m.name || m.id}
|
|
133
|
+
</span>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Key input form */}
|
|
139
|
+
{settingKey ? (
|
|
140
|
+
<div className="space-y-1.5">
|
|
141
|
+
<div className="flex gap-1.5">
|
|
142
|
+
<div className="flex-1 relative">
|
|
143
|
+
<input
|
|
144
|
+
value={keyInput}
|
|
145
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
146
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
147
|
+
type={showKey ? 'text' : 'password'}
|
|
148
|
+
placeholder="Paste API key..."
|
|
149
|
+
className="w-full h-7 px-2.5 pr-7 text-2xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
150
|
+
autoFocus
|
|
151
|
+
/>
|
|
152
|
+
<button onClick={() => setShowKey(!showKey)} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
153
|
+
{showKey ? <EyeOff size={10} /> : <Eye size={10} />}
|
|
154
|
+
</button>
|
|
192
155
|
</div>
|
|
193
156
|
</div>
|
|
194
|
-
|
|
157
|
+
<div className="flex gap-1.5">
|
|
158
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="flex-1 h-7 text-2xs">Save</Button>
|
|
159
|
+
<Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-7 text-2xs px-2">Cancel</Button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : provider.hasKey ? (
|
|
163
|
+
/* Has API key — show connected state */
|
|
164
|
+
<div className="flex items-center gap-1.5">
|
|
165
|
+
<div className="flex-1 flex items-center gap-1.5 h-7 px-2 bg-success/8 border border-success/20 rounded text-2xs font-sans text-success">
|
|
166
|
+
<Check size={10} /> API Connected
|
|
167
|
+
</div>
|
|
168
|
+
<button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
|
|
169
|
+
<button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
|
|
170
|
+
</div>
|
|
171
|
+
) : provider.authType === 'subscription' ? (
|
|
172
|
+
/* Subscription provider (Claude) — show subscription status + option to add API key */
|
|
173
|
+
<div className="space-y-1.5">
|
|
174
|
+
<div className="flex items-center gap-1.5 h-7 px-2 bg-accent/8 border border-accent/20 rounded text-2xs font-sans text-accent">
|
|
175
|
+
<Check size={10} /> Subscription active
|
|
176
|
+
</div>
|
|
177
|
+
<button
|
|
178
|
+
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
179
|
+
className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans flex items-center gap-1"
|
|
180
|
+
>
|
|
181
|
+
<Key size={9} /> Add API key for headless mode
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
) : (
|
|
185
|
+
/* No key, needs one */
|
|
186
|
+
<Button variant="primary" size="sm" onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="w-full h-7 text-2xs gap-1">
|
|
187
|
+
<Key size={10} /> Add API Key
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ── Config Card ───────────────────────────────────────────── */
|
|
196
|
+
|
|
197
|
+
function ConfigCard({ icon: Icon, label, description, children }) {
|
|
198
|
+
return (
|
|
199
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5 flex flex-col gap-2">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
|
|
202
|
+
<Icon size={12} className="text-accent" />
|
|
195
203
|
</div>
|
|
196
|
-
|
|
204
|
+
<div className="flex-1 min-w-0">
|
|
205
|
+
<div className="text-[13px] font-medium text-text-0 font-sans leading-tight">{label}</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="text-2xs text-text-4 font-sans leading-relaxed">{description}</div>
|
|
209
|
+
<div className="mt-auto pt-1">{children}</div>
|
|
197
210
|
</div>
|
|
198
211
|
);
|
|
199
212
|
}
|
|
@@ -212,20 +225,13 @@ export default function SettingsView() {
|
|
|
212
225
|
const marketplaceLogout = useGrooveStore((s) => s.marketplaceLogout);
|
|
213
226
|
|
|
214
227
|
function loadProviders() {
|
|
215
|
-
api.get('/providers').then((
|
|
228
|
+
api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
|
|
216
229
|
}
|
|
217
230
|
|
|
218
231
|
useEffect(() => {
|
|
219
|
-
Promise.all([
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
api.get('/status'),
|
|
223
|
-
]).then(([provs, cfg, info]) => {
|
|
224
|
-
setProviders(Array.isArray(provs) ? provs : []);
|
|
225
|
-
setConfig(cfg);
|
|
226
|
-
setDaemonInfo(info);
|
|
227
|
-
setLoading(false);
|
|
228
|
-
}).catch(() => setLoading(false));
|
|
232
|
+
Promise.all([api.get('/providers'), api.get('/config'), api.get('/status')])
|
|
233
|
+
.then(([p, c, s]) => { setProviders(Array.isArray(p) ? p : []); setConfig(c); setDaemonInfo(s); setLoading(false); })
|
|
234
|
+
.catch(() => setLoading(false));
|
|
229
235
|
}, []);
|
|
230
236
|
|
|
231
237
|
async function updateConfig(key, value) {
|
|
@@ -239,9 +245,12 @@ export default function SettingsView() {
|
|
|
239
245
|
|
|
240
246
|
if (loading) {
|
|
241
247
|
return (
|
|
242
|
-
<div className="
|
|
243
|
-
<
|
|
244
|
-
<div className="
|
|
248
|
+
<div className="flex flex-col h-full">
|
|
249
|
+
<div className="h-16 bg-surface-1 border-b border-border" />
|
|
250
|
+
<div className="flex-1 p-4 space-y-4">
|
|
251
|
+
<div className="flex gap-3">{[...Array(4)].map((_, i) => <Skeleton key={i} className="flex-1 h-36 rounded-lg" />)}</div>
|
|
252
|
+
<div className="grid grid-cols-3 gap-3">{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-28 rounded-lg" />)}</div>
|
|
253
|
+
</div>
|
|
245
254
|
</div>
|
|
246
255
|
);
|
|
247
256
|
}
|
|
@@ -249,152 +258,145 @@ export default function SettingsView() {
|
|
|
249
258
|
const installedProviders = providers.filter((p) => p.installed || p.hasKey);
|
|
250
259
|
|
|
251
260
|
return (
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
<div className="flex flex-col h-full">
|
|
262
|
+
|
|
263
|
+
{/* ═══════ ACCOUNT HERO BAR ═══════ */}
|
|
264
|
+
<div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
|
|
265
|
+
<h2 className="text-sm font-semibold text-text-0 font-sans">Settings</h2>
|
|
266
|
+
<div className="flex-1" />
|
|
267
|
+
|
|
268
|
+
{/* Daemon info */}
|
|
269
|
+
<div className="flex items-center gap-4 text-2xs text-text-3 font-sans">
|
|
270
|
+
{daemonInfo?.version && <span>v{daemonInfo.version}</span>}
|
|
271
|
+
{daemonInfo?.port && <span>:{daemonInfo.port}</span>}
|
|
272
|
+
{daemonInfo?.uptime > 0 && <span>Up {fmtUptime(daemonInfo.uptime)}</span>}
|
|
259
273
|
</div>
|
|
260
274
|
|
|
261
|
-
|
|
262
|
-
<section className="mb-10">
|
|
263
|
-
<SectionHeader
|
|
264
|
-
icon={Layers}
|
|
265
|
-
title="Providers"
|
|
266
|
-
description="AI providers that power your agents. Each provider manages its own authentication."
|
|
267
|
-
/>
|
|
268
|
-
<div className="space-y-2">
|
|
269
|
-
{providers.map((p) => (
|
|
270
|
-
<ProviderCard key={p.id} provider={p} onKeyChange={loadProviders} />
|
|
271
|
-
))}
|
|
272
|
-
</div>
|
|
273
|
-
</section>
|
|
274
|
-
|
|
275
|
-
{/* ═══════ CONFIGURATION ═══════ */}
|
|
276
|
-
{config && (
|
|
277
|
-
<section className="mb-10">
|
|
278
|
-
<SectionHeader
|
|
279
|
-
icon={Settings}
|
|
280
|
-
title="Configuration"
|
|
281
|
-
description="Daemon behavior and defaults. Changes save automatically."
|
|
282
|
-
/>
|
|
283
|
-
<div className="rounded-lg border border-border-subtle bg-surface-1 px-4">
|
|
284
|
-
<ConfigRow icon={Cpu} label="Default Provider" description="Provider for new agents">
|
|
285
|
-
<select
|
|
286
|
-
value={config.defaultProvider || 'claude-code'}
|
|
287
|
-
onChange={(e) => updateConfig('defaultProvider', e.target.value)}
|
|
288
|
-
className="h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer appearance-none pr-7"
|
|
289
|
-
>
|
|
290
|
-
{installedProviders.map((p) => (
|
|
291
|
-
<option key={p.id} value={p.id}>{p.name}</option>
|
|
292
|
-
))}
|
|
293
|
-
</select>
|
|
294
|
-
</ConfigRow>
|
|
295
|
-
|
|
296
|
-
<ConfigRow icon={FolderOpen} label="Working Directory" description="Default root directory for agents">
|
|
297
|
-
<code className="h-8 px-2.5 flex items-center bg-surface-0 border border-border-subtle rounded-md text-2xs font-mono text-text-2 max-w-[200px] truncate">
|
|
298
|
-
{config.defaultWorkingDir || 'Project root'}
|
|
299
|
-
</code>
|
|
300
|
-
</ConfigRow>
|
|
301
|
-
|
|
302
|
-
<ConfigRow icon={RotateCw} label="Auto Rotation" description="Rotate agents when context degrades">
|
|
303
|
-
<Toggle value={config.autoRotation !== false} onChange={(v) => updateConfig('autoRotation', v)} />
|
|
304
|
-
</ConfigRow>
|
|
305
|
-
|
|
306
|
-
<ConfigRow icon={Gauge} label="Rotation Threshold" description="Tokens before rotation (0 = adaptive)">
|
|
307
|
-
<NumberInput value={config.rotationThreshold || 0} onChange={(v) => updateConfig('rotationThreshold', v)} min={0} step={10000} />
|
|
308
|
-
</ConfigRow>
|
|
309
|
-
|
|
310
|
-
<ConfigRow icon={ShieldCheck} label="QC Threshold" description="Agents count that triggers auto-QC">
|
|
311
|
-
<NumberInput value={config.qcThreshold || 4} onChange={(v) => updateConfig('qcThreshold', v)} min={2} max={20} />
|
|
312
|
-
</ConfigRow>
|
|
313
|
-
|
|
314
|
-
<ConfigRow icon={Users} label="Max Agents" description="Concurrent agent limit (0 = unlimited)">
|
|
315
|
-
<NumberInput value={config.maxAgents || 0} onChange={(v) => updateConfig('maxAgents', v)} min={0} max={50} />
|
|
316
|
-
</ConfigRow>
|
|
317
|
-
|
|
318
|
-
<ConfigRow icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles">
|
|
319
|
-
<NumberInput value={config.journalistInterval || 120} onChange={(v) => updateConfig('journalistInterval', v)} min={30} step={30} suffix="sec" />
|
|
320
|
-
</ConfigRow>
|
|
321
|
-
</div>
|
|
322
|
-
</section>
|
|
323
|
-
)}
|
|
275
|
+
<div className="w-px h-4 bg-border-subtle" />
|
|
324
276
|
|
|
325
|
-
{/*
|
|
326
|
-
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
description="Marketplace identity and daemon information."
|
|
331
|
-
/>
|
|
332
|
-
|
|
333
|
-
{/* Marketplace */}
|
|
334
|
-
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4 mb-3">
|
|
335
|
-
<div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-3">Marketplace</div>
|
|
336
|
-
{marketplaceAuthenticated ? (
|
|
337
|
-
<div className="flex items-center gap-3">
|
|
338
|
-
{marketplaceUser?.avatar ? (
|
|
339
|
-
<img src={marketplaceUser.avatar} alt="" className="w-9 h-9 rounded-full" />
|
|
340
|
-
) : (
|
|
341
|
-
<div className="w-9 h-9 rounded-full bg-accent/10 flex items-center justify-center">
|
|
342
|
-
<User size={16} className="text-accent" />
|
|
343
|
-
</div>
|
|
344
|
-
)}
|
|
345
|
-
<div className="flex-1 min-w-0">
|
|
346
|
-
<div className="text-[13px] font-semibold text-text-0 font-sans">{marketplaceUser?.displayName || 'User'}</div>
|
|
347
|
-
<div className="text-2xs text-text-3 font-sans">{marketplaceUser?.email || 'Connected'}</div>
|
|
348
|
-
</div>
|
|
349
|
-
<Button variant="ghost" size="sm" onClick={marketplaceLogout} className="h-8 px-2.5 text-2xs gap-1 text-text-3">
|
|
350
|
-
<LogOut size={11} /> Sign Out
|
|
351
|
-
</Button>
|
|
352
|
-
</div>
|
|
277
|
+
{/* Account */}
|
|
278
|
+
{marketplaceAuthenticated ? (
|
|
279
|
+
<div className="flex items-center gap-2.5">
|
|
280
|
+
{marketplaceUser?.avatar ? (
|
|
281
|
+
<img src={marketplaceUser.avatar} alt="" className="w-6 h-6 rounded-full" />
|
|
353
282
|
) : (
|
|
354
|
-
<div className="flex items-center
|
|
355
|
-
<
|
|
356
|
-
<User size={16} className="text-text-4" />
|
|
357
|
-
</div>
|
|
358
|
-
<div className="flex-1">
|
|
359
|
-
<div className="text-xs text-text-2 font-sans">Sign in for premium skills, ratings, and favorites.</div>
|
|
360
|
-
</div>
|
|
361
|
-
<Button variant="primary" size="sm" onClick={marketplaceLogin} className="h-8 px-3 text-2xs gap-1.5">
|
|
362
|
-
<LogIn size={11} /> Sign In
|
|
363
|
-
</Button>
|
|
283
|
+
<div className="w-6 h-6 rounded-full bg-accent/10 flex items-center justify-center">
|
|
284
|
+
<User size={12} className="text-accent" />
|
|
364
285
|
</div>
|
|
365
286
|
)}
|
|
287
|
+
<span className="text-xs font-medium text-text-0 font-sans">{marketplaceUser?.displayName || 'User'}</span>
|
|
288
|
+
<button onClick={marketplaceLogout} className="text-2xs text-text-4 hover:text-text-1 cursor-pointer font-sans flex items-center gap-1">
|
|
289
|
+
<LogOut size={10} /> Sign out
|
|
290
|
+
</button>
|
|
366
291
|
</div>
|
|
292
|
+
) : (
|
|
293
|
+
<Button variant="ghost" size="sm" onClick={marketplaceLogin} className="h-7 text-2xs gap-1.5 text-text-3">
|
|
294
|
+
<LogIn size={11} /> Sign in
|
|
295
|
+
</Button>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
<StatusDot status="running" size="sm" />
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* ═══════ SCROLLABLE BODY ═══════ */}
|
|
302
|
+
<ScrollArea className="flex-1">
|
|
303
|
+
<div className="p-4 space-y-4">
|
|
367
304
|
|
|
368
|
-
{/*
|
|
369
|
-
<div
|
|
370
|
-
<div className="
|
|
371
|
-
<
|
|
305
|
+
{/* ═══════ PROVIDERS ROW ═══════ */}
|
|
306
|
+
<div>
|
|
307
|
+
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
308
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Providers</span>
|
|
309
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
310
|
+
<span className="text-2xs text-text-4 font-sans">{installedProviders.length}/{providers.length} connected</span>
|
|
372
311
|
</div>
|
|
373
|
-
<div className="grid grid-cols-
|
|
374
|
-
{
|
|
375
|
-
|
|
376
|
-
['Port', daemonInfo?.port || '31415'],
|
|
377
|
-
['Host', daemonInfo?.host || '127.0.0.1'],
|
|
378
|
-
['PID', daemonInfo?.pid || '—'],
|
|
379
|
-
['Uptime', daemonInfo?.uptime ? `${Math.round(daemonInfo.uptime / 60)}m` : '—'],
|
|
380
|
-
['Agents', daemonInfo?.agents || '0'],
|
|
381
|
-
].map(([label, value]) => (
|
|
382
|
-
<div key={label} className="flex items-center justify-between px-4 py-2 border-b border-border-subtle last:border-b-0">
|
|
383
|
-
<span className="text-2xs text-text-4 font-sans">{label}</span>
|
|
384
|
-
<span className="text-2xs text-text-1 font-mono">{value}</span>
|
|
385
|
-
</div>
|
|
312
|
+
<div className="grid grid-cols-4 gap-3">
|
|
313
|
+
{providers.map((p) => (
|
|
314
|
+
<ProviderCard key={p.id} provider={p} onKeyChange={loadProviders} />
|
|
386
315
|
))}
|
|
387
316
|
</div>
|
|
388
317
|
</div>
|
|
389
|
-
</section>
|
|
390
318
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
319
|
+
{/* ═══════ CONFIGURATION GRID ═══════ */}
|
|
320
|
+
{config && (
|
|
321
|
+
<div>
|
|
322
|
+
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
323
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Configuration</span>
|
|
324
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
325
|
+
<span className="text-2xs text-text-4 font-sans">Auto-saves</span>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="grid grid-cols-3 gap-3">
|
|
328
|
+
<ConfigCard icon={Cpu} label="Default Provider" description="Provider used when spawning new agents.">
|
|
329
|
+
<select
|
|
330
|
+
value={config.defaultProvider || 'claude-code'}
|
|
331
|
+
onChange={(e) => updateConfig('defaultProvider', e.target.value)}
|
|
332
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer"
|
|
333
|
+
>
|
|
334
|
+
{installedProviders.map((p) => (
|
|
335
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
336
|
+
))}
|
|
337
|
+
</select>
|
|
338
|
+
</ConfigCard>
|
|
339
|
+
|
|
340
|
+
<ConfigCard icon={FolderOpen} label="Working Directory" description="Default root directory for new agents.">
|
|
341
|
+
<code className="block w-full h-8 px-2.5 flex items-center bg-surface-0 border border-border-subtle rounded-md text-2xs font-mono text-text-2 truncate">
|
|
342
|
+
{config.defaultWorkingDir || 'Project root'}
|
|
343
|
+
</code>
|
|
344
|
+
</ConfigCard>
|
|
345
|
+
|
|
346
|
+
<ConfigCard icon={RotateCw} label="Auto Rotation" description="Rotate agents automatically when context window degrades.">
|
|
347
|
+
<div className="flex items-center justify-between">
|
|
348
|
+
<span className="text-xs font-mono text-text-2">{config.autoRotation !== false ? 'On' : 'Off'}</span>
|
|
349
|
+
<Toggle value={config.autoRotation !== false} onChange={(v) => updateConfig('autoRotation', v)} />
|
|
350
|
+
</div>
|
|
351
|
+
</ConfigCard>
|
|
352
|
+
|
|
353
|
+
<ConfigCard icon={Gauge} label="Rotation Threshold" description="Token count that triggers rotation. 0 uses adaptive threshold.">
|
|
354
|
+
<input
|
|
355
|
+
type="number"
|
|
356
|
+
value={config.rotationThreshold || 0}
|
|
357
|
+
onChange={(e) => updateConfig('rotationThreshold', parseInt(e.target.value, 10) || 0)}
|
|
358
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent"
|
|
359
|
+
min={0} step={10000}
|
|
360
|
+
/>
|
|
361
|
+
</ConfigCard>
|
|
362
|
+
|
|
363
|
+
<ConfigCard icon={ShieldCheck} label="QC Threshold" description="Number of running agents that triggers an auto-QC agent.">
|
|
364
|
+
<input
|
|
365
|
+
type="number"
|
|
366
|
+
value={config.qcThreshold || 4}
|
|
367
|
+
onChange={(e) => updateConfig('qcThreshold', parseInt(e.target.value, 10) || 4)}
|
|
368
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent"
|
|
369
|
+
min={2} max={20}
|
|
370
|
+
/>
|
|
371
|
+
</ConfigCard>
|
|
372
|
+
|
|
373
|
+
<ConfigCard icon={Users} label="Max Agents" description="Maximum concurrent agents. 0 means unlimited.">
|
|
374
|
+
<input
|
|
375
|
+
type="number"
|
|
376
|
+
value={config.maxAgents || 0}
|
|
377
|
+
onChange={(e) => updateConfig('maxAgents', parseInt(e.target.value, 10) || 0)}
|
|
378
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent"
|
|
379
|
+
min={0} max={50}
|
|
380
|
+
/>
|
|
381
|
+
</ConfigCard>
|
|
382
|
+
|
|
383
|
+
<ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between automatic synthesis cycles.">
|
|
384
|
+
<div className="flex items-center gap-1.5">
|
|
385
|
+
<input
|
|
386
|
+
type="number"
|
|
387
|
+
value={config.journalistInterval || 120}
|
|
388
|
+
onChange={(e) => updateConfig('journalistInterval', parseInt(e.target.value, 10) || 120)}
|
|
389
|
+
className="flex-1 h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent"
|
|
390
|
+
min={30} step={30}
|
|
391
|
+
/>
|
|
392
|
+
<span className="text-2xs text-text-4 font-sans">sec</span>
|
|
393
|
+
</div>
|
|
394
|
+
</ConfigCard>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
396
398
|
</div>
|
|
397
|
-
</
|
|
398
|
-
</
|
|
399
|
+
</ScrollArea>
|
|
400
|
+
</div>
|
|
399
401
|
);
|
|
400
402
|
}
|