groove-dev 0.27.70 → 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.
- package/CLAUDE.md +0 -7
- package/MOE_TRAINING_PIPELINE.md +720 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +272 -2
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/providers/base.js +8 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +52 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +15 -0
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +36 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-74E3YTkT.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-D5BpdcWS.js → index-BK6tvmxx.js} +1736 -1735
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +258 -84
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +272 -2
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/providers/base.js +8 -0
- package/packages/daemon/src/providers/claude-code.js +52 -0
- package/packages/daemon/src/providers/codex.js +15 -0
- package/packages/daemon/src/providers/gemini.js +14 -0
- package/packages/daemon/src/providers/index.js +36 -0
- package/packages/gui/dist/assets/index-74E3YTkT.css +1 -0
- package/packages/gui/dist/assets/{index-D5BpdcWS.js → index-BK6tvmxx.js} +1736 -1735
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/editor/code-editor.jsx +5 -5
- package/packages/gui/src/components/editor/editor-tabs.jsx +4 -4
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/packages/gui/src/stores/groove.js +107 -2
- package/packages/gui/src/views/settings.jsx +258 -84
- package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +0 -1
- 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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-74E3YTkT.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -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: '#
|
|
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: '#
|
|
34
|
-
'.cm-activeLineGutter': { backgroundColor: '#
|
|
35
|
-
'.cm-activeLine': { backgroundColor: 'rgba(
|
|
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: '#
|
|
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-
|
|
78
|
+
'border-r border-white/5',
|
|
79
79
|
'transition-colors duration-75 flex-shrink-0',
|
|
80
80
|
isActive
|
|
81
|
-
? 'bg-surface-
|
|
82
|
-
: 'bg-surface-3 text-text-
|
|
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)}
|
|
@@ -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
|
+
}
|