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.
Files changed (75) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +225 -16
  5. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  6. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  9. package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
  20. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  22. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  23. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
  24. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  25. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
  30. package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
  31. package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
  32. package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
  33. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  34. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/integrations-registry.json +12 -44
  38. package/packages/daemon/package.json +1 -1
  39. package/packages/daemon/src/api.js +225 -16
  40. package/packages/daemon/src/index.js +2 -0
  41. package/packages/daemon/src/integrations.js +10 -0
  42. package/packages/daemon/src/introducer.js +17 -1
  43. package/packages/daemon/src/journalist.js +169 -0
  44. package/packages/daemon/src/keeper.js +277 -0
  45. package/packages/daemon/src/model-lab.js +11 -0
  46. package/packages/daemon/src/process.js +76 -0
  47. package/packages/daemon/src/validate.js +9 -0
  48. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  49. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  50. package/packages/gui/dist/index.html +2 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/app.jsx +4 -0
  53. package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
  55. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  56. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  57. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  58. package/packages/gui/src/components/editor/file-tree.jsx +2 -86
  59. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  60. package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
  61. package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  63. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  64. package/packages/gui/src/stores/groove.js +302 -20
  65. package/packages/gui/src/views/agents.jsx +118 -60
  66. package/packages/gui/src/views/editor.jsx +67 -219
  67. package/packages/gui/src/views/memory.jsx +460 -0
  68. package/packages/gui/src/views/model-lab.jsx +1 -6
  69. package/packages/gui/src/views/models.jsx +658 -565
  70. package/plan_files/keeper-manual.md +295 -0
  71. package/plan_files/keeper-memory-system.md +223 -0
  72. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
  73. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
  74. package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
  75. package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
@@ -0,0 +1,460 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { useGrooveStore } from '../stores/groove';
4
+ import { Button } from '../components/ui/button';
5
+ import { ScrollArea } from '../components/ui/scroll-area';
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';
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 formatRelative(iso) {
22
+ if (!iso) return '';
23
+ const diff = Date.now() - new Date(iso).getTime();
24
+ const mins = Math.floor(diff / 60000);
25
+ if (mins < 1) return 'just now';
26
+ if (mins < 60) return `${mins}m ago`;
27
+ const hrs = Math.floor(mins / 60);
28
+ if (hrs < 24) return `${hrs}h ago`;
29
+ const days = Math.floor(hrs / 24);
30
+ if (days < 30) return `${days}d ago`;
31
+ return new Date(iso).toLocaleDateString();
32
+ }
33
+
34
+ function MemoryCard({ item, onEdit, onDelete }) {
35
+ const [preview, setPreview] = useState(null);
36
+ const getKeeperItem = useGrooveStore((s) => s.getKeeperItem);
37
+
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+ getKeeperItem(item.tag).then((data) => {
41
+ if (!cancelled && data) setPreview(data.content);
42
+ });
43
+ return () => { cancelled = true; };
44
+ }, [item.tag, item.updatedAt]);
45
+
46
+ const parts = item.tag.split('/');
47
+ const displayName = parts[parts.length - 1];
48
+ const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : null;
49
+ const lines = (preview || '').split('\n').filter(Boolean);
50
+ const previewText = lines.slice(0, 3).join('\n');
51
+ const isDoc = item.type === 'doc';
52
+ const hasLinks = item.links?.length > 0;
53
+
54
+ return (
55
+ <div className="group p-3 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all">
56
+ <div className="flex items-start justify-between gap-2">
57
+ <div className="flex-1 min-w-0">
58
+ <div className="flex items-center gap-2 mb-1">
59
+ {parentPath && (
60
+ <span className="text-2xs text-text-4 font-mono">{parentPath}/</span>
61
+ )}
62
+ <span className="text-sm font-semibold text-text-0">{displayName}</span>
63
+ {isDoc && (
64
+ <span className="inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-2xs font-medium bg-purple/10 text-purple">
65
+ <Sparkles size={8} />AI
66
+ </span>
67
+ )}
68
+ {hasLinks && (
69
+ <span className="inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-2xs font-medium bg-info/10 text-info">
70
+ <Link2 size={8} />{item.links.length}
71
+ </span>
72
+ )}
73
+ </div>
74
+ {previewText && (
75
+ <p className="text-xs text-text-3 line-clamp-3 whitespace-pre-wrap font-mono leading-relaxed">{previewText}</p>
76
+ )}
77
+ {hasLinks && (
78
+ <div className="flex items-center gap-1.5 mt-1.5">
79
+ {item.links.map((link) => (
80
+ <span key={link} className="inline-flex items-center gap-0.5 text-2xs text-info">
81
+ <FileText size={9} />{link.split('/').pop()}
82
+ </span>
83
+ ))}
84
+ </div>
85
+ )}
86
+ <div className="flex items-center gap-3 mt-2">
87
+ <span className="flex items-center gap-1 text-2xs text-text-4">
88
+ <Clock size={10} />
89
+ {formatRelative(item.updatedAt)}
90
+ </span>
91
+ {item.size != null && (
92
+ <span className="text-2xs text-text-4">
93
+ {item.size > 1024 ? `${(item.size / 1024).toFixed(1)}KB` : `${item.size}B`}
94
+ </span>
95
+ )}
96
+ </div>
97
+ </div>
98
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
99
+ <button
100
+ onClick={() => onEdit(item)}
101
+ className="p-1.5 rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
102
+ >
103
+ <Pencil size={13} />
104
+ </button>
105
+ <button
106
+ onClick={() => onDelete(item.tag)}
107
+ className="p-1.5 rounded-md text-text-3 hover:text-danger hover:bg-danger/10 transition-colors cursor-pointer"
108
+ >
109
+ <Trash2 size={13} />
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ function EditorModal({ open, onOpenChange, editing, onSave }) {
118
+ const [tag, setTag] = useState('');
119
+ const [content, setContent] = useState('');
120
+ const textareaRef = useRef(null);
121
+
122
+ useEffect(() => {
123
+ if (editing) {
124
+ setTag(editing.tag || '');
125
+ setContent(editing.content || '');
126
+ }
127
+ }, [editing]);
128
+
129
+ useEffect(() => {
130
+ if (open && textareaRef.current) {
131
+ setTimeout(() => textareaRef.current?.focus(), 100);
132
+ }
133
+ }, [open]);
134
+
135
+ const handleSave = () => {
136
+ if (!tag.trim() || editing?.readOnly) return;
137
+ onSave(tag.trim(), content);
138
+ onOpenChange(false);
139
+ };
140
+
141
+ const handleKeyDown = (e) => {
142
+ if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
143
+ e.preventDefault();
144
+ handleSave();
145
+ }
146
+ };
147
+
148
+ const isNew = editing?.isNew;
149
+ const readOnly = editing?.readOnly;
150
+ const title = readOnly ? `#${editing?.tag || ''}` : isNew ? 'New Memory' : `Edit #${editing?.tag || ''}`;
151
+
152
+ return (
153
+ <Dialog open={open} onOpenChange={onOpenChange}>
154
+ <DialogContent title={title} description="Memory content" className="max-w-2xl">
155
+ <div className="p-5 space-y-4" onKeyDown={handleKeyDown}>
156
+ {isNew && (
157
+ <div>
158
+ <label className="block text-xs font-medium text-text-2 mb-1.5">Tag</label>
159
+ <div className="flex items-center gap-1">
160
+ <span className="text-sm text-text-3">#</span>
161
+ <input
162
+ type="text"
163
+ value={tag}
164
+ onChange={(e) => setTag(e.target.value.replace(/[^a-zA-Z0-9/_-]/g, '').toLowerCase())}
165
+ placeholder="project/feature-name"
166
+ 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"
167
+ />
168
+ </div>
169
+ <p className="text-2xs text-text-4 mt-1">Use / for hierarchy: groove/memory-system</p>
170
+ </div>
171
+ )}
172
+ <div>
173
+ {!readOnly && <label className="block text-xs font-medium text-text-2 mb-1.5">Content</label>}
174
+ <textarea
175
+ ref={textareaRef}
176
+ value={content}
177
+ onChange={(e) => !readOnly && setContent(e.target.value)}
178
+ readOnly={readOnly}
179
+ rows={16}
180
+ 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"
181
+ placeholder="Write your thoughts, ideas, context..."
182
+ />
183
+ </div>
184
+ <div className="flex items-center justify-between pt-1">
185
+ {readOnly ? (
186
+ <p className="text-2xs text-text-4">Read-only view</p>
187
+ ) : (
188
+ <p className="text-2xs text-text-4">
189
+ {navigator.platform?.includes('Mac') ? 'Cmd' : 'Ctrl'}+S to save
190
+ </p>
191
+ )}
192
+ <div className="flex items-center gap-2">
193
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
194
+ {readOnly ? 'Close' : 'Cancel'}
195
+ </Button>
196
+ {!readOnly && (
197
+ <Button variant="primary" size="sm" onClick={handleSave} disabled={!tag.trim()}>
198
+ <Save size={14} />
199
+ Save
200
+ </Button>
201
+ )}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </DialogContent>
206
+ </Dialog>
207
+ );
208
+ }
209
+
210
+ function InstructModal({ open, onOpenChange }) {
211
+ return (
212
+ <Dialog open={open} onOpenChange={onOpenChange}>
213
+ <DialogContent title="Keeper Commands" description="Memory system command reference" className="max-w-lg">
214
+ <div className="p-5 space-y-4">
215
+ <p className="text-xs text-text-2 leading-relaxed">
216
+ Type these commands in any agent chat to manage your tagged memories. Commands are intercepted by the Keeper sidecar — the agent never sees them.
217
+ </p>
218
+ <div className="space-y-1.5">
219
+ {COMMANDS.map((c) => (
220
+ <div key={c.cmd} className="flex items-start gap-3 py-1.5 border-b border-border-subtle last:border-0">
221
+ <div className="flex-shrink-0 flex items-center gap-1">
222
+ <span className="px-1.5 py-0.5 rounded bg-accent/15 text-accent font-mono text-xs font-semibold">{c.cmd}</span>
223
+ {c.args && <span className="text-xs text-text-3 font-mono">{c.args}</span>}
224
+ </div>
225
+ <span className="text-xs text-text-3 pt-0.5">{c.desc}</span>
226
+ </div>
227
+ ))}
228
+ </div>
229
+ <div className="pt-2 space-y-2">
230
+ <h3 className="text-xs font-semibold text-text-1">Tag Hierarchy</h3>
231
+ <p className="text-xs text-text-3 leading-relaxed">
232
+ Use <span className="font-mono text-accent">/</span> to create nested tags: <span className="font-mono text-accent">#groove/memory-system</span> lives under <span className="font-mono text-accent">#groove</span>. Pull a parent tag to get all children.
233
+ </p>
234
+ <h3 className="text-xs font-semibold text-text-1">Memory Types</h3>
235
+ <p className="text-xs text-text-3 leading-relaxed">
236
+ <span className="font-semibold">Manual</span> — you write it via [save], [append], [update]. <span className="font-semibold">Doc</span> — AI writes it via [doc], synthesizing your conversation. Both are fully editable.
237
+ </p>
238
+ <h3 className="text-xs font-semibold text-text-1">Spawning with Context</h3>
239
+ <p className="text-xs text-text-3 leading-relaxed">
240
+ When spawning a new agent, use [pull] #tag to inject memories into their context. You can also link memories to NORTHSTAR docs with [link] for cross-referencing.
241
+ </p>
242
+ </div>
243
+ <div className="flex justify-end pt-1">
244
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Close</Button>
245
+ </div>
246
+ </div>
247
+ </DialogContent>
248
+ </Dialog>
249
+ );
250
+ }
251
+
252
+ function TreeGroup({ node, onSelect }) {
253
+ const [expanded, setExpanded] = useState(true);
254
+ const hasChildren = node.children && node.children.length > 0;
255
+
256
+ return (
257
+ <div>
258
+ <button
259
+ onClick={() => hasChildren ? setExpanded(!expanded) : onSelect(node)}
260
+ 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"
261
+ >
262
+ {hasChildren ? (
263
+ <ChevronRight size={12} className={`transition-transform ${expanded ? 'rotate-90' : ''}`} />
264
+ ) : (
265
+ <Hash size={12} className="text-text-4" />
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
+ )}
272
+ </button>
273
+ {expanded && hasChildren && (
274
+ <div className="ml-4 mt-0.5 space-y-0.5">
275
+ {node.children.map((child) => (
276
+ <button
277
+ key={child.tag}
278
+ onClick={() => onSelect(child)}
279
+ className="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-text-3 hover:text-text-1 hover:bg-surface-2 transition-colors cursor-pointer"
280
+ >
281
+ <Hash size={10} className="text-text-4" />
282
+ <span>{child.tag.split('/').pop()}</span>
283
+ </button>
284
+ ))}
285
+ </div>
286
+ )}
287
+ </div>
288
+ );
289
+ }
290
+
291
+ export default function MemoryView() {
292
+ const keeperItems = useGrooveStore((s) => s.keeperItems);
293
+ const keeperTree = useGrooveStore((s) => s.keeperTree);
294
+ const keeperEditing = useGrooveStore((s) => s.keeperEditing);
295
+ const keeperInstructOpen = useGrooveStore((s) => s.keeperInstructOpen);
296
+ const fetchKeeperItems = useGrooveStore((s) => s.fetchKeeperItems);
297
+ const saveKeeperItem = useGrooveStore((s) => s.saveKeeperItem);
298
+ const updateKeeperItem = useGrooveStore((s) => s.updateKeeperItem);
299
+ const deleteKeeperItem = useGrooveStore((s) => s.deleteKeeperItem);
300
+ const getKeeperItem = useGrooveStore((s) => s.getKeeperItem);
301
+ const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
302
+
303
+ const [search, setSearch] = useState('');
304
+ const [viewMode, setViewMode] = useState('list');
305
+ const [editorOpen, setEditorOpen] = useState(false);
306
+
307
+ useEffect(() => { fetchKeeperItems(); }, []);
308
+
309
+ useEffect(() => {
310
+ if (keeperEditing) setEditorOpen(true);
311
+ }, [keeperEditing]);
312
+
313
+ const filtered = search
314
+ ? keeperItems.filter((item) => item.tag.includes(search.toLowerCase()))
315
+ : keeperItems;
316
+
317
+ const handleNew = () => {
318
+ setKeeperEditing({ tag: '', content: '', isNew: true });
319
+ setEditorOpen(true);
320
+ };
321
+
322
+ const handleEdit = async (item) => {
323
+ const full = await getKeeperItem(item.tag);
324
+ setKeeperEditing({ tag: item.tag, content: full?.content || '', isNew: false });
325
+ setEditorOpen(true);
326
+ };
327
+
328
+ const handleSave = async (tag, content) => {
329
+ if (keeperEditing?.isNew) {
330
+ await saveKeeperItem(tag, content);
331
+ } else {
332
+ await updateKeeperItem(tag, content);
333
+ }
334
+ setKeeperEditing(null);
335
+ };
336
+
337
+ const handleEditorClose = (open) => {
338
+ setEditorOpen(open);
339
+ if (!open) setKeeperEditing(null);
340
+ };
341
+
342
+ const handleTreeSelect = async (node) => {
343
+ if (node.virtual) return;
344
+ await handleEdit(node);
345
+ };
346
+
347
+ return (
348
+ <div className="flex-1 flex flex-col overflow-hidden">
349
+ {/* Header */}
350
+ <div className="flex-shrink-0 px-4 py-3 border-b border-border">
351
+ <div className="flex items-center justify-between gap-3 mb-3">
352
+ <h1 className="text-sm font-semibold text-text-0 flex items-center gap-2">
353
+ <BookOpen size={16} />
354
+ Memory
355
+ </h1>
356
+ <div className="flex items-center gap-2">
357
+ <button
358
+ onClick={() => useGrooveStore.setState({ keeperInstructOpen: true })}
359
+ className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
360
+ title="Command reference"
361
+ >
362
+ <HelpCircle size={14} />
363
+ </button>
364
+ <div className="flex items-center rounded-md border border-border overflow-hidden">
365
+ <button
366
+ onClick={() => setViewMode('list')}
367
+ className={`px-2 py-1 text-2xs font-medium transition-colors cursor-pointer ${viewMode === 'list' ? 'bg-accent/10 text-accent' : 'text-text-3 hover:text-text-1'}`}
368
+ >
369
+ List
370
+ </button>
371
+ <button
372
+ onClick={() => setViewMode('tree')}
373
+ className={`px-2 py-1 text-2xs font-medium transition-colors cursor-pointer ${viewMode === 'tree' ? 'bg-accent/10 text-accent' : 'text-text-3 hover:text-text-1'}`}
374
+ >
375
+ Tree
376
+ </button>
377
+ </div>
378
+ <Button variant="primary" size="sm" onClick={handleNew}>
379
+ <Plus size={14} />
380
+ New
381
+ </Button>
382
+ </div>
383
+ </div>
384
+ <div className="relative">
385
+ <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
386
+ <input
387
+ type="text"
388
+ value={search}
389
+ onChange={(e) => setSearch(e.target.value)}
390
+ placeholder="Search memories..."
391
+ className="w-full pl-8 pr-3 py-1.5 text-xs rounded-md bg-surface-0 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
392
+ />
393
+ </div>
394
+ </div>
395
+
396
+ {/* Content */}
397
+ <ScrollArea className="flex-1">
398
+ {keeperItems.length === 0 ? (
399
+ <div className="flex flex-col items-center justify-center h-64 gap-3">
400
+ <BookOpen size={32} className="text-text-4" />
401
+ <p className="text-sm text-text-3">No memories yet</p>
402
+ <p className="text-xs text-text-4 max-w-xs text-center">
403
+ Save ideas, context, and decisions with tagged memories. Type <span className="font-mono text-accent">[save] #tag</span> in any agent chat, or click New above.
404
+ </p>
405
+ <div className="flex items-center gap-2">
406
+ <Button variant="secondary" size="sm" onClick={handleNew}>
407
+ <Plus size={14} />
408
+ Create first memory
409
+ </Button>
410
+ <Button variant="ghost" size="sm" onClick={() => useGrooveStore.setState({ keeperInstructOpen: true })}>
411
+ <HelpCircle size={14} />
412
+ Commands
413
+ </Button>
414
+ </div>
415
+ </div>
416
+ ) : viewMode === 'tree' ? (
417
+ <div className="p-3 space-y-1">
418
+ {keeperTree.map((node) => (
419
+ <TreeGroup key={node.tag} node={node} onSelect={handleTreeSelect} />
420
+ ))}
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
+ </div>
436
+ ) : (
437
+ <div className="p-3 space-y-2">
438
+ {filtered.map((item) => (
439
+ <MemoryCard key={item.tag} item={item} onEdit={handleEdit} onDelete={(tag) => deleteKeeperItem(tag)} />
440
+ ))}
441
+ </div>
442
+ )}
443
+ </ScrollArea>
444
+
445
+ {/* Editor Modal (responds to both view clicks and chat commands) */}
446
+ <EditorModal
447
+ open={editorOpen}
448
+ onOpenChange={handleEditorClose}
449
+ editing={keeperEditing}
450
+ onSave={handleSave}
451
+ />
452
+
453
+ {/* Instruct Modal (command reference) */}
454
+ <InstructModal
455
+ open={keeperInstructOpen}
456
+ onOpenChange={(open) => useGrooveStore.setState({ keeperInstructOpen: open })}
457
+ />
458
+ </div>
459
+ );
460
+ }
@@ -171,17 +171,12 @@ export default function ModelLabView() {
171
171
  <PanelToggle collapsed={false} onClick={() => setLeftCollapsed(true)} side="left" />
172
172
  </div>
173
173
  <ScrollArea className="flex-1 min-h-0">
174
- <div className="px-4 pb-4 space-y-5">
174
+ <div className="px-4 pb-4 space-y-5 divide-y divide-border-subtle [&>*]:pt-5 [&>*:first-child]:pt-0">
175
175
  <LaunchModel />
176
- <div className="h-px bg-border-subtle" />
177
176
  <RuntimeConfig />
178
- <div className="h-px bg-border-subtle" />
179
177
  <ModelSelector />
180
- <div className="h-px bg-border-subtle" />
181
178
  <ParameterPanel />
182
- <div className="h-px bg-border-subtle" />
183
179
  <PresetManager />
184
- <div className="h-px bg-border-subtle" />
185
180
  <SystemPromptEditor />
186
181
  </div>
187
182
  </ScrollArea>