groove-dev 0.27.69 → 0.27.71

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 (42) hide show
  1. package/MOE_TRAINING_PIPELINE.md +720 -0
  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 +272 -2
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/providers/base.js +8 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +52 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/codex.js +15 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/index.js +36 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-74E3YTkT.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +5 -5
  16. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +4 -4
  17. package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +1 -0
  18. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -2
  20. package/node_modules/@groove-dev/gui/src/views/settings.jsx +258 -84
  21. package/package.json +1 -1
  22. package/packages/cli/package.json +1 -1
  23. package/packages/daemon/package.json +1 -1
  24. package/packages/daemon/src/api.js +272 -2
  25. package/packages/daemon/src/index.js +3 -0
  26. package/packages/daemon/src/providers/base.js +8 -0
  27. package/packages/daemon/src/providers/claude-code.js +52 -0
  28. package/packages/daemon/src/providers/codex.js +15 -0
  29. package/packages/daemon/src/providers/gemini.js +14 -0
  30. package/packages/daemon/src/providers/index.js +36 -0
  31. package/packages/gui/dist/assets/index-74E3YTkT.css +1 -0
  32. package/packages/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
  33. package/packages/gui/dist/index.html +2 -2
  34. package/packages/gui/package.json +1 -1
  35. package/packages/gui/src/components/editor/code-editor.jsx +5 -5
  36. package/packages/gui/src/components/editor/editor-tabs.jsx +4 -4
  37. package/packages/gui/src/components/network/activity-chart.jsx +1 -0
  38. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
  39. package/packages/gui/src/stores/groove.js +107 -2
  40. package/packages/gui/src/views/settings.jsx +258 -84
  41. package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +0 -1
  42. package/packages/gui/dist/assets/index-oQ0ejlfH.css +0 -1
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-DhnTm_1P.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BK6tvmxx.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-oQ0ejlfH.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-74E3YTkT.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.69",
3
+ "version": "0.27.71",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -26,16 +26,16 @@ const LANGS = {
26
26
 
27
27
  // Custom theme overrides to match our design tokens
28
28
  const grooveTheme = EditorView.theme({
29
- '&': { backgroundColor: '#24282f', color: '#bcc2cd', fontFamily: 'var(--font-mono)', fontSize: '13px', height: '100%' },
29
+ '&': { backgroundColor: '#1a1e25', color: '#bcc2cd', fontFamily: 'var(--font-mono)', fontSize: '13px', height: '100%' },
30
30
  '.cm-scroller': { overflow: 'auto' },
31
31
  '.cm-content': { caretColor: '#33afbc' },
32
32
  '.cm-cursor': { borderLeftColor: '#33afbc' },
33
- '.cm-gutters': { backgroundColor: '#24282f', borderRight: '1px solid #2c313a', color: '#505862' },
34
- '.cm-activeLineGutter': { backgroundColor: '#2c313a' },
35
- '.cm-activeLine': { backgroundColor: 'rgba(44, 49, 58, 0.5)' },
33
+ '.cm-gutters': { backgroundColor: '#1a1e25', borderRight: '1px solid #22272e', color: '#505862' },
34
+ '.cm-activeLineGutter': { backgroundColor: '#22272e' },
35
+ '.cm-activeLine': { backgroundColor: 'rgba(34, 39, 46, 0.5)' },
36
36
  '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'rgba(51, 175, 188, 0.15)' },
37
37
  // Search panel styling
38
- '.cm-panels': { backgroundColor: '#24282f', borderBottom: '1px solid #3e4451' },
38
+ '.cm-panels': { backgroundColor: '#1a1e25', borderBottom: '1px solid #3e4451' },
39
39
  '.cm-panels.cm-panels-top': { borderBottom: '1px solid #3e4451' },
40
40
  '.cm-panels.cm-panels-bottom': { borderTop: '1px solid #3e4451' },
41
41
  '.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '12px', display: 'flex', flexWrap: 'wrap', alignItems: 'center' },
@@ -55,7 +55,7 @@ export function EditorTabs() {
55
55
  if (openTabs.length === 0) return null;
56
56
 
57
57
  return (
58
- <div className="flex items-center h-8 bg-surface-3 border-b border-border flex-shrink-0">
58
+ <div className="flex items-center h-8 bg-surface-3 border-b border-border-subtle flex-shrink-0">
59
59
  {overflows && (
60
60
  <button onClick={scrollLeft} className="flex-shrink-0 px-1 h-full text-text-4 hover:text-text-1 hover:bg-surface-4 transition-colors cursor-pointer">
61
61
  <ChevronLeft size={14} />
@@ -75,11 +75,11 @@ export function EditorTabs() {
75
75
  <div
76
76
  className={cn(
77
77
  'flex items-center gap-1.5 h-full px-3 text-xs font-sans cursor-pointer select-none',
78
- 'border-r border-border-subtle',
78
+ 'border-r border-white/5',
79
79
  'transition-colors duration-75 flex-shrink-0',
80
80
  isActive
81
- ? 'bg-surface-2 text-text-0 border-b-2 border-b-accent'
82
- : 'bg-surface-3 text-text-3 hover:text-text-1 hover:bg-surface-4',
81
+ ? 'bg-surface-1 text-text-0'
82
+ : 'bg-surface-3 text-text-4 hover:text-text-1 hover:bg-surface-4',
83
83
  )}
84
84
  style={{ scrollSnapAlign: 'start' }}
85
85
  onClick={() => setActiveFile(path)}
@@ -2,6 +2,7 @@
2
2
  import { useRef, useEffect, useState, useCallback, useMemo, memo } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { HEX, hexAlpha } from '../../lib/theme-hex';
5
+ import { cn } from '../../lib/cn';
5
6
  import { Badge } from '../ui/badge';
6
7
 
7
8
  function shortAddr(addr) {
@@ -0,0 +1,480 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+ import { Dialog, DialogContent } from '../ui/dialog';
6
+ import { Button } from '../ui/button';
7
+ import { Badge } from '../ui/badge';
8
+ import { cn } from '../../lib/cn';
9
+ import { api } from '../../lib/api';
10
+ import {
11
+ Download, Loader2, Check, ChevronDown, ChevronUp,
12
+ Eye, EyeOff, Key, RotateCcw, ExternalLink, Sparkles,
13
+ } from 'lucide-react';
14
+
15
+ const PROVIDER_META = {
16
+ 'claude-code': {
17
+ name: 'Claude Code',
18
+ letter: 'C',
19
+ color: 'bg-purple/20 text-purple',
20
+ installLabel: 'We are downloading Claude Code for you',
21
+ authLabel: 'Sign in with your Anthropic account',
22
+ keyPlaceholder: 'sk-ant-...',
23
+ keyHint: 'Paste from console.anthropic.com',
24
+ loginLabel: 'Sign In with Anthropic',
25
+ authType: 'subscription',
26
+ },
27
+ codex: {
28
+ name: 'Codex',
29
+ letter: 'X',
30
+ color: 'bg-success/20 text-success',
31
+ installLabel: 'We are downloading Codex for you',
32
+ authLabel: 'Connect your OpenAI account',
33
+ keyPlaceholder: 'sk-...',
34
+ keyHint: 'Paste from platform.openai.com',
35
+ loginLabel: 'Sign in with ChatGPT Plus',
36
+ authType: 'apikey',
37
+ },
38
+ gemini: {
39
+ name: 'Gemini CLI',
40
+ letter: 'G',
41
+ color: 'bg-info/20 text-info',
42
+ installLabel: 'We are downloading Gemini CLI for you',
43
+ authLabel: 'Add your Gemini API key',
44
+ keyPlaceholder: 'AIza...',
45
+ keyHint: 'Paste from aistudio.google.com',
46
+ authType: 'apikey',
47
+ },
48
+ };
49
+
50
+ const STEPS = ['Install', 'Authenticate', 'Verify', 'Done'];
51
+
52
+ function StepIndicator({ current }) {
53
+ return (
54
+ <div className="flex items-center gap-1 mb-6">
55
+ {STEPS.map((label, i) => (
56
+ <div key={label} className="flex items-center gap-1">
57
+ <div className={cn(
58
+ 'w-6 h-6 rounded-full flex items-center justify-center text-2xs font-bold font-mono transition-colors',
59
+ i < current ? 'bg-success/15 text-success' :
60
+ i === current ? 'bg-accent/15 text-accent ring-1 ring-accent/30' :
61
+ 'bg-surface-4 text-text-4',
62
+ )}>
63
+ {i < current ? <Check size={10} strokeWidth={3} /> : i + 1}
64
+ </div>
65
+ <span className={cn(
66
+ 'text-2xs font-sans font-medium hidden sm:inline',
67
+ i === current ? 'text-text-0' : 'text-text-4',
68
+ )}>{label}</span>
69
+ {i < STEPS.length - 1 && (
70
+ <div className={cn('w-6 h-px mx-1', i < current ? 'bg-success/40' : 'bg-border-subtle')} />
71
+ )}
72
+ </div>
73
+ ))}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function InstallStep({ providerId, meta }) {
79
+ const installProvider = useGrooveStore((s) => s.installProvider);
80
+ const progress = useGrooveStore((s) => s.providerInstallProgress[providerId]);
81
+ const [showDetails, setShowDetails] = useState(false);
82
+ const [started, setStarted] = useState(false);
83
+
84
+ const isInstalling = progress?.installing;
85
+ const isDone = progress?.done;
86
+ const hasError = progress?.error;
87
+ const percent = progress?.percent || 0;
88
+
89
+ useEffect(() => {
90
+ if (!started) {
91
+ setStarted(true);
92
+ installProvider(providerId).catch(() => {});
93
+ }
94
+ }, []);
95
+
96
+ return (
97
+ <div className="space-y-4">
98
+ <div className="flex items-center gap-3">
99
+ <div className={cn('w-10 h-10 rounded-lg flex items-center justify-center text-sm font-bold font-mono', meta.color)}>
100
+ {meta.letter}
101
+ </div>
102
+ <div>
103
+ <p className="text-sm font-medium text-text-0 font-sans">{meta.installLabel}</p>
104
+ <p className="text-2xs text-text-3 font-sans">This may take a minute</p>
105
+ </div>
106
+ </div>
107
+
108
+ <div className="space-y-2">
109
+ <div className="h-2 bg-surface-4 rounded-full overflow-hidden">
110
+ {isInstalling ? (
111
+ <div
112
+ className="h-full bg-gradient-to-r from-accent to-accent/60 rounded-full transition-all duration-500 ease-out"
113
+ style={{ width: `${Math.max(percent, 5)}%` }}
114
+ />
115
+ ) : isDone ? (
116
+ <div className="h-full bg-success rounded-full w-full" />
117
+ ) : hasError ? (
118
+ <div className="h-full bg-danger rounded-full" style={{ width: '100%' }} />
119
+ ) : null}
120
+ </div>
121
+ <div className="flex items-center justify-between">
122
+ <span className={cn(
123
+ 'text-2xs font-sans',
124
+ hasError ? 'text-danger' : isDone ? 'text-success' : 'text-text-3',
125
+ )}>
126
+ {hasError ? 'Something went wrong' : isDone ? 'Installed successfully' : progress?.message || 'Preparing...'}
127
+ </span>
128
+ {isInstalling && (
129
+ <button
130
+ onClick={() => setShowDetails(!showDetails)}
131
+ className="flex items-center gap-1 text-2xs text-text-4 hover:text-text-2 cursor-pointer font-sans"
132
+ >
133
+ {showDetails ? 'Hide' : 'Show'} details
134
+ {showDetails ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
135
+ </button>
136
+ )}
137
+ </div>
138
+ </div>
139
+
140
+ {showDetails && progress?.message && (
141
+ <div className="p-3 bg-surface-0 border border-border-subtle rounded-md max-h-24 overflow-y-auto">
142
+ <p className="text-2xs font-mono text-text-3 whitespace-pre-wrap">{progress.message}</p>
143
+ </div>
144
+ )}
145
+
146
+ {hasError && (
147
+ <div className="space-y-3">
148
+ <p className="text-xs text-text-2 font-sans">Make sure you are connected to the internet and try again.</p>
149
+ <Button
150
+ variant="primary"
151
+ size="sm"
152
+ onClick={() => installProvider(providerId).catch(() => {})}
153
+ className="gap-1.5"
154
+ >
155
+ <RotateCcw size={11} /> Try Again
156
+ </Button>
157
+ </div>
158
+ )}
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function AuthenticateStep({ providerId, meta, onSaveKey }) {
164
+ const loginProvider = useGrooveStore((s) => s.loginProvider);
165
+ const addToast = useGrooveStore((s) => s.addToast);
166
+ const [key, setKey] = useState('');
167
+ const [showKey, setShowKey] = useState(false);
168
+ const [saving, setSaving] = useState(false);
169
+ const [loginStarted, setLoginStarted] = useState(false);
170
+ const [authMode, setAuthMode] = useState(meta.authType === 'subscription' ? 'subscription' : 'apikey');
171
+
172
+ async function handleSaveKey() {
173
+ if (!key.trim()) return;
174
+ setSaving(true);
175
+ try {
176
+ await api.post(`/credentials/${encodeURIComponent(providerId)}`, { key: key.trim() });
177
+ addToast('success', `API key saved for ${meta.name}`);
178
+ onSaveKey();
179
+ } catch (err) {
180
+ addToast('error', 'Failed to save key', err.message);
181
+ } finally {
182
+ setSaving(false);
183
+ }
184
+ }
185
+
186
+ async function handleLogin() {
187
+ const body = authMode === 'chatgpt-plus' ? { method: 'chatgpt-plus' } : undefined;
188
+ try {
189
+ await loginProvider(providerId, body);
190
+ setLoginStarted(true);
191
+ } catch { /* handled in store */ }
192
+ }
193
+
194
+ return (
195
+ <div className="space-y-4">
196
+ <p className="text-sm font-medium text-text-0 font-sans">{meta.authLabel}</p>
197
+
198
+ {providerId === 'claude-code' && (
199
+ <div className="flex gap-1 bg-surface-3 p-0.5 rounded-md mb-4">
200
+ <button
201
+ onClick={() => { setAuthMode('subscription'); setLoginStarted(false); }}
202
+ className={cn(
203
+ 'flex-1 h-7 rounded text-xs font-medium transition-colors cursor-pointer font-sans',
204
+ authMode === 'subscription' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
205
+ )}
206
+ >
207
+ Subscription
208
+ </button>
209
+ <button
210
+ onClick={() => { setAuthMode('apikey'); setLoginStarted(false); }}
211
+ className={cn(
212
+ 'flex-1 h-7 rounded text-xs font-medium transition-colors cursor-pointer font-sans',
213
+ authMode === 'apikey' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
214
+ )}
215
+ >
216
+ API Key
217
+ </button>
218
+ </div>
219
+ )}
220
+
221
+ {providerId === 'codex' && (
222
+ <div className="flex gap-1 bg-surface-3 p-0.5 rounded-md mb-4">
223
+ <button
224
+ onClick={() => { setAuthMode('apikey'); setLoginStarted(false); }}
225
+ className={cn(
226
+ 'flex-1 h-7 rounded text-xs font-medium transition-colors cursor-pointer font-sans',
227
+ authMode === 'apikey' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
228
+ )}
229
+ >
230
+ API Key
231
+ </button>
232
+ <button
233
+ onClick={() => { setAuthMode('chatgpt-plus'); setLoginStarted(false); }}
234
+ className={cn(
235
+ 'flex-1 h-7 rounded text-xs font-medium transition-colors cursor-pointer font-sans',
236
+ authMode === 'chatgpt-plus' ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
237
+ )}
238
+ >
239
+ ChatGPT Plus
240
+ </button>
241
+ </div>
242
+ )}
243
+
244
+ {authMode === 'subscription' && (
245
+ <div className="space-y-3">
246
+ <p className="text-xs text-text-2 font-sans">
247
+ Click below to sign in with your existing Claude subscription.
248
+ </p>
249
+ {!loginStarted ? (
250
+ <Button variant="primary" size="md" onClick={handleLogin} className="gap-1.5">
251
+ <ExternalLink size={12} /> {meta.loginLabel}
252
+ </Button>
253
+ ) : (
254
+ <div className="space-y-3">
255
+ <div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
256
+ <ExternalLink size={12} className="text-accent" />
257
+ <span className="text-xs text-accent font-sans">Sign-in opened in your browser</span>
258
+ </div>
259
+ <Button variant="primary" size="sm" onClick={onSaveKey} className="gap-1.5">
260
+ <Check size={11} /> I've signed in
261
+ </Button>
262
+ </div>
263
+ )}
264
+ <p className="text-2xs text-text-4 font-sans">
265
+ A browser window will open for you to sign in.
266
+ </p>
267
+ </div>
268
+ )}
269
+
270
+ {authMode === 'chatgpt-plus' && (
271
+ <div className="space-y-3">
272
+ <p className="text-xs text-text-2 font-sans">
273
+ Click below to sign in with your ChatGPT Plus subscription.
274
+ </p>
275
+ {!loginStarted ? (
276
+ <Button variant="primary" size="md" onClick={handleLogin} className="gap-1.5">
277
+ <ExternalLink size={12} /> {meta.loginLabel}
278
+ </Button>
279
+ ) : (
280
+ <div className="space-y-3">
281
+ <div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
282
+ <ExternalLink size={12} className="text-accent" />
283
+ <span className="text-xs text-accent font-sans">Sign-in opened in your browser</span>
284
+ </div>
285
+ <Button variant="primary" size="sm" onClick={onSaveKey} className="gap-1.5">
286
+ <Check size={11} /> I've signed in
287
+ </Button>
288
+ </div>
289
+ )}
290
+ </div>
291
+ )}
292
+
293
+ {authMode === 'apikey' && (
294
+ <div className="space-y-3">
295
+ <label className="text-2xs font-semibold text-text-2 font-sans block">
296
+ {meta.name} API Key
297
+ </label>
298
+ <div className="flex gap-2">
299
+ <div className="relative flex-1">
300
+ <input
301
+ type={showKey ? 'text' : 'password'}
302
+ value={key}
303
+ onChange={(e) => setKey(e.target.value)}
304
+ onKeyDown={(e) => e.key === 'Enter' && handleSaveKey()}
305
+ placeholder={meta.keyPlaceholder}
306
+ 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"
307
+ autoFocus
308
+ />
309
+ <button
310
+ onClick={() => setShowKey(!showKey)}
311
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer"
312
+ >
313
+ {showKey ? <EyeOff size={12} /> : <Eye size={12} />}
314
+ </button>
315
+ </div>
316
+ <Button variant="primary" size="lg" onClick={handleSaveKey} disabled={!key.trim() || saving}>
317
+ {saving ? 'Saving...' : 'Save'}
318
+ </Button>
319
+ </div>
320
+ <p className="text-2xs text-text-4 font-sans">{meta.keyHint}</p>
321
+ </div>
322
+ )}
323
+ </div>
324
+ );
325
+ }
326
+
327
+ function VerifyStep({ providerId, meta, onVerified }) {
328
+ const verifyProvider = useGrooveStore((s) => s.verifyProvider);
329
+ const [status, setStatus] = useState('verifying');
330
+ const [error, setError] = useState(null);
331
+
332
+ async function runVerify() {
333
+ setStatus('verifying');
334
+ setError(null);
335
+ try {
336
+ const result = await verifyProvider(providerId);
337
+ if (result?.installed && !result?.error) {
338
+ setStatus('success');
339
+ onVerified();
340
+ } else {
341
+ setStatus('failed');
342
+ setError(result?.error || 'Could not verify the provider. Please check your credentials.');
343
+ }
344
+ } catch (err) {
345
+ setStatus('failed');
346
+ setError(err.message || 'Verification failed. Please try again.');
347
+ }
348
+ }
349
+
350
+ useEffect(() => { runVerify(); }, []);
351
+
352
+ return (
353
+ <div className="space-y-4">
354
+ <div className="flex items-center gap-3">
355
+ <div className={cn('w-10 h-10 rounded-lg flex items-center justify-center text-sm font-bold font-mono', meta.color)}>
356
+ {meta.letter}
357
+ </div>
358
+ <p className="text-sm font-medium text-text-0 font-sans">
359
+ {status === 'verifying' ? 'Checking your connection...' :
360
+ status === 'success' ? 'Everything looks good!' :
361
+ 'We could not verify the connection'}
362
+ </p>
363
+ </div>
364
+
365
+ {status === 'verifying' && (
366
+ <div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
367
+ <Loader2 size={14} className="text-accent animate-spin" />
368
+ <span className="text-xs text-accent font-sans">Verifying {meta.name}...</span>
369
+ </div>
370
+ )}
371
+
372
+ {status === 'success' && (
373
+ <div className="flex items-center gap-2 p-3 bg-success/8 border border-success/20 rounded-md">
374
+ <Check size={14} className="text-success" />
375
+ <span className="text-xs text-success font-sans">{meta.name} is connected and ready to use</span>
376
+ </div>
377
+ )}
378
+
379
+ {status === 'failed' && (
380
+ <div className="space-y-3">
381
+ <div className="flex items-center gap-2 p-3 bg-danger/8 border border-danger/20 rounded-md">
382
+ <span className="text-xs text-danger font-sans">{error}</span>
383
+ </div>
384
+ <Button variant="primary" size="sm" onClick={runVerify} className="gap-1.5">
385
+ <RotateCcw size={11} /> Try Again
386
+ </Button>
387
+ </div>
388
+ )}
389
+ </div>
390
+ );
391
+ }
392
+
393
+ function DoneStep({ meta }) {
394
+ return (
395
+ <div className="flex flex-col items-center text-center gap-4 py-4">
396
+ <div className="w-14 h-14 rounded-full bg-success/15 flex items-center justify-center">
397
+ <Check size={24} className="text-success" strokeWidth={2.5} />
398
+ </div>
399
+ <div>
400
+ <p className="text-base font-semibold text-text-0 font-sans">{meta.name} is ready!</p>
401
+ <p className="text-xs text-text-3 font-sans mt-1">You can now spawn agents using {meta.name}.</p>
402
+ </div>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ export function ProviderSetupWizard({ open, onOpenChange, providerId, initialStep = 0, onComplete }) {
408
+ const meta = PROVIDER_META[providerId] || PROVIDER_META['claude-code'];
409
+ const [step, setStep] = useState(initialStep);
410
+ const [verified, setVerified] = useState(false);
411
+ const installDone = useGrooveStore((s) => s.providerInstallProgress[providerId]?.done);
412
+
413
+ useEffect(() => {
414
+ if (open) {
415
+ setStep(initialStep);
416
+ setVerified(false);
417
+ }
418
+ }, [open, providerId]);
419
+
420
+ function handleAuthDone() {
421
+ setStep(2);
422
+ }
423
+
424
+ function handleVerified() {
425
+ setVerified(true);
426
+ }
427
+
428
+ function handleClose() {
429
+ onOpenChange(false);
430
+ if (onComplete) onComplete();
431
+ }
432
+
433
+ const canNext =
434
+ step === 0 ? !!installDone :
435
+ step === 1 ? true :
436
+ step === 2 ? verified :
437
+ true;
438
+
439
+ return (
440
+ <Dialog open={open} onOpenChange={onOpenChange}>
441
+ <DialogContent
442
+ title={`Set up ${meta.name}`}
443
+ description={`Install and configure ${meta.name}`}
444
+ className="max-w-md"
445
+ >
446
+ <div className="px-5 py-4">
447
+ <StepIndicator current={step} />
448
+
449
+ {step === 0 && <InstallStep providerId={providerId} meta={meta} />}
450
+ {step === 1 && <AuthenticateStep providerId={providerId} meta={meta} onSaveKey={handleAuthDone} />}
451
+ {step === 2 && <VerifyStep providerId={providerId} meta={meta} onVerified={handleVerified} />}
452
+ {step === 3 && <DoneStep meta={meta} />}
453
+
454
+ <div className="flex justify-between mt-6 pt-4 border-t border-border-subtle">
455
+ {step > 0 && step < 3 ? (
456
+ <Button variant="ghost" size="sm" onClick={() => setStep(step - 1)}>
457
+ Back
458
+ </Button>
459
+ ) : <div />}
460
+ {step < 3 ? (
461
+ <Button
462
+ variant="primary"
463
+ size="sm"
464
+ disabled={!canNext}
465
+ onClick={() => setStep(step + 1)}
466
+ className="gap-1.5"
467
+ >
468
+ {step === 0 ? 'Next' : step === 1 ? 'Skip verification' : 'Continue'}
469
+ </Button>
470
+ ) : (
471
+ <Button variant="primary" size="sm" onClick={handleClose} className="gap-1.5">
472
+ <Sparkles size={11} /> Done
473
+ </Button>
474
+ )}
475
+ </div>
476
+ </div>
477
+ </DialogContent>
478
+ </Dialog>
479
+ );
480
+ }