groove-dev 0.19.8 → 0.19.9

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-Db0ZssmH.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-CdbNHOqF.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">
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-2">
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,9 +140,27 @@ function ProviderCard({ provider, onKeyChange }) {
135
140
  </div>
136
141
  )}
137
142
 
143
+ {/* Subscription info for Claude */}
144
+ {isSubscription && isReady && !provider.hasKey && (
145
+ <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 mb-2">
146
+ <Check size={10} /> Subscription active
147
+ </div>
148
+ )}
149
+
150
+ {/* Connected state */}
151
+ {provider.hasKey && !settingKey && (
152
+ <div className="flex items-center gap-1.5 mb-2">
153
+ <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">
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
+
138
161
  {/* Key input form */}
139
- {settingKey ? (
140
- <div className="space-y-1.5">
162
+ {settingKey && (
163
+ <div className="space-y-1.5 mb-2">
141
164
  <div className="flex gap-1.5">
142
165
  <div className="flex-1 relative">
143
166
  <input
@@ -159,32 +182,21 @@ function ProviderCard({ provider, onKeyChange }) {
159
182
  <Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-7 text-2xs px-2">Cancel</Button>
160
183
  </div>
161
184
  </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
185
+ )}
186
+
187
+ {/* Spacer to push button to bottom */}
188
+ <div className="flex-1" />
189
+
190
+ {/* Bottom action — always at card bottom */}
191
+ {!settingKey && !provider.hasKey && (
192
+ <Button
193
+ variant={isSubscription ? 'secondary' : 'primary'}
194
+ size="sm"
195
+ onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
196
+ className="w-full h-7 text-2xs gap-1 mt-2"
197
+ >
198
+ <Key size={10} />
199
+ {isSubscription ? 'Add API key for headless mode' : 'Add API Key'}
188
200
  </Button>
189
201
  )}
190
202
  </div>
@@ -201,9 +213,7 @@ function ConfigCard({ icon: Icon, label, description, children }) {
201
213
  <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
202
214
  <Icon size={12} className="text-accent" />
203
215
  </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>
216
+ <div className="text-[13px] font-medium text-text-0 font-sans leading-tight">{label}</div>
207
217
  </div>
208
218
  <div className="text-2xs text-text-4 font-sans leading-relaxed">{description}</div>
209
219
  <div className="mt-auto pt-1">{children}</div>
@@ -218,6 +228,7 @@ export default function SettingsView() {
218
228
  const [config, setConfig] = useState(null);
219
229
  const [daemonInfo, setDaemonInfo] = useState(null);
220
230
  const [loading, setLoading] = useState(true);
231
+ const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
221
232
  const addToast = useGrooveStore((s) => s.addToast);
222
233
  const marketplaceUser = useGrooveStore((s) => s.marketplaceUser);
223
234
  const marketplaceAuthenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
@@ -246,26 +257,33 @@ export default function SettingsView() {
246
257
  if (loading) {
247
258
  return (
248
259
  <div className="flex flex-col h-full">
249
- <div className="h-16 bg-surface-1 border-b border-border" />
260
+ <div className="h-12 bg-surface-1 border-b border-border" />
250
261
  <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>
262
+ <div className="grid grid-cols-4 gap-3">{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-40 rounded-lg" />)}</div>
252
263
  <div className="grid grid-cols-3 gap-3">{[...Array(6)].map((_, i) => <Skeleton key={i} className="h-28 rounded-lg" />)}</div>
253
264
  </div>
254
265
  </div>
255
266
  );
256
267
  }
257
268
 
258
- const installedProviders = providers.filter((p) => p.installed || p.hasKey);
269
+ const connectedCount = providers.filter((p) => {
270
+ if (p.authType === 'local') return p.installed;
271
+ if (p.authType === 'subscription') return p.installed;
272
+ return p.hasKey;
273
+ }).length;
274
+
275
+ // Rotation threshold display: 0 = auto, otherwise show as percentage
276
+ const rotationValue = config?.rotationThreshold || 0;
277
+ const rotationDisplay = rotationValue === 0 ? 'auto' : `${Math.round(rotationValue * 100)}%`;
259
278
 
260
279
  return (
261
280
  <div className="flex flex-col h-full">
262
281
 
263
- {/* ═══════ ACCOUNT HERO BAR ═══════ */}
282
+ {/* ═══════ HEADER BAR ═══════ */}
264
283
  <div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border-b border-border flex-shrink-0">
265
284
  <h2 className="text-sm font-semibold text-text-0 font-sans">Settings</h2>
266
285
  <div className="flex-1" />
267
286
 
268
- {/* Daemon info */}
269
287
  <div className="flex items-center gap-4 text-2xs text-text-3 font-sans">
270
288
  {daemonInfo?.version && <span>v{daemonInfo.version}</span>}
271
289
  {daemonInfo?.port && <span>:{daemonInfo.port}</span>}
@@ -274,7 +292,6 @@ export default function SettingsView() {
274
292
 
275
293
  <div className="w-px h-4 bg-border-subtle" />
276
294
 
277
- {/* Account */}
278
295
  {marketplaceAuthenticated ? (
279
296
  <div className="flex items-center gap-2.5">
280
297
  {marketplaceUser?.avatar ? (
@@ -302,12 +319,12 @@ export default function SettingsView() {
302
319
  <ScrollArea className="flex-1">
303
320
  <div className="p-4 space-y-4">
304
321
 
305
- {/* ═══════ PROVIDERS ROW ═══════ */}
322
+ {/* ═══════ PROVIDERS ═══════ */}
306
323
  <div>
307
324
  <div className="flex items-center gap-2 mb-2.5 px-0.5">
308
325
  <span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Providers</span>
309
326
  <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>
327
+ <span className="text-2xs text-text-4 font-sans">{connectedCount}/{providers.length} connected</span>
311
328
  </div>
312
329
  <div className="grid grid-cols-4 gap-3">
313
330
  {providers.map((p) => (
@@ -316,7 +333,7 @@ export default function SettingsView() {
316
333
  </div>
317
334
  </div>
318
335
 
319
- {/* ═══════ CONFIGURATION GRID ═══════ */}
336
+ {/* ═══════ CONFIGURATION ═══════ */}
320
337
  {config && (
321
338
  <div>
322
339
  <div className="flex items-center gap-2 mb-2.5 px-0.5">
@@ -325,78 +342,125 @@ export default function SettingsView() {
325
342
  <span className="text-2xs text-text-4 font-sans">Auto-saves</span>
326
343
  </div>
327
344
  <div className="grid grid-cols-3 gap-3">
345
+
328
346
  <ConfigCard icon={Cpu} label="Default Provider" description="Provider used when spawning new agents.">
329
347
  <select
330
348
  value={config.defaultProvider || 'claude-code'}
331
349
  onChange={(e) => updateConfig('defaultProvider', e.target.value)}
332
350
  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
351
  >
334
- {installedProviders.map((p) => (
352
+ {providers.filter((p) => p.installed || p.hasKey).map((p) => (
335
353
  <option key={p.id} value={p.id}>{p.name}</option>
336
354
  ))}
337
355
  </select>
338
356
  </ConfigCard>
339
357
 
340
358
  <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)} />
359
+ <div className="flex items-center gap-1.5">
360
+ <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">
361
+ {config.defaultWorkingDir || 'Project root'}
362
+ </code>
363
+ <Button variant="secondary" size="sm" onClick={() => setFolderBrowserOpen(true)} className="h-8 px-2 flex-shrink-0">
364
+ <FolderSearch size={12} />
365
+ </Button>
350
366
  </div>
351
367
  </ConfigCard>
352
368
 
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
- />
369
+ <ConfigCard icon={Gauge} label="Rotation Threshold" description="Context usage that triggers auto-rotation.">
370
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
371
+ {['auto', '50%', '65%', '75%', '85%'].map((opt) => {
372
+ const val = opt === 'auto' ? 0 : parseInt(opt, 10) / 100;
373
+ const isActive = rotationValue === val;
374
+ return (
375
+ <button
376
+ key={opt}
377
+ onClick={() => updateConfig('rotationThreshold', val)}
378
+ className={cn(
379
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
380
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
381
+ )}
382
+ >
383
+ {opt === 'auto' ? 'Auto' : opt}
384
+ </button>
385
+ );
386
+ })}
387
+ </div>
361
388
  </ConfigCard>
362
389
 
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
- />
390
+ <ConfigCard icon={ShieldCheck} label="QC Threshold" description="Running agents count that triggers auto-QC.">
391
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
392
+ {[2, 3, 4, 6, 8].map((n) => {
393
+ const isActive = (config.qcThreshold || 2) === n;
394
+ return (
395
+ <button
396
+ key={n}
397
+ onClick={() => updateConfig('qcThreshold', n)}
398
+ className={cn(
399
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
400
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
401
+ )}
402
+ >
403
+ {n}
404
+ </button>
405
+ );
406
+ })}
407
+ </div>
371
408
  </ConfigCard>
372
409
 
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
- />
410
+ <ConfigCard icon={Users} label="Max Agents" description="Concurrent agent limit. 0 = unlimited.">
411
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
412
+ {[0, 4, 8, 12, 20].map((n) => {
413
+ const isActive = (config.maxAgents || 0) === n;
414
+ return (
415
+ <button
416
+ key={n}
417
+ onClick={() => updateConfig('maxAgents', n)}
418
+ className={cn(
419
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
420
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
421
+ )}
422
+ >
423
+ {n === 0 ? '\u221E' : n}
424
+ </button>
425
+ );
426
+ })}
427
+ </div>
381
428
  </ConfigCard>
382
429
 
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>
430
+ <ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles.">
431
+ <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
432
+ {[60, 120, 300, 600].map((n) => {
433
+ const isActive = (config.journalistInterval || 120) === n;
434
+ const label = n < 60 ? `${n}s` : `${n / 60}m`;
435
+ return (
436
+ <button
437
+ key={n}
438
+ onClick={() => updateConfig('journalistInterval', n)}
439
+ className={cn(
440
+ 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
441
+ isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
442
+ )}
443
+ >
444
+ {label}
445
+ </button>
446
+ );
447
+ })}
393
448
  </div>
394
449
  </ConfigCard>
450
+
395
451
  </div>
396
452
  </div>
397
453
  )}
398
454
  </div>
399
455
  </ScrollArea>
456
+
457
+ {/* Folder Browser Modal */}
458
+ <FolderBrowser
459
+ open={folderBrowserOpen}
460
+ onOpenChange={setFolderBrowserOpen}
461
+ currentPath={config?.defaultWorkingDir || '/'}
462
+ onSelect={(dir) => updateConfig('defaultWorkingDir', dir)}
463
+ />
400
464
  </div>
401
465
  );
402
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.19.8",
3
+ "version": "0.19.9",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -24,7 +24,7 @@ import { federationPair, federationUnpair, federationList, federationStatus } fr
24
24
  program
25
25
  .name('groove')
26
26
  .description('Agent orchestration layer for AI coding tools')
27
- .version('0.19.8');
27
+ .version('0.19.9');
28
28
 
29
29
  program
30
30
  .command('start')