groove-dev 0.27.74 → 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 (70) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +256 -4
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +41 -1
  7. package/node_modules/@groove-dev/daemon/src/preview.js +18 -2
  8. package/node_modules/@groove-dev/daemon/src/process.js +6 -1
  9. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/codex.js +38 -0
  11. package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
  12. package/node_modules/@groove-dev/daemon/src/providers/index.js +5 -1
  13. package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-CAT9SCJi.js +8620 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/app.css +29 -0
  19. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +40 -7
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +149 -31
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
  24. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
  25. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
  26. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
  27. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +81 -0
  28. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +263 -0
  29. package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +203 -0
  30. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
  31. package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
  32. package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
  33. package/node_modules/@groove-dev/gui/src/views/settings.jsx +199 -114
  34. package/package.json +1 -1
  35. package/packages/cli/package.json +1 -1
  36. package/packages/daemon/package.json +1 -1
  37. package/packages/daemon/src/api.js +256 -4
  38. package/packages/daemon/src/conversations.js +16 -0
  39. package/packages/daemon/src/index.js +41 -1
  40. package/packages/daemon/src/preview.js +18 -2
  41. package/packages/daemon/src/process.js +6 -1
  42. package/packages/daemon/src/providers/base.js +4 -0
  43. package/packages/daemon/src/providers/codex.js +38 -0
  44. package/packages/daemon/src/providers/grok.js +156 -0
  45. package/packages/daemon/src/providers/index.js +5 -1
  46. package/packages/daemon/src/providers/nano-banana.js +103 -0
  47. package/packages/gui/dist/assets/index-CAT9SCJi.js +8620 -0
  48. package/packages/gui/dist/assets/index-CVzz6zyb.css +1 -0
  49. package/packages/gui/dist/index.html +2 -2
  50. package/packages/gui/package.json +1 -1
  51. package/packages/gui/src/app.css +29 -0
  52. package/packages/gui/src/app.jsx +2 -0
  53. package/packages/gui/src/components/chat/chat-header.jsx +16 -5
  54. package/packages/gui/src/components/chat/chat-input.jsx +40 -7
  55. package/packages/gui/src/components/chat/chat-messages.jsx +149 -31
  56. package/packages/gui/src/components/chat/chat-view.jsx +26 -2
  57. package/packages/gui/src/components/chat/model-picker.jsx +105 -52
  58. package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
  59. package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
  60. package/packages/gui/src/components/preview/preview-toolbar.jsx +81 -0
  61. package/packages/gui/src/components/preview/preview-workspace.jsx +263 -0
  62. package/packages/gui/src/components/preview/screenshot-overlay.jsx +203 -0
  63. package/packages/gui/src/components/ui/toast.jsx +6 -2
  64. package/packages/gui/src/stores/groove.js +149 -9
  65. package/packages/gui/src/views/preview.jsx +6 -0
  66. package/packages/gui/src/views/settings.jsx +199 -114
  67. package/node_modules/@groove-dev/gui/dist/assets/index-DFP3r2yE.js +0 -8615
  68. package/node_modules/@groove-dev/gui/dist/assets/index-QR7lyguO.css +0 -1
  69. package/packages/gui/dist/assets/index-DFP3r2yE.js +0 -8615
  70. package/packages/gui/dist/assets/index-QR7lyguO.css +0 -1
@@ -56,8 +56,13 @@ export const useGrooveStore = create((set, get) => ({
56
56
  selectedPeerId: null,
57
57
  },
58
58
 
59
+ // ── Preview ───────────────────────────────────────────────
60
+ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false },
61
+ previewChat: [],
62
+ previewIterating: false,
63
+
59
64
  // ── Navigation ────────────────────────────────────────────
60
- activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
65
+ activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings' | 'preview'
61
66
  detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
62
67
  teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
63
68
  commandPaletteOpen: false,
@@ -455,21 +460,30 @@ export const useGrooveStore = create((set, get) => ({
455
460
  'success',
456
461
  'Project ready to preview',
457
462
  msg.url,
458
- { label: 'View Site', url: msg.url },
463
+ { label: 'Open Preview', onClick: () => get().openPreview(msg.url, msg.teamId, msg.kind) },
459
464
  { persistent: true },
460
465
  );
461
466
  break;
462
467
 
463
- case 'preview:failed':
464
- get().addToast(
465
- 'warning',
466
- 'Preview could not launch',
467
- msg.reason ? String(msg.reason).slice(0, 200) : 'Unknown error',
468
- );
468
+ case 'preview:failed': {
469
+ const failKind = msg.kind || '';
470
+ if (failKind !== 'no_preview' && failKind !== 'cli' && failKind !== 'none') {
471
+ get().addToast(
472
+ 'warning',
473
+ 'Preview could not launch',
474
+ msg.reason ? String(msg.reason).slice(0, 200) : 'Unknown error',
475
+ );
476
+ }
469
477
  break;
478
+ }
470
479
 
471
- case 'preview:stopped':
480
+ case 'preview:stopped': {
481
+ const ps = get().previewState;
482
+ if (ps.teamId && ps.teamId === msg.teamId) {
483
+ set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false });
484
+ }
472
485
  break;
486
+ }
473
487
 
474
488
  case 'agent:stalled': {
475
489
  const name = msg.agentName || msg.agentId;
@@ -899,6 +913,55 @@ export const useGrooveStore = create((set, get) => ({
899
913
  break;
900
914
  }
901
915
 
916
+ case 'conversation:image': {
917
+ const { conversationId, prompt, url, b64_json, mimeType, model: imgModel, provider: imgProvider } = msg.data || msg;
918
+ if (!conversationId) break;
919
+ const imageUrl = url || (b64_json ? `data:${mimeType || 'image/png'};base64,${b64_json}` : null);
920
+ set((s) => {
921
+ const msgs = { ...s.conversationMessages };
922
+ if (!msgs[conversationId]) msgs[conversationId] = [];
923
+ const arr = [...msgs[conversationId]];
924
+ const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading' && m.prompt === prompt);
925
+ if (loadingIdx >= 0) {
926
+ arr[loadingIdx] = { from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() };
927
+ } else {
928
+ arr.push({ from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() });
929
+ }
930
+ msgs[conversationId] = arr.slice(-200);
931
+ persistJSON('groove:conversationMessages', msgs);
932
+ const isActive = s.streamingConversationId === conversationId;
933
+ return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
934
+ });
935
+ break;
936
+ }
937
+
938
+ case 'conversation:image-progress': {
939
+ const { conversationId, status, prompt: imgPrompt, error: imgError } = msg.data || msg;
940
+ if (!conversationId) break;
941
+ if (status === 'generating') {
942
+ set((s) => {
943
+ const msgs = { ...s.conversationMessages };
944
+ if (!msgs[conversationId]) msgs[conversationId] = [];
945
+ msgs[conversationId] = [...msgs[conversationId], { from: 'assistant', type: 'image-loading', prompt: imgPrompt, timestamp: Date.now() }];
946
+ return { conversationMessages: msgs, streamingConversationId: conversationId };
947
+ });
948
+ } else if (status === 'error') {
949
+ set((s) => {
950
+ const msgs = { ...s.conversationMessages };
951
+ if (!msgs[conversationId]) msgs[conversationId] = [];
952
+ const arr = [...msgs[conversationId]];
953
+ const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading');
954
+ if (loadingIdx >= 0) arr.splice(loadingIdx, 1);
955
+ arr.push({ from: 'system', text: `Image generation failed: ${imgError || 'Unknown error'}`, timestamp: Date.now() });
956
+ msgs[conversationId] = arr;
957
+ persistJSON('groove:conversationMessages', msgs);
958
+ const isActive = s.streamingConversationId === conversationId;
959
+ return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
960
+ });
961
+ }
962
+ break;
963
+ }
964
+
902
965
  case 'conversation:error': {
903
966
  const { conversationId, error } = msg.data || msg;
904
967
  if (conversationId) {
@@ -1107,6 +1170,50 @@ export const useGrooveStore = create((set, get) => ({
1107
1170
  persistJSON('groove:expandedNodes', expanded);
1108
1171
  },
1109
1172
 
1173
+ // ── Preview ──────────────────────────────────────────────
1174
+
1175
+ openPreview(url, teamId, kind) {
1176
+ set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], activeView: 'preview' });
1177
+ },
1178
+ closePreview() {
1179
+ const { previewState } = get();
1180
+ if (previewState.teamId) {
1181
+ api.delete(`/preview/${previewState.teamId}`).catch(() => {});
1182
+ }
1183
+ set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false, activeView: 'agents' });
1184
+ },
1185
+ setPreviewDevice(size) {
1186
+ set((s) => ({ previewState: { ...s.previewState, deviceSize: size } }));
1187
+ },
1188
+ toggleScreenshotMode() {
1189
+ set((s) => ({ previewState: { ...s.previewState, screenshotMode: !s.previewState.screenshotMode } }));
1190
+ },
1191
+ async iteratePreview(message, screenshotBase64) {
1192
+ const { previewState } = get();
1193
+ if (!previewState.teamId) return;
1194
+
1195
+ const userMsg = { role: 'user', content: message, screenshot: screenshotBase64 || null, timestamp: Date.now() };
1196
+ set((s) => ({ previewChat: [...s.previewChat, userMsg], previewIterating: true }));
1197
+
1198
+ try {
1199
+ const body = { message };
1200
+ if (screenshotBase64) body.screenshot = screenshotBase64;
1201
+ const res = await api.post(`/preview/${previewState.teamId}/iterate`, body);
1202
+ const assistantMsg = { role: 'assistant', content: res.response || res.message || 'Changes routed to planner.', timestamp: Date.now() };
1203
+ set((s) => ({ previewChat: [...s.previewChat, assistantMsg], previewIterating: false }));
1204
+ } catch (err) {
1205
+ const errMsg = { role: 'assistant', content: `Failed to iterate: ${err.message}`, timestamp: Date.now() };
1206
+ set((s) => ({ previewChat: [...s.previewChat, errMsg], previewIterating: false }));
1207
+ }
1208
+ },
1209
+ addPreviewChatMessage(role, content, screenshot) {
1210
+ const msg = { role, content, screenshot: screenshot || null, timestamp: Date.now() };
1211
+ set((s) => ({ previewChat: [...s.previewChat, msg] }));
1212
+ },
1213
+ clearPreviewChat() {
1214
+ set({ previewChat: [] });
1215
+ },
1216
+
1110
1217
  // ── Toasts ────────────────────────────────────────────────
1111
1218
 
1112
1219
  addToast(type, message, detail, action, options = {}) {
@@ -1488,6 +1595,13 @@ export const useGrooveStore = create((set, get) => ({
1488
1595
  get().fetchTreeDir('');
1489
1596
  },
1490
1597
 
1598
+ async removeRecentProject(path) {
1599
+ try {
1600
+ await api.delete('/projects/recent', { path });
1601
+ } catch {}
1602
+ get().fetchProjectDir();
1603
+ },
1604
+
1491
1605
  toggleProjectPicker() {
1492
1606
  set((s) => ({ showProjectPicker: !s.showProjectPicker }));
1493
1607
  },
@@ -1977,6 +2091,32 @@ export const useGrooveStore = create((set, get) => ({
1977
2091
  }
1978
2092
  },
1979
2093
 
2094
+ async sendImageMessage(conversationId, prompt, { model, size, quality } = {}) {
2095
+ const conv = get().conversations.find((c) => c.id === conversationId);
2096
+ if (!conv) return;
2097
+
2098
+ set((s) => {
2099
+ const msgs = { ...s.conversationMessages };
2100
+ if (!msgs[conversationId]) msgs[conversationId] = [];
2101
+ msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: prompt, timestamp: Date.now() }];
2102
+ persistJSON('groove:conversationMessages', msgs);
2103
+ return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
2104
+ });
2105
+
2106
+ try {
2107
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/generate-image`, { prompt, model, size, quality });
2108
+ } catch (err) {
2109
+ set((s) => {
2110
+ const msgs = { ...s.conversationMessages };
2111
+ if (!msgs[conversationId]) msgs[conversationId] = [];
2112
+ msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Image failed: ${err.message}`, timestamp: Date.now() }];
2113
+ persistJSON('groove:conversationMessages', msgs);
2114
+ return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
2115
+ });
2116
+ get().addToast('error', 'Image generation failed', err.message);
2117
+ }
2118
+ },
2119
+
1980
2120
  // ── Editor ────────────────────────────────────────────────
1981
2121
 
1982
2122
  async openFile(path) {
@@ -0,0 +1,6 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { PreviewWorkspace } from '../components/preview/preview-workspace';
3
+
4
+ export default function PreviewView() {
5
+ return <PreviewWorkspace />;
6
+ }
@@ -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,6 +59,7 @@ 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);
@@ -97,8 +99,11 @@ function ProviderCard({ provider, onKeyChange }) {
97
99
 
98
100
  async function handleLogin(body) {
99
101
  try {
102
+ setLoginPending(true);
100
103
  await loginProvider(provider.id, body);
101
- } catch { /* handled in store */ }
104
+ } catch {
105
+ setLoginPending(false);
106
+ }
102
107
  }
103
108
 
104
109
  async function handleSavePath() {
@@ -310,54 +315,154 @@ function ProviderCard({ provider, onKeyChange }) {
310
315
 
311
316
  {/* Installed but needs auth */}
312
317
  {provider.installed && !isReady && !settingKey && !isInstalling && (
313
- <div className="flex flex-col gap-2.5 flex-1">
314
- {isSubscription ? (
318
+ <div className="flex flex-col gap-3 flex-1">
319
+ {/* ── Claude Code auth ── */}
320
+ {provider.id === 'claude-code' && !loginPending && (
315
321
  <>
316
- <Button
317
- variant="primary"
318
- size="sm"
319
- onClick={handleLogin}
320
- 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"
321
332
  >
322
- <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
323
347
  </Button>
324
- <Button
325
- variant="secondary"
326
- size="sm"
348
+ <button
327
349
  onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
328
- 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"
329
351
  >
330
- <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
331
395
  </Button>
332
396
  </>
333
- ) : provider.id === 'codex' ? (
397
+ )}
398
+
399
+ {/* ── Grok (xAI) auth ── */}
400
+ {provider.id === 'grok' && (
334
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>
335
451
  <Button
336
452
  variant="primary"
337
453
  size="sm"
338
- onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
339
- 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"
340
456
  >
341
- <Key size={11} /> Add API Key
457
+ <Check size={12} /> I've signed in
342
458
  </Button>
343
- <Button
344
- variant="secondary"
345
- size="sm"
346
- onClick={() => handleLogin({ method: 'chatgpt-plus' })}
347
- 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"
348
462
  >
349
- <ExternalLink size={11} /> Sign in with ChatGPT Plus
350
- </Button>
351
- </>
352
- ) : (
353
- <Button
354
- variant="primary"
355
- size="sm"
356
- onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
357
- className="w-full h-8 text-2xs gap-1.5"
358
- >
359
- <Key size={11} /> Add API Key
360
- </Button>
463
+ Cancel
464
+ </button>
465
+ </div>
361
466
  )}
362
467
  </div>
363
468
  )}
@@ -390,6 +495,26 @@ function ProviderCard({ provider, onKeyChange }) {
390
495
  <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
391
496
  {provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
392
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
+ )}
393
518
  <div className="relative">
394
519
  <input
395
520
  value={keyInput}
@@ -1445,85 +1570,45 @@ export default function SettingsView() {
1445
1570
  </div>
1446
1571
  </ConfigCard>
1447
1572
 
1448
- <ConfigCard icon={Gauge} label="Rotation Threshold" description="Context usage that triggers auto-rotation.">
1449
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1450
- {['auto', '50%', '65%', '75%', '85%'].map((opt) => {
1451
- const val = opt === 'auto' ? 0 : parseInt(opt, 10) / 100;
1452
- const isActive = rotationValue === val;
1453
- return (
1454
- <button
1455
- key={opt}
1456
- onClick={() => updateConfig('rotationThreshold', val)}
1457
- className={cn(
1458
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1459
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1460
- )}
1461
- >
1462
- {opt === 'auto' ? 'Auto' : opt}
1463
- </button>
1464
- );
1465
- })}
1466
- </div>
1467
- </ConfigCard>
1468
-
1469
- <ConfigCard icon={ShieldCheck} label="QC Threshold" description="Running agents count that triggers auto-QC.">
1470
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1471
- {[2, 3, 4, 6, 8].map((n) => {
1472
- const isActive = (config.qcThreshold || 2) === n;
1473
- return (
1474
- <button
1475
- key={n}
1476
- onClick={() => updateConfig('qcThreshold', n)}
1477
- className={cn(
1478
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1479
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1480
- )}
1481
- >
1482
- {n}
1483
- </button>
1484
- );
1485
- })}
1486
- </div>
1487
- </ConfigCard>
1488
-
1489
- <ConfigCard icon={Users} label="Max Agents" description="Concurrent agent limit. 0 = unlimited.">
1490
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1491
- {[0, 4, 8, 12, 20].map((n) => {
1492
- const isActive = (config.maxAgents || 0) === n;
1493
- return (
1494
- <button
1495
- key={n}
1496
- onClick={() => updateConfig('maxAgents', n)}
1497
- className={cn(
1498
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1499
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1500
- )}
1501
- >
1502
- {n === 0 ? '\u221E' : n}
1503
- </button>
1504
- );
1505
- })}
1506
- </div>
1507
- </ConfigCard>
1508
-
1509
- <ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles.">
1510
- <div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
1511
- {[60, 120, 300, 600].map((n) => {
1512
- const isActive = (config.journalistInterval || 120) === n;
1513
- const label = n < 60 ? `${n}s` : `${n / 60}m`;
1514
- return (
1515
- <button
1516
- key={n}
1517
- onClick={() => updateConfig('journalistInterval', n)}
1518
- className={cn(
1519
- 'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1520
- isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
1521
- )}
1522
- >
1523
- {label}
1524
- </button>
1525
- );
1526
- })}
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>
1527
1612
  </div>
1528
1613
  </ConfigCard>
1529
1614
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.74",
3
+ "version": "0.27.75",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, 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, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.74",
3
+ "version": "0.27.75",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.74",
3
+ "version": "0.27.75",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",