groove-dev 0.25.20 → 0.26.0

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 (45) hide show
  1. package/node_modules/@groove-dev/daemon/src/agent-loop.js +479 -0
  2. package/node_modules/@groove-dev/daemon/src/api.js +104 -5
  3. package/node_modules/@groove-dev/daemon/src/index.js +6 -1
  4. package/node_modules/@groove-dev/daemon/src/llama-server.js +268 -0
  5. package/node_modules/@groove-dev/daemon/src/model-manager.js +411 -0
  6. package/node_modules/@groove-dev/daemon/src/process.js +179 -11
  7. package/node_modules/@groove-dev/daemon/src/providers/codex.js +51 -1
  8. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +3 -2
  9. package/node_modules/@groove-dev/daemon/src/providers/index.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/local.js +183 -0
  11. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/tool-executor.js +367 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-BQnZrh4f.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BqL4GcgZ.js +633 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +7 -2
  18. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +6 -2
  20. package/node_modules/@groove-dev/gui/src/views/models.jsx +380 -0
  21. package/package.json +2 -2
  22. package/packages/daemon/src/agent-loop.js +479 -0
  23. package/packages/daemon/src/api.js +104 -5
  24. package/packages/daemon/src/index.js +6 -1
  25. package/packages/daemon/src/llama-server.js +268 -0
  26. package/packages/daemon/src/model-manager.js +411 -0
  27. package/packages/daemon/src/process.js +179 -11
  28. package/packages/daemon/src/providers/codex.js +51 -1
  29. package/packages/daemon/src/providers/gemini.js +3 -2
  30. package/packages/daemon/src/providers/index.js +4 -0
  31. package/packages/daemon/src/providers/local.js +183 -0
  32. package/packages/daemon/src/registry.js +1 -1
  33. package/packages/daemon/src/tool-executor.js +367 -0
  34. package/packages/gui/dist/assets/index-BQnZrh4f.css +1 -0
  35. package/packages/gui/dist/assets/index-BqL4GcgZ.js +633 -0
  36. package/packages/gui/dist/index.html +2 -2
  37. package/packages/gui/src/app.jsx +2 -0
  38. package/packages/gui/src/components/agents/agent-config.jsx +7 -2
  39. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  40. package/packages/gui/src/stores/groove.js +6 -2
  41. package/packages/gui/src/views/models.jsx +380 -0
  42. package/node_modules/@groove-dev/gui/dist/assets/index-GYcMwmjs.css +0 -1
  43. package/node_modules/@groove-dev/gui/dist/assets/index-H_e3KvZp.js +0 -623
  44. package/packages/gui/dist/assets/index-GYcMwmjs.css +0 -1
  45. package/packages/gui/dist/assets/index-H_e3KvZp.js +0 -623
@@ -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-H_e3KvZp.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BqL4GcgZ.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-GYcMwmjs.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BQnZrh4f.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -12,6 +12,7 @@ import DashboardView from './views/dashboard';
12
12
  import MarketplaceView from './views/marketplace';
13
13
  import TeamsView from './views/teams';
14
14
  import SettingsView from './views/settings';
15
+ import ModelsView from './views/models';
15
16
 
16
17
  // Agent components
17
18
  import { AgentPanel } from './components/agents/agent-panel';
@@ -57,6 +58,7 @@ function ViewRouter() {
57
58
  case 'dashboard': content = <DashboardView />; break;
58
59
  case 'marketplace': content = <MarketplaceView />; break;
59
60
  case 'teams': content = <TeamsView />; break;
61
+ case 'models': content = <ModelsView />; break;
60
62
  case 'settings': content = <SettingsView />; break;
61
63
  default: content = <AgentsView />;
62
64
  }
@@ -186,8 +186,13 @@ export function AgentConfig({ agent }) {
186
186
  async function handleModelSwap(providerId, modelId) {
187
187
  setSelectedModel(modelId);
188
188
  try {
189
- await api.patch(`/agents/${agent.id}`, { model: modelId });
190
- addToast('success', `Model ${modelId}`);
189
+ const updates = { model: modelId };
190
+ // Switch provider if selecting a model from a different provider
191
+ if (providerId && providerId !== agent.provider) {
192
+ updates.provider = providerId;
193
+ }
194
+ await api.patch(`/agents/${agent.id}`, updates);
195
+ addToast('success', `Model → ${modelId}${updates.provider ? ` (${providerId})` : ''}`);
191
196
  } catch (err) {
192
197
  addToast('error', 'Model swap failed', err.message);
193
198
  }
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, BarChart3, Puzzle, Users, Newspaper, Settings } from 'lucide-react';
2
+ import { Network, Code2, BarChart3, Puzzle, Users, Box, Newspaper, Settings } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
 
@@ -8,6 +8,7 @@ const NAV_ITEMS = [
8
8
  { id: 'editor', icon: Code2, label: 'Editor' },
9
9
  { id: 'dashboard', icon: BarChart3, label: 'Dashboard' },
10
10
  { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
11
+ { id: 'models', icon: Box, label: 'Models' },
11
12
  { id: 'teams', icon: Users, label: 'Teams' },
12
13
  ];
13
14
 
@@ -197,13 +197,17 @@ export const useGrooveStore = create((set, get) => ({
197
197
  case 'agent:exit': {
198
198
  const agent = get().agents.find((a) => a.id === msg.agentId);
199
199
  const name = agent?.name || msg.agentId;
200
- // Exit 143 = SIGTERM (kill), exit 137 = SIGKILL — treat as intentional kill
201
200
  const isKill = msg.status === 'killed' || msg.code === 143 || msg.code === 137;
202
201
  const text = msg.status === 'completed' ? `${name} completed`
203
202
  : isKill ? `${name} stopped`
204
203
  : `${name} crashed (exit ${msg.code})`;
205
204
  const type = msg.status === 'completed' ? 'success' : isKill ? 'info' : 'warning';
206
- get().addToast(type, text);
205
+ get().addToast(type, text, msg.error ? msg.error.slice(0, 200) : undefined);
206
+
207
+ // Log crash error to agent chat so user can see what happened
208
+ if (msg.error && msg.agentId) {
209
+ get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
210
+ }
207
211
  // Check for recommended team when planner completes
208
212
  if (agent?.role === 'planner' && msg.status === 'completed') {
209
213
  setTimeout(() => get().checkRecommendedTeam(), 1000);
@@ -0,0 +1,380 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import { ScrollArea } from '../components/ui/scroll-area';
4
+ import { Badge } from '../components/ui/badge';
5
+ import { Button } from '../components/ui/button';
6
+ import { Input } from '../components/ui/input';
7
+ import { api } from '../lib/api';
8
+ import { useToast } from '../lib/hooks/use-toast';
9
+ import { useGrooveStore } from '../stores/groove';
10
+ import {
11
+ Search, Download, Trash2, HardDrive, Cpu, MemoryStick,
12
+ Check, X, Loader2, ExternalLink, Box, ChevronDown, ChevronRight,
13
+ } from 'lucide-react';
14
+ import { cn } from '../lib/cn';
15
+
16
+ function formatBytes(bytes) {
17
+ if (!bytes) return '—';
18
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
19
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
20
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
21
+ }
22
+
23
+ function formatSpeed(bytesPerSec) {
24
+ if (!bytesPerSec) return '';
25
+ if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(0)} KB/s`;
26
+ return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
27
+ }
28
+
29
+ // ---- Hardware Info ----
30
+ function HardwareBar({ hardware }) {
31
+ if (!hardware) return null;
32
+ return (
33
+ <div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border border-border-subtle rounded-lg text-xs font-sans text-text-2">
34
+ <div className="flex items-center gap-1.5">
35
+ <MemoryStick size={14} className="text-text-3" />
36
+ <span>{hardware.totalRamGb} GB RAM</span>
37
+ </div>
38
+ <div className="flex items-center gap-1.5">
39
+ <Cpu size={14} className="text-text-3" />
40
+ <span>{hardware.cores} cores</span>
41
+ </div>
42
+ {hardware.gpu && (
43
+ <div className="flex items-center gap-1.5">
44
+ <HardDrive size={14} className="text-text-3" />
45
+ <span>{hardware.gpu.name}{hardware.gpu.vram ? ` (${hardware.gpu.vram} GB)` : ''}</span>
46
+ </div>
47
+ )}
48
+ {hardware.recommended?.code && (
49
+ <div className="ml-auto text-accent">
50
+ Recommended: {hardware.recommended.code}
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // ---- Download Progress Bar ----
58
+ function DownloadProgress({ download }) {
59
+ const pct = Math.round((download.percent || 0) * 100);
60
+ return (
61
+ <div className="space-y-1">
62
+ <div className="flex items-center justify-between text-2xs font-sans text-text-3">
63
+ <span>{download.filename}</span>
64
+ <span>{pct}% {formatSpeed(download.speed)}</span>
65
+ </div>
66
+ <div className="h-1.5 bg-surface-3 rounded-full overflow-hidden">
67
+ <div className="h-full bg-accent rounded-full transition-all" style={{ width: `${pct}%` }} />
68
+ </div>
69
+ <div className="text-2xs text-text-4">
70
+ {formatBytes(download.downloaded)} / {formatBytes(download.totalBytes)}
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ // ---- Installed Model Card ----
77
+ function InstalledModel({ model, onDelete }) {
78
+ const [deleting, setDeleting] = useState(false);
79
+ const tierColors = { light: 'text-green-400', medium: 'text-blue-400', heavy: 'text-orange-400' };
80
+
81
+ return (
82
+ <div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg">
83
+ <Box size={18} className="text-accent flex-shrink-0" />
84
+ <div className="flex-1 min-w-0">
85
+ <div className="flex items-center gap-2">
86
+ <span className="text-sm font-mono font-bold text-text-0 truncate">{model.id}</span>
87
+ {model.quantization && <Badge variant="subtle" className="text-2xs">{model.quantization}</Badge>}
88
+ {model.parameters && <Badge variant="subtle" className="text-2xs">{model.parameters}</Badge>}
89
+ <span className={cn('text-2xs font-medium capitalize', tierColors[model.tier] || 'text-text-3')}>{model.tier}</span>
90
+ </div>
91
+ <div className="text-2xs text-text-3 font-sans mt-0.5">
92
+ {formatBytes(model.sizeBytes)} &middot; ctx {(model.contextWindow || 0).toLocaleString()} &middot; {model.category}
93
+ {model.repoId && <span className="text-text-4"> &middot; {model.repoId}</span>}
94
+ </div>
95
+ </div>
96
+ <button
97
+ onClick={async () => { setDeleting(true); await onDelete(model.id); setDeleting(false); }}
98
+ disabled={deleting}
99
+ className="p-1.5 rounded-md text-text-4 hover:text-red-400 hover:bg-red-400/10 transition-colors"
100
+ >
101
+ {deleting ? <Loader2 size={14} className="animate-spin" /> : <Trash2 size={14} />}
102
+ </button>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // ---- Search Result Card (HuggingFace) ----
108
+ function SearchResult({ result, onExpand, expanded }) {
109
+ return (
110
+ <button
111
+ onClick={() => onExpand(expanded ? null : result.id)}
112
+ className="w-full text-left px-4 py-3 bg-surface-1 border border-border-subtle rounded-lg hover:border-accent/30 transition-colors cursor-pointer"
113
+ >
114
+ <div className="flex items-center gap-2">
115
+ <span className="text-sm font-mono font-bold text-text-0 truncate flex-1">{result.name}</span>
116
+ <span className="text-2xs text-text-4 font-sans">{result.author}</span>
117
+ {expanded ? <ChevronDown size={14} className="text-text-3" /> : <ChevronRight size={14} className="text-text-3" />}
118
+ </div>
119
+ <div className="text-2xs text-text-3 font-sans mt-0.5 flex gap-3">
120
+ <span>{result.downloads?.toLocaleString()} downloads</span>
121
+ <span>{result.likes} likes</span>
122
+ </div>
123
+ </button>
124
+ );
125
+ }
126
+
127
+ // ---- File Picker (quantization variants) ----
128
+ function FilePicker({ repoId, onDownload }) {
129
+ const [files, setFiles] = useState(null);
130
+ const [loading, setLoading] = useState(true);
131
+ const [downloading, setDownloading] = useState(null);
132
+ const toast = useToast();
133
+
134
+ useEffect(() => {
135
+ setLoading(true);
136
+ api.get(`/models/${repoId}/files`)
137
+ .then((data) => setFiles(data.files || []))
138
+ .catch(() => toast.error('Failed to load model files'))
139
+ .finally(() => setLoading(false));
140
+ }, [repoId]);
141
+
142
+ async function handleDownload(file) {
143
+ setDownloading(file.filename);
144
+ try {
145
+ await api.post('/models/download', { repoId, filename: file.filename });
146
+ toast.success(`Downloading ${file.filename}`);
147
+ onDownload?.(file.filename);
148
+ } catch (err) {
149
+ toast.error(err.message);
150
+ }
151
+ setDownloading(null);
152
+ }
153
+
154
+ if (loading) {
155
+ return <div className="py-3 px-4 text-2xs text-text-4 font-sans">Loading quantization variants...</div>;
156
+ }
157
+
158
+ if (!files?.length) {
159
+ return <div className="py-3 px-4 text-2xs text-text-4 font-sans">No GGUF files found in this repo.</div>;
160
+ }
161
+
162
+ return (
163
+ <div className="pl-6 pr-4 pb-2 space-y-1.5">
164
+ {files.map((f) => (
165
+ <div key={f.filename} className="flex items-center gap-2 py-1.5 px-3 rounded-md bg-surface-2 text-xs font-sans">
166
+ <span className="font-mono text-text-1 truncate flex-1">{f.filename}</span>
167
+ {f.quantization && <Badge variant="subtle" className="text-2xs">{f.quantization}</Badge>}
168
+ <span className="text-text-3 text-2xs w-16 text-right">{formatBytes(f.size)}</span>
169
+ {f.estimatedRamGb && <span className="text-text-4 text-2xs w-14 text-right">~{f.estimatedRamGb}GB</span>}
170
+ <button
171
+ onClick={() => handleDownload(f)}
172
+ disabled={downloading === f.filename}
173
+ className="p-1 rounded text-accent hover:bg-accent/10 transition-colors disabled:opacity-40"
174
+ >
175
+ {downloading === f.filename ? <Loader2 size={13} className="animate-spin" /> : <Download size={13} />}
176
+ </button>
177
+ </div>
178
+ ))}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ // ---- Main View ----
184
+ export default function ModelsView() {
185
+ const [tab, setTab] = useState('installed'); // installed | search
186
+ const [searchQuery, setSearchQuery] = useState('');
187
+ const [searchResults, setSearchResults] = useState([]);
188
+ const [searching, setSearching] = useState(false);
189
+ const [installed, setInstalled] = useState([]);
190
+ const [downloads, setDownloads] = useState([]);
191
+ const [hardware, setHardware] = useState(null);
192
+ const [expandedResult, setExpandedResult] = useState(null);
193
+ const toast = useToast();
194
+
195
+ // Fetch installed models
196
+ const fetchInstalled = useCallback(() => {
197
+ api.get('/models/installed').then((data) => {
198
+ setInstalled(data.models || []);
199
+ }).catch(() => {});
200
+ }, []);
201
+
202
+ // Fetch hardware info
203
+ useEffect(() => {
204
+ api.get('/providers/ollama/hardware').then(setHardware).catch(() => {});
205
+ fetchInstalled();
206
+ }, [fetchInstalled]);
207
+
208
+ // Listen for download progress via WebSocket
209
+ useEffect(() => {
210
+ const unsub = useGrooveStore.subscribe((state, prev) => {
211
+ // Refresh on model events
212
+ });
213
+
214
+ // Poll active downloads
215
+ const poll = setInterval(() => {
216
+ api.get('/models/downloads').then(setDownloads).catch(() => {});
217
+ }, 2000);
218
+
219
+ return () => { unsub(); clearInterval(poll); };
220
+ }, []);
221
+
222
+ // WebSocket events for download progress
223
+ useEffect(() => {
224
+ function handleWs(event) {
225
+ try {
226
+ const msg = JSON.parse(event.data);
227
+ if (msg.type === 'model:download:progress') {
228
+ setDownloads((prev) => {
229
+ const idx = prev.findIndex((d) => d.filename === msg.data.filename);
230
+ if (idx >= 0) {
231
+ const next = [...prev];
232
+ next[idx] = msg.data;
233
+ return next;
234
+ }
235
+ return [...prev, msg.data];
236
+ });
237
+ }
238
+ if (msg.type === 'model:download:complete') {
239
+ setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
240
+ fetchInstalled();
241
+ toast.success(`${msg.data.filename} downloaded`);
242
+ }
243
+ if (msg.type === 'model:download:error') {
244
+ setDownloads((prev) => prev.filter((d) => d.filename !== msg.data.filename));
245
+ toast.error(`Download failed: ${msg.data.error}`);
246
+ }
247
+ } catch {}
248
+ }
249
+ const ws = useGrooveStore.getState()._ws;
250
+ if (ws) ws.addEventListener('message', handleWs);
251
+ return () => { if (ws) ws.removeEventListener('message', handleWs); };
252
+ }, [fetchInstalled, toast]);
253
+
254
+ async function handleSearch() {
255
+ if (!searchQuery.trim()) return;
256
+ setSearching(true);
257
+ setTab('search');
258
+ try {
259
+ const results = await api.get(`/models/search?q=${encodeURIComponent(searchQuery.trim())}`);
260
+ setSearchResults(results);
261
+ } catch (err) {
262
+ toast.error(err.message);
263
+ }
264
+ setSearching(false);
265
+ }
266
+
267
+ async function handleDelete(modelId) {
268
+ try {
269
+ await api.delete(`/models/${modelId}`);
270
+ setInstalled((prev) => prev.filter((m) => m.id !== modelId));
271
+ toast.success('Model deleted');
272
+ } catch (err) {
273
+ toast.error(err.message);
274
+ }
275
+ }
276
+
277
+ return (
278
+ <div className="h-full flex flex-col bg-surface-0">
279
+ {/* Header */}
280
+ <div className="flex-shrink-0 px-5 pt-4 pb-3 border-b border-border space-y-3">
281
+ <div className="flex items-center justify-between">
282
+ <h1 className="text-base font-bold font-sans text-text-0">Local Models</h1>
283
+ <Badge variant="subtle" className="text-2xs">{installed.length} installed</Badge>
284
+ </div>
285
+
286
+ <HardwareBar hardware={hardware} />
287
+
288
+ {/* Search */}
289
+ <div className="flex gap-2">
290
+ <div className="relative flex-1">
291
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4" />
292
+ <input
293
+ value={searchQuery}
294
+ onChange={(e) => setSearchQuery(e.target.value)}
295
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
296
+ placeholder="Search HuggingFace for GGUF models..."
297
+ className="w-full h-8 pl-9 pr-3 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
298
+ />
299
+ </div>
300
+ <Button onClick={handleSearch} disabled={searching} size="sm" variant="accent">
301
+ {searching ? <Loader2 size={14} className="animate-spin" /> : 'Search'}
302
+ </Button>
303
+ </div>
304
+
305
+ {/* Tabs */}
306
+ <div className="flex gap-1">
307
+ {['installed', 'search'].map((t) => (
308
+ <button
309
+ key={t}
310
+ onClick={() => setTab(t)}
311
+ className={cn(
312
+ 'px-3 py-1 rounded-md text-xs font-sans font-medium transition-colors cursor-pointer capitalize',
313
+ tab === t ? 'bg-accent/12 text-accent' : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
314
+ )}
315
+ >
316
+ {t === 'installed' ? `Installed (${installed.length})` : `Search Results (${searchResults.length})`}
317
+ </button>
318
+ ))}
319
+ </div>
320
+ </div>
321
+
322
+ {/* Active Downloads */}
323
+ {downloads.length > 0 && (
324
+ <div className="px-5 py-3 border-b border-border space-y-2">
325
+ <div className="text-xs font-sans font-semibold text-text-2">Downloading</div>
326
+ {downloads.map((d) => <DownloadProgress key={d.filename} download={d} />)}
327
+ </div>
328
+ )}
329
+
330
+ {/* Content */}
331
+ <ScrollArea className="flex-1">
332
+ <div className="px-5 py-4 space-y-2">
333
+ {tab === 'installed' && (
334
+ <>
335
+ {installed.length === 0 ? (
336
+ <div className="text-center py-12">
337
+ <Box size={40} className="mx-auto text-text-4 mb-3" />
338
+ <p className="text-sm text-text-2 font-sans font-medium">No local models yet</p>
339
+ <p className="text-xs text-text-3 font-sans mt-1">Search HuggingFace to download GGUF models, or pull models via Ollama.</p>
340
+ </div>
341
+ ) : (
342
+ installed.map((m) => <InstalledModel key={m.id} model={m} onDelete={handleDelete} />)
343
+ )}
344
+ </>
345
+ )}
346
+
347
+ {tab === 'search' && (
348
+ <>
349
+ {searching ? (
350
+ <div className="text-center py-12">
351
+ <Loader2 size={24} className="mx-auto text-accent animate-spin mb-3" />
352
+ <p className="text-sm text-text-3 font-sans">Searching HuggingFace...</p>
353
+ </div>
354
+ ) : searchResults.length === 0 ? (
355
+ <div className="text-center py-12">
356
+ <Search size={40} className="mx-auto text-text-4 mb-3" />
357
+ <p className="text-sm text-text-2 font-sans font-medium">Search for GGUF models</p>
358
+ <p className="text-xs text-text-3 font-sans mt-1">Try "qwen coder", "deepseek", "codestral", "llama"</p>
359
+ </div>
360
+ ) : (
361
+ searchResults.map((r) => (
362
+ <div key={r.id} className="space-y-1">
363
+ <SearchResult
364
+ result={r}
365
+ expanded={expandedResult === r.id}
366
+ onExpand={setExpandedResult}
367
+ />
368
+ {expandedResult === r.id && (
369
+ <FilePicker repoId={r.id} onDownload={() => fetchInstalled()} />
370
+ )}
371
+ </div>
372
+ ))
373
+ )}
374
+ </>
375
+ )}
376
+ </div>
377
+ </ScrollArea>
378
+ </div>
379
+ );
380
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.25.20",
4
- "description": "Open-source agent orchestration layer — the AI company OS. 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.",
3
+ "version": "0.26.0",
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)",
7
7
  "homepage": "https://groovedev.ai",