groove-dev 0.27.73 → 0.27.75

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.
Files changed (73) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +256 -4
  4. package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +41 -1
  6. package/node_modules/@groove-dev/daemon/src/preview.js +18 -2
  7. package/node_modules/@groove-dev/daemon/src/process.js +6 -1
  8. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +2 -1
  10. package/node_modules/@groove-dev/daemon/src/providers/codex.js +41 -1
  11. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
  13. package/node_modules/@groove-dev/daemon/src/providers/index.js +26 -9
  14. package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-CAT9SCJi.js +8620 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/app.css +29 -0
  20. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +40 -7
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +149 -31
  24. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
  25. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
  27. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
  28. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +81 -0
  29. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +263 -0
  30. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +203 -0
  31. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
  32. package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
  33. package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
  34. package/node_modules/@groove-dev/gui/src/views/settings.jsx +278 -123
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/package.json +1 -1
  38. package/packages/daemon/src/api.js +256 -4
  39. package/packages/daemon/src/conversations.js +16 -0
  40. package/packages/daemon/src/index.js +41 -1
  41. package/packages/daemon/src/preview.js +18 -2
  42. package/packages/daemon/src/process.js +6 -1
  43. package/packages/daemon/src/providers/base.js +4 -0
  44. package/packages/daemon/src/providers/claude-code.js +2 -1
  45. package/packages/daemon/src/providers/codex.js +41 -1
  46. package/packages/daemon/src/providers/gemini.js +2 -1
  47. package/packages/daemon/src/providers/grok.js +156 -0
  48. package/packages/daemon/src/providers/index.js +26 -9
  49. package/packages/daemon/src/providers/nano-banana.js +103 -0
  50. package/packages/gui/dist/assets/index-CAT9SCJi.js +8620 -0
  51. package/packages/gui/dist/assets/index-CVzz6zyb.css +1 -0
  52. package/packages/gui/dist/index.html +2 -2
  53. package/packages/gui/package.json +1 -1
  54. package/packages/gui/src/app.css +29 -0
  55. package/packages/gui/src/app.jsx +2 -0
  56. package/packages/gui/src/components/chat/chat-header.jsx +16 -5
  57. package/packages/gui/src/components/chat/chat-input.jsx +40 -7
  58. package/packages/gui/src/components/chat/chat-messages.jsx +149 -31
  59. package/packages/gui/src/components/chat/chat-view.jsx +26 -2
  60. package/packages/gui/src/components/chat/model-picker.jsx +105 -52
  61. package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
  62. package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
  63. package/packages/gui/src/components/preview/preview-toolbar.jsx +81 -0
  64. package/packages/gui/src/components/preview/preview-workspace.jsx +263 -0
  65. package/packages/gui/src/components/preview/screenshot-overlay.jsx +203 -0
  66. package/packages/gui/src/components/ui/toast.jsx +6 -2
  67. package/packages/gui/src/stores/groove.js +149 -9
  68. package/packages/gui/src/views/preview.jsx +6 -0
  69. package/packages/gui/src/views/settings.jsx +278 -123
  70. package/node_modules/@groove-dev/gui/dist/assets/index-BFc7Ov6v.css +0 -1
  71. package/node_modules/@groove-dev/gui/dist/assets/index-Deza1S0i.js +0 -8615
  72. package/packages/gui/dist/assets/index-BFc7Ov6v.css +0 -1
  73. package/packages/gui/dist/assets/index-Deza1S0i.js +0 -8615
@@ -14,7 +14,7 @@ import { api } from '../lib/api';
14
14
  import { cn } from '../lib/cn';
15
15
  import { fmtUptime } from '../lib/format';
16
16
  import {
17
- Key, Eye, EyeOff, Check, Cpu, Download, Loader2,
17
+ Key, Eye, EyeOff, Check, Cpu, Download, Loader2, RefreshCw, Terminal, Copy,
18
18
  FolderOpen, FolderSearch, Users, Gauge, ChevronRight,
19
19
  ShieldCheck, Settings, Lock,
20
20
  Newspaper, Radio, Send, MessageSquare, MessageCircle,
@@ -46,6 +46,7 @@ const KEY_PLACEHOLDERS = {
46
46
  'claude-code': 'sk-ant-...',
47
47
  codex: 'Paste from platform.openai.com',
48
48
  gemini: 'Paste from aistudio.google.com',
49
+ grok: 'xai-...',
49
50
  };
50
51
 
51
52
  function ProviderCard({ provider, onKeyChange }) {
@@ -58,10 +59,14 @@ function ProviderCard({ provider, onKeyChange }) {
58
59
  const [customPathOpen, setCustomPathOpen] = useState(false);
59
60
  const [customPath, setCustomPath] = useState('');
60
61
  const [savingPath, setSavingPath] = useState(false);
62
+ const [loginPending, setLoginPending] = useState(false);
61
63
  const addToast = useGrooveStore((s) => s.addToast);
62
64
  const installProgress = useGrooveStore((s) => s.providerInstallProgress[provider.id]);
63
65
  const loginProvider = useGrooveStore((s) => s.loginProvider);
64
66
  const setProviderPath = useGrooveStore((s) => s.setProviderPath);
67
+ const verifyProvider = useGrooveStore((s) => s.verifyProvider);
68
+ const installProvider = useGrooveStore((s) => s.installProvider);
69
+ const [checking, setChecking] = useState(false);
65
70
 
66
71
  const isLocal = provider.authType === 'local';
67
72
  const isSubscription = provider.authType === 'subscription';
@@ -94,8 +99,11 @@ function ProviderCard({ provider, onKeyChange }) {
94
99
 
95
100
  async function handleLogin(body) {
96
101
  try {
102
+ setLoginPending(true);
97
103
  await loginProvider(provider.id, body);
98
- } catch { /* handled in store */ }
104
+ } catch {
105
+ setLoginPending(false);
106
+ }
99
107
  }
100
108
 
101
109
  async function handleSavePath() {
@@ -221,73 +229,240 @@ function ProviderCard({ provider, onKeyChange }) {
221
229
  </div>
222
230
  )}
223
231
 
224
- {/* Not installed — prominent Install button */}
232
+ {/* Not installed */}
225
233
  {!provider.installed && !isInstalling && !settingKey && (
226
- <div className="flex flex-col items-center justify-center flex-1 gap-3 py-2">
227
- <p className="text-xs text-text-3 font-sans text-center">
228
- {provider.name} is not installed on this machine.
229
- </p>
234
+ <div className="flex flex-col gap-2.5 flex-1">
235
+ {/* Install error from last attempt */}
236
+ {installProgress?.error && (
237
+ <div className="p-2.5 bg-danger/5 border border-danger/15 rounded-md">
238
+ <p className="text-2xs text-danger font-sans break-all">{installProgress.error}</p>
239
+ </div>
240
+ )}
241
+
242
+ {/* Auto-install button */}
230
243
  <Button
231
244
  variant="primary"
232
245
  size="sm"
233
- onClick={() => openWizard(0)}
234
- className="h-8 px-4 text-xs gap-1.5"
246
+ onClick={() => installProvider(provider.id).catch(() => {})}
247
+ className="w-full h-8 text-2xs gap-1.5"
235
248
  >
236
- <Download size={12} /> Install {provider.name}
249
+ <Download size={11} /> Install {provider.name}
237
250
  </Button>
251
+
252
+ {/* Manual install command */}
253
+ {provider.installCommand && (
254
+ <div className="space-y-1">
255
+ <p className="text-2xs text-text-4 font-sans">Or install manually in your terminal:</p>
256
+ <div className="flex items-center gap-1">
257
+ <code className="flex-1 px-2 py-1.5 bg-surface-0 border border-border-subtle rounded text-2xs font-mono text-text-2 select-all">
258
+ {provider.installCommand}
259
+ </code>
260
+ <button
261
+ onClick={() => { navigator.clipboard.writeText(provider.installCommand); addToast('success', 'Copied'); }}
262
+ className="p-1.5 text-text-4 hover:text-text-2 cursor-pointer"
263
+ >
264
+ <Copy size={10} />
265
+ </button>
266
+ </div>
267
+ </div>
268
+ )}
269
+
270
+ {/* Re-check + custom path */}
271
+ <div className="flex items-center gap-2 pt-1">
272
+ <Button
273
+ variant="ghost"
274
+ size="sm"
275
+ onClick={async () => {
276
+ setChecking(true);
277
+ try {
278
+ await verifyProvider(provider.id);
279
+ if (onKeyChange) onKeyChange();
280
+ } catch { /* handled in store */ }
281
+ setChecking(false);
282
+ }}
283
+ disabled={checking}
284
+ className="h-7 text-2xs gap-1 px-2"
285
+ >
286
+ <RefreshCw size={10} className={checking ? 'animate-spin' : ''} /> Re-check
287
+ </Button>
288
+ <button
289
+ onClick={() => setCustomPathOpen(!customPathOpen)}
290
+ className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans"
291
+ >
292
+ Set custom path
293
+ </button>
294
+ </div>
295
+
296
+ {/* Custom path input (shown for not-installed too) */}
297
+ {customPathOpen && (
298
+ <div className="space-y-2">
299
+ <div className="flex gap-2">
300
+ <input
301
+ value={customPath}
302
+ onChange={(e) => setCustomPath(e.target.value)}
303
+ onKeyDown={(e) => e.key === 'Enter' && handleSavePath()}
304
+ placeholder={`/path/to/${provider.id}`}
305
+ className="flex-1 h-7 px-2 text-2xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
306
+ />
307
+ <Button variant="primary" size="sm" onClick={handleSavePath} disabled={!customPath.trim() || savingPath} className="h-7 text-2xs px-2.5">
308
+ {savingPath ? '...' : 'Save'}
309
+ </Button>
310
+ </div>
311
+ </div>
312
+ )}
238
313
  </div>
239
314
  )}
240
315
 
241
316
  {/* Installed but needs auth */}
242
317
  {provider.installed && !isReady && !settingKey && !isInstalling && (
243
- <div className="flex flex-col gap-2.5 flex-1">
244
- {isSubscription ? (
318
+ <div className="flex flex-col gap-3 flex-1">
319
+ {/* ── Claude Code auth ── */}
320
+ {provider.id === 'claude-code' && !loginPending && (
245
321
  <>
246
- <Button
247
- variant="primary"
248
- size="sm"
249
- onClick={handleLogin}
250
- className="w-full h-8 text-2xs gap-1.5"
322
+ <div className="space-y-1.5">
323
+ <p className="text-xs text-text-1 font-sans font-medium">Sign in with your Claude account</p>
324
+ <p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your existing Anthropic account or Claude subscription.</p>
325
+ </div>
326
+ <Button variant="primary" size="sm" onClick={() => handleLogin()} className="w-full h-9 text-xs gap-1.5">
327
+ <ExternalLink size={12} /> Sign In
328
+ </Button>
329
+ <button
330
+ onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
331
+ className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
251
332
  >
252
- <ExternalLink size={11} /> Sign In with Anthropic
333
+ I have an API key instead
334
+ </button>
335
+ </>
336
+ )}
337
+
338
+ {/* ── Codex auth ── */}
339
+ {provider.id === 'codex' && !loginPending && (
340
+ <>
341
+ <div className="space-y-1.5">
342
+ <p className="text-xs text-text-1 font-sans font-medium">Sign in with your ChatGPT account</p>
343
+ <p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your ChatGPT Plus or Teams subscription.</p>
344
+ </div>
345
+ <Button variant="primary" size="sm" onClick={() => handleLogin({ method: 'chatgpt-plus' })} className="w-full h-9 text-xs gap-1.5">
346
+ <ExternalLink size={12} /> Sign In
253
347
  </Button>
254
- <Button
255
- variant="secondary"
256
- size="sm"
348
+ <button
257
349
  onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
258
- className="w-full h-8 text-2xs gap-1.5"
350
+ className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
259
351
  >
260
- <Key size={11} /> Add API Key Instead
352
+ I have an API key instead
353
+ </button>
354
+ </>
355
+ )}
356
+
357
+ {/* ── Gemini auth ── */}
358
+ {provider.id === 'gemini' && (
359
+ <>
360
+ <div className="space-y-2">
361
+ <p className="text-xs text-text-1 font-sans font-medium">Add your Gemini API key</p>
362
+ <div className="space-y-1.5">
363
+ <div className="flex items-start gap-2">
364
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
365
+ <p className="text-2xs text-text-2 font-sans">
366
+ Go to <button onClick={() => window.open('https://aistudio.google.com/apikey', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">aistudio.google.com</button> and sign in with Google
367
+ </p>
368
+ </div>
369
+ <div className="flex items-start gap-2">
370
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
371
+ <p className="text-2xs text-text-2 font-sans">Click "Create API Key" and copy it</p>
372
+ </div>
373
+ <div className="flex items-start gap-2">
374
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
375
+ <p className="text-2xs text-text-2 font-sans">Paste it below</p>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ <div className="relative">
380
+ <input
381
+ value={keyInput}
382
+ onChange={(e) => setKeyInput(e.target.value)}
383
+ onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
384
+ type={showKey ? 'text' : 'password'}
385
+ placeholder="AIza..."
386
+ 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"
387
+ autoFocus
388
+ />
389
+ <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">
390
+ {showKey ? <EyeOff size={12} /> : <Eye size={12} />}
391
+ </button>
392
+ </div>
393
+ <Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="w-full h-8 text-xs">
394
+ Save Key
261
395
  </Button>
262
396
  </>
263
- ) : provider.id === 'codex' ? (
397
+ )}
398
+
399
+ {/* ── Grok (xAI) auth ── */}
400
+ {provider.id === 'grok' && (
264
401
  <>
402
+ <div className="space-y-2">
403
+ <p className="text-xs text-text-1 font-sans font-medium">Add your xAI API key</p>
404
+ <div className="space-y-1.5">
405
+ <div className="flex items-start gap-2">
406
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
407
+ <p className="text-2xs text-text-2 font-sans">
408
+ Go to <button onClick={() => window.open('https://console.x.ai', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.x.ai</button> and sign in
409
+ </p>
410
+ </div>
411
+ <div className="flex items-start gap-2">
412
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
413
+ <p className="text-2xs text-text-2 font-sans">Create an API key and copy it</p>
414
+ </div>
415
+ <div className="flex items-start gap-2">
416
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
417
+ <p className="text-2xs text-text-2 font-sans">Paste it below</p>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ <div className="relative">
422
+ <input
423
+ value={keyInput}
424
+ onChange={(e) => setKeyInput(e.target.value)}
425
+ onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
426
+ type={showKey ? 'text' : 'password'}
427
+ placeholder="xai-..."
428
+ 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"
429
+ autoFocus
430
+ />
431
+ <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">
432
+ {showKey ? <EyeOff size={12} /> : <Eye size={12} />}
433
+ </button>
434
+ </div>
435
+ <Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="w-full h-8 text-xs">
436
+ Save Key
437
+ </Button>
438
+ </>
439
+ )}
440
+
441
+ {/* ── Any provider: login pending state ── */}
442
+ {(provider.id === 'claude-code' || provider.id === 'codex') && loginPending && (
443
+ <div className="flex flex-col gap-3">
444
+ <div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
445
+ <Loader2 size={14} className="text-accent animate-spin" />
446
+ <div>
447
+ <p className="text-xs text-accent font-sans font-medium">Check your browser</p>
448
+ <p className="text-2xs text-text-3 font-sans">Complete the sign-in in the browser window that opened.</p>
449
+ </div>
450
+ </div>
265
451
  <Button
266
452
  variant="primary"
267
453
  size="sm"
268
- onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
269
- className="w-full h-8 text-2xs gap-1.5"
454
+ onClick={() => { setLoginPending(false); if (onKeyChange) onKeyChange(); }}
455
+ className="w-full h-8 text-xs gap-1.5"
270
456
  >
271
- <Key size={11} /> Add API Key
457
+ <Check size={12} /> I've signed in
272
458
  </Button>
273
- <Button
274
- variant="secondary"
275
- size="sm"
276
- onClick={() => handleLogin({ method: 'chatgpt-plus' })}
277
- className="w-full h-8 text-2xs gap-1.5"
459
+ <button
460
+ onClick={() => setLoginPending(false)}
461
+ className="text-2xs text-text-4 hover:text-text-2 cursor-pointer font-sans text-center"
278
462
  >
279
- <ExternalLink size={11} /> Sign in with ChatGPT Plus
280
- </Button>
281
- </>
282
- ) : (
283
- <Button
284
- variant="primary"
285
- size="sm"
286
- onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
287
- className="w-full h-8 text-2xs gap-1.5"
288
- >
289
- <Key size={11} /> Add API Key
290
- </Button>
463
+ Cancel
464
+ </button>
465
+ </div>
291
466
  )}
292
467
  </div>
293
468
  )}
@@ -320,6 +495,26 @@ function ProviderCard({ provider, onKeyChange }) {
320
495
  <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
321
496
  {provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
322
497
  </label>
498
+ {!provider.hasKey && provider.id === 'claude-code' && (
499
+ <p className="text-2xs text-text-3 font-sans mb-1.5">
500
+ Get yours at <button onClick={() => window.open('https://console.anthropic.com/settings/keys', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.anthropic.com</button>
501
+ </p>
502
+ )}
503
+ {!provider.hasKey && provider.id === 'codex' && (
504
+ <p className="text-2xs text-text-3 font-sans mb-1.5">
505
+ Get yours at <button onClick={() => window.open('https://platform.openai.com/api-keys', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">platform.openai.com</button>
506
+ </p>
507
+ )}
508
+ {!provider.hasKey && provider.id === 'gemini' && (
509
+ <p className="text-2xs text-text-3 font-sans mb-1.5">
510
+ Get yours at <button onClick={() => window.open('https://aistudio.google.com/apikey', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">aistudio.google.com</button>
511
+ </p>
512
+ )}
513
+ {!provider.hasKey && provider.id === 'grok' && (
514
+ <p className="text-2xs text-text-3 font-sans mb-1.5">
515
+ Get yours at <button onClick={() => window.open('https://console.x.ai', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.x.ai</button>
516
+ </p>
517
+ )}
323
518
  <div className="relative">
324
519
  <input
325
520
  value={keyInput}
@@ -1375,85 +1570,45 @@ export default function SettingsView() {
1375
1570
  </div>
1376
1571
  </ConfigCard>
1377
1572
 
1378
- <ConfigCard icon={Gauge} label="Rotation Threshold" description="Context usage that triggers auto-rotation.">
1379
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1380
- {['auto', '50%', '65%', '75%', '85%'].map((opt) => {
1381
- const val = opt === 'auto' ? 0 : parseInt(opt, 10) / 100;
1382
- const isActive = rotationValue === val;
1383
- return (
1384
- <button
1385
- key={opt}
1386
- onClick={() => updateConfig('rotationThreshold', val)}
1387
- className={cn(
1388
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1389
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1390
- )}
1391
- >
1392
- {opt === 'auto' ? 'Auto' : opt}
1393
- </button>
1394
- );
1395
- })}
1396
- </div>
1397
- </ConfigCard>
1398
-
1399
- <ConfigCard icon={ShieldCheck} label="QC Threshold" description="Running agents count that triggers auto-QC.">
1400
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1401
- {[2, 3, 4, 6, 8].map((n) => {
1402
- const isActive = (config.qcThreshold || 2) === n;
1403
- return (
1404
- <button
1405
- key={n}
1406
- onClick={() => updateConfig('qcThreshold', n)}
1407
- className={cn(
1408
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1409
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1410
- )}
1411
- >
1412
- {n}
1413
- </button>
1414
- );
1415
- })}
1416
- </div>
1417
- </ConfigCard>
1418
-
1419
- <ConfigCard icon={Users} label="Max Agents" description="Concurrent agent limit. 0 = unlimited.">
1420
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1421
- {[0, 4, 8, 12, 20].map((n) => {
1422
- const isActive = (config.maxAgents || 0) === n;
1423
- return (
1424
- <button
1425
- key={n}
1426
- onClick={() => updateConfig('maxAgents', n)}
1427
- className={cn(
1428
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1429
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1430
- )}
1431
- >
1432
- {n === 0 ? '\u221E' : n}
1433
- </button>
1434
- );
1435
- })}
1436
- </div>
1437
- </ConfigCard>
1438
-
1439
- <ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles.">
1440
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1441
- {[60, 120, 300, 600].map((n) => {
1442
- const isActive = (config.journalistInterval || 120) === n;
1443
- const label = n < 60 ? `${n}s` : `${n / 60}m`;
1444
- return (
1445
- <button
1446
- key={n}
1447
- onClick={() => updateConfig('journalistInterval', n)}
1448
- className={cn(
1449
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1450
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1451
- )}
1452
- >
1453
- {label}
1454
- </button>
1455
- );
1456
- })}
1573
+ <ConfigCard icon={MessageSquare} label="Default Chat Model" description="Provider and model for new chat conversations.">
1574
+ <div className="space-y-2">
1575
+ <select
1576
+ value={config.defaultChatProvider || config.defaultProvider || 'claude-code'}
1577
+ onChange={(e) => {
1578
+ updateConfig('defaultChatProvider', e.target.value);
1579
+ const prov = providers.find((p) => p.id === e.target.value);
1580
+ const chatModels = (prov?.models || []).filter((m) => {
1581
+ const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
1582
+ return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
1583
+ });
1584
+ if (chatModels.length > 0) {
1585
+ const first = typeof chatModels[0] === 'string' ? chatModels[0] : chatModels[0].id;
1586
+ updateConfig('defaultChatModel', first);
1587
+ }
1588
+ }}
1589
+ 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"
1590
+ >
1591
+ {visibleProviders.filter((p) => p.installed && (p.authType === 'local' || p.authType === 'subscription' || p.hasKey)).map((p) => (
1592
+ <option key={p.id} value={p.id}>{p.name}</option>
1593
+ ))}
1594
+ </select>
1595
+ <select
1596
+ value={config.defaultChatModel || ''}
1597
+ onChange={(e) => updateConfig('defaultChatModel', e.target.value || null)}
1598
+ 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"
1599
+ >
1600
+ <option value="">Auto (Sonnet)</option>
1601
+ {(providers.find((p) => p.id === (config.defaultChatProvider || config.defaultProvider || 'claude-code'))?.models || [])
1602
+ .filter((m) => {
1603
+ const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
1604
+ return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
1605
+ })
1606
+ .map((m) => {
1607
+ const id = typeof m === 'string' ? m : m.id;
1608
+ const name = typeof m === 'string' ? m : m.name || m.id;
1609
+ return <option key={id} value={id}>{name}</option>;
1610
+ })}
1611
+ </select>
1457
1612
  </div>
1458
1613
  </ConfigCard>
1459
1614