groove-dev 0.19.4 → 0.19.5
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 +8 -0
- package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/timeline.json +112 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BU5gUCxA.js +537 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CK7vwVXU.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +1 -1
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +440 -0
- package/package.json +1 -1
- package/packages/cli/bin/groove.js +1 -1
- package/packages/gui/dist/assets/index-BU5gUCxA.js +537 -0
- package/packages/gui/dist/assets/index-CK7vwVXU.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +1 -1
- package/packages/gui/src/stores/groove.js +1 -1
- package/packages/gui/src/views/settings.jsx +440 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.host +0 -1
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +0 -1
- package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +0 -9
- package/node_modules/@groove-dev/gui/dist/assets/index-DgIS4D-j.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-eysbqREF.js +0 -537
- package/packages/gui/dist/assets/index-DgIS4D-j.css +0 -1
- package/packages/gui/dist/assets/index-eysbqREF.js +0 -537
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../stores/groove';
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/tabs';
|
|
5
|
+
import { Button } from '../components/ui/button';
|
|
6
|
+
import { Badge } from '../components/ui/badge';
|
|
7
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
8
|
+
import { Skeleton } from '../components/ui/skeleton';
|
|
9
|
+
import { OllamaSetup } from '../components/agents/ollama-setup';
|
|
10
|
+
import { api } from '../lib/api';
|
|
11
|
+
import { cn } from '../lib/cn';
|
|
12
|
+
import {
|
|
13
|
+
Key, Eye, EyeOff, Check, ChevronDown, Cpu, Layers,
|
|
14
|
+
FolderOpen, RotateCw, Users, Gauge, Zap, Server,
|
|
15
|
+
LogIn, LogOut, User, ShieldCheck,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
/* ── Provider Card ─────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
function ProviderCard({ provider, onKeyChange }) {
|
|
21
|
+
const [expanded, setExpanded] = useState(false);
|
|
22
|
+
const [keyInput, setKeyInput] = useState('');
|
|
23
|
+
const [showKey, setShowKey] = useState(false);
|
|
24
|
+
const [settingKey, setSettingKey] = useState(false);
|
|
25
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
26
|
+
|
|
27
|
+
const available = provider.installed || provider.hasKey;
|
|
28
|
+
const isLocal = provider.authType === 'local';
|
|
29
|
+
|
|
30
|
+
async function handleSetKey() {
|
|
31
|
+
if (!keyInput.trim()) return;
|
|
32
|
+
try {
|
|
33
|
+
await api.post(`/credentials/${provider.id}`, { key: keyInput.trim() });
|
|
34
|
+
addToast('success', `API key set for ${provider.name}`);
|
|
35
|
+
setKeyInput('');
|
|
36
|
+
setSettingKey(false);
|
|
37
|
+
if (onKeyChange) onKeyChange();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
addToast('error', 'Failed to set key', err.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handleDeleteKey() {
|
|
44
|
+
try {
|
|
45
|
+
await api.delete(`/credentials/${provider.id}`);
|
|
46
|
+
addToast('info', `API key removed for ${provider.name}`);
|
|
47
|
+
if (onKeyChange) onKeyChange();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
addToast('error', 'Failed to remove key', err.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 overflow-hidden">
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => setExpanded(!expanded)}
|
|
58
|
+
className="w-full flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-surface-4/30 transition-colors"
|
|
59
|
+
>
|
|
60
|
+
<div className={cn('w-2.5 h-2.5 rounded-full flex-shrink-0', available ? 'bg-success' : 'bg-text-4')} />
|
|
61
|
+
<div className="flex-1 text-left">
|
|
62
|
+
<div className="text-sm font-semibold text-text-0 font-sans">{provider.name}</div>
|
|
63
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">
|
|
64
|
+
{isLocal ? (available ? 'Installed' : 'Not installed') : (available ? 'Connected' : 'No API key')}
|
|
65
|
+
{provider.models?.length > 0 && ` · ${provider.models.length} models`}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
{available && <Badge variant="success" className="text-2xs">Ready</Badge>}
|
|
69
|
+
<ChevronDown size={14} className={cn('text-text-4 transition-transform', expanded && 'rotate-180')} />
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
{/* Expanded */}
|
|
73
|
+
{expanded && (
|
|
74
|
+
<div className="border-t border-border-subtle">
|
|
75
|
+
{/* Ollama gets the full setup */}
|
|
76
|
+
{isLocal ? (
|
|
77
|
+
<OllamaSetup isInstalled={available} onModelChange={onKeyChange} />
|
|
78
|
+
) : (
|
|
79
|
+
<div className="p-4 space-y-3">
|
|
80
|
+
{/* Models list */}
|
|
81
|
+
{provider.models?.length > 0 && (
|
|
82
|
+
<div className="space-y-1">
|
|
83
|
+
<label className="text-xs font-semibold text-text-1 font-sans">Models</label>
|
|
84
|
+
<div className="flex flex-wrap gap-1.5">
|
|
85
|
+
{provider.models.map((m) => (
|
|
86
|
+
<Badge key={m.id} variant="default" className="font-mono text-xs px-2.5 py-1">
|
|
87
|
+
{m.name || m.id}
|
|
88
|
+
<span className="text-text-4 ml-1.5">{m.tier}</span>
|
|
89
|
+
</Badge>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{/* API Key management */}
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<label className="text-xs font-semibold text-text-1 font-sans">API Key</label>
|
|
98
|
+
{settingKey ? (
|
|
99
|
+
<div className="flex gap-2">
|
|
100
|
+
<div className="flex-1 relative">
|
|
101
|
+
<input
|
|
102
|
+
value={keyInput}
|
|
103
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
104
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
105
|
+
type={showKey ? 'text' : 'password'}
|
|
106
|
+
placeholder={`${provider.name} API key...`}
|
|
107
|
+
className="w-full h-9 px-3 pr-9 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"
|
|
108
|
+
autoFocus
|
|
109
|
+
/>
|
|
110
|
+
<button onClick={() => setShowKey(!showKey)} className="absolute right-3 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
111
|
+
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="h-9 px-3">
|
|
115
|
+
Save
|
|
116
|
+
</Button>
|
|
117
|
+
<Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-9 px-3">
|
|
118
|
+
Cancel
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
<div className="flex-1 h-9 px-3 flex items-center bg-surface-0 border border-border-subtle rounded-md text-xs font-mono text-text-3">
|
|
124
|
+
{provider.hasKey ? '••••••••••••••••' : 'Not set'}
|
|
125
|
+
</div>
|
|
126
|
+
<Button variant="secondary" size="sm" onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="h-9 px-3 gap-1.5">
|
|
127
|
+
<Key size={12} />
|
|
128
|
+
{provider.hasKey ? 'Update' : 'Add Key'}
|
|
129
|
+
</Button>
|
|
130
|
+
{provider.hasKey && (
|
|
131
|
+
<Button variant="danger" size="sm" onClick={handleDeleteKey} className="h-9 px-3">
|
|
132
|
+
Remove
|
|
133
|
+
</Button>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Auth info */}
|
|
140
|
+
<div className="text-2xs text-text-4 font-sans">
|
|
141
|
+
Auth type: {provider.authType === 'subscription' ? 'Subscription (no key needed)' : 'API Key'}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── Providers Tab ─────────────────────────────────────────── */
|
|
152
|
+
|
|
153
|
+
function ProvidersTab() {
|
|
154
|
+
const [providers, setProviders] = useState([]);
|
|
155
|
+
const [loading, setLoading] = useState(true);
|
|
156
|
+
|
|
157
|
+
function load() {
|
|
158
|
+
api.get('/providers').then((data) => {
|
|
159
|
+
setProviders(Array.isArray(data) ? data : []);
|
|
160
|
+
setLoading(false);
|
|
161
|
+
}).catch(() => setLoading(false));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
useEffect(() => { load(); }, []);
|
|
165
|
+
|
|
166
|
+
if (loading) return <div className="p-6 space-y-3">{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-20 rounded-lg" />)}</div>;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<ScrollArea className="flex-1">
|
|
170
|
+
<div className="p-6 space-y-3">
|
|
171
|
+
<p className="text-xs text-text-3 font-sans mb-1">
|
|
172
|
+
Manage AI provider connections. Groove spawns agents using these providers — each manages its own authentication.
|
|
173
|
+
</p>
|
|
174
|
+
{providers.map((p) => (
|
|
175
|
+
<ProviderCard key={p.id} provider={p} onKeyChange={load} />
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</ScrollArea>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── Account Tab ───────────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
function AccountTab() {
|
|
185
|
+
const marketplaceUser = useGrooveStore((s) => s.marketplaceUser);
|
|
186
|
+
const marketplaceAuthenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
|
|
187
|
+
const marketplaceLogin = useGrooveStore((s) => s.marketplaceLogin);
|
|
188
|
+
const marketplaceLogout = useGrooveStore((s) => s.marketplaceLogout);
|
|
189
|
+
const [daemonInfo, setDaemonInfo] = useState(null);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
api.get('/status').then(setDaemonInfo).catch(() => {});
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<ScrollArea className="flex-1">
|
|
197
|
+
<div className="p-6 space-y-6">
|
|
198
|
+
{/* Marketplace Account */}
|
|
199
|
+
<div className="space-y-3">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<User size={14} className="text-text-3" />
|
|
202
|
+
<h3 className="text-sm font-semibold text-text-0 font-sans">Marketplace Account</h3>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
205
|
+
{marketplaceAuthenticated ? (
|
|
206
|
+
<div className="space-y-3">
|
|
207
|
+
<div className="flex items-center gap-3">
|
|
208
|
+
{marketplaceUser?.avatar ? (
|
|
209
|
+
<img src={marketplaceUser.avatar} alt="" className="w-10 h-10 rounded-full" />
|
|
210
|
+
) : (
|
|
211
|
+
<div className="w-10 h-10 rounded-full bg-accent/15 flex items-center justify-center">
|
|
212
|
+
<User size={18} className="text-accent" />
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
<div>
|
|
216
|
+
<div className="text-sm font-semibold text-text-0 font-sans">{marketplaceUser?.displayName || 'User'}</div>
|
|
217
|
+
<div className="text-2xs text-text-3 font-sans">{marketplaceUser?.email || marketplaceUser?.id || 'Signed in'}</div>
|
|
218
|
+
</div>
|
|
219
|
+
<Badge variant="success" className="text-2xs ml-auto gap-1"><ShieldCheck size={10} /> Connected</Badge>
|
|
220
|
+
</div>
|
|
221
|
+
<Button variant="secondary" size="sm" onClick={marketplaceLogout} className="gap-1.5">
|
|
222
|
+
<LogOut size={12} /> Sign Out
|
|
223
|
+
</Button>
|
|
224
|
+
</div>
|
|
225
|
+
) : (
|
|
226
|
+
<div className="space-y-3">
|
|
227
|
+
<p className="text-xs text-text-2 font-sans">
|
|
228
|
+
Sign in to install premium skills, rate marketplace content, and sync your favorites.
|
|
229
|
+
</p>
|
|
230
|
+
<Button variant="primary" size="md" onClick={marketplaceLogin} className="gap-1.5">
|
|
231
|
+
<LogIn size={14} /> Sign in to Marketplace
|
|
232
|
+
</Button>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* Daemon Info */}
|
|
239
|
+
<div className="space-y-3">
|
|
240
|
+
<div className="flex items-center gap-2">
|
|
241
|
+
<Server size={14} className="text-text-3" />
|
|
242
|
+
<h3 className="text-sm font-semibold text-text-0 font-sans">Daemon</h3>
|
|
243
|
+
</div>
|
|
244
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 divide-y divide-border-subtle">
|
|
245
|
+
{[
|
|
246
|
+
['Version', daemonInfo?.version || '—'],
|
|
247
|
+
['Port', daemonInfo?.port || '31415'],
|
|
248
|
+
['Host', daemonInfo?.host || '127.0.0.1'],
|
|
249
|
+
['PID', daemonInfo?.pid || '—'],
|
|
250
|
+
['Uptime', daemonInfo?.uptime ? `${Math.round(daemonInfo.uptime / 60)} min` : '—'],
|
|
251
|
+
].map(([label, value]) => (
|
|
252
|
+
<div key={label} className="flex items-center justify-between px-4 py-2.5">
|
|
253
|
+
<span className="text-xs text-text-3 font-sans">{label}</span>
|
|
254
|
+
<span className="text-xs text-text-1 font-mono">{value}</span>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</ScrollArea>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ── Configuration Tab ─────────────────────────────────────── */
|
|
265
|
+
|
|
266
|
+
function ConfigTab() {
|
|
267
|
+
const [config, setConfig] = useState(null);
|
|
268
|
+
const [loading, setLoading] = useState(true);
|
|
269
|
+
const [providers, setProviders] = useState([]);
|
|
270
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
Promise.all([
|
|
274
|
+
api.get('/config'),
|
|
275
|
+
api.get('/providers'),
|
|
276
|
+
]).then(([cfg, provs]) => {
|
|
277
|
+
setConfig(cfg);
|
|
278
|
+
setProviders(Array.isArray(provs) ? provs : []);
|
|
279
|
+
setLoading(false);
|
|
280
|
+
}).catch(() => setLoading(false));
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
async function updateConfig(key, value) {
|
|
284
|
+
try {
|
|
285
|
+
const updated = await api.patch('/config', { [key]: value });
|
|
286
|
+
setConfig(updated);
|
|
287
|
+
addToast('success', `Updated ${key}`);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
addToast('error', 'Update failed', err.message);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (loading || !config) return <div className="p-6 space-y-3">{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-14 rounded-lg" />)}</div>;
|
|
294
|
+
|
|
295
|
+
const installedProviders = providers.filter((p) => p.installed || p.hasKey);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<ScrollArea className="flex-1">
|
|
299
|
+
<div className="p-6 space-y-6">
|
|
300
|
+
|
|
301
|
+
{/* Default Provider */}
|
|
302
|
+
<ConfigRow icon={Cpu} label="Default Provider" description="Provider used when spawning new agents">
|
|
303
|
+
<select
|
|
304
|
+
value={config.defaultProvider || 'claude-code'}
|
|
305
|
+
onChange={(e) => updateConfig('defaultProvider', e.target.value)}
|
|
306
|
+
className="h-9 px-3 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"
|
|
307
|
+
>
|
|
308
|
+
{installedProviders.map((p) => (
|
|
309
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
310
|
+
))}
|
|
311
|
+
</select>
|
|
312
|
+
</ConfigRow>
|
|
313
|
+
|
|
314
|
+
{/* Default Working Directory */}
|
|
315
|
+
<ConfigRow icon={FolderOpen} label="Default Working Directory" description="Root directory for new agents">
|
|
316
|
+
<div className="flex items-center gap-2">
|
|
317
|
+
<code className="flex-1 h-9 px-3 flex items-center bg-surface-0 border border-border-subtle rounded-md text-xs font-mono text-text-1 truncate min-w-0">
|
|
318
|
+
{config.defaultWorkingDir || process.cwd?.() || '/'}
|
|
319
|
+
</code>
|
|
320
|
+
</div>
|
|
321
|
+
</ConfigRow>
|
|
322
|
+
|
|
323
|
+
{/* Auto Rotation */}
|
|
324
|
+
<ConfigRow icon={RotateCw} label="Auto Rotation" description="Automatically rotate agents when context degrades">
|
|
325
|
+
<ToggleSwitch
|
|
326
|
+
value={config.autoRotation !== false}
|
|
327
|
+
onChange={(v) => updateConfig('autoRotation', v)}
|
|
328
|
+
/>
|
|
329
|
+
</ConfigRow>
|
|
330
|
+
|
|
331
|
+
{/* Rotation Threshold */}
|
|
332
|
+
<ConfigRow icon={Gauge} label="Rotation Threshold" description="Token count that triggers auto-rotation (0 = adaptive)">
|
|
333
|
+
<input
|
|
334
|
+
type="number"
|
|
335
|
+
value={config.rotationThreshold || 0}
|
|
336
|
+
onChange={(e) => updateConfig('rotationThreshold', parseInt(e.target.value, 10) || 0)}
|
|
337
|
+
className="w-24 h-9 px-3 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"
|
|
338
|
+
min={0}
|
|
339
|
+
step={10000}
|
|
340
|
+
/>
|
|
341
|
+
</ConfigRow>
|
|
342
|
+
|
|
343
|
+
{/* QC Threshold */}
|
|
344
|
+
<ConfigRow icon={ShieldCheck} label="QC Threshold" description="Number of agents that triggers auto-QC agent">
|
|
345
|
+
<input
|
|
346
|
+
type="number"
|
|
347
|
+
value={config.qcThreshold || 4}
|
|
348
|
+
onChange={(e) => updateConfig('qcThreshold', parseInt(e.target.value, 10) || 4)}
|
|
349
|
+
className="w-24 h-9 px-3 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"
|
|
350
|
+
min={2}
|
|
351
|
+
max={20}
|
|
352
|
+
/>
|
|
353
|
+
</ConfigRow>
|
|
354
|
+
|
|
355
|
+
{/* Max Agents */}
|
|
356
|
+
<ConfigRow icon={Users} label="Max Agents" description="Maximum concurrent agents (0 = unlimited)">
|
|
357
|
+
<input
|
|
358
|
+
type="number"
|
|
359
|
+
value={config.maxAgents || 0}
|
|
360
|
+
onChange={(e) => updateConfig('maxAgents', parseInt(e.target.value, 10) || 0)}
|
|
361
|
+
className="w-24 h-9 px-3 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"
|
|
362
|
+
min={0}
|
|
363
|
+
max={50}
|
|
364
|
+
/>
|
|
365
|
+
</ConfigRow>
|
|
366
|
+
|
|
367
|
+
{/* Journalist Interval */}
|
|
368
|
+
<ConfigRow icon={Zap} label="Journalist Interval" description="Seconds between automatic synthesis cycles">
|
|
369
|
+
<input
|
|
370
|
+
type="number"
|
|
371
|
+
value={config.journalistInterval || 120}
|
|
372
|
+
onChange={(e) => updateConfig('journalistInterval', parseInt(e.target.value, 10) || 120)}
|
|
373
|
+
className="w-24 h-9 px-3 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"
|
|
374
|
+
min={30}
|
|
375
|
+
step={30}
|
|
376
|
+
/>
|
|
377
|
+
</ConfigRow>
|
|
378
|
+
</div>
|
|
379
|
+
</ScrollArea>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function ConfigRow({ icon: Icon, label, description, children }) {
|
|
384
|
+
return (
|
|
385
|
+
<div className="flex items-center gap-4 rounded-lg border border-border-subtle bg-surface-1 px-4 py-3">
|
|
386
|
+
<Icon size={16} className="text-text-3 flex-shrink-0" />
|
|
387
|
+
<div className="flex-1 min-w-0">
|
|
388
|
+
<div className="text-sm font-semibold text-text-0 font-sans">{label}</div>
|
|
389
|
+
<div className="text-2xs text-text-3 font-sans mt-0.5">{description}</div>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="flex-shrink-0">{children}</div>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function ToggleSwitch({ value, onChange }) {
|
|
397
|
+
return (
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => onChange(!value)}
|
|
400
|
+
className={cn(
|
|
401
|
+
'w-10 h-6 rounded-full p-0.5 transition-colors cursor-pointer',
|
|
402
|
+
value ? 'bg-accent' : 'bg-surface-5',
|
|
403
|
+
)}
|
|
404
|
+
>
|
|
405
|
+
<div className={cn(
|
|
406
|
+
'w-5 h-5 rounded-full bg-white shadow-sm transition-transform',
|
|
407
|
+
value ? 'translate-x-4' : 'translate-x-0',
|
|
408
|
+
)} />
|
|
409
|
+
</button>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ── Main View ─────────────────────────────────────────────── */
|
|
414
|
+
|
|
415
|
+
export default function SettingsView() {
|
|
416
|
+
return (
|
|
417
|
+
<Tabs defaultValue="providers" className="flex flex-col h-full">
|
|
418
|
+
<div className="px-6 pt-3 bg-surface-1 border-b border-border">
|
|
419
|
+
<div className="flex items-center gap-4 mb-0">
|
|
420
|
+
<h2 className="text-base font-semibold text-text-0 font-sans">Settings</h2>
|
|
421
|
+
</div>
|
|
422
|
+
<TabsList className="border-b-0">
|
|
423
|
+
<TabsTrigger value="providers">Providers</TabsTrigger>
|
|
424
|
+
<TabsTrigger value="config">Configuration</TabsTrigger>
|
|
425
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
426
|
+
</TabsList>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<TabsContent value="providers" className="flex-1 min-h-0">
|
|
430
|
+
<ProvidersTab />
|
|
431
|
+
</TabsContent>
|
|
432
|
+
<TabsContent value="config" className="flex-1 min-h-0">
|
|
433
|
+
<ConfigTab />
|
|
434
|
+
</TabsContent>
|
|
435
|
+
<TabsContent value="account" className="flex-1 min-h-0">
|
|
436
|
+
<AccountTab />
|
|
437
|
+
</TabsContent>
|
|
438
|
+
</Tabs>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
127.0.0.1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
55857
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# AGENTS REGISTRY
|
|
2
|
-
|
|
3
|
-
*Auto-generated by GROOVE. Do not edit manually.*
|
|
4
|
-
|
|
5
|
-
| ID | Name | Role | Provider | Directory | Scope | Status |
|
|
6
|
-
|----|------|------|----------|-----------|-------|--------|
|
|
7
|
-
| 15f1e784 | planner-1 | planner | claude-code | /Users/rok/Desktop/groove/packages/gui | - | stopped |
|
|
8
|
-
|
|
9
|
-
*Updated: 2026-04-09T05:52:27.188Z*
|