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
@@ -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
- Plus, FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
9
- ChevronsDownUp, PanelLeftClose, GitBranch, Activity,
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 key={tab.id} tabId={tab.id} visible={tab.id === activeTab} registerKill={registerKill} />
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, label: 'Agents' },
10
- { id: 'editor', icon: Code2, label: 'Editor' },
11
- { id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
12
- { id: 'teams', icon: Users, label: 'Teams' },
13
- { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
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}