groove-dev 0.19.8 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-CF0k082p.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DKov-d0e.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-DqtVdTZe.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-B8ZmjJeV.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -7,13 +7,15 @@ import { ScrollArea } from '../components/ui/scroll-area';
7
7
  import { Skeleton } from '../components/ui/skeleton';
8
8
  import { StatusDot } from '../components/ui/status-dot';
9
9
  import { OllamaSetup } from '../components/agents/ollama-setup';
10
+ import { FolderBrowser } from '../components/agents/folder-browser';
10
11
  import { api } from '../lib/api';
11
12
  import { cn } from '../lib/cn';
12
13
  import { fmtUptime } from '../lib/format';
13
14
  import {
14
15
  Key, Eye, EyeOff, Check, Cpu, ChevronDown,
15
- FolderOpen, RotateCw, Users, Gauge, Zap,
16
- LogIn, LogOut, User, ShieldCheck, Newspaper,
16
+ FolderOpen, FolderSearch, RotateCw, Users, Gauge, Zap,
17
+ LogIn, LogOut, User, ShieldCheck, Settings,
18
+ Newspaper, Layers,
17
19
  } from 'lucide-react';
18
20
 
19
21
  /* ── Toggle ────────────────────────────────────────────────── */
@@ -35,7 +37,7 @@ function Toggle({ value, onChange }) {
35
37
  );
36
38
  }
37
39
 
38
- /* ── Provider Card (always visible, no expand) ─────────────── */
40
+ /* ── Provider Card ─────────────────────────────────────────── */
39
41
 
40
42
  function ProviderCard({ provider, onKeyChange }) {
41
43
  const [settingKey, setSettingKey] = useState(false);
@@ -44,8 +46,10 @@ function ProviderCard({ provider, onKeyChange }) {
44
46
  const [ollamaOpen, setOllamaOpen] = useState(false);
45
47
  const addToast = useGrooveStore((s) => s.addToast);
46
48
 
47
- const available = provider.installed || provider.hasKey;
48
49
  const isLocal = provider.authType === 'local';
50
+ const isSubscription = provider.authType === 'subscription';
51
+ // "Ready" means: local + installed, subscription + installed, api-key + hasKey
52
+ const isReady = isLocal ? provider.installed : isSubscription ? provider.installed : provider.hasKey;
49
53
 
50
54
  async function handleSetKey() {
51
55
  if (!keyInput.trim()) return;
@@ -70,15 +74,15 @@ function ProviderCard({ provider, onKeyChange }) {
70
74
  }
71
75
  }
72
76
 
73
- // Ollama gets its own tall card with setup inline
77
+ // Ollama card
74
78
  if (isLocal) {
75
79
  return (
76
80
  <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
77
81
  <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" />
82
+ <StatusDot status={isReady ? 'running' : 'crashed'} size="sm" />
79
83
  <span className="text-[13px] font-semibold text-text-0 font-sans">{provider.name}</span>
80
84
  <div className="flex-1" />
81
- {available ? (
85
+ {isReady ? (
82
86
  <Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
83
87
  ) : (
84
88
  <Badge variant="default" className="text-2xs">Not installed</Badge>
@@ -86,20 +90,20 @@ function ProviderCard({ provider, onKeyChange }) {
86
90
  </div>
87
91
  <div className="flex-1">
88
92
  {ollamaOpen ? (
89
- <OllamaSetup isInstalled={available} onModelChange={onKeyChange} />
93
+ <OllamaSetup isInstalled={isReady} onModelChange={onKeyChange} />
90
94
  ) : (
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'}
95
+ <div className="px-4 py-3 flex flex-col h-full">
96
+ <div className="text-xs text-text-3 font-sans flex-1">
97
+ {isReady ? `${provider.models?.length || 0} models available` : 'Local AI models — free, private, no API key'}
94
98
  </div>
95
99
  <Button
96
- variant={available ? 'secondary' : 'primary'}
100
+ variant={isReady ? 'secondary' : 'primary'}
97
101
  size="sm"
98
102
  onClick={() => setOllamaOpen(true)}
99
- className="w-full h-8 text-2xs gap-1.5"
103
+ className="w-full h-7 text-2xs gap-1.5 mt-3"
100
104
  >
101
105
  <Cpu size={11} />
102
- {available ? 'Manage Models' : 'Set Up Ollama'}
106
+ {isReady ? 'Manage Models' : 'Set Up Ollama'}
103
107
  </Button>
104
108
  </div>
105
109
  )}
@@ -108,25 +112,26 @@ function ProviderCard({ provider, onKeyChange }) {
108
112
  );
109
113
  }
110
114
 
115
+ // Standard provider card (Claude, Codex, Gemini)
111
116
  return (
112
117
  <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
113
118
  {/* Header */}
114
119
  <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" />
120
+ <StatusDot status={isReady ? 'running' : 'crashed'} size="sm" />
116
121
  <span className="text-[13px] font-semibold text-text-0 font-sans">{provider.name}</span>
117
122
  <div className="flex-1" />
118
- {available ? (
123
+ {isReady ? (
119
124
  <Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
120
125
  ) : (
121
- <Badge variant="default" className="text-2xs">No key</Badge>
126
+ <Badge variant="default" className="text-2xs">{isSubscription ? 'Not installed' : 'No key'}</Badge>
122
127
  )}
123
128
  </div>
124
129
 
125
130
  {/* Body */}
126
- <div className="flex-1 px-4 py-3 space-y-2.5">
131
+ <div className="flex-1 flex flex-col px-4 py-3 min-h-[120px]">
127
132
  {/* Models */}
128
133
  {provider.models?.length > 0 && (
129
- <div className="flex flex-wrap gap-1">
134
+ <div className="flex flex-wrap gap-1 mb-3">
130
135
  {provider.models.map((m) => (
131
136
  <span key={m.id} className="px-1.5 py-0.5 rounded bg-surface-4 text-2xs font-mono text-text-3">
132
137
  {m.name || m.id}
@@ -135,56 +140,70 @@ function ProviderCard({ provider, onKeyChange }) {
135
140
  </div>
136
141
  )}
137
142
 
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
+ {/* Subscription info for Claude */}
144
+ {isSubscription && isReady && !provider.hasKey && !settingKey && (
145
+ <div className="flex items-center gap-1.5 h-8 px-2.5 bg-accent/8 border border-accent/20 rounded-md text-2xs font-sans text-accent mb-3">
146
+ <Check size={10} /> Subscription active
147
+ </div>
148
+ )}
149
+
150
+ {/* Connected state */}
151
+ {provider.hasKey && !settingKey && (
152
+ <div className="flex items-center gap-2 mb-3">
153
+ <div className="flex-1 flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success">
154
+ <Check size={10} /> API Connected
155
+ </div>
156
+ <button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
157
+ <button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
158
+ </div>
159
+ )}
160
+
161
+ {/* Spacer */}
162
+ <div className="flex-1" />
163
+
164
+ {/* Key input form — takes over the bottom area */}
165
+ {settingKey && (
166
+ <div className="space-y-2.5 pt-1">
167
+ <div>
168
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
169
+ {provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
170
+ </label>
171
+ <div className="relative">
143
172
  <input
144
173
  value={keyInput}
145
174
  onChange={(e) => setKeyInput(e.target.value)}
146
175
  onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
147
176
  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"
177
+ placeholder="sk-..."
178
+ 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"
150
179
  autoFocus
151
180
  />
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} />}
181
+ <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">
182
+ {showKey ? <EyeOff size={12} /> : <Eye size={12} />}
154
183
  </button>
155
184
  </div>
156
185
  </div>
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
186
+ <div className="flex gap-2">
187
+ <Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="flex-1 h-8 text-xs">
188
+ Save Key
189
+ </Button>
190
+ <Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-8 text-xs px-3">
191
+ Cancel
192
+ </Button>
176
193
  </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
194
  </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
195
+ )}
196
+
197
+ {/* Bottom action always at card bottom */}
198
+ {!settingKey && !provider.hasKey && (
199
+ <Button
200
+ variant={isSubscription ? 'secondary' : 'primary'}
201
+ size="sm"
202
+ onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
203
+ className="w-full h-8 text-2xs gap-1.5 mt-2"
204
+ >
205
+ <Key size={11} />
206
+ {isSubscription ? 'Add API key for headless mode' : 'Add API Key'}
188
207
  </Button>
189
208
  )}
190
209
  </div>
@@ -201,9 +220,7 @@ function ConfigCard({ icon: Icon, label, description, children }) {
201
220
  <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
202
221
  <Icon size={12} className="text-accent" />
203
222
  </div>
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>
223
+ <div className="text-[13px] font-medium text-text-0 font-sans leading-tight">{label}</div>
207
224
  </div>
208
225
  <div className="text-2xs text-text-4 font-sans leading-relaxed">{description}</div>
209
226
  <div className="mt-auto pt-1">{children}</div>
@@ -218,6 +235,7 @@ export default function SettingsView() {
218
235
  const [config, setConfig] = useState(null);
219
236
  const [daemonInfo, setDaemonInfo] = useState(null);
220
237
  const [loading, setLoading] = useState(true);
238
+ const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
221
239
  const addToast = useGrooveStore((s) => s.addToast);
222
240
  const marketplaceUser = useGrooveStore((s) => s.marketplaceUser);
223
241
  const marketplaceAuthenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
@@ -246,26 +264,33 @@ export default function SettingsView() {
246
264
  if (loading) {
247
265
  return (
248
266
  <div className="flex flex-col h-full">
249
- <div className="h-16 bg-surface-1 border-b border-border" />
267
+ <div className="h-12 bg-surface-1 border-b border-border" />
250
268
  <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>
269
+ <div className="grid grid-cols-4 gap-3">{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-40 rounded-lg" />)}</div>
252
270
  <div className="grid grid-cols-3 gap-3">{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-28 rounded-lg" />)}</div>
253
271
  </div>
254
272
  </div>
255
273
  );
256
274
  }
257
275
 
258
- const installedProviders = providers.filter((p) => p.installed || p.hasKey);
276
+ const connectedCount = providers.filter((p) => {
277
+ if (p.authType === 'local') return p.installed;
278
+ if (p.authType === 'subscription') return p.installed;
279
+ return p.hasKey;
280
+ }).length;
281
+
282
+ // Rotation threshold display: 0 = auto, otherwise show as percentage
283
+ const rotationValue = config?.rotationThreshold || 0;
284
+ const rotationDisplay = rotationValue === 0 ? 'auto' : `${Math.round(rotationValue * 100)}%`;
259
285
 
260
286
  return (
261
287
  <div className="flex flex-col h-full">
262
288
 
263
- {/* ═══════ ACCOUNT HERO BAR ═══════ */}
289
+ {/* ═══════ HEADER BAR ═══════ */}
264
290
  <div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
265
291
  <h2 className="text-sm font-semibold text-text-0 font-sans">Settings</h2>
266
292
  <div className="flex-1" />
267
293
 
268
- {/* Daemon info */}
269
294
  <div className="flex items-center gap-4 text-2xs text-text-3 font-sans">
270
295
  {daemonInfo?.version && <span>v{daemonInfo.version}</span>}
271
296
  {daemonInfo?.port && <span>:{daemonInfo.port}</span>}
@@ -274,7 +299,6 @@ export default function SettingsView() {
274
299
 
275
300
  <div className="w-px h-4 bg-border-subtle" />
276
301
 
277
- {/* Account */}
278
302
  {marketplaceAuthenticated ? (
279
303
  <div className="flex items-center gap-2.5">
280
304
  {marketplaceUser?.avatar ? (
@@ -302,12 +326,12 @@ export default function SettingsView() {
302
326
  <ScrollArea className="flex-1">
303
327
  <div className="p-4 space-y-4">
304
328
 
305
- {/* ═══════ PROVIDERS ROW ═══════ */}
329
+ {/* ═══════ PROVIDERS ═══════ */}
306
330
  <div>
307
331
  <div className="flex items-center gap-2 mb-2.5 px-0.5">
308
332
  <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Providers</span>
309
333
  <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>
334
+ <span className="text-2xs text-text-4 font-sans">{connectedCount}/{providers.length} connected</span>
311
335
  </div>
312
336
  <div className="grid grid-cols-4 gap-3">
313
337
  {providers.map((p) => (
@@ -316,7 +340,7 @@ export default function SettingsView() {
316
340
  </div>
317
341
  </div>
318
342
 
319
- {/* ═══════ CONFIGURATION GRID ═══════ */}
343
+ {/* ═══════ CONFIGURATION ═══════ */}
320
344
  {config && (
321
345
  <div>
322
346
  <div className="flex items-center gap-2 mb-2.5 px-0.5">
@@ -325,78 +349,125 @@ export default function SettingsView() {
325
349
  <span className="text-2xs text-text-4 font-sans">Auto-saves</span>
326
350
  </div>
327
351
  <div className="grid grid-cols-3 gap-3">
352
+
328
353
  <ConfigCard icon={Cpu} label="Default Provider" description="Provider used when spawning new agents.">
329
354
  <select
330
355
  value={config.defaultProvider || 'claude-code'}
331
356
  onChange={(e) => updateConfig('defaultProvider', e.target.value)}
332
357
  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
358
  >
334
- {installedProviders.map((p) => (
359
+ {providers.filter((p) => p.installed || p.hasKey).map((p) => (
335
360
  <option key={p.id} value={p.id}>{p.name}</option>
336
361
  ))}
337
362
  </select>
338
363
  </ConfigCard>
339
364
 
340
365
  <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)} />
366
+ <div className="flex items-center gap-1.5">
367
+ <code className="flex-1 h-8 px-2 flex items-center bg-surface-0 border border-border-subtle rounded-md text-2xs font-mono text-text-2 truncate min-w-0">
368
+ {config.defaultWorkingDir || 'Project root'}
369
+ </code>
370
+ <Button variant="secondary" size="sm" onClick={() => setFolderBrowserOpen(true)} className="h-8 px-2 flex-shrink-0">
371
+ <FolderSearch size={12} />
372
+ </Button>
350
373
  </div>
351
374
  </ConfigCard>
352
375
 
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
- />
376
+ <ConfigCard icon={Gauge} label="Rotation Threshold" description="Context usage that triggers auto-rotation.">
377
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
378
+ {['auto', '50%', '65%', '75%', '85%'].map((opt) => {
379
+ const val = opt === 'auto' ? 0 : parseInt(opt, 10) / 100;
380
+ const isActive = rotationValue === val;
381
+ return (
382
+ <button
383
+ key={opt}
384
+ onClick={() => updateConfig('rotationThreshold', val)}
385
+ className={cn(
386
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
387
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
388
+ )}
389
+ >
390
+ {opt === 'auto' ? 'Auto' : opt}
391
+ </button>
392
+ );
393
+ })}
394
+ </div>
361
395
  </ConfigCard>
362
396
 
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
- />
397
+ <ConfigCard icon={ShieldCheck} label="QC Threshold" description="Running agents count that triggers auto-QC.">
398
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
399
+ {[2, 3, 4, 6, 8].map((n) => {
400
+ const isActive = (config.qcThreshold || 2) === n;
401
+ return (
402
+ <button
403
+ key={n}
404
+ onClick={() => updateConfig('qcThreshold', n)}
405
+ className={cn(
406
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
407
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
408
+ )}
409
+ >
410
+ {n}
411
+ </button>
412
+ );
413
+ })}
414
+ </div>
371
415
  </ConfigCard>
372
416
 
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
- />
417
+ <ConfigCard icon={Users} label="Max Agents" description="Concurrent agent limit. 0 = unlimited.">
418
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
419
+ {[0, 4, 8, 12, 20].map((n) => {
420
+ const isActive = (config.maxAgents || 0) === n;
421
+ return (
422
+ <button
423
+ key={n}
424
+ onClick={() => updateConfig('maxAgents', n)}
425
+ className={cn(
426
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
427
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
428
+ )}
429
+ >
430
+ {n === 0 ? '\u221E' : n}
431
+ </button>
432
+ );
433
+ })}
434
+ </div>
381
435
  </ConfigCard>
382
436
 
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>
437
+ <ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles.">
438
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
439
+ {[60, 120, 300, 600].map((n) => {
440
+ const isActive = (config.journalistInterval || 120) === n;
441
+ const label = n < 60 ? `${n}s` : `${n / 60}m`;
442
+ return (
443
+ <button
444
+ key={n}
445
+ onClick={() => updateConfig('journalistInterval', n)}
446
+ className={cn(
447
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
448
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
449
+ )}
450
+ >
451
+ {label}
452
+ </button>
453
+ );
454
+ })}
393
455
  </div>
394
456
  </ConfigCard>
457
+
395
458
  </div>
396
459
  </div>
397
460
  )}
398
461
  </div>
399
462
  </ScrollArea>
463
+
464
+ {/* Folder Browser Modal */}
465
+ <FolderBrowser
466
+ open={folderBrowserOpen}
467
+ onOpenChange={setFolderBrowserOpen}
468
+ currentPath={config?.defaultWorkingDir || '/'}
469
+ onSelect={(dir) => updateConfig('defaultWorkingDir', dir)}
470
+ />
400
471
  </div>
401
472
  );
402
473
  }
@@ -1 +0,0 @@
1
- 127.0.0.1
@@ -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-09T06:18:20.893Z*