groove-dev 0.27.128 → 0.27.130
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/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 +21 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +46 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DXIaW0aK.js → index-6zTBb9ik.js} +1749 -1749
- package/node_modules/@groove-dev/gui/dist/assets/index-Ch9Mlf9w.css +1 -0
- 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/lab/runtime-config.jsx +193 -3
- package/node_modules/@groove-dev/gui/src/components/ui/combobox.jsx +118 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +54 -1
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +25 -21
- 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 +21 -0
- package/packages/daemon/src/model-lab.js +46 -0
- package/packages/gui/dist/assets/{index-DXIaW0aK.js → index-6zTBb9ik.js} +1749 -1749
- package/packages/gui/dist/assets/index-Ch9Mlf9w.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/lab/runtime-config.jsx +193 -3
- package/packages/gui/src/components/ui/combobox.jsx +118 -0
- package/packages/gui/src/stores/groove.js +54 -1
- package/packages/gui/src/views/model-lab.jsx +25 -21
- package/node_modules/@groove-dev/gui/dist/assets/index-CY-CITov.css +0 -1
- package/packages/gui/dist/assets/index-CY-CITov.css +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
import { cn } from '../../lib/cn';
|
|
4
|
+
import { ChevronDown } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export function Combobox({ value, onChange, options = [], placeholder, className, renderOption }) {
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const [query, setQuery] = useState('');
|
|
9
|
+
const [highlightIndex, setHighlightIndex] = useState(-1);
|
|
10
|
+
const wrapperRef = useRef(null);
|
|
11
|
+
const inputRef = useRef(null);
|
|
12
|
+
|
|
13
|
+
const filtered = query
|
|
14
|
+
? options.filter((o) => (o.name || o.id).toLowerCase().includes(query.toLowerCase()))
|
|
15
|
+
: options;
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
function handleClickOutside(e) {
|
|
19
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
|
|
20
|
+
setOpen(false);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
24
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setHighlightIndex(-1);
|
|
29
|
+
}, [query, open]);
|
|
30
|
+
|
|
31
|
+
const handleSelect = useCallback((val) => {
|
|
32
|
+
onChange(val);
|
|
33
|
+
setQuery('');
|
|
34
|
+
setOpen(false);
|
|
35
|
+
inputRef.current?.blur();
|
|
36
|
+
}, [onChange]);
|
|
37
|
+
|
|
38
|
+
function handleKeyDown(e) {
|
|
39
|
+
if (!open && (e.key === 'ArrowDown' || e.key === 'Enter')) {
|
|
40
|
+
setOpen(true);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!open) return;
|
|
44
|
+
|
|
45
|
+
if (e.key === 'ArrowDown') {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setHighlightIndex((i) => Math.min(i + 1, filtered.length - 1));
|
|
48
|
+
} else if (e.key === 'ArrowUp') {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
setHighlightIndex((i) => Math.max(i - 1, 0));
|
|
51
|
+
} else if (e.key === 'Enter') {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
if (highlightIndex >= 0 && filtered[highlightIndex]) {
|
|
54
|
+
handleSelect(filtered[highlightIndex].id || filtered[highlightIndex].name);
|
|
55
|
+
} else if (query.trim()) {
|
|
56
|
+
handleSelect(query.trim());
|
|
57
|
+
}
|
|
58
|
+
} else if (e.key === 'Escape') {
|
|
59
|
+
setOpen(false);
|
|
60
|
+
setQuery('');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const displayValue = value && !open ? (options.find((o) => (o.id || o.name) === value)?.name || value) : query;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div ref={wrapperRef} className={cn('relative', className)}>
|
|
68
|
+
<div className="relative">
|
|
69
|
+
<input
|
|
70
|
+
ref={inputRef}
|
|
71
|
+
value={displayValue}
|
|
72
|
+
onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
|
|
73
|
+
onFocus={() => setOpen(true)}
|
|
74
|
+
onKeyDown={handleKeyDown}
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
className={cn(
|
|
77
|
+
'h-8 w-full rounded-md pl-3 pr-8 text-sm',
|
|
78
|
+
'bg-surface-1 border border-border text-text-0 font-sans',
|
|
79
|
+
'placeholder:text-text-4',
|
|
80
|
+
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
|
81
|
+
'transition-colors duration-100',
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
<ChevronDown
|
|
85
|
+
size={12}
|
|
86
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{open && filtered.length > 0 && (
|
|
91
|
+
<div className="absolute left-0 right-0 top-full mt-1 z-30 bg-surface-1 border border-border rounded-md shadow-xl py-1 max-h-48 overflow-y-auto">
|
|
92
|
+
{filtered.map((option, i) => (
|
|
93
|
+
<button
|
|
94
|
+
key={option.id || option.name}
|
|
95
|
+
onClick={() => handleSelect(option.id || option.name)}
|
|
96
|
+
onMouseEnter={() => setHighlightIndex(i)}
|
|
97
|
+
className={cn(
|
|
98
|
+
'w-full text-left px-3 py-1.5 text-sm font-sans cursor-pointer transition-colors',
|
|
99
|
+
(option.id || option.name) === value && 'text-accent',
|
|
100
|
+
highlightIndex === i ? 'bg-accent/10 text-text-0' : 'text-text-2 hover:bg-surface-5 hover:text-text-0',
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{renderOption ? renderOption(option) : (option.name || option.id)}
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{open && filtered.length === 0 && query && (
|
|
110
|
+
<div className="absolute left-0 right-0 top-full mt-1 z-30 bg-surface-1 border border-border rounded-md shadow-xl py-2 px-3">
|
|
111
|
+
<p className="text-xs text-text-3 font-sans">
|
|
112
|
+
Press Enter to use “{query}”
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -219,6 +219,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
219
219
|
}),
|
|
220
220
|
labSystemPrompt: localStorage.getItem('groove:labSystemPrompt') || '',
|
|
221
221
|
labStreaming: false,
|
|
222
|
+
labLocalModels: [],
|
|
223
|
+
labLaunching: null,
|
|
224
|
+
labLlamaInstalled: null,
|
|
225
|
+
labLaunchPhase: null,
|
|
226
|
+
labLaunchError: null,
|
|
222
227
|
|
|
223
228
|
// ── Onboarding ────────────────────────────────────────────
|
|
224
229
|
onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
|
|
@@ -628,6 +633,17 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
628
633
|
get().fetchOllamaStatus();
|
|
629
634
|
break;
|
|
630
635
|
|
|
636
|
+
case 'lab:runtime:added':
|
|
637
|
+
case 'lab:runtime:updated':
|
|
638
|
+
case 'lab:runtime:removed':
|
|
639
|
+
get().fetchLabRuntimes();
|
|
640
|
+
break;
|
|
641
|
+
|
|
642
|
+
case 'lab:preset:created':
|
|
643
|
+
case 'lab:preset:updated':
|
|
644
|
+
case 'lab:preset:deleted':
|
|
645
|
+
break;
|
|
646
|
+
|
|
631
647
|
case 'rotation:start':
|
|
632
648
|
break;
|
|
633
649
|
|
|
@@ -3282,7 +3298,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
3282
3298
|
|
|
3283
3299
|
async fetchLabRuntimes() {
|
|
3284
3300
|
try {
|
|
3285
|
-
const
|
|
3301
|
+
const raw = await api.get('/lab/runtimes');
|
|
3302
|
+
const data = raw.map((rt) => ({
|
|
3303
|
+
...rt,
|
|
3304
|
+
status: rt.online === true ? 'connected' : rt.online === false ? 'error' : rt.status,
|
|
3305
|
+
}));
|
|
3286
3306
|
set({ labRuntimes: data });
|
|
3287
3307
|
persistJSON('groove:labRuntimes', data);
|
|
3288
3308
|
if (data.length > 0 && !get().labActiveRuntime) {
|
|
@@ -3293,6 +3313,39 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
3293
3313
|
} catch { /* backend may not have lab endpoints yet */ }
|
|
3294
3314
|
},
|
|
3295
3315
|
|
|
3316
|
+
async fetchLabLocalModels() {
|
|
3317
|
+
try {
|
|
3318
|
+
const data = await api.get('/lab/local-models');
|
|
3319
|
+
set({ labLocalModels: data });
|
|
3320
|
+
} catch { set({ labLocalModels: [] }); }
|
|
3321
|
+
},
|
|
3322
|
+
|
|
3323
|
+
async checkLlamaStatus() {
|
|
3324
|
+
try {
|
|
3325
|
+
const data = await api.get('/llama/status');
|
|
3326
|
+
set({ labLlamaInstalled: !!data.installed });
|
|
3327
|
+
} catch { set({ labLlamaInstalled: false }); }
|
|
3328
|
+
},
|
|
3329
|
+
|
|
3330
|
+
async launchLocalModel(modelId) {
|
|
3331
|
+
set({ labLaunching: modelId, labLaunchPhase: 'starting', labLaunchError: null });
|
|
3332
|
+
try {
|
|
3333
|
+
const result = await api.post('/lab/launch-local', { modelId });
|
|
3334
|
+
const runtimes = await api.get('/lab/runtimes');
|
|
3335
|
+
set({ labRuntimes: runtimes });
|
|
3336
|
+
persistJSON('groove:labRuntimes', runtimes);
|
|
3337
|
+
get().setLabActiveRuntime(result.runtime.id);
|
|
3338
|
+
set({ labActiveModel: result.model, labLaunching: null, labLaunchPhase: 'ready' });
|
|
3339
|
+
get().addToast('success', `Launched ${result.model}`);
|
|
3340
|
+
setTimeout(() => { if (get().labLaunchPhase === 'ready') set({ labLaunchPhase: null }); }, 3000);
|
|
3341
|
+
return result;
|
|
3342
|
+
} catch (err) {
|
|
3343
|
+
set({ labLaunching: null, labLaunchPhase: 'error', labLaunchError: err.message });
|
|
3344
|
+
get().addToast('error', 'Failed to launch model', err.message);
|
|
3345
|
+
throw err;
|
|
3346
|
+
}
|
|
3347
|
+
},
|
|
3348
|
+
|
|
3296
3349
|
async addLabRuntime(runtime) {
|
|
3297
3350
|
try {
|
|
3298
3351
|
const created = await api.post('/lab/runtimes', runtime);
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../stores/groove';
|
|
4
4
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
5
|
-
import { Select, SelectTrigger, SelectContent, SelectItem } from '../components/ui/select';
|
|
6
5
|
import { Badge } from '../components/ui/badge';
|
|
7
6
|
import { Tooltip } from '../components/ui/tooltip';
|
|
8
|
-
import {
|
|
7
|
+
import { Combobox } from '../components/ui/combobox';
|
|
8
|
+
import { RuntimeConfig, LaunchModel } from '../components/lab/runtime-config';
|
|
9
9
|
import { ParameterPanel } from '../components/lab/parameter-panel';
|
|
10
10
|
import { SystemPromptEditor } from '../components/lab/system-prompt-editor';
|
|
11
11
|
import { ChatPlayground } from '../components/lab/chat-playground';
|
|
@@ -32,25 +32,21 @@ function ModelSelector() {
|
|
|
32
32
|
return (
|
|
33
33
|
<div className="space-y-1.5">
|
|
34
34
|
<span className="text-xs font-semibold font-sans text-text-2 uppercase tracking-wider">Model</span>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
</
|
|
50
|
-
) : (
|
|
51
|
-
<div className="px-3 py-2 rounded-md bg-surface-1 border border-border-subtle">
|
|
52
|
-
<p className="text-2xs text-text-4 font-sans">No models discovered</p>
|
|
53
|
-
</div>
|
|
35
|
+
<Combobox
|
|
36
|
+
value={activeModel || ''}
|
|
37
|
+
onChange={setActiveModel}
|
|
38
|
+
options={models.map((m) => ({ id: m.id || m.name, name: m.name, size: m.size }))}
|
|
39
|
+
placeholder="Select or type a model name"
|
|
40
|
+
renderOption={(o) => (
|
|
41
|
+
<div className="flex items-center gap-2">
|
|
42
|
+
<Box size={12} className="text-text-3 flex-shrink-0" />
|
|
43
|
+
<span className="truncate">{o.name}</span>
|
|
44
|
+
{o.size && <span className="text-2xs text-text-4 font-mono flex-shrink-0">{o.size}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
/>
|
|
48
|
+
{!activeModel && models.length === 0 && (
|
|
49
|
+
<p className="text-2xs text-text-4 font-sans px-1">No models discovered — type a model name or test the runtime</p>
|
|
54
50
|
)}
|
|
55
51
|
</div>
|
|
56
52
|
);
|
|
@@ -70,8 +66,14 @@ function ResizeHandle({ onMouseDown, direction = 'vertical' }) {
|
|
|
70
66
|
|
|
71
67
|
export default function ModelLabView() {
|
|
72
68
|
const fetchLabRuntimes = useGrooveStore((s) => s.fetchLabRuntimes);
|
|
69
|
+
|
|
73
70
|
useEffect(() => { fetchLabRuntimes(); }, [fetchLabRuntimes]);
|
|
74
71
|
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const interval = setInterval(fetchLabRuntimes, 30000);
|
|
74
|
+
return () => clearInterval(interval);
|
|
75
|
+
}, [fetchLabRuntimes]);
|
|
76
|
+
|
|
75
77
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
|
76
78
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
|
77
79
|
const [leftWidth, setLeftWidth] = useState(LEFT_DEFAULT);
|
|
@@ -169,6 +171,8 @@ export default function ModelLabView() {
|
|
|
169
171
|
>
|
|
170
172
|
<ScrollArea className="h-full">
|
|
171
173
|
<div className="px-4 py-4 space-y-5">
|
|
174
|
+
<LaunchModel />
|
|
175
|
+
<div className="border-t border-border-subtle" />
|
|
172
176
|
<RuntimeConfig />
|
|
173
177
|
<div className="border-t border-border-subtle" />
|
|
174
178
|
<ModelSelector />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.130",
|
|
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)",
|
|
@@ -6669,6 +6669,27 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6669
6669
|
}
|
|
6670
6670
|
});
|
|
6671
6671
|
|
|
6672
|
+
app.get('/api/lab/local-models', (req, res) => {
|
|
6673
|
+
try {
|
|
6674
|
+
res.json(daemon.modelLab.listLocalModels());
|
|
6675
|
+
} catch (err) {
|
|
6676
|
+
res.status(500).json({ error: err.message });
|
|
6677
|
+
}
|
|
6678
|
+
});
|
|
6679
|
+
|
|
6680
|
+
app.post('/api/lab/launch-local', async (req, res) => {
|
|
6681
|
+
try {
|
|
6682
|
+
const { modelId } = req.body;
|
|
6683
|
+
if (!modelId || typeof modelId !== 'string') {
|
|
6684
|
+
return res.status(400).json({ error: 'modelId is required' });
|
|
6685
|
+
}
|
|
6686
|
+
const result = await daemon.modelLab.launchLocalModel(modelId);
|
|
6687
|
+
res.json(result);
|
|
6688
|
+
} catch (err) {
|
|
6689
|
+
res.status(400).json({ error: err.message });
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
6692
|
+
|
|
6672
6693
|
app.post('/api/lab/inference', async (req, res) => {
|
|
6673
6694
|
try {
|
|
6674
6695
|
const params = validateLabInferenceParams(req.body);
|
|
@@ -442,6 +442,52 @@ export class ModelLab {
|
|
|
442
442
|
this._saveSession(session);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
// ─── Launch Local GGUF ───────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
async launchLocalModel(modelId) {
|
|
448
|
+
const mm = this.daemon.modelManager;
|
|
449
|
+
const ls = this.daemon.llamaServer;
|
|
450
|
+
if (!mm || !ls) throw new Error('Local model serving not available');
|
|
451
|
+
|
|
452
|
+
const model = mm.getModel(modelId);
|
|
453
|
+
if (!model) throw new Error('Model not found in local store');
|
|
454
|
+
|
|
455
|
+
const modelPath = mm.getModelPath(modelId);
|
|
456
|
+
if (!modelPath) throw new Error('Model file not found on disk');
|
|
457
|
+
|
|
458
|
+
const endpoint = await ls.ensureServer(modelPath);
|
|
459
|
+
if (!endpoint) throw new Error('Failed to start inference server');
|
|
460
|
+
|
|
461
|
+
const existing = [...this.runtimes.values()].find(
|
|
462
|
+
(r) => r._localModelId === modelId,
|
|
463
|
+
);
|
|
464
|
+
if (existing) {
|
|
465
|
+
existing.endpoint = endpoint;
|
|
466
|
+
existing.models = [{ id: model.filename, name: model.filename, size: model.sizeBytes }];
|
|
467
|
+
this._saveRuntimes();
|
|
468
|
+
this.daemon.broadcast({ type: 'lab:runtime:updated', data: existing });
|
|
469
|
+
return { runtime: existing, model: model.filename };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const label = model.filename.replace(/\.gguf$/i, '');
|
|
473
|
+
const runtime = await this.addRuntime({
|
|
474
|
+
name: `${label} (local)`,
|
|
475
|
+
type: 'llama-cpp',
|
|
476
|
+
endpoint,
|
|
477
|
+
models: [{ id: model.filename, name: model.filename, size: model.sizeBytes }],
|
|
478
|
+
});
|
|
479
|
+
runtime._localModelId = modelId;
|
|
480
|
+
this._saveRuntimes();
|
|
481
|
+
|
|
482
|
+
return { runtime, model: model.filename };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
listLocalModels() {
|
|
486
|
+
const mm = this.daemon.modelManager;
|
|
487
|
+
if (!mm) return [];
|
|
488
|
+
return mm.getInstalled().filter((m) => m.exists);
|
|
489
|
+
}
|
|
490
|
+
|
|
445
491
|
// ─── Auto-detect Ollama ─────────────────────────────────────
|
|
446
492
|
|
|
447
493
|
async autoDetectOllama() {
|