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
|
@@ -43,6 +43,9 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
43
43
|
// ── Gateways ──────────────────────────────────────────────
|
|
44
44
|
gateways: [],
|
|
45
45
|
|
|
46
|
+
// ── Providers ────────────────────────────────────────────
|
|
47
|
+
_providerRefreshTick: 0,
|
|
48
|
+
|
|
46
49
|
// ── Federation ────────────────────────────────────────────
|
|
47
50
|
federation: {
|
|
48
51
|
peers: [],
|
|
@@ -585,6 +588,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
585
588
|
set({ gateways: msg.data || [] });
|
|
586
589
|
break;
|
|
587
590
|
|
|
591
|
+
case 'provider:status-changed':
|
|
592
|
+
set({ _providerRefreshTick: Date.now() });
|
|
593
|
+
break;
|
|
594
|
+
|
|
588
595
|
case 'federation:whitelist':
|
|
589
596
|
set((s) => ({ federation: { ...s.federation, whitelist: msg.data || [] } }));
|
|
590
597
|
break;
|
|
@@ -1640,17 +1647,115 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1640
1647
|
api.post('/onboarding/dismiss').catch(() => {});
|
|
1641
1648
|
},
|
|
1642
1649
|
|
|
1650
|
+
// ── Provider Setup (Settings) ──────────────────────────────
|
|
1651
|
+
|
|
1652
|
+
providerInstallProgress: {},
|
|
1653
|
+
|
|
1643
1654
|
async installProvider(providerId) {
|
|
1655
|
+
set((s) => ({
|
|
1656
|
+
providerInstallProgress: {
|
|
1657
|
+
...s.providerInstallProgress,
|
|
1658
|
+
[providerId]: { installing: true, percent: 0, message: 'Starting install...', error: null, done: false },
|
|
1659
|
+
},
|
|
1660
|
+
}));
|
|
1644
1661
|
try {
|
|
1645
|
-
const
|
|
1662
|
+
const res = await fetch(`/api/providers/${encodeURIComponent(providerId)}/install`, {
|
|
1663
|
+
method: 'POST',
|
|
1664
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1665
|
+
});
|
|
1666
|
+
if (!res.ok) {
|
|
1667
|
+
const err = await res.text();
|
|
1668
|
+
throw new Error(err || `Install failed (${res.status})`);
|
|
1669
|
+
}
|
|
1670
|
+
const ct = res.headers.get('content-type') || '';
|
|
1671
|
+
if (ct.includes('ndjson') || ct.includes('octet-stream') || ct.includes('chunked')) {
|
|
1672
|
+
const reader = res.body.getReader();
|
|
1673
|
+
const decoder = new TextDecoder();
|
|
1674
|
+
let buf = '';
|
|
1675
|
+
let lastError = null;
|
|
1676
|
+
while (true) {
|
|
1677
|
+
const { done, value } = await reader.read();
|
|
1678
|
+
if (done) break;
|
|
1679
|
+
buf += decoder.decode(value, { stream: true });
|
|
1680
|
+
const lines = buf.split('\n');
|
|
1681
|
+
buf = lines.pop();
|
|
1682
|
+
for (const line of lines) {
|
|
1683
|
+
if (!line.trim()) continue;
|
|
1684
|
+
try {
|
|
1685
|
+
const ev = JSON.parse(line);
|
|
1686
|
+
const isError = ev.status === 'error';
|
|
1687
|
+
const isDone = ev.status === 'complete';
|
|
1688
|
+
if (isError) lastError = ev.output || 'Install failed';
|
|
1689
|
+
set((s) => ({
|
|
1690
|
+
providerInstallProgress: {
|
|
1691
|
+
...s.providerInstallProgress,
|
|
1692
|
+
[providerId]: {
|
|
1693
|
+
...s.providerInstallProgress[providerId],
|
|
1694
|
+
percent: ev.progress ?? s.providerInstallProgress[providerId]?.percent ?? 0,
|
|
1695
|
+
message: ev.output || s.providerInstallProgress[providerId]?.message,
|
|
1696
|
+
error: isError ? (ev.output || 'Install failed') : null,
|
|
1697
|
+
done: isDone,
|
|
1698
|
+
installing: !isDone && !isError,
|
|
1699
|
+
},
|
|
1700
|
+
},
|
|
1701
|
+
}));
|
|
1702
|
+
} catch { /* skip malformed NDJSON line */ }
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (lastError) throw new Error(lastError);
|
|
1706
|
+
}
|
|
1707
|
+
const progress = get().providerInstallProgress[providerId];
|
|
1708
|
+
if (progress?.error) throw new Error(progress.error);
|
|
1709
|
+
set((s) => ({
|
|
1710
|
+
providerInstallProgress: {
|
|
1711
|
+
...s.providerInstallProgress,
|
|
1712
|
+
[providerId]: { installing: false, percent: 100, message: 'Installed', error: null, done: true },
|
|
1713
|
+
},
|
|
1714
|
+
}));
|
|
1646
1715
|
get().addToast('success', `${providerId} installed`);
|
|
1647
|
-
return data;
|
|
1648
1716
|
} catch (err) {
|
|
1717
|
+
set((s) => ({
|
|
1718
|
+
providerInstallProgress: {
|
|
1719
|
+
...s.providerInstallProgress,
|
|
1720
|
+
[providerId]: { installing: false, percent: 0, message: null, error: err.message, done: false },
|
|
1721
|
+
},
|
|
1722
|
+
}));
|
|
1649
1723
|
get().addToast('error', `Install failed: ${providerId}`, err.message);
|
|
1650
1724
|
throw err;
|
|
1651
1725
|
}
|
|
1652
1726
|
},
|
|
1653
1727
|
|
|
1728
|
+
async loginProvider(providerId, body) {
|
|
1729
|
+
try {
|
|
1730
|
+
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/login`, body);
|
|
1731
|
+
if (data?.url) window.open(data.url, '_blank');
|
|
1732
|
+
return data;
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
get().addToast('error', `Login failed`, err.message);
|
|
1735
|
+
throw err;
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1739
|
+
async setProviderPath(providerId, path) {
|
|
1740
|
+
try {
|
|
1741
|
+
await api.post(`/providers/${encodeURIComponent(providerId)}/set-path`, { path });
|
|
1742
|
+
get().addToast('success', `Custom path set for ${providerId}`);
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
get().addToast('error', 'Failed to set path', err.message);
|
|
1745
|
+
throw err;
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
|
|
1749
|
+
async verifyProvider(providerId) {
|
|
1750
|
+
try {
|
|
1751
|
+
const data = await api.post(`/providers/${encodeURIComponent(providerId)}/verify`);
|
|
1752
|
+
return data;
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
get().addToast('error', `Verification failed`, err.message);
|
|
1755
|
+
throw err;
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
|
|
1654
1759
|
async setDefaultProvider(provider, model) {
|
|
1655
1760
|
try {
|
|
1656
1761
|
await api.post('/onboarding/set-default', { provider, model });
|
|
@@ -8,13 +8,14 @@ import { Skeleton } from '../components/ui/skeleton';
|
|
|
8
8
|
import { StatusDot } from '../components/ui/status-dot';
|
|
9
9
|
import { OllamaSetup } from '../components/agents/ollama-setup';
|
|
10
10
|
import { FolderBrowser } from '../components/agents/folder-browser';
|
|
11
|
+
import { ProviderSetupWizard } from '../components/settings/ProviderSetupWizard';
|
|
11
12
|
import { Sheet, SheetContent } from '../components/ui/sheet';
|
|
12
13
|
import { api } from '../lib/api';
|
|
13
14
|
import { cn } from '../lib/cn';
|
|
14
15
|
import { fmtUptime } from '../lib/format';
|
|
15
16
|
import {
|
|
16
|
-
Key, Eye, EyeOff, Check, Cpu,
|
|
17
|
-
FolderOpen, FolderSearch, Users, Gauge,
|
|
17
|
+
Key, Eye, EyeOff, Check, Cpu, Download, Loader2,
|
|
18
|
+
FolderOpen, FolderSearch, Users, Gauge, ChevronRight,
|
|
18
19
|
ShieldCheck, Settings, Lock,
|
|
19
20
|
Newspaper, Radio, Send, MessageSquare, MessageCircle,
|
|
20
21
|
Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
|
|
@@ -41,12 +42,26 @@ function Toggle({ value, onChange }) {
|
|
|
41
42
|
|
|
42
43
|
/* ── Provider Card ─────────────────────────────────────────── */
|
|
43
44
|
|
|
45
|
+
const KEY_PLACEHOLDERS = {
|
|
46
|
+
'claude-code': 'sk-ant-...',
|
|
47
|
+
codex: 'Paste from platform.openai.com',
|
|
48
|
+
gemini: 'Paste from aistudio.google.com',
|
|
49
|
+
};
|
|
50
|
+
|
|
44
51
|
function ProviderCard({ provider, onKeyChange }) {
|
|
45
52
|
const [settingKey, setSettingKey] = useState(false);
|
|
46
53
|
const [keyInput, setKeyInput] = useState('');
|
|
47
54
|
const [showKey, setShowKey] = useState(false);
|
|
48
55
|
const [ollamaOpen, setOllamaOpen] = useState(false);
|
|
56
|
+
const [wizardOpen, setWizardOpen] = useState(false);
|
|
57
|
+
const [wizardStep, setWizardStep] = useState(0);
|
|
58
|
+
const [customPathOpen, setCustomPathOpen] = useState(false);
|
|
59
|
+
const [customPath, setCustomPath] = useState('');
|
|
60
|
+
const [savingPath, setSavingPath] = useState(false);
|
|
49
61
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
62
|
+
const installProgress = useGrooveStore((s) => s.providerInstallProgress[provider.id]);
|
|
63
|
+
const loginProvider = useGrooveStore((s) => s.loginProvider);
|
|
64
|
+
const setProviderPath = useGrooveStore((s) => s.setProviderPath);
|
|
50
65
|
|
|
51
66
|
const isLocal = provider.authType === 'local';
|
|
52
67
|
const isSubscription = provider.authType === 'subscription';
|
|
@@ -77,6 +92,28 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
async function handleLogin(body) {
|
|
96
|
+
try {
|
|
97
|
+
await loginProvider(provider.id, body);
|
|
98
|
+
} catch { /* handled in store */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleSavePath() {
|
|
102
|
+
if (!customPath.trim()) return;
|
|
103
|
+
setSavingPath(true);
|
|
104
|
+
try {
|
|
105
|
+
await setProviderPath(provider.id, customPath.trim());
|
|
106
|
+
setCustomPathOpen(false);
|
|
107
|
+
if (onKeyChange) onKeyChange();
|
|
108
|
+
} catch { /* handled in store */ }
|
|
109
|
+
setSavingPath(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function openWizard(step = 0) {
|
|
113
|
+
setWizardStep(step);
|
|
114
|
+
setWizardOpen(true);
|
|
115
|
+
}
|
|
116
|
+
|
|
80
117
|
// Local models card
|
|
81
118
|
if (isLocal) {
|
|
82
119
|
const installedCount = provider.models?.filter(m => !m.disabled)?.length || 0;
|
|
@@ -138,102 +175,234 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
138
175
|
);
|
|
139
176
|
}
|
|
140
177
|
|
|
178
|
+
const isInstalling = installProgress?.installing;
|
|
179
|
+
|
|
141
180
|
// Standard provider card (Claude, Codex, Gemini)
|
|
142
181
|
return (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
<>
|
|
183
|
+
<div className="flex flex-col rounded-lg border border-border-subtle bg-surface-1 overflow-hidden min-w-[220px]">
|
|
184
|
+
{/* Header */}
|
|
185
|
+
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-border-subtle">
|
|
186
|
+
<StatusDot status={isReady ? 'running' : isInstalling ? 'idle' : 'crashed'} size="sm" />
|
|
187
|
+
<span className="text-[13px] font-semibold text-text-0 font-sans">{provider.name}</span>
|
|
188
|
+
<div className="flex-1" />
|
|
189
|
+
{isReady ? (
|
|
190
|
+
<Badge variant="success" className="text-2xs gap-1"><Check size={8} /> Ready</Badge>
|
|
191
|
+
) : isInstalling ? (
|
|
192
|
+
<Badge variant="default" className="text-2xs gap-1"><Loader2 size={8} className="animate-spin" /> Installing</Badge>
|
|
193
|
+
) : (
|
|
194
|
+
<Badge variant="default" className="text-2xs">{!provider.installed ? 'Not installed' : isSubscription ? 'Not signed in' : 'No key'}</Badge>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
155
197
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
{/* Body */}
|
|
199
|
+
<div className="flex-1 flex flex-col px-4 py-3 min-h-[120px]">
|
|
200
|
+
{/* Models */}
|
|
201
|
+
{provider.models?.length > 0 && (
|
|
202
|
+
<div className="flex flex-wrap gap-1 mb-3">
|
|
203
|
+
{provider.models.map((m) => (
|
|
204
|
+
<span key={m.id} className="px-1.5 py-0.5 rounded bg-surface-4 text-2xs font-mono text-text-3">
|
|
205
|
+
{m.name || m.id}
|
|
206
|
+
</span>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
168
210
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
211
|
+
{/* Installing progress bar */}
|
|
212
|
+
{isInstalling && (
|
|
213
|
+
<div className="space-y-2 mb-3">
|
|
214
|
+
<div className="h-1.5 bg-surface-4 rounded-full overflow-hidden">
|
|
215
|
+
<div
|
|
216
|
+
className="h-full bg-gradient-to-r from-accent to-accent/60 rounded-full transition-all duration-500 ease-out"
|
|
217
|
+
style={{ width: `${Math.max(installProgress?.percent || 0, 5)}%` }}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<p className="text-2xs text-text-3 font-sans">{installProgress?.message || 'Installing...'}</p>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
175
223
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
224
|
+
{/* Not installed — prominent Install button */}
|
|
225
|
+
{!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>
|
|
230
|
+
<Button
|
|
231
|
+
variant="primary"
|
|
232
|
+
size="sm"
|
|
233
|
+
onClick={() => openWizard(0)}
|
|
234
|
+
className="h-8 px-4 text-xs gap-1.5"
|
|
235
|
+
>
|
|
236
|
+
<Download size={12} /> Install {provider.name}
|
|
237
|
+
</Button>
|
|
181
238
|
</div>
|
|
182
|
-
|
|
183
|
-
<button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
239
|
+
)}
|
|
186
240
|
|
|
187
|
-
|
|
188
|
-
|
|
241
|
+
{/* Installed but needs auth */}
|
|
242
|
+
{provider.installed && !isReady && !settingKey && !isInstalling && (
|
|
243
|
+
<div className="flex flex-col gap-2.5 flex-1">
|
|
244
|
+
{isSubscription ? (
|
|
245
|
+
<>
|
|
246
|
+
<Button
|
|
247
|
+
variant="primary"
|
|
248
|
+
size="sm"
|
|
249
|
+
onClick={handleLogin}
|
|
250
|
+
className="w-full h-8 text-2xs gap-1.5"
|
|
251
|
+
>
|
|
252
|
+
<ExternalLink size={11} /> Sign In with Anthropic
|
|
253
|
+
</Button>
|
|
254
|
+
<Button
|
|
255
|
+
variant="secondary"
|
|
256
|
+
size="sm"
|
|
257
|
+
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
258
|
+
className="w-full h-8 text-2xs gap-1.5"
|
|
259
|
+
>
|
|
260
|
+
<Key size={11} /> Add API Key Instead
|
|
261
|
+
</Button>
|
|
262
|
+
</>
|
|
263
|
+
) : provider.id === 'codex' ? (
|
|
264
|
+
<>
|
|
265
|
+
<Button
|
|
266
|
+
variant="primary"
|
|
267
|
+
size="sm"
|
|
268
|
+
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
269
|
+
className="w-full h-8 text-2xs gap-1.5"
|
|
270
|
+
>
|
|
271
|
+
<Key size={11} /> Add API Key
|
|
272
|
+
</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"
|
|
278
|
+
>
|
|
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>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
189
294
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
type={showKey ? 'text' : 'password'}
|
|
203
|
-
placeholder="sk-..."
|
|
204
|
-
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"
|
|
205
|
-
autoFocus
|
|
206
|
-
/>
|
|
207
|
-
<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">
|
|
208
|
-
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
209
|
-
</button>
|
|
295
|
+
{/* Subscription info for Claude */}
|
|
296
|
+
{isSubscription && isReady && !provider.hasKey && !settingKey && (
|
|
297
|
+
<div className="flex items-center gap-1.5 h-8 px-2.5 bg-accent/8 border border-accent/20 rounded-md text-2xs font-sans text-accent mb-3">
|
|
298
|
+
<Check size={10} /> Subscription active
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Connected state */}
|
|
303
|
+
{provider.hasKey && !settingKey && (
|
|
304
|
+
<div className="flex items-center gap-2 mb-3">
|
|
305
|
+
<div className="flex-1 flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success">
|
|
306
|
+
<Check size={10} /> API Connected
|
|
210
307
|
</div>
|
|
308
|
+
<button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
|
|
309
|
+
<button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
|
|
211
310
|
</div>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{/* Spacer */}
|
|
314
|
+
<div className="flex-1" />
|
|
315
|
+
|
|
316
|
+
{/* Key input form */}
|
|
317
|
+
{settingKey && (
|
|
318
|
+
<div className="space-y-2.5 pt-1">
|
|
319
|
+
<div>
|
|
320
|
+
<label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
|
|
321
|
+
{provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
|
|
322
|
+
</label>
|
|
323
|
+
<div className="relative">
|
|
324
|
+
<input
|
|
325
|
+
value={keyInput}
|
|
326
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
327
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
328
|
+
type={showKey ? 'text' : 'password'}
|
|
329
|
+
placeholder={KEY_PLACEHOLDERS[provider.id] || 'sk-...'}
|
|
330
|
+
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"
|
|
331
|
+
autoFocus
|
|
332
|
+
/>
|
|
333
|
+
<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">
|
|
334
|
+
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div className="flex gap-2">
|
|
339
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="flex-1 h-8 text-xs">
|
|
340
|
+
Save Key
|
|
341
|
+
</Button>
|
|
342
|
+
<Button variant="ghost" size="sm" onClick={() => { setSettingKey(false); setKeyInput(''); }} className="h-8 text-xs px-3">
|
|
343
|
+
Cancel
|
|
344
|
+
</Button>
|
|
345
|
+
</div>
|
|
219
346
|
</div>
|
|
220
|
-
|
|
221
|
-
)}
|
|
347
|
+
)}
|
|
222
348
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
349
|
+
{/* Bottom action for ready cards — add key for headless */}
|
|
350
|
+
{isReady && !settingKey && !provider.hasKey && isSubscription && (
|
|
351
|
+
<Button
|
|
352
|
+
variant="secondary"
|
|
353
|
+
size="sm"
|
|
354
|
+
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
355
|
+
className="w-full h-8 text-2xs gap-1.5 mt-2"
|
|
356
|
+
>
|
|
357
|
+
<Key size={11} />
|
|
358
|
+
Add API key for headless mode
|
|
359
|
+
</Button>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{/* Custom path section */}
|
|
364
|
+
{provider.installed && (
|
|
365
|
+
<div className="border-t border-border-subtle">
|
|
366
|
+
<button
|
|
367
|
+
onClick={() => setCustomPathOpen(!customPathOpen)}
|
|
368
|
+
className="w-full flex items-center gap-2 px-4 py-2 text-left cursor-pointer hover:bg-surface-5/30 transition-colors"
|
|
369
|
+
>
|
|
370
|
+
<ChevronRight
|
|
371
|
+
size={10}
|
|
372
|
+
className={cn('text-text-4 transition-transform duration-200', customPathOpen && 'rotate-90')}
|
|
373
|
+
/>
|
|
374
|
+
<span className="text-2xs text-text-4 font-sans">Set custom path</span>
|
|
375
|
+
</button>
|
|
376
|
+
{customPathOpen && (
|
|
377
|
+
<div className="px-4 pb-3 space-y-2">
|
|
378
|
+
<div className="flex gap-2">
|
|
379
|
+
<input
|
|
380
|
+
value={customPath}
|
|
381
|
+
onChange={(e) => setCustomPath(e.target.value)}
|
|
382
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSavePath()}
|
|
383
|
+
placeholder={`/path/to/${provider.id}`}
|
|
384
|
+
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"
|
|
385
|
+
/>
|
|
386
|
+
<Button variant="primary" size="sm" onClick={handleSavePath} disabled={!customPath.trim() || savingPath} className="h-7 text-2xs px-2.5">
|
|
387
|
+
{savingPath ? '...' : 'Save'}
|
|
388
|
+
</Button>
|
|
389
|
+
</div>
|
|
390
|
+
<p className="text-2xs text-text-4 font-sans">For non-standard install locations</p>
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
234
394
|
)}
|
|
235
395
|
</div>
|
|
236
|
-
|
|
396
|
+
|
|
397
|
+
{/* Setup wizard modal */}
|
|
398
|
+
<ProviderSetupWizard
|
|
399
|
+
open={wizardOpen}
|
|
400
|
+
onOpenChange={setWizardOpen}
|
|
401
|
+
providerId={provider.id}
|
|
402
|
+
initialStep={wizardStep}
|
|
403
|
+
onComplete={onKeyChange}
|
|
404
|
+
/>
|
|
405
|
+
</>
|
|
237
406
|
);
|
|
238
407
|
}
|
|
239
408
|
|
|
@@ -1036,6 +1205,7 @@ export default function SettingsView() {
|
|
|
1036
1205
|
const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
|
|
1037
1206
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
1038
1207
|
const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
|
|
1208
|
+
const providerRefreshTick = useGrooveStore((s) => s._providerRefreshTick);
|
|
1039
1209
|
|
|
1040
1210
|
function loadProviders() {
|
|
1041
1211
|
api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
|
|
@@ -1051,6 +1221,10 @@ export default function SettingsView() {
|
|
|
1051
1221
|
.catch(() => setLoading(false));
|
|
1052
1222
|
}, []);
|
|
1053
1223
|
|
|
1224
|
+
useEffect(() => {
|
|
1225
|
+
if (providerRefreshTick) loadProviders();
|
|
1226
|
+
}, [providerRefreshTick]);
|
|
1227
|
+
|
|
1054
1228
|
async function addGateway(type) {
|
|
1055
1229
|
try {
|
|
1056
1230
|
await api.post('/gateways', { type });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.71",
|
|
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)",
|