groove-dev 0.27.139 → 0.27.141
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/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +225 -16
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +76 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -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/app.jsx +4 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +225 -16
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +17 -1
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +277 -0
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +9 -0
- package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +4 -0
- package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
- package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/file-tree.jsx +2 -86
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
- package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +302 -20
- package/packages/gui/src/views/agents.jsx +118 -60
- package/packages/gui/src/views/editor.jsx +67 -219
- package/packages/gui/src/views/memory.jsx +460 -0
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +295 -0
- package/plan_files/keeper-memory-system.md +223 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
- package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
|
@@ -5,8 +5,8 @@ import { cn } from '../../lib/cn';
|
|
|
5
5
|
import { api } from '../../lib/api';
|
|
6
6
|
import {
|
|
7
7
|
ChevronRight, ChevronDown, File, Folder, FolderOpen,
|
|
8
|
-
|
|
9
|
-
ChevronsDownUp, PanelLeftClose,
|
|
8
|
+
FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
|
|
9
|
+
ChevronsDownUp, PanelLeftClose,
|
|
10
10
|
} from 'lucide-react';
|
|
11
11
|
import { ScrollArea } from '../ui/scroll-area';
|
|
12
12
|
|
|
@@ -240,41 +240,18 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
240
240
|
|
|
241
241
|
// ── Main FileTree ────────────────────────────────────────────
|
|
242
242
|
|
|
243
|
-
// ── Collapsible Section ──────────────────────────────────────
|
|
244
|
-
function CollapsibleSection({ title, icon: Icon, count, defaultOpen = true, children }) {
|
|
245
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
246
|
-
return (
|
|
247
|
-
<div className="border-b border-border-subtle">
|
|
248
|
-
<button
|
|
249
|
-
onClick={() => setOpen(!open)}
|
|
250
|
-
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-2xs font-sans font-medium text-text-2 uppercase tracking-wide hover:bg-surface-4 transition-colors cursor-pointer"
|
|
251
|
-
>
|
|
252
|
-
{open ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
253
|
-
<Icon size={11} className="text-text-3" />
|
|
254
|
-
<span className="flex-1 text-left">{title}</span>
|
|
255
|
-
{count > 0 && <span className="text-text-4">{count}</span>}
|
|
256
|
-
</button>
|
|
257
|
-
{open && children}
|
|
258
|
-
</div>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
243
|
export function FileTree({ rootDir, onCollapse }) {
|
|
263
244
|
const treeCache = useGrooveStore((s) => s.editorTreeCache);
|
|
264
245
|
const activeFile = useGrooveStore((s) => s.editorActiveFile);
|
|
265
246
|
const openFile = useGrooveStore((s) => s.openFile);
|
|
266
247
|
const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
|
|
267
248
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
268
|
-
const agents = useGrooveStore((s) => s.agents);
|
|
269
|
-
const editorSelectedAgent = useGrooveStore((s) => s.editorSelectedAgent);
|
|
270
|
-
|
|
271
249
|
const [expanded, setExpanded] = useState(new Set(['']));
|
|
272
250
|
const [filter, setFilter] = useState('');
|
|
273
251
|
const [contextMenu, setContextMenu] = useState(null);
|
|
274
252
|
const [inlineInput, setInlineInput] = useState(null);
|
|
275
253
|
const [dragState, setDragState] = useState({ draggingPath: null, dragOverPath: null });
|
|
276
254
|
const [gitChanges, setGitChanges] = useState([]);
|
|
277
|
-
const [agentFiles, setAgentFiles] = useState([]);
|
|
278
255
|
|
|
279
256
|
useEffect(() => {
|
|
280
257
|
fetchTreeDir('');
|
|
@@ -286,13 +263,6 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
286
263
|
}).catch(() => setGitChanges([]));
|
|
287
264
|
}, [rootDir]);
|
|
288
265
|
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
if (!editorSelectedAgent) { setAgentFiles([]); return; }
|
|
291
|
-
api.get(`/agents/${editorSelectedAgent}/files-touched`).then((data) => {
|
|
292
|
-
setAgentFiles((data.files || []).filter((f) => f.exists !== false));
|
|
293
|
-
}).catch(() => setAgentFiles([]));
|
|
294
|
-
}, [editorSelectedAgent]);
|
|
295
|
-
|
|
296
266
|
const gitStatusMap = {};
|
|
297
267
|
for (const entry of gitChanges) {
|
|
298
268
|
gitStatusMap[entry.path] = entry.status;
|
|
@@ -519,60 +489,6 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
519
489
|
|
|
520
490
|
{/* Tree */}
|
|
521
491
|
<ScrollArea className="flex-1">
|
|
522
|
-
{/* Git Changes section */}
|
|
523
|
-
{gitChanges.length > 0 && (
|
|
524
|
-
<CollapsibleSection title="Git Changes" icon={GitBranch} count={gitChanges.length} defaultOpen={true}>
|
|
525
|
-
<div className="py-0.5">
|
|
526
|
-
{gitChanges.map((entry) => {
|
|
527
|
-
const name = entry.path.split('/').pop();
|
|
528
|
-
const statusColor = entry.status === 'A' || entry.status === '?' ? 'text-success' : entry.status === 'D' ? 'text-danger' : 'text-warning';
|
|
529
|
-
return (
|
|
530
|
-
<button
|
|
531
|
-
key={entry.path}
|
|
532
|
-
onClick={() => openFile(entry.path)}
|
|
533
|
-
className={cn(
|
|
534
|
-
'w-full flex items-center gap-1.5 px-3 py-[3px] text-xs font-sans cursor-pointer',
|
|
535
|
-
'hover:bg-surface-5 transition-colors text-left',
|
|
536
|
-
activeFile === entry.path ? 'bg-accent/10 text-text-0' : 'text-text-1',
|
|
537
|
-
)}
|
|
538
|
-
>
|
|
539
|
-
<File size={12} className={cn('flex-shrink-0', getFileColor(name))} />
|
|
540
|
-
<span className="truncate flex-1">{name}</span>
|
|
541
|
-
<span className={cn('text-2xs font-mono flex-shrink-0', statusColor)}>{entry.status}</span>
|
|
542
|
-
</button>
|
|
543
|
-
);
|
|
544
|
-
})}
|
|
545
|
-
</div>
|
|
546
|
-
</CollapsibleSection>
|
|
547
|
-
)}
|
|
548
|
-
|
|
549
|
-
{/* Agent Activity section */}
|
|
550
|
-
{agentFiles.length > 0 && (
|
|
551
|
-
<CollapsibleSection title="Agent Activity" icon={Activity} count={agentFiles.length} defaultOpen={true}>
|
|
552
|
-
<div className="py-0.5">
|
|
553
|
-
{agentFiles.slice(0, 20).map((f) => {
|
|
554
|
-
const name = f.path.split('/').pop();
|
|
555
|
-
return (
|
|
556
|
-
<button
|
|
557
|
-
key={f.path}
|
|
558
|
-
onClick={() => openFile(f.path)}
|
|
559
|
-
className={cn(
|
|
560
|
-
'w-full flex items-center gap-1.5 px-3 py-[3px] text-xs font-sans cursor-pointer',
|
|
561
|
-
'hover:bg-surface-5 transition-colors text-left',
|
|
562
|
-
activeFile === f.path ? 'bg-accent/10 text-text-0' : 'text-text-1',
|
|
563
|
-
)}
|
|
564
|
-
>
|
|
565
|
-
<File size={12} className={cn('flex-shrink-0', getFileColor(name))} />
|
|
566
|
-
<span className="truncate flex-1">{name}</span>
|
|
567
|
-
<span className="text-2xs text-text-4">{f.writes || 0}w</span>
|
|
568
|
-
</button>
|
|
569
|
-
);
|
|
570
|
-
})}
|
|
571
|
-
</div>
|
|
572
|
-
</CollapsibleSection>
|
|
573
|
-
)}
|
|
574
|
-
|
|
575
|
-
{/* File Explorer */}
|
|
576
492
|
<div
|
|
577
493
|
className="py-1"
|
|
578
494
|
onDragOver={(e) => { if (!dragState.draggingPath) return; e.preventDefault(); setDragOverDir(null); }}
|
|
@@ -22,7 +22,7 @@ const THEME = {
|
|
|
22
22
|
let tabCounter = 0;
|
|
23
23
|
let spawnSeq = 0;
|
|
24
24
|
|
|
25
|
-
function TerminalInstance({ tabId, visible, registerKill }) {
|
|
25
|
+
function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
26
26
|
const containerRef = useRef(null);
|
|
27
27
|
const termRef = useRef(null);
|
|
28
28
|
const fitRef = useRef(null);
|
|
@@ -70,6 +70,11 @@ function TerminalInstance({ tabId, visible, registerKill }) {
|
|
|
70
70
|
termRef.current = term;
|
|
71
71
|
fitRef.current = fitAddon;
|
|
72
72
|
|
|
73
|
+
term.onSelectionChange(() => {
|
|
74
|
+
const text = term.getSelection();
|
|
75
|
+
onSelectionChange?.(text || '');
|
|
76
|
+
});
|
|
77
|
+
|
|
73
78
|
let spawnAttempts = 0;
|
|
74
79
|
function trySpawn() {
|
|
75
80
|
spawnAttempts++;
|
|
@@ -117,7 +122,6 @@ function TerminalInstance({ tabId, visible, registerKill }) {
|
|
|
117
122
|
});
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
// Fit first, then spawn — ensures PTY gets correct column count
|
|
121
125
|
requestAnimationFrame(() => {
|
|
122
126
|
try { fitAddon.fit(); } catch {}
|
|
123
127
|
trySpawn();
|
|
@@ -169,6 +173,7 @@ export function TerminalManager() {
|
|
|
169
173
|
|
|
170
174
|
const [tabs, setTabs] = useState([{ id: 'term-0', label: 'Terminal' }]);
|
|
171
175
|
const [activeTab, setActiveTab] = useState('term-0');
|
|
176
|
+
const [selectedText, setSelectedText] = useState('');
|
|
172
177
|
const killFns = useRef({});
|
|
173
178
|
|
|
174
179
|
const registerKill = useCallback((tabId, fn) => { killFns.current[tabId] = fn; }, []);
|
|
@@ -217,13 +222,19 @@ export function TerminalManager() {
|
|
|
217
222
|
onToggleFullHeight={() => setFullHeight(true)}
|
|
218
223
|
onMinimize={() => setFullHeight(false)}
|
|
219
224
|
onClose={() => setTerminalVisible(false)}
|
|
225
|
+
selectedText={selectedText}
|
|
220
226
|
>
|
|
221
227
|
{tabs.map((tab) => (
|
|
222
|
-
<TerminalInstance
|
|
228
|
+
<TerminalInstance
|
|
229
|
+
key={tab.id}
|
|
230
|
+
tabId={tab.id}
|
|
231
|
+
visible={tab.id === activeTab}
|
|
232
|
+
registerKill={registerKill}
|
|
233
|
+
onSelectionChange={tab.id === activeTab ? setSelectedText : undefined}
|
|
234
|
+
/>
|
|
223
235
|
))}
|
|
224
236
|
</TerminalPanel>
|
|
225
237
|
);
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
// Keep backward-compat export for existing imports
|
|
229
240
|
export { TerminalManager as TerminalEmulator };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
// Global Keeper modals — rendered at App level so they work from any view (chat commands, etc.)
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { Dialog, DialogContent } from '../ui/dialog';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { Save } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
const COMMANDS = [
|
|
10
|
+
{ cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
|
|
11
|
+
{ cmd: 'append', args: '#tag', desc: 'Add to an existing memory and send to agent' },
|
|
12
|
+
{ cmd: 'update', args: '#tag', desc: 'Open the editor to modify a memory in place' },
|
|
13
|
+
{ cmd: 'delete', args: '#tag', desc: 'Remove a memory permanently' },
|
|
14
|
+
{ cmd: 'view', args: '#tag', desc: 'Read a memory in the viewer' },
|
|
15
|
+
{ cmd: 'read', args: '#tag1 #tag2 ...', desc: 'Send memory content to the agent — chat stays clean' },
|
|
16
|
+
{ cmd: 'doc', args: '#tag', desc: 'AI synthesizes the full conversation into a document' },
|
|
17
|
+
{ cmd: 'link', args: '#tag path/to/doc', desc: 'Link a memory to a NORTHSTAR or external document' },
|
|
18
|
+
{ cmd: '[instruct]', args: '', desc: 'Show this command reference' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function KeeperEditorModal() {
|
|
22
|
+
const keeperEditing = useGrooveStore((s) => s.keeperEditing);
|
|
23
|
+
const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
|
|
24
|
+
const saveKeeperItem = useGrooveStore((s) => s.saveKeeperItem);
|
|
25
|
+
const updateKeeperItem = useGrooveStore((s) => s.updateKeeperItem);
|
|
26
|
+
const activeView = useGrooveStore((s) => s.activeView);
|
|
27
|
+
|
|
28
|
+
const [tag, setTag] = useState('');
|
|
29
|
+
const [content, setContent] = useState('');
|
|
30
|
+
const textareaRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
// Only render this global modal when NOT on the memory view (memory view has its own)
|
|
33
|
+
const open = !!keeperEditing && activeView !== 'memory';
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (keeperEditing) {
|
|
37
|
+
setTag(keeperEditing.tag || '');
|
|
38
|
+
setContent(keeperEditing.content || '');
|
|
39
|
+
}
|
|
40
|
+
}, [keeperEditing]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (open && textareaRef.current) {
|
|
44
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
45
|
+
}
|
|
46
|
+
}, [open]);
|
|
47
|
+
|
|
48
|
+
const handleClose = (isOpen) => {
|
|
49
|
+
if (!isOpen) setKeeperEditing(null);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleSave = async () => {
|
|
53
|
+
if (!tag.trim() || keeperEditing?.readOnly) return;
|
|
54
|
+
if (keeperEditing?.isNew) {
|
|
55
|
+
await saveKeeperItem(tag.trim(), content);
|
|
56
|
+
} else {
|
|
57
|
+
await updateKeeperItem(tag.trim(), content);
|
|
58
|
+
}
|
|
59
|
+
setKeeperEditing(null);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleKeyDown = (e) => {
|
|
63
|
+
if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
handleSave();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!keeperEditing) return null;
|
|
70
|
+
|
|
71
|
+
const readOnly = keeperEditing.readOnly;
|
|
72
|
+
const isNew = keeperEditing.isNew;
|
|
73
|
+
const title = readOnly ? `#${keeperEditing.tag}` : isNew ? 'New Memory' : `Edit #${keeperEditing.tag}`;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Dialog open={open} onOpenChange={handleClose}>
|
|
77
|
+
<DialogContent title={title} description="Memory content" className="max-w-2xl">
|
|
78
|
+
<div className="p-5 space-y-4" onKeyDown={handleKeyDown}>
|
|
79
|
+
{isNew && (
|
|
80
|
+
<div>
|
|
81
|
+
<label className="block text-xs font-medium text-text-2 mb-1.5">Tag</label>
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<span className="text-sm text-text-3">#</span>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
value={tag}
|
|
87
|
+
onChange={(e) => setTag(e.target.value.replace(/[^a-zA-Z0-9/_-]/g, '').toLowerCase())}
|
|
88
|
+
placeholder="project/feature-name"
|
|
89
|
+
className="flex-1 px-2 py-1.5 text-sm font-mono rounded-md bg-surface-0 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
<textarea
|
|
95
|
+
ref={textareaRef}
|
|
96
|
+
value={content}
|
|
97
|
+
onChange={(e) => !readOnly && setContent(e.target.value)}
|
|
98
|
+
readOnly={readOnly}
|
|
99
|
+
rows={16}
|
|
100
|
+
className="w-full px-3 py-2 text-sm font-mono leading-relaxed rounded-md bg-surface-0 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent resize-y"
|
|
101
|
+
placeholder="Write your thoughts, ideas, context..."
|
|
102
|
+
/>
|
|
103
|
+
<div className="flex items-center justify-between pt-1">
|
|
104
|
+
{readOnly ? (
|
|
105
|
+
<p className="text-2xs text-text-4">Read-only view</p>
|
|
106
|
+
) : (
|
|
107
|
+
<p className="text-2xs text-text-4">{navigator.platform?.includes('Mac') ? 'Cmd' : 'Ctrl'}+S to save</p>
|
|
108
|
+
)}
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<Button variant="ghost" size="sm" onClick={() => setKeeperEditing(null)}>
|
|
111
|
+
{readOnly ? 'Close' : 'Cancel'}
|
|
112
|
+
</Button>
|
|
113
|
+
{!readOnly && (
|
|
114
|
+
<Button variant="primary" size="sm" onClick={handleSave} disabled={!tag.trim()}>
|
|
115
|
+
<Save size={14} /> Save
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</DialogContent>
|
|
122
|
+
</Dialog>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function KeeperInstructModal() {
|
|
127
|
+
const open = useGrooveStore((s) => s.keeperInstructOpen);
|
|
128
|
+
const activeView = useGrooveStore((s) => s.activeView);
|
|
129
|
+
|
|
130
|
+
// Only render globally when NOT on memory view
|
|
131
|
+
if (activeView === 'memory') return null;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Dialog open={open} onOpenChange={(o) => useGrooveStore.setState({ keeperInstructOpen: o })}>
|
|
135
|
+
<DialogContent title="Keeper Commands" description="Memory system command reference" className="max-w-lg">
|
|
136
|
+
<div className="p-5 space-y-4">
|
|
137
|
+
<p className="text-xs text-text-2 leading-relaxed">
|
|
138
|
+
Type these commands in any agent chat to manage your tagged memories. Commands are intercepted by Keeper — the agent never sees them.
|
|
139
|
+
</p>
|
|
140
|
+
<div className="space-y-1.5">
|
|
141
|
+
{COMMANDS.map((c) => (
|
|
142
|
+
<div key={c.cmd} className="flex items-start gap-3 py-1.5 border-b border-border-subtle last:border-0">
|
|
143
|
+
<div className="flex-shrink-0 flex items-center gap-1">
|
|
144
|
+
<span className="px-1.5 py-0.5 rounded bg-accent/15 text-accent font-mono text-xs font-semibold">{c.cmd}</span>
|
|
145
|
+
{c.args && <span className="text-xs text-text-3 font-mono">{c.args}</span>}
|
|
146
|
+
</div>
|
|
147
|
+
<span className="text-xs text-text-3 pt-0.5">{c.desc}</span>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="pt-2 space-y-2">
|
|
152
|
+
<h3 className="text-xs font-semibold text-text-1">Tag Hierarchy</h3>
|
|
153
|
+
<p className="text-xs text-text-3 leading-relaxed">
|
|
154
|
+
Use <span className="font-mono text-accent">/</span> for nesting: <span className="font-mono text-accent">#groove/memory-system</span>. Pull a parent to get all children.
|
|
155
|
+
</p>
|
|
156
|
+
<h3 className="text-xs font-semibold text-text-1">Memory Types</h3>
|
|
157
|
+
<p className="text-xs text-text-3 leading-relaxed">
|
|
158
|
+
<span className="font-semibold">Manual</span> — you write via [save]/[append]/[update]. <span className="font-semibold">Doc</span> — AI writes via [doc]. Both editable.
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex justify-end pt-1">
|
|
162
|
+
<Button variant="ghost" size="sm" onClick={() => useGrooveStore.setState({ keeperInstructOpen: false })}>Close</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</DialogContent>
|
|
166
|
+
</Dialog>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function KeeperGlobalModals() {
|
|
171
|
+
return (
|
|
172
|
+
<>
|
|
173
|
+
<KeeperEditorModal />
|
|
174
|
+
<KeeperInstructModal />
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Network, Code2, ChartSpline, Puzzle, Users, Newspaper, Settings, Globe, Eye } from 'lucide-react';
|
|
2
|
+
import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, Eye, BookOpen } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { Tooltip } from '../ui/tooltip';
|
|
5
5
|
import { useGrooveStore } from '../../stores/groove';
|
|
6
6
|
import { isElectron, getPlatform } from '../../lib/electron';
|
|
7
7
|
|
|
8
8
|
const BASE_NAV_ITEMS = [
|
|
9
|
-
{ id: 'agents', icon: Network,
|
|
10
|
-
{ id: '
|
|
11
|
-
{ id: '
|
|
12
|
-
{ id: '
|
|
13
|
-
{ id: '
|
|
9
|
+
{ id: 'agents', icon: Network, label: 'Agents' },
|
|
10
|
+
{ id: 'chat', icon: MessageCircle, label: 'Chat' },
|
|
11
|
+
{ id: 'editor', icon: Code2, label: 'Editor' },
|
|
12
|
+
{ id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
|
|
13
|
+
{ id: 'memory', icon: BookOpen, label: 'Memory' },
|
|
14
|
+
{ id: 'teams', icon: Users, label: 'Teams' },
|
|
15
|
+
{ id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
|
|
16
|
+
{ id: 'toys', icon: Gamepad2, label: 'Toys' },
|
|
17
|
+
{ id: 'models', icon: Box, label: 'Models' },
|
|
18
|
+
{ id: 'model-lab', icon: FlaskConical, label: 'Model Lab' },
|
|
14
19
|
];
|
|
15
20
|
|
|
16
21
|
const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
|
|
@@ -1,7 +1,87 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useRef, useCallback, useState } from 'react';
|
|
3
|
-
import { Maximize2, Minimize2, Plus, X, Terminal } from 'lucide-react';
|
|
2
|
+
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
3
|
+
import { Maximize2, Minimize2, Plus, X, Terminal, Send, ChevronDown, Sparkles } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
6
|
+
import { Tooltip } from '../ui/tooltip';
|
|
7
|
+
|
|
8
|
+
function AgentPicker({ onSelect, onClose }) {
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
11
|
+
const teams = useGrooveStore((s) => s.teams);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
function handleClick(e) {
|
|
15
|
+
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
16
|
+
}
|
|
17
|
+
function handleKey(e) {
|
|
18
|
+
if (e.key === 'Escape') onClose();
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener('mousedown', handleClick);
|
|
21
|
+
document.addEventListener('keydown', handleKey);
|
|
22
|
+
return () => {
|
|
23
|
+
document.removeEventListener('mousedown', handleClick);
|
|
24
|
+
document.removeEventListener('keydown', handleKey);
|
|
25
|
+
};
|
|
26
|
+
}, [onClose]);
|
|
27
|
+
|
|
28
|
+
const running = agents.filter((a) => a.status === 'running' || a.status === 'starting');
|
|
29
|
+
const stopped = agents.filter((a) => a.status !== 'running' && a.status !== 'starting');
|
|
30
|
+
|
|
31
|
+
function teamName(teamId) {
|
|
32
|
+
const team = teams.find((t) => t.id === teamId);
|
|
33
|
+
return team?.name || 'Default';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className="absolute bottom-full right-0 mb-1 z-50 min-w-[220px] max-h-[300px] overflow-y-auto py-1 bg-surface-2 border border-border rounded-lg shadow-xl"
|
|
40
|
+
>
|
|
41
|
+
{running.length > 0 && (
|
|
42
|
+
<>
|
|
43
|
+
<div className="px-3 py-1 text-2xs text-text-4 font-sans font-medium uppercase tracking-wider">Active</div>
|
|
44
|
+
{running.map((agent) => (
|
|
45
|
+
<button
|
|
46
|
+
key={agent.id}
|
|
47
|
+
onClick={() => onSelect(agent.id)}
|
|
48
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-5 cursor-pointer transition-colors text-left"
|
|
49
|
+
>
|
|
50
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success flex-shrink-0" />
|
|
51
|
+
<span className="truncate flex-1">{agent.name}</span>
|
|
52
|
+
<span className="text-2xs text-text-4">{teamName(agent.teamId)}</span>
|
|
53
|
+
</button>
|
|
54
|
+
))}
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
{stopped.length > 0 && (
|
|
58
|
+
<>
|
|
59
|
+
<div className="px-3 py-1 text-2xs text-text-4 font-sans font-medium uppercase tracking-wider">
|
|
60
|
+
{running.length > 0 ? 'Other' : 'Agents'}
|
|
61
|
+
</div>
|
|
62
|
+
{stopped.slice(0, 10).map((agent) => (
|
|
63
|
+
<button
|
|
64
|
+
key={agent.id}
|
|
65
|
+
onClick={() => onSelect(agent.id)}
|
|
66
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs font-sans text-text-2 hover:bg-surface-5 cursor-pointer transition-colors text-left"
|
|
67
|
+
>
|
|
68
|
+
<span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0',
|
|
69
|
+
agent.status === 'completed' ? 'bg-accent' : agent.status === 'crashed' ? 'bg-danger' : 'bg-text-4',
|
|
70
|
+
)} />
|
|
71
|
+
<span className="truncate flex-1">{agent.name}</span>
|
|
72
|
+
<span className="text-2xs text-text-4">{teamName(agent.teamId)}</span>
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
{agents.length === 0 && (
|
|
78
|
+
<div className="px-3 py-3 text-xs text-text-4 font-sans text-center">
|
|
79
|
+
No agents available
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
5
85
|
|
|
6
86
|
export function TerminalPanel({
|
|
7
87
|
children,
|
|
@@ -18,13 +98,22 @@ export function TerminalPanel({
|
|
|
18
98
|
onMinimize,
|
|
19
99
|
onClose,
|
|
20
100
|
onRenameTab,
|
|
101
|
+
selectedText,
|
|
21
102
|
}) {
|
|
22
103
|
const dragging = useRef(false);
|
|
23
104
|
const [renamingId, setRenamingId] = useState(null);
|
|
24
105
|
const [renameValue, setRenameValue] = useState('');
|
|
106
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
25
107
|
const startY = useRef(0);
|
|
26
108
|
const startH = useRef(0);
|
|
27
109
|
|
|
110
|
+
const activeAgent = useGrooveStore((s) => s.editorSelectedAgent);
|
|
111
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
112
|
+
const attachSnippet = useGrooveStore((s) => s.attachSnippet);
|
|
113
|
+
|
|
114
|
+
const agent = agents.find((a) => a.id === activeAgent);
|
|
115
|
+
const hasSelection = selectedText && selectedText.trim().length > 0;
|
|
116
|
+
|
|
28
117
|
const onMouseDown = useCallback((e) => {
|
|
29
118
|
if (fullHeight) return;
|
|
30
119
|
e.preventDefault();
|
|
@@ -49,6 +138,21 @@ export function TerminalPanel({
|
|
|
49
138
|
document.addEventListener('mouseup', onMouseUp);
|
|
50
139
|
}, [height, onHeightChange, fullHeight]);
|
|
51
140
|
|
|
141
|
+
function sendToAgent(agentId) {
|
|
142
|
+
if (!agentId || !selectedText?.trim()) return;
|
|
143
|
+
setShowPicker(false);
|
|
144
|
+
useGrooveStore.setState({ editorSelectedAgent: agentId });
|
|
145
|
+
attachSnippet({ type: 'terminal', code: selectedText.trim() });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleSendClick() {
|
|
149
|
+
if (activeAgent) {
|
|
150
|
+
sendToAgent(activeAgent);
|
|
151
|
+
} else {
|
|
152
|
+
setShowPicker(true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
52
156
|
const tabList = tabs || [{ id: 'default', label: 'Terminal' }];
|
|
53
157
|
|
|
54
158
|
return (
|
|
@@ -116,8 +220,53 @@ export function TerminalPanel({
|
|
|
116
220
|
</button>
|
|
117
221
|
</div>
|
|
118
222
|
|
|
119
|
-
{/* Window controls */}
|
|
223
|
+
{/* Send to Agent + Window controls */}
|
|
120
224
|
<div className="flex items-center gap-0.5 flex-shrink-0 ml-2">
|
|
225
|
+
{/* Send to Agent */}
|
|
226
|
+
{hasSelection && (
|
|
227
|
+
<div className="relative flex items-center">
|
|
228
|
+
<Tooltip content={activeAgent ? `Send to ${agent?.name || 'agent'}` : 'Send to agent'} side="top">
|
|
229
|
+
<button
|
|
230
|
+
onClick={handleSendClick}
|
|
231
|
+
disabled={sending}
|
|
232
|
+
className={cn(
|
|
233
|
+
'flex items-center gap-1.5 px-2 py-1 rounded text-xs font-sans cursor-pointer transition-colors mr-1',
|
|
234
|
+
'bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-50',
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
{activeAgent ? (
|
|
238
|
+
<>
|
|
239
|
+
<Send size={11} />
|
|
240
|
+
<span className="text-2xs max-w-[80px] truncate">{agent?.name || 'Agent'}</span>
|
|
241
|
+
</>
|
|
242
|
+
) : (
|
|
243
|
+
<>
|
|
244
|
+
<Sparkles size={11} />
|
|
245
|
+
<span className="text-2xs">Agent</span>
|
|
246
|
+
<ChevronDown size={9} />
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
</button>
|
|
250
|
+
</Tooltip>
|
|
251
|
+
{!activeAgent && hasSelection && (
|
|
252
|
+
<Tooltip content="Pick agent" side="top">
|
|
253
|
+
<button
|
|
254
|
+
onClick={() => setShowPicker(!showPicker)}
|
|
255
|
+
className="p-1 rounded text-text-3 hover:text-accent hover:bg-accent/10 cursor-pointer transition-colors mr-1"
|
|
256
|
+
>
|
|
257
|
+
<ChevronDown size={10} />
|
|
258
|
+
</button>
|
|
259
|
+
</Tooltip>
|
|
260
|
+
)}
|
|
261
|
+
{showPicker && (
|
|
262
|
+
<AgentPicker
|
|
263
|
+
onSelect={(id) => sendToAgent(id)}
|
|
264
|
+
onClose={() => setShowPicker(false)}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
121
270
|
{fullHeight ? (
|
|
122
271
|
<button
|
|
123
272
|
onClick={onMinimize}
|