groove-dev 0.19.2 → 0.19.4

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 (24) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/node_modules/@groove-dev/cli/bin/groove.js +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +68 -0
  4. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +164 -26
  5. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +3 -3
  6. package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
  7. package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
  8. package/node_modules/@groove-dev/gui/.groove/state.json +1 -1
  9. package/node_modules/@groove-dev/gui/.groove/timeline.json +504 -0
  10. package/node_modules/@groove-dev/gui/AGENTS_REGISTRY.md +9 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/{index-DCakRxDE.css → index-DgIS4D-j.css} +1 -1
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-DbujgqhS.js → index-eysbqREF.js} +123 -123
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +9 -3
  15. package/node_modules/@groove-dev/gui/src/components/agents/ollama-setup.jsx +375 -0
  16. package/package.json +1 -1
  17. package/packages/cli/bin/groove.js +1 -1
  18. package/packages/daemon/src/api.js +68 -0
  19. package/packages/daemon/src/providers/ollama.js +164 -26
  20. package/packages/gui/dist/assets/{index-DCakRxDE.css → index-DgIS4D-j.css} +1 -1
  21. package/packages/gui/dist/assets/{index-DbujgqhS.js → index-eysbqREF.js} +123 -123
  22. package/packages/gui/dist/index.html +2 -2
  23. package/packages/gui/src/components/agents/agent-config.jsx +9 -3
  24. package/packages/gui/src/components/agents/ollama-setup.jsx +375 -0
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-DbujgqhS.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-eysbqREF.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-DCakRxDE.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-DgIS4D-j.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -14,6 +14,7 @@ import { FolderBrowser } from './folder-browser';
14
14
  import { api } from '../../lib/api';
15
15
  import { cn } from '../../lib/cn';
16
16
  import { timeAgo } from '../../lib/format';
17
+ import { OllamaSetup } from './ollama-setup';
17
18
 
18
19
  /* ── Segmented Control ─────────────────────────────────────── */
19
20
 
@@ -318,10 +319,15 @@ export function AgentConfig({ agent }) {
318
319
  </button>
319
320
 
320
321
  {/* Expanded: models + key management */}
321
- {isExpanded && (
322
+ {isExpanded && p.authType === 'local' && (
322
323
  <div className="border-t border-border-subtle">
323
- {/* API Key row — skip for local providers like Ollama */}
324
- {p.authType !== 'local' && (!available || p.hasKey) && (
324
+ <OllamaSetup isInstalled={available} onModelChange={loadProviders} />
325
+ </div>
326
+ )}
327
+ {isExpanded && p.authType !== 'local' && (
328
+ <div className="border-t border-border-subtle">
329
+ {/* API Key row */}
330
+ {(!available || p.hasKey) && (
325
331
  <div className="px-3 py-2 bg-surface-1/50">
326
332
  {settingKeyFor === p.id ? (
327
333
  <div className="flex gap-1.5">
@@ -0,0 +1,375 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import {
4
+ Download, Check, Cpu, HardDrive, RefreshCw, Copy,
5
+ Trash2, ChevronDown, Star, Zap, AlertCircle, Monitor,
6
+ } from 'lucide-react';
7
+ import { Button } from '../ui/button';
8
+ import { Badge } from '../ui/badge';
9
+ import { cn } from '../../lib/cn';
10
+ import { api } from '../../lib/api';
11
+ import { useGrooveStore } from '../../stores/groove';
12
+
13
+ const CATEGORY_LABELS = { code: 'Code', general: 'General' };
14
+ const TIER_COLORS = { light: 'text-success', medium: 'text-accent', heavy: 'text-warning' };
15
+
16
+ function formatSize(gb) {
17
+ return gb < 1 ? `${Math.round(gb * 1024)} MB` : `${gb} GB`;
18
+ }
19
+
20
+ /* ── Hardware Info Bar ──────────────────────────────────────── */
21
+
22
+ function HardwareBar({ hardware }) {
23
+ if (!hardware) return null;
24
+ const { totalRamGb, gpu, isAppleSilicon } = hardware;
25
+ return (
26
+ <div className="flex items-center gap-3 bg-surface-0 rounded-lg border border-border-subtle px-3 py-2.5">
27
+ <Monitor size={14} className="text-text-3 flex-shrink-0" />
28
+ <div className="flex-1 min-w-0">
29
+ <div className="flex items-center gap-2 text-xs font-sans">
30
+ <span className="text-text-0 font-semibold">{totalRamGb} GB RAM</span>
31
+ {gpu && (
32
+ <>
33
+ <span className="text-text-4">·</span>
34
+ <span className="text-text-2">{gpu.name}</span>
35
+ </>
36
+ )}
37
+ {isAppleSilicon && (
38
+ <Badge variant="accent" className="text-2xs">Unified Memory</Badge>
39
+ )}
40
+ </div>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ /* ── Install Section (not installed) ───────────────────────── */
47
+
48
+ function InstallSection({ onRecheck }) {
49
+ const [data, setData] = useState(null);
50
+ const [checking, setChecking] = useState(false);
51
+ const [starting, setStarting] = useState(false);
52
+ const [copied, setCopied] = useState(false);
53
+ const addToast = useGrooveStore((s) => s.addToast);
54
+
55
+ useEffect(() => {
56
+ api.post('/providers/ollama/check').then(setData).catch(() => {});
57
+ }, []);
58
+
59
+ async function handleRecheck() {
60
+ setChecking(true);
61
+ try {
62
+ const result = await api.post('/providers/ollama/check');
63
+ setData(result);
64
+ if (result.installed && result.serverRunning) {
65
+ addToast('success', 'Ollama is ready!');
66
+ onRecheck();
67
+ } else if (result.installed) {
68
+ addToast('info', 'Ollama installed — server needs to start');
69
+ } else {
70
+ addToast('info', 'Ollama not found — install and try again');
71
+ }
72
+ } catch {}
73
+ setChecking(false);
74
+ }
75
+
76
+ async function handleStartServer() {
77
+ setStarting(true);
78
+ try {
79
+ const result = await api.post('/providers/ollama/serve');
80
+ if (result.ok) {
81
+ addToast('success', 'Ollama server started!');
82
+ // Recheck to update state
83
+ const check = await api.post('/providers/ollama/check');
84
+ setData(check);
85
+ if (check.serverRunning) onRecheck();
86
+ }
87
+ } catch (err) {
88
+ addToast('error', 'Could not start server', err.message);
89
+ }
90
+ setStarting(false);
91
+ }
92
+
93
+ function handleCopy(text) {
94
+ navigator.clipboard.writeText(text);
95
+ setCopied(true);
96
+ setTimeout(() => setCopied(false), 2000);
97
+ }
98
+
99
+ if (!data) return <div className="py-4 text-center text-xs text-text-4 font-sans">Loading...</div>;
100
+
101
+ const { hardware, install, requirements, installed, serverRunning } = data;
102
+ const canRun = hardware.totalRamGb >= requirements.minRAM;
103
+ const recommended = hardware.recommended;
104
+
105
+ return (
106
+ <div className="space-y-3 p-3">
107
+ <HardwareBar hardware={hardware} />
108
+
109
+ {canRun ? (
110
+ <div className="flex items-start gap-2 bg-success/8 border border-success/20 rounded-lg px-3 py-2.5">
111
+ <Check size={14} className="text-success flex-shrink-0 mt-0.5" />
112
+ <div className="text-xs font-sans">
113
+ <span className="text-success font-semibold">Your system is ready.</span>
114
+ <span className="text-text-2 ml-1">
115
+ {recommended.code
116
+ ? `Recommended: ${recommended.code}`
117
+ : `${hardware.totalRamGb} GB RAM available`}
118
+ </span>
119
+ </div>
120
+ </div>
121
+ ) : (
122
+ <div className="flex items-start gap-2 bg-warning/8 border border-warning/20 rounded-lg px-3 py-2.5">
123
+ <AlertCircle size={14} className="text-warning flex-shrink-0 mt-0.5" />
124
+ <div className="text-xs font-sans text-text-2">
125
+ <span className="text-warning font-semibold">{hardware.totalRamGb} GB RAM detected.</span>
126
+ {' '}Minimum {requirements.minRAM} GB needed. Smallest models may still work.
127
+ </div>
128
+ </div>
129
+ )}
130
+
131
+ {/* State: Installed but server not running */}
132
+ {installed && !serverRunning && (
133
+ <div className="space-y-2">
134
+ <div className="flex items-start gap-2 bg-warning/8 border border-warning/20 rounded-lg px-3 py-2.5">
135
+ <AlertCircle size={14} className="text-warning flex-shrink-0 mt-0.5" />
136
+ <div className="text-xs font-sans text-text-2">
137
+ <span className="text-warning font-semibold">Ollama installed but server not running.</span>
138
+ {' '}The server needs to be running to pull and use models.
139
+ </div>
140
+ </div>
141
+ <Button variant="primary" size="md" onClick={handleStartServer} disabled={starting} className="w-full gap-1.5">
142
+ <Zap size={12} />
143
+ {starting ? 'Starting...' : 'Start Ollama Server'}
144
+ </Button>
145
+ </div>
146
+ )}
147
+
148
+ {/* State: Not installed */}
149
+ {!installed && (
150
+ <div className="space-y-1.5">
151
+ <p className="text-xs font-semibold text-text-1 font-sans">Install Ollama</p>
152
+ <div className="flex items-center gap-2">
153
+ <code className="flex-1 bg-surface-0 border border-border-subtle rounded-md px-3 py-2 text-xs font-mono text-text-1 truncate">
154
+ {install.command}
155
+ </code>
156
+ <Button
157
+ variant="secondary"
158
+ size="sm"
159
+ onClick={() => handleCopy(install.command)}
160
+ className="h-8 px-2.5 gap-1 flex-shrink-0"
161
+ >
162
+ {copied ? <Check size={12} /> : <Copy size={12} />}
163
+ {copied ? 'Copied' : 'Copy'}
164
+ </Button>
165
+ </div>
166
+ {install.alt && (
167
+ <p className="text-2xs text-text-4 font-sans">{install.alt}</p>
168
+ )}
169
+ </div>
170
+ )}
171
+
172
+ <Button variant="secondary" size="md" onClick={handleRecheck} disabled={checking} className="w-full gap-1.5">
173
+ <RefreshCw size={12} className={checking ? 'animate-spin' : ''} />
174
+ {checking ? 'Checking...' : installed ? 'Check again' : 'I installed it — check again'}
175
+ </Button>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ /* ── Model Row ─────────────────────────────────────────────── */
181
+
182
+ function ModelRow({ model, isInstalled, isRecommended, canRun, onPull, onDelete, pulling }) {
183
+ const isPulling = pulling === model.id;
184
+
185
+ return (
186
+ <div className={cn(
187
+ 'flex items-center gap-2 px-3 py-2 border-t border-border-subtle transition-colors',
188
+ !canRun && 'opacity-40',
189
+ )}>
190
+ {isInstalled ? (
191
+ <Check size={12} className="text-success flex-shrink-0" />
192
+ ) : (
193
+ <div className="w-3" />
194
+ )}
195
+ <div className="flex-1 min-w-0">
196
+ <div className="flex items-center gap-1.5">
197
+ <span className="text-xs font-mono text-text-1 truncate">{model.name}</span>
198
+ {isRecommended && <Star size={10} className="text-warning flex-shrink-0" />}
199
+ </div>
200
+ <div className="flex items-center gap-2 mt-0.5">
201
+ <span className={cn('text-2xs font-semibold font-sans', TIER_COLORS[model.tier])}>
202
+ {model.tier}
203
+ </span>
204
+ <span className="text-2xs text-text-4 font-sans">{formatSize(model.sizeGb)}</span>
205
+ <span className="text-2xs text-text-4 font-sans">· {model.ramGb} GB RAM</span>
206
+ </div>
207
+ </div>
208
+ {isInstalled ? (
209
+ <button
210
+ onClick={() => onDelete(model.id)}
211
+ className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
212
+ title="Remove model"
213
+ >
214
+ <Trash2 size={12} />
215
+ </button>
216
+ ) : canRun ? (
217
+ <Button
218
+ variant="secondary"
219
+ size="sm"
220
+ onClick={() => onPull(model.id)}
221
+ disabled={!!pulling}
222
+ className="h-7 px-2 text-2xs gap-1"
223
+ >
224
+ {isPulling ? (
225
+ <><RefreshCw size={10} className="animate-spin" /> Pulling...</>
226
+ ) : (
227
+ <><Download size={10} /> Pull</>
228
+ )}
229
+ </Button>
230
+ ) : (
231
+ <span className="text-2xs text-text-4 font-sans">Needs {model.ramGb} GB</span>
232
+ )}
233
+ </div>
234
+ );
235
+ }
236
+
237
+ /* ── Model Browser (installed) ─────────────────────────────── */
238
+
239
+ function ModelBrowser({ onModelChange }) {
240
+ const [data, setData] = useState(null);
241
+ const [pulling, setPulling] = useState(null);
242
+ const [category, setCategory] = useState('code');
243
+ const [showAll, setShowAll] = useState(false);
244
+ const addToast = useGrooveStore((s) => s.addToast);
245
+
246
+ function load() {
247
+ api.get('/providers/ollama/models').then(setData).catch(() => {});
248
+ }
249
+
250
+ useEffect(() => { load(); }, []);
251
+
252
+ async function handlePull(modelId) {
253
+ setPulling(modelId);
254
+ try {
255
+ await api.post('/providers/ollama/pull', { model: modelId });
256
+ addToast('success', `Pulled ${modelId}`);
257
+ load();
258
+ if (onModelChange) onModelChange();
259
+ } catch (err) {
260
+ addToast('error', `Pull failed: ${err.message}`);
261
+ }
262
+ setPulling(null);
263
+ }
264
+
265
+ async function handleDelete(modelId) {
266
+ try {
267
+ await api.delete(`/providers/ollama/models/${encodeURIComponent(modelId)}`);
268
+ addToast('info', `Removed ${modelId}`);
269
+ load();
270
+ if (onModelChange) onModelChange();
271
+ } catch (err) {
272
+ addToast('error', `Delete failed: ${err.message}`);
273
+ }
274
+ }
275
+
276
+ if (!data) return <div className="py-4 text-center text-xs text-text-4 font-sans">Loading...</div>;
277
+
278
+ const { installed, catalog, hardware } = data;
279
+ const installedIds = new Set(installed.map((m) => m.id));
280
+ const maxRam = hardware.totalRamGb;
281
+ const recommended = [hardware.recommended?.code, hardware.recommended?.general].filter(Boolean);
282
+
283
+ const filtered = catalog.filter((m) => m.category === category);
284
+ const visible = showAll ? filtered : filtered.filter((m) => m.ramGb <= maxRam);
285
+
286
+ return (
287
+ <div className="space-y-2 p-3">
288
+ <HardwareBar hardware={hardware} />
289
+
290
+ {/* Installed count */}
291
+ {installed.length > 0 && (
292
+ <div className="flex items-center gap-1.5 text-xs font-sans text-text-2">
293
+ <HardDrive size={12} className="text-text-3" />
294
+ <span className="font-semibold">{installed.length}</span> model{installed.length !== 1 ? 's' : ''} installed
295
+ </div>
296
+ )}
297
+
298
+ {/* Category tabs */}
299
+ <div className="flex bg-surface-0 rounded-lg p-0.5 border border-border-subtle">
300
+ {Object.entries(CATEGORY_LABELS).map(([key, label]) => (
301
+ <button
302
+ key={key}
303
+ onClick={() => setCategory(key)}
304
+ className={cn(
305
+ 'flex-1 px-3 py-1.5 text-2xs font-semibold font-sans rounded-md transition-all cursor-pointer',
306
+ category === key
307
+ ? 'bg-accent/15 text-accent shadow-sm'
308
+ : 'text-text-3 hover:text-text-1',
309
+ )}
310
+ >
311
+ {label}
312
+ </button>
313
+ ))}
314
+ </div>
315
+
316
+ {/* Model list */}
317
+ <div className="rounded-lg border border-border-subtle bg-surface-0 overflow-hidden">
318
+ {visible.map((model) => (
319
+ <ModelRow
320
+ key={model.id}
321
+ model={model}
322
+ isInstalled={installedIds.has(model.id)}
323
+ isRecommended={recommended.includes(model.id)}
324
+ canRun={model.ramGb <= maxRam}
325
+ onPull={handlePull}
326
+ onDelete={handleDelete}
327
+ pulling={pulling}
328
+ />
329
+ ))}
330
+ {visible.length === 0 && (
331
+ <div className="px-3 py-4 text-center text-xs text-text-4 font-sans">
332
+ No {category} models available for your hardware
333
+ </div>
334
+ )}
335
+ </div>
336
+
337
+ {/* Show models beyond RAM */}
338
+ {!showAll && filtered.length > visible.length && (
339
+ <button
340
+ onClick={() => setShowAll(true)}
341
+ className="flex items-center gap-1 text-2xs text-text-3 hover:text-accent font-sans cursor-pointer transition-colors"
342
+ >
343
+ <ChevronDown size={10} />
344
+ Show {filtered.length - visible.length} more (exceed your RAM)
345
+ </button>
346
+ )}
347
+ </div>
348
+ );
349
+ }
350
+
351
+ /* ── Main Export ────────────────────────────────────────────── */
352
+
353
+ export function OllamaSetup({ isInstalled: initialInstalled, onModelChange }) {
354
+ const [ready, setReady] = useState(false);
355
+ const [checked, setChecked] = useState(false);
356
+
357
+ // On mount, verify server is actually running (not just binary installed)
358
+ useEffect(() => {
359
+ if (initialInstalled) {
360
+ api.post('/providers/ollama/check')
361
+ .then((data) => { setReady(data.installed && data.serverRunning); setChecked(true); })
362
+ .catch(() => setChecked(true));
363
+ } else {
364
+ setChecked(true);
365
+ }
366
+ }, [initialInstalled]);
367
+
368
+ if (!checked) return <div className="py-4 text-center text-xs text-text-4 font-sans">Checking Ollama...</div>;
369
+
370
+ if (!ready) {
371
+ return <InstallSection onRecheck={() => { setReady(true); if (onModelChange) onModelChange(); }} />;
372
+ }
373
+
374
+ return <ModelBrowser onModelChange={onModelChange} />;
375
+ }