groove-dev 0.27.140 → 0.27.142

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 (98) 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 +100 -23
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +171 -1
  8. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  9. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +65 -0
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  13. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  15. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +12 -8
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  24. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  25. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +109 -12
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  28. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  29. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  30. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  31. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  32. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  33. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  34. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +151 -3
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  36. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -29
  37. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  38. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  39. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  40. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  41. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  42. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  43. package/package.json +1 -1
  44. package/packages/cli/package.json +1 -1
  45. package/packages/daemon/integrations-registry.json +12 -44
  46. package/packages/daemon/package.json +1 -1
  47. package/packages/daemon/src/api.js +100 -23
  48. package/packages/daemon/src/integrations.js +10 -0
  49. package/packages/daemon/src/introducer.js +1 -1
  50. package/packages/daemon/src/journalist.js +171 -1
  51. package/packages/daemon/src/keeper.js +2 -2
  52. package/packages/daemon/src/memory.js +8 -5
  53. package/packages/daemon/src/model-lab.js +11 -0
  54. package/packages/daemon/src/process.js +65 -0
  55. package/packages/daemon/src/rotator.js +25 -8
  56. package/packages/daemon/src/validate.js +8 -0
  57. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  58. package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
  59. package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
  60. package/packages/gui/dist/index.html +3 -3
  61. package/packages/gui/package.json +1 -1
  62. package/packages/gui/src/app.jsx +0 -2
  63. package/packages/gui/src/components/agents/agent-chat.jsx +3 -4
  64. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  65. package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -8
  66. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  67. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  68. package/packages/gui/src/components/agents/workspace-mode.jsx +109 -12
  69. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  70. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  71. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  72. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  73. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  74. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  75. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  76. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  77. package/packages/gui/src/components/layout/terminal-panel.jsx +151 -3
  78. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  79. package/packages/gui/src/stores/groove.js +107 -29
  80. package/packages/gui/src/views/agents.jsx +114 -56
  81. package/packages/gui/src/views/dashboard.jsx +2 -0
  82. package/packages/gui/src/views/marketplace.jsx +3 -71
  83. package/packages/gui/src/views/memory.jsx +9 -9
  84. package/packages/gui/src/views/model-lab.jsx +1 -6
  85. package/packages/gui/src/views/models.jsx +658 -565
  86. package/plan_files/keeper-manual.md +53 -42
  87. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  88. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  89. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  90. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  91. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  92. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  93. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  94. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  95. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  96. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  97. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  98. package/packages/gui/src/views/toys.jsx +0 -162
@@ -1,7 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useRef, useEffect, useCallback } from 'react';
3
- import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, Decoration, ViewPlugin, gutter, GutterMarker } from '@codemirror/view';
4
- import { EditorState, Compartment, StateField, StateEffect, RangeSet } from '@codemirror/state';
3
+ import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view';
4
+ import { EditorState, Compartment } from '@codemirror/state';
5
5
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
6
6
  import { bracketMatching } from '@codemirror/language';
7
7
  import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
@@ -26,7 +26,6 @@ import {
26
26
  vscodeLight, xcodeLight,
27
27
  } from '@uiw/codemirror-themes-all';
28
28
  import { useGrooveStore } from '../../stores/groove';
29
- import { api } from '../../lib/api';
30
29
 
31
30
  const LANGS = {
32
31
  javascript: () => javascript({ jsx: true, typescript: false }),
@@ -79,52 +78,6 @@ export const EDITOR_THEMES = {
79
78
  basicLight: { label: 'Basic Light', ext: basicLight },
80
79
  };
81
80
 
82
- // ── Git gutter decorations ───────────────────────────────────
83
- const setGitLines = StateEffect.define();
84
-
85
- const gitLinesField = StateField.define({
86
- create() { return { added: new Set(), modified: new Set(), deleted: new Set() }; },
87
- update(value, tr) {
88
- for (const e of tr.effects) {
89
- if (e.is(setGitLines)) return e.value;
90
- }
91
- return value;
92
- },
93
- });
94
-
95
- class GitGutterMarker extends GutterMarker {
96
- constructor(type) { super(); this.type = type; }
97
- toDOM() {
98
- const el = document.createElement('div');
99
- el.style.width = '3px';
100
- el.style.height = '100%';
101
- el.style.borderRadius = '1px';
102
- if (this.type === 'added') el.style.background = 'var(--color-success)';
103
- else if (this.type === 'modified') el.style.background = 'var(--color-warning)';
104
- else el.style.background = 'var(--color-danger)';
105
- return el;
106
- }
107
- }
108
-
109
- const addedMarker = new GitGutterMarker('added');
110
- const modifiedMarker = new GitGutterMarker('modified');
111
- const deletedMarker = new GitGutterMarker('deleted');
112
-
113
- const gitGutter = gutter({
114
- class: 'cm-git-gutter',
115
- markers(view) {
116
- const lines = view.state.field(gitLinesField);
117
- const markers = [];
118
- for (let i = 1; i <= view.state.doc.lines; i++) {
119
- const lineStart = view.state.doc.line(i).from;
120
- if (lines.added.has(i)) markers.push(addedMarker.range(lineStart));
121
- else if (lines.modified.has(i)) markers.push(modifiedMarker.range(lineStart));
122
- else if (lines.deleted.has(i)) markers.push(deletedMarker.range(lineStart));
123
- }
124
- return RangeSet.of(markers);
125
- },
126
- });
127
-
128
81
  const editorChrome = EditorView.theme({
129
82
  '&': { fontFamily: 'var(--font-mono)', fontSize: '12px', height: '100%', lineHeight: '1.6' },
130
83
  '.cm-scroller': { overflow: 'auto', padding: '4px 0' },
@@ -139,8 +92,6 @@ const editorChrome = EditorView.theme({
139
92
  '.cm-search .cm-button, .cm-button': { borderRadius: '4px', padding: '2px 8px', fontSize: '10px', fontFamily: 'var(--font-sans)', cursor: 'pointer', backgroundImage: 'none' },
140
93
  '.cm-search br': { display: 'none' },
141
94
  '.cm-panel.cm-search [name=close]': { cursor: 'pointer', padding: '0 4px' },
142
- '.cm-git-gutter': { width: '4px', marginRight: '2px' },
143
- '.cm-git-gutter .cm-gutterElement': { padding: '0', minWidth: '3px' },
144
95
  });
145
96
 
146
97
  function getThemeExt(key) {
@@ -186,8 +137,6 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
186
137
  const state = EditorState.create({
187
138
  doc: content || '',
188
139
  extensions: [
189
- gitLinesField,
190
- gitGutter,
191
140
  lineNumbers(),
192
141
  highlightActiveLine(),
193
142
  highlightActiveLineGutter(),
@@ -220,21 +169,6 @@ export function CodeEditor({ content, language, onChange, onSave, onCursorChange
220
169
  return () => { view.destroy(); viewRef.current = null; if (externalViewRef) externalViewRef.current = null; };
221
170
  }, []);
222
171
 
223
- // Fetch and apply git line status when filePath changes
224
- useEffect(() => {
225
- const view = viewRef.current;
226
- if (!view || !filePath) return;
227
- api.get(`/files/git-line-status?path=${encodeURIComponent(filePath)}`).then((data) => {
228
- if (!data?.lines) return;
229
- const lines = {
230
- added: new Set(data.lines.added || []),
231
- modified: new Set(data.lines.modified || []),
232
- deleted: new Set(data.lines.deleted || []),
233
- };
234
- view.dispatch({ effects: setGitLines.of(lines) });
235
- }).catch(() => {});
236
- }, [filePath, content]);
237
-
238
172
  useEffect(() => {
239
173
  const view = viewRef.current;
240
174
  if (!view) return;
@@ -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,
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,25 +240,6 @@ 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);
@@ -508,34 +489,6 @@ export function FileTree({ rootDir, onCollapse }) {
508
489
 
509
490
  {/* Tree */}
510
491
  <ScrollArea className="flex-1">
511
- {/* Git Changes section */}
512
- {gitChanges.length > 0 && (
513
- <CollapsibleSection title="Git Changes" icon={GitBranch} count={gitChanges.length} defaultOpen={true}>
514
- <div className="py-0.5">
515
- {gitChanges.map((entry) => {
516
- const name = entry.path.split('/').pop();
517
- const statusColor = entry.status === 'A' || entry.status === '?' ? 'text-success' : entry.status === 'D' ? 'text-danger' : 'text-warning';
518
- return (
519
- <button
520
- key={entry.path}
521
- onClick={() => openFile(entry.path)}
522
- className={cn(
523
- 'w-full flex items-center gap-1.5 px-3 py-[3px] text-xs font-sans cursor-pointer',
524
- 'hover:bg-surface-5 transition-colors text-left',
525
- activeFile === entry.path ? 'bg-accent/10 text-text-0' : 'text-text-1',
526
- )}
527
- >
528
- <File size={12} className={cn('flex-shrink-0', getFileColor(name))} />
529
- <span className="truncate flex-1">{name}</span>
530
- <span className={cn('text-2xs font-mono flex-shrink-0', statusColor)}>{entry.status}</span>
531
- </button>
532
- );
533
- })}
534
- </div>
535
- </CollapsibleSection>
536
- )}
537
-
538
- {/* File Explorer */}
539
492
  <div
540
493
  className="py-1"
541
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 };
@@ -4,18 +4,18 @@ import { useState, useEffect, useRef } from 'react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { Dialog, DialogContent } from '../ui/dialog';
6
6
  import { Button } from '../ui/button';
7
- import { Save, HelpCircle, Sparkles, Link2, FileText } from 'lucide-react';
7
+ import { Save } from 'lucide-react';
8
8
 
9
9
  const COMMANDS = [
10
- { cmd: '[save]', args: '#tag', desc: 'Save the current message as a tagged memory' },
11
- { cmd: '[append]', args: '#tag', desc: 'Add to an existing memory without overwriting' },
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 — agent reads it, 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' },
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
19
  ];
20
20
 
21
21
  function KeeperEditorModal() {
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, Eye, BookOpen } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, 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';
@@ -13,7 +13,6 @@ const BASE_NAV_ITEMS = [
13
13
  { id: 'memory', icon: BookOpen, label: 'Memory' },
14
14
  { id: 'teams', icon: Users, label: 'Teams' },
15
15
  { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
16
- { id: 'toys', icon: Gamepad2, label: 'Toys' },
17
16
  { id: 'models', icon: Box, label: 'Models' },
18
17
  { id: 'model-lab', icon: FlaskConical, label: 'Model Lab' },
19
18
  ];
@@ -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,52 @@ 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
+ className={cn(
232
+ 'flex items-center gap-1.5 px-2 py-1 rounded text-xs font-sans cursor-pointer transition-colors mr-1',
233
+ 'bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-50',
234
+ )}
235
+ >
236
+ {activeAgent ? (
237
+ <>
238
+ <Send size={11} />
239
+ <span className="text-2xs max-w-[80px] truncate">{agent?.name || 'Agent'}</span>
240
+ </>
241
+ ) : (
242
+ <>
243
+ <Sparkles size={11} />
244
+ <span className="text-2xs">Agent</span>
245
+ <ChevronDown size={9} />
246
+ </>
247
+ )}
248
+ </button>
249
+ </Tooltip>
250
+ {!activeAgent && hasSelection && (
251
+ <Tooltip content="Pick agent" side="top">
252
+ <button
253
+ onClick={() => setShowPicker(!showPicker)}
254
+ className="p-1 rounded text-text-3 hover:text-accent hover:bg-accent/10 cursor-pointer transition-colors mr-1"
255
+ >
256
+ <ChevronDown size={10} />
257
+ </button>
258
+ </Tooltip>
259
+ )}
260
+ {showPicker && (
261
+ <AgentPicker
262
+ onSelect={(id) => sendToAgent(id)}
263
+ onClose={() => setShowPicker(false)}
264
+ />
265
+ )}
266
+ </div>
267
+ )}
268
+
121
269
  {fullHeight ? (
122
270
  <button
123
271
  onClick={onMinimize}