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.
@@ -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, ChevronDown, Cpu,
13
- FolderOpen, RotateCw, Users, Gauge, Zap, Server,
14
- LogIn, LogOut, User, ShieldCheck, Settings,
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-10 h-[22px] rounded-full p-0.5 transition-colors cursor-pointer',
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-[18px] h-[18px] rounded-full bg-white shadow-sm transition-transform',
62
- value ? 'translate-x-[18px]' : 'translate-x-0',
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
- /* ── Number Input ──────────────────────────────────────────── */
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 [expanded, setExpanded] = useState(false);
41
+ const [settingKey, setSettingKey] = useState(false);
89
42
  const [keyInput, setKeyInput] = useState('');
90
43
  const [showKey, setShowKey] = useState(false);
91
- const [settingKey, setSettingKey] = useState(false);
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', `API key removed for ${provider.name}`);
66
+ addToast('info', `Removed ${provider.name} key`);
114
67
  if (onKeyChange) onKeyChange();
115
68
  } catch (err) {
116
- addToast('error', 'Failed to remove key', err.message);
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={cn(
122
- 'rounded-lg border overflow-hidden transition-colors',
123
- available ? 'border-border-subtle bg-surface-1' : 'border-border-subtle bg-surface-1/60',
124
- )}>
125
- <button
126
- onClick={() => setExpanded(!expanded)}
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={9} /> Ready</Badge>
119
+ <Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
133
120
  ) : (
134
- <span className="text-2xs text-text-4 font-sans">{isLocal ? 'Not installed' : 'No key'}</span>
121
+ <Badge variant="default" className="text-2xs">No key</Badge>
135
122
  )}
136
- <ChevronDown size={13} className={cn('text-text-4 transition-transform', expanded && 'rotate-180')} />
137
- </button>
123
+ </div>
138
124
 
139
- {expanded && (
140
- <div className="border-t border-border-subtle">
141
- {isLocal ? (
142
- <OllamaSetup isInstalled={available} onModelChange={onKeyChange} />
143
- ) : (
144
- <div className="px-4 py-3 space-y-3">
145
- {/* Models */}
146
- {provider.models?.length > 0 && (
147
- <div className="flex flex-wrap gap-1.5">
148
- {provider.models.map((m) => (
149
- <span key={m.id} className="px-2 py-0.5 rounded bg-surface-4 text-2xs font-mono text-text-2">
150
- {m.name || m.id}
151
- </span>
152
- ))}
153
- </div>
154
- )}
155
-
156
- {/* API Key */}
157
- {settingKey ? (
158
- <div className="flex gap-2">
159
- <div className="flex-1 relative">
160
- <input
161
- value={keyInput}
162
- onChange={(e) => setKeyInput(e.target.value)}
163
- onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
164
- type={showKey ? 'text' : 'password'}
165
- placeholder={`Paste ${provider.name} API key...`}
166
- className="w-full h-8 px-3 pr-8 text-xs 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"
167
- autoFocus
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((data) => setProviders(Array.isArray(data) ? data : [])).catch(() => {});
228
+ api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
216
229
  }
217
230
 
218
231
  useEffect(() => {
219
- Promise.all([
220
- api.get('/providers'),
221
- api.get('/config'),
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="h-full bg-surface-0 p-8">
243
- <Skeleton className="h-8 w-40 rounded-md mb-8" />
244
- <div className="space-y-4">{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-16 rounded-lg" />)}</div>
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
- <ScrollArea className="h-full">
253
- <div className="max-w-2xl mx-auto px-8 py-8">
254
-
255
- {/* Page header */}
256
- <div className="mb-10">
257
- <h1 className="text-xl font-bold text-text-0 font-sans tracking-tight">Settings</h1>
258
- <p className="text-sm text-text-3 font-sans mt-1">Manage providers, configuration, and your account.</p>
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
- {/* ═══════ PROVIDERS ═══════ */}
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
- {/* ═══════ ACCOUNT ═══════ */}
326
- <section className="mb-10">
327
- <SectionHeader
328
- icon={User}
329
- title="Account"
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 gap-3">
355
- <div className="w-9 h-9 rounded-full bg-surface-4 flex items-center justify-center">
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
- {/* Daemon info */}
369
- <div className="rounded-lg border border-border-subtle bg-surface-1 overflow-hidden">
370
- <div className="px-4 py-2.5 border-b border-border-subtle">
371
- <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Daemon</div>
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-2 gap-x-6">
374
- {[
375
- ['Version', daemonInfo?.version || '—'],
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
- {/* Footer */}
392
- <div className="text-center pb-6">
393
- <p className="text-2xs text-text-4 font-sans">
394
- Groove Dev · <a href="https://groovedev.ai" target="_blank" rel="noopener" className="text-accent hover:underline">groovedev.ai</a> · <a href="https://docs.groovedev.ai" target="_blank" rel="noopener" className="text-accent hover:underline">docs</a>
395
- </p>
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
- </div>
398
- </ScrollArea>
399
+ </ScrollArea>
400
+ </div>
399
401
  );
400
402
  }