groove-dev 0.27.152 → 0.27.154
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/journalist.js +52 -21
- package/node_modules/@groove-dev/daemon/src/keeper.js +37 -2
- package/node_modules/@groove-dev/daemon/src/llama-server.js +96 -3
- package/node_modules/@groove-dev/daemon/src/model-manager.js +52 -10
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +16 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +71 -2
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +11 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BTLb6zTD.js +1015 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.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/agents/agent-feed.jsx +252 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +9 -2
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +40 -3
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +23 -8
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +8 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +24 -5
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +13 -0
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +87 -44
- package/node_modules/@groove-dev/gui/src/views/models.jsx +15 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/journalist.js +52 -21
- package/packages/daemon/src/keeper.js +37 -2
- package/packages/daemon/src/llama-server.js +96 -3
- package/packages/daemon/src/model-manager.js +52 -10
- package/packages/daemon/src/routes/coordination.js +16 -0
- package/packages/daemon/src/routes/files.js +71 -2
- package/packages/daemon/src/routes/providers.js +11 -0
- package/packages/gui/dist/assets/index-BTLb6zTD.js +1015 -0
- package/packages/gui/dist/assets/index-Diw6wDPU.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +252 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
- package/packages/gui/src/components/agents/spawn-wizard.jsx +9 -2
- package/packages/gui/src/components/editor/file-tree.jsx +40 -3
- package/packages/gui/src/components/lab/runtime-config.jsx +23 -8
- package/packages/gui/src/components/settings/quick-connect.jsx +8 -1
- package/packages/gui/src/stores/groove.js +9 -1
- package/packages/gui/src/stores/slices/agents-slice.js +24 -5
- package/packages/gui/src/stores/slices/providers-slice.js +13 -0
- package/packages/gui/src/views/memory.jsx +87 -44
- package/packages/gui/src/views/models.jsx +15 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CReKPWhY.js +0 -1011
- package/packages/gui/dist/assets/index-CEkPsSAm.css +0 -1
- package/packages/gui/dist/assets/index-CReKPWhY.js +0 -1011
|
@@ -9,7 +9,7 @@ import { Dialog, DialogContent } from '../ui/dialog';
|
|
|
9
9
|
import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
|
|
10
10
|
import { Tooltip } from '../ui/tooltip';
|
|
11
11
|
import { ScrollArea } from '../ui/scroll-area';
|
|
12
|
-
import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2 } from 'lucide-react';
|
|
12
|
+
import { Plus, Trash2, Loader2, WifiOff, RotateCcw, HardDrive, Play, Square, CheckCircle, AlertTriangle, ChevronRight, Wrench, Settings2, Download } from 'lucide-react';
|
|
13
13
|
import { cn } from '../../lib/cn';
|
|
14
14
|
|
|
15
15
|
const IS_APPLE = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || '');
|
|
@@ -229,9 +229,11 @@ export function LaunchModel() {
|
|
|
229
229
|
const localModels = useGrooveStore((s) => s.labLocalModels);
|
|
230
230
|
const fetchLocalModels = useGrooveStore((s) => s.fetchLabLocalModels);
|
|
231
231
|
const checkLlama = useGrooveStore((s) => s.checkLlamaStatus);
|
|
232
|
+
const installLlama = useGrooveStore((s) => s.installLlamaServer);
|
|
232
233
|
const launchModel = useGrooveStore((s) => s.launchLocalModel);
|
|
233
234
|
const launching = useGrooveStore((s) => s.labLaunching);
|
|
234
235
|
const llamaInstalled = useGrooveStore((s) => s.labLlamaInstalled);
|
|
236
|
+
const llamaInstalling = useGrooveStore((s) => s.labLlamaInstalling);
|
|
235
237
|
const launchPhase = useGrooveStore((s) => s.labLaunchPhase);
|
|
236
238
|
const launchError = useGrooveStore((s) => s.labLaunchError);
|
|
237
239
|
const launchLabAssistant = useGrooveStore((s) => s.launchLabAssistant);
|
|
@@ -395,13 +397,26 @@ export function LaunchModel() {
|
|
|
395
397
|
<div className="flex items-center gap-2 text-[11px] text-danger font-sans">
|
|
396
398
|
<AlertTriangle size={10} /> llama-server not found
|
|
397
399
|
</div>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<
|
|
404
|
-
|
|
400
|
+
{llamaInstalling ? (
|
|
401
|
+
<div className="flex items-center gap-2 text-[11px] text-accent font-sans">
|
|
402
|
+
<Loader2 size={10} className="animate-spin" /> Installing llama-server...
|
|
403
|
+
</div>
|
|
404
|
+
) : (
|
|
405
|
+
<div className="flex items-center gap-2">
|
|
406
|
+
<button
|
|
407
|
+
onClick={installLlama}
|
|
408
|
+
className="flex items-center gap-1.5 text-[11px] font-sans font-medium text-surface-0 bg-accent hover:bg-accent/90 px-2.5 py-1 rounded transition-colors cursor-pointer"
|
|
409
|
+
>
|
|
410
|
+
<Download size={10} /> Install
|
|
411
|
+
</button>
|
|
412
|
+
<button
|
|
413
|
+
onClick={checkLlama}
|
|
414
|
+
className="flex items-center gap-1.5 text-[11px] font-sans text-text-3 hover:text-text-2 transition-colors cursor-pointer"
|
|
415
|
+
>
|
|
416
|
+
<RotateCcw size={10} /> Recheck
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
405
420
|
</div>
|
|
406
421
|
)}
|
|
407
422
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useRef } from 'react';
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -20,6 +20,13 @@ export function QuickConnect() {
|
|
|
20
20
|
const [showWizard, setShowWizard] = useState(false);
|
|
21
21
|
const wizardTunnelId = useRef(null);
|
|
22
22
|
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (open) {
|
|
25
|
+
setShowWizard(false);
|
|
26
|
+
useGrooveStore.getState().fetchTunnels();
|
|
27
|
+
}
|
|
28
|
+
}, [open]);
|
|
29
|
+
|
|
23
30
|
if (!open) return null;
|
|
24
31
|
|
|
25
32
|
async function handleConnect(id) {
|
|
@@ -306,7 +306,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
306
306
|
const filePath = block.input?.file_path || block.input?.path;
|
|
307
307
|
if (filePath && agentId === get().workspaceAgentId) {
|
|
308
308
|
const relPath = filePath.replace(/^\/[^/]+.*?\/groove\//, '');
|
|
309
|
-
get().
|
|
309
|
+
const hasActiveFile = get().editorActiveFile;
|
|
310
|
+
if (hasActiveFile) {
|
|
311
|
+
// Add to tabs without switching away from the user's current file
|
|
312
|
+
set((s) => ({
|
|
313
|
+
editorOpenTabs: s.editorOpenTabs.includes(relPath) ? s.editorOpenTabs : [...s.editorOpenTabs, relPath],
|
|
314
|
+
}));
|
|
315
|
+
} else {
|
|
316
|
+
get().openFile(relPath);
|
|
317
|
+
}
|
|
310
318
|
}
|
|
311
319
|
}
|
|
312
320
|
}
|
|
@@ -95,12 +95,19 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
95
95
|
|
|
96
96
|
// ── Chat ──────────────────────────────────────────────────
|
|
97
97
|
|
|
98
|
-
addChatMessage(agentId, from, text, isQuery = false) {
|
|
98
|
+
addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
|
|
99
99
|
set((s) => {
|
|
100
100
|
const history = { ...s.chatHistory };
|
|
101
101
|
if (!history[agentId]) history[agentId] = [];
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const msg = { from, text, timestamp: Date.now(), isQuery };
|
|
103
|
+
if (attachments?.length) msg.attachments = attachments;
|
|
104
|
+
history[agentId] = [...history[agentId].slice(-100), msg];
|
|
105
|
+
const forStorage = { ...history };
|
|
106
|
+
forStorage[agentId] = forStorage[agentId].map((m) => {
|
|
107
|
+
if (!m.attachments?.length) return m;
|
|
108
|
+
return { ...m, attachments: m.attachments.map(({ dataUrl, ...rest }) => rest) };
|
|
109
|
+
});
|
|
110
|
+
persistJSON('groove:chatHistory', forStorage);
|
|
104
111
|
return { chatHistory: history };
|
|
105
112
|
});
|
|
106
113
|
},
|
|
@@ -120,7 +127,7 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
120
127
|
}
|
|
121
128
|
},
|
|
122
129
|
|
|
123
|
-
async instructAgent(id, message) {
|
|
130
|
+
async instructAgent(id, message, attachments = undefined) {
|
|
124
131
|
// ── Keeper command interception ─────────────────────────
|
|
125
132
|
const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
|
|
126
133
|
if (keeperCmd) {
|
|
@@ -131,7 +138,7 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
get().addChatMessage(id, 'user', message, false);
|
|
141
|
+
get().addChatMessage(id, 'user', message, false, attachments);
|
|
135
142
|
set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
|
|
136
143
|
|
|
137
144
|
// Auto-attach active file context when in workspace mode
|
|
@@ -332,6 +339,18 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
332
339
|
}
|
|
333
340
|
},
|
|
334
341
|
|
|
342
|
+
async moveKeeperItem(oldTag, newTag) {
|
|
343
|
+
try {
|
|
344
|
+
const item = await api.post('/keeper/move', { oldTag, newTag });
|
|
345
|
+
get().fetchKeeperItems();
|
|
346
|
+
get().addToast('success', `Moved #${oldTag} → #${item.tag}`);
|
|
347
|
+
return item;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
get().addToast('error', 'Failed to move memory', err.message);
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
335
354
|
async getKeeperItem(tag) {
|
|
336
355
|
try {
|
|
337
356
|
return await api.get(`/keeper/${tag}`);
|
|
@@ -42,6 +42,7 @@ export const createProvidersSlice = (set, get) => ({
|
|
|
42
42
|
labLocalModels: [],
|
|
43
43
|
labLaunching: null,
|
|
44
44
|
labLlamaInstalled: null,
|
|
45
|
+
labLlamaInstalling: false,
|
|
45
46
|
labLaunchPhase: null,
|
|
46
47
|
labLaunchError: null,
|
|
47
48
|
labAssistantAgentId: localStorage.getItem('groove:labAssistantAgentId') || null,
|
|
@@ -356,6 +357,18 @@ export const createProvidersSlice = (set, get) => ({
|
|
|
356
357
|
} catch { set({ labLlamaInstalled: false }); }
|
|
357
358
|
},
|
|
358
359
|
|
|
360
|
+
async installLlamaServer() {
|
|
361
|
+
set({ labLlamaInstalling: true });
|
|
362
|
+
try {
|
|
363
|
+
await api.post('/llama/install');
|
|
364
|
+
set({ labLlamaInstalled: true, labLlamaInstalling: false });
|
|
365
|
+
get().addToast('success', 'llama-server installed successfully');
|
|
366
|
+
} catch (err) {
|
|
367
|
+
set({ labLlamaInstalling: false });
|
|
368
|
+
get().addToast('error', 'Install failed', err.message);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
|
|
359
372
|
async launchLocalModel(modelId) {
|
|
360
373
|
set({ labLaunching: modelId, labLaunchPhase: 'starting', labLaunchError: null });
|
|
361
374
|
try {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../stores/groove';
|
|
4
4
|
import { Button } from '../components/ui/button';
|
|
5
5
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
6
6
|
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
7
|
-
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle } from 'lucide-react';
|
|
7
|
+
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical } from 'lucide-react';
|
|
8
8
|
|
|
9
9
|
const COMMANDS = [
|
|
10
10
|
{ cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
|
|
@@ -249,38 +249,71 @@ function InstructModal({ open, onOpenChange }) {
|
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
function
|
|
252
|
+
function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
draggable
|
|
256
|
+
onDragStart={(e) => { e.dataTransfer.setData('text/plain', tag); e.dataTransfer.effectAllowed = 'move'; onDragStart?.(tag); }}
|
|
257
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(tag); }}
|
|
258
|
+
onDragLeave={() => onDragLeave?.()}
|
|
259
|
+
onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), tag); }}
|
|
260
|
+
onClick={() => onSelect({ tag })}
|
|
261
|
+
className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer group ${isDragOver ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
|
|
262
|
+
style={indent ? { paddingLeft: `${8 + indent * 16}px` } : undefined}
|
|
263
|
+
>
|
|
264
|
+
<GripVertical size={10} className="text-text-4 opacity-0 group-hover:opacity-50 flex-shrink-0 cursor-grab" />
|
|
265
|
+
<Hash size={11} className="text-text-4 flex-shrink-0" />
|
|
266
|
+
<span className="font-medium text-text-2 truncate">{label}</span>
|
|
267
|
+
{isDoc && <Sparkles size={9} className="text-purple flex-shrink-0" />}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
253
273
|
const [expanded, setExpanded] = useState(true);
|
|
254
274
|
const hasChildren = node.children && node.children.length > 0;
|
|
255
275
|
|
|
276
|
+
if (!hasChildren) {
|
|
277
|
+
return (
|
|
278
|
+
<TreeItem
|
|
279
|
+
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'}
|
|
280
|
+
isDragOver={dragOverTag === node.tag}
|
|
281
|
+
onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
256
286
|
return (
|
|
257
|
-
<div
|
|
287
|
+
<div
|
|
288
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(node.tag); }}
|
|
289
|
+
onDragLeave={() => onDragLeave?.()}
|
|
290
|
+
onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), node.tag); }}
|
|
291
|
+
>
|
|
258
292
|
<button
|
|
259
|
-
onClick={() =>
|
|
260
|
-
className=
|
|
293
|
+
onClick={() => setExpanded(!expanded)}
|
|
294
|
+
className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer ${dragOverTag === node.tag ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
|
|
261
295
|
>
|
|
262
|
-
{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<FolderOpen size={12} className={hasChildren ? 'text-accent' : 'text-text-4'} />
|
|
268
|
-
<span className="font-medium">{node.tag}</span>
|
|
269
|
-
{hasChildren && (
|
|
270
|
-
<span className="text-2xs text-text-4 ml-auto">{node.children.length}</span>
|
|
271
|
-
)}
|
|
296
|
+
<ChevronRight size={12} className={`transition-transform text-text-4 ${expanded ? 'rotate-90' : ''}`} />
|
|
297
|
+
<FolderOpen size={12} className="text-accent" />
|
|
298
|
+
<span className="font-medium text-text-1">{node.tag}</span>
|
|
299
|
+
{!node.virtual && node.type === 'doc' && <Sparkles size={9} className="text-purple" />}
|
|
300
|
+
<span className="text-2xs text-text-4 ml-auto">{node.children.length}</span>
|
|
272
301
|
</button>
|
|
273
|
-
{expanded &&
|
|
274
|
-
<div className="
|
|
302
|
+
{expanded && (
|
|
303
|
+
<div className="mt-0.5 space-y-0.5">
|
|
304
|
+
{!node.virtual && (
|
|
305
|
+
<TreeItem
|
|
306
|
+
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'} indent={1}
|
|
307
|
+
isDragOver={false}
|
|
308
|
+
onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
275
311
|
{node.children.map((child) => (
|
|
276
|
-
<
|
|
277
|
-
key={child.tag}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
<Hash size={10} className="text-text-4" />
|
|
282
|
-
<span>{child.tag.split('/').pop()}</span>
|
|
283
|
-
</button>
|
|
312
|
+
<TreeItem
|
|
313
|
+
key={child.tag} tag={child.tag} label={child.tag.split('/').pop()} isDoc={child.type === 'doc'} indent={1}
|
|
314
|
+
isDragOver={dragOverTag === child.tag}
|
|
315
|
+
onSelect={onSelect} onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
316
|
+
/>
|
|
284
317
|
))}
|
|
285
318
|
</div>
|
|
286
319
|
)}
|
|
@@ -297,12 +330,15 @@ export default function MemoryView() {
|
|
|
297
330
|
const saveKeeperItem = useGrooveStore((s) => s.saveKeeperItem);
|
|
298
331
|
const updateKeeperItem = useGrooveStore((s) => s.updateKeeperItem);
|
|
299
332
|
const deleteKeeperItem = useGrooveStore((s) => s.deleteKeeperItem);
|
|
333
|
+
const moveKeeperItem = useGrooveStore((s) => s.moveKeeperItem);
|
|
300
334
|
const getKeeperItem = useGrooveStore((s) => s.getKeeperItem);
|
|
301
335
|
const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
|
|
302
336
|
|
|
303
337
|
const [search, setSearch] = useState('');
|
|
304
338
|
const [viewMode, setViewMode] = useState('list');
|
|
305
339
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
340
|
+
const [dragOverTag, setDragOverTag] = useState(null);
|
|
341
|
+
const [draggingTag, setDraggingTag] = useState(null);
|
|
306
342
|
|
|
307
343
|
useEffect(() => { fetchKeeperItems(); }, []);
|
|
308
344
|
|
|
@@ -344,8 +380,22 @@ export default function MemoryView() {
|
|
|
344
380
|
await handleEdit(node);
|
|
345
381
|
};
|
|
346
382
|
|
|
383
|
+
const handleDrop = useCallback(async (sourceTag, targetTag) => {
|
|
384
|
+
setDragOverTag(null);
|
|
385
|
+
setDraggingTag(null);
|
|
386
|
+
if (!sourceTag || !targetTag || sourceTag === targetTag) return;
|
|
387
|
+
// Don't drop onto self or own children
|
|
388
|
+
if (targetTag.startsWith(sourceTag + '/')) return;
|
|
389
|
+
const sourceName = sourceTag.split('/').pop();
|
|
390
|
+
const newTag = targetTag + '/' + sourceName;
|
|
391
|
+
if (sourceTag === newTag) return;
|
|
392
|
+
try {
|
|
393
|
+
await moveKeeperItem(sourceTag, newTag);
|
|
394
|
+
} catch { /* toast handles */ }
|
|
395
|
+
}, [moveKeeperItem]);
|
|
396
|
+
|
|
347
397
|
return (
|
|
348
|
-
<div className="flex
|
|
398
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
349
399
|
{/* Header */}
|
|
350
400
|
<div className="flex-shrink-0 px-4 py-3 border-b border-border">
|
|
351
401
|
<div className="flex items-center justify-between gap-3 mb-3">
|
|
@@ -394,7 +444,7 @@ export default function MemoryView() {
|
|
|
394
444
|
</div>
|
|
395
445
|
|
|
396
446
|
{/* Content */}
|
|
397
|
-
<ScrollArea className="flex-1">
|
|
447
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
398
448
|
{keeperItems.length === 0 ? (
|
|
399
449
|
<div className="flex flex-col items-center justify-center h-64 gap-3">
|
|
400
450
|
<BookOpen size={32} className="text-text-4" />
|
|
@@ -414,24 +464,17 @@ export default function MemoryView() {
|
|
|
414
464
|
</div>
|
|
415
465
|
</div>
|
|
416
466
|
) : viewMode === 'tree' ? (
|
|
417
|
-
<div className="p-3 space-y-
|
|
467
|
+
<div className="p-3 space-y-0.5" onDragOver={(e) => e.preventDefault()}>
|
|
418
468
|
{keeperTree.map((node) => (
|
|
419
|
-
<TreeGroup
|
|
469
|
+
<TreeGroup
|
|
470
|
+
key={node.tag} node={node} onSelect={handleTreeSelect}
|
|
471
|
+
dragOverTag={dragOverTag}
|
|
472
|
+
onDragStart={(tag) => setDraggingTag(tag)}
|
|
473
|
+
onDragOver={(tag) => { if (tag !== draggingTag) setDragOverTag(tag); }}
|
|
474
|
+
onDragLeave={() => setDragOverTag(null)}
|
|
475
|
+
onDrop={handleDrop}
|
|
476
|
+
/>
|
|
420
477
|
))}
|
|
421
|
-
{keeperItems
|
|
422
|
-
.filter((item) => !item.tag.includes('/') && !keeperTree.some((t) => t.tag === item.tag && t.children?.length))
|
|
423
|
-
.map((item) => (
|
|
424
|
-
<button
|
|
425
|
-
key={item.tag}
|
|
426
|
-
onClick={() => handleEdit(item)}
|
|
427
|
-
className="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-text-2 hover:bg-surface-2 transition-colors cursor-pointer"
|
|
428
|
-
>
|
|
429
|
-
<Hash size={12} className="text-text-4" />
|
|
430
|
-
<span className="font-medium">{item.tag}</span>
|
|
431
|
-
{item.type === 'doc' && <Sparkles size={10} className="text-purple" />}
|
|
432
|
-
<span className="text-2xs text-text-4 ml-auto">{formatRelative(item.updatedAt)}</span>
|
|
433
|
-
</button>
|
|
434
|
-
))}
|
|
435
478
|
</div>
|
|
436
479
|
) : (
|
|
437
480
|
<div className="p-3 space-y-2">
|
|
@@ -720,7 +720,7 @@ export default function ModelsView() {
|
|
|
720
720
|
<Box size={28} className="text-text-4 mb-3" />
|
|
721
721
|
<div className="text-sm font-mono font-semibold text-text-1 mb-1">No local models</div>
|
|
722
722
|
<div className="text-xs font-mono text-text-3 text-center max-w-sm mb-5">
|
|
723
|
-
Pull from Ollama or search HuggingFace for
|
|
723
|
+
Pull from Ollama or search HuggingFace for models to run locally.
|
|
724
724
|
</div>
|
|
725
725
|
<div className="flex gap-2">
|
|
726
726
|
<Button variant="primary" onClick={() => {
|
|
@@ -855,7 +855,7 @@ export default function ModelsView() {
|
|
|
855
855
|
</div>
|
|
856
856
|
) : searchResults.length === 0 ? (
|
|
857
857
|
<div className="py-6 text-center">
|
|
858
|
-
<div className="text-xs font-mono text-text-3">Search for
|
|
858
|
+
<div className="text-xs font-mono text-text-3">Search for models on HuggingFace</div>
|
|
859
859
|
<div className="text-2xs font-mono text-text-4 mt-1">
|
|
860
860
|
Try "qwen coder", "deepseek", "codestral", "llama"
|
|
861
861
|
</div>
|
|
@@ -869,6 +869,19 @@ export default function ModelsView() {
|
|
|
869
869
|
className="w-full text-left flex items-center gap-2 py-1.5 border-b border-border hover:bg-surface-2/50 transition-colors cursor-pointer"
|
|
870
870
|
>
|
|
871
871
|
<span className="text-xs font-mono font-semibold text-text-1 truncate flex-1">{r.name}</span>
|
|
872
|
+
{r.recommendedRuntimes?.length > 0 && (
|
|
873
|
+
<span className="flex gap-1 flex-shrink-0">
|
|
874
|
+
{r.recommendedRuntimes.map((rt) => (
|
|
875
|
+
<span key={rt} className={cn(
|
|
876
|
+
'px-1.5 py-0.5 rounded text-[9px] font-mono font-medium leading-none',
|
|
877
|
+
rt === 'llama.cpp' && 'bg-blue-500/15 text-blue-400',
|
|
878
|
+
rt === 'vLLM' && 'bg-purple-500/15 text-purple-400',
|
|
879
|
+
rt === 'MLX' && 'bg-emerald-500/15 text-emerald-400',
|
|
880
|
+
rt === 'TGI' && 'bg-amber-500/15 text-amber-400',
|
|
881
|
+
)}>{rt}</span>
|
|
882
|
+
))}
|
|
883
|
+
</span>
|
|
884
|
+
)}
|
|
872
885
|
<span className="text-2xs font-mono text-text-4">{r.author}</span>
|
|
873
886
|
<span className="text-2xs font-mono text-text-4 tabular-nums">{r.downloads?.toLocaleString()}</span>
|
|
874
887
|
{expandedResult === r.id
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.154",
|
|
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)",
|
|
@@ -644,20 +644,14 @@ export class Journalist {
|
|
|
644
644
|
proc.stdin.write(stdinData);
|
|
645
645
|
proc.stdin.end();
|
|
646
646
|
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
647
|
-
const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); },
|
|
647
|
+
const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); }, 120_000);
|
|
648
648
|
proc.on('exit', (code) => {
|
|
649
649
|
clearTimeout(timer);
|
|
650
650
|
if (code !== 0) return reject(new Error(`Headless exited with code ${code}`));
|
|
651
651
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const json = JSON.parse(line);
|
|
656
|
-
if (json.result) return resolve(json.result);
|
|
657
|
-
if (json.content?.[0]?.text) return resolve(json.content[0].text);
|
|
658
|
-
} catch { /* not json */ }
|
|
659
|
-
}
|
|
660
|
-
resolve(stdout.trim());
|
|
652
|
+
const extracted = this._parseHeadlessOutput(stdout);
|
|
653
|
+
if (extracted) return resolve(extracted);
|
|
654
|
+
reject(new Error('Headless produced no usable output'));
|
|
661
655
|
});
|
|
662
656
|
return;
|
|
663
657
|
}
|
|
@@ -666,24 +660,61 @@ export class Journalist {
|
|
|
666
660
|
env: { ...process.env, ...env },
|
|
667
661
|
cwd: this.daemon.projectDir,
|
|
668
662
|
maxBuffer: 1024 * 1024 * 5,
|
|
669
|
-
timeout:
|
|
663
|
+
timeout: 120_000,
|
|
670
664
|
}, (err, stdout, stderr) => {
|
|
671
665
|
if (err) return reject(err);
|
|
672
666
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const data = JSON.parse(line);
|
|
677
|
-
if (data.type === 'result' && data.result) {
|
|
678
|
-
return resolve(data.result);
|
|
679
|
-
}
|
|
680
|
-
} catch { /* skip */ }
|
|
681
|
-
}
|
|
682
|
-
resolve(stdout);
|
|
667
|
+
const extracted = this._parseHeadlessOutput(stdout);
|
|
668
|
+
if (extracted) return resolve(extracted);
|
|
669
|
+
reject(new Error('Headless produced no usable output'));
|
|
683
670
|
});
|
|
684
671
|
});
|
|
685
672
|
}
|
|
686
673
|
|
|
674
|
+
_parseHeadlessOutput(stdout) {
|
|
675
|
+
const lines = stdout.split('\n');
|
|
676
|
+
let resultText = '';
|
|
677
|
+
let assistantText = '';
|
|
678
|
+
let codexText = '';
|
|
679
|
+
let networkText = '';
|
|
680
|
+
|
|
681
|
+
for (const line of lines) {
|
|
682
|
+
try {
|
|
683
|
+
const json = JSON.parse(line);
|
|
684
|
+
|
|
685
|
+
// Claude/Gemini stream-json: {"type":"result","result":"..."}
|
|
686
|
+
if (typeof json.result === 'string' && json.result.trim()) {
|
|
687
|
+
resultText = json.result;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Claude/Gemini assistant message: {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
691
|
+
const msgContent = json.message?.content?.[0]?.text || json.content?.[0]?.text;
|
|
692
|
+
if (msgContent && msgContent.trim()) {
|
|
693
|
+
assistantText = msgContent;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Codex --json: {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
|
|
697
|
+
if (json.type === 'item.completed' && json.item?.type === 'agent_message' && json.item.text?.trim()) {
|
|
698
|
+
codexText = json.item.text;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Groove Network: {"type":"done|complete|result","text":"..."}
|
|
702
|
+
if ((json.type === 'done' || json.type === 'complete') && typeof json.text === 'string' && json.text.trim()) {
|
|
703
|
+
networkText = json.text;
|
|
704
|
+
}
|
|
705
|
+
} catch { /* not json */ }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (resultText) return resultText;
|
|
709
|
+
if (codexText) return codexText;
|
|
710
|
+
if (networkText) return networkText;
|
|
711
|
+
if (assistantText) return assistantText;
|
|
712
|
+
// Ollama / plain-text providers: raw text output (no JSON)
|
|
713
|
+
const plain = stdout.trim();
|
|
714
|
+
if (plain && !plain.startsWith('{') && plain.length > 20) return plain;
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
687
718
|
parseSynthesisResult(text, agents) {
|
|
688
719
|
// Parse the structured output from AI
|
|
689
720
|
const sections = {
|
|
@@ -58,7 +58,7 @@ export class Keeper {
|
|
|
58
58
|
|
|
59
59
|
save(tag, content) {
|
|
60
60
|
if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
|
|
61
|
-
if (content === undefined || content === null) throw new Error('Content is required');
|
|
61
|
+
if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
|
|
62
62
|
const normalized = this._normalize(tag);
|
|
63
63
|
if (!normalized) throw new Error('Tag is required');
|
|
64
64
|
const filePath = this._tagToPath(normalized);
|
|
@@ -129,7 +129,7 @@ export class Keeper {
|
|
|
129
129
|
update(tag, content) {
|
|
130
130
|
const normalized = this._normalize(tag);
|
|
131
131
|
if (!normalized) throw new Error('Tag is required');
|
|
132
|
-
if (content === undefined || content === null) throw new Error('Content is required');
|
|
132
|
+
if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
|
|
133
133
|
const filePath = this._tagToPath(normalized);
|
|
134
134
|
if (!existsSync(filePath)) throw new Error(`Memory #${normalized} does not exist`);
|
|
135
135
|
writeFileSync(filePath, String(content));
|
|
@@ -153,6 +153,41 @@ export class Keeper {
|
|
|
153
153
|
return true;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
move(oldTag, newTag) {
|
|
157
|
+
const oldNorm = this._normalize(oldTag);
|
|
158
|
+
const newNorm = this._normalize(newTag);
|
|
159
|
+
if (!oldNorm || !newNorm) throw new Error('Both old and new tags are required');
|
|
160
|
+
if (oldNorm === newNorm) return this._index[oldNorm];
|
|
161
|
+
const oldPath = this._tagToPath(oldNorm);
|
|
162
|
+
if (!existsSync(oldPath)) throw new Error(`Memory #${oldNorm} does not exist`);
|
|
163
|
+
if (this._index[newNorm]) throw new Error(`Memory #${newNorm} already exists`);
|
|
164
|
+
const content = readFileSync(oldPath, 'utf8');
|
|
165
|
+
const newPath = this._tagToPath(newNorm);
|
|
166
|
+
this._ensureParentDir(newPath);
|
|
167
|
+
writeFileSync(newPath, content);
|
|
168
|
+
unlinkSync(oldPath);
|
|
169
|
+
this._index[newNorm] = { ...this._index[oldNorm], tag: newNorm, updatedAt: new Date().toISOString() };
|
|
170
|
+
delete this._index[oldNorm];
|
|
171
|
+
// Move children too (e.g. moving "a" to "b/a" also moves "a/child" to "b/a/child")
|
|
172
|
+
const prefix = oldNorm + '/';
|
|
173
|
+
for (const tag of Object.keys(this._index)) {
|
|
174
|
+
if (tag.startsWith(prefix)) {
|
|
175
|
+
const childSuffix = tag.slice(prefix.length);
|
|
176
|
+
const childNewTag = newNorm + '/' + childSuffix;
|
|
177
|
+
const childOldPath = this._tagToPath(tag);
|
|
178
|
+
const childNewPath = this._tagToPath(childNewTag);
|
|
179
|
+
const childContent = readFileSync(childOldPath, 'utf8');
|
|
180
|
+
this._ensureParentDir(childNewPath);
|
|
181
|
+
writeFileSync(childNewPath, childContent);
|
|
182
|
+
unlinkSync(childOldPath);
|
|
183
|
+
this._index[childNewTag] = { ...this._index[tag], tag: childNewTag, updatedAt: new Date().toISOString() };
|
|
184
|
+
delete this._index[tag];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this._saveIndex();
|
|
188
|
+
return { tag: newNorm, ...this._index[newNorm] };
|
|
189
|
+
}
|
|
190
|
+
|
|
156
191
|
// ── Doc (AI-generated) ───────────────────────────────────
|
|
157
192
|
|
|
158
193
|
saveDoc(tag, content) {
|