groove-dev 0.27.138 → 0.27.139

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 (58) hide show
  1. package/CLAUDE.md +34 -2
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +124 -6
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +7 -2
  6. package/node_modules/@groove-dev/daemon/src/process.js +11 -8
  7. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
  8. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +8694 -0
  10. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  11. package/node_modules/@groove-dev/gui/package.json +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +87 -39
  13. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +174 -70
  14. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +199 -0
  15. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +81 -4
  16. package/node_modules/@groove-dev/gui/src/components/editor/editor-toolbar.jsx +179 -0
  17. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +111 -4
  18. package/node_modules/@groove-dev/gui/src/components/editor/inline-prompt.jsx +67 -0
  19. package/node_modules/@groove-dev/gui/src/components/editor/quick-search.jsx +170 -0
  20. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +88 -0
  21. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -0
  22. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -9
  23. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +8 -0
  24. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +13 -8
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +70 -2
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -7
  27. package/node_modules/@groove-dev/gui/src/views/editor.jsx +219 -67
  28. package/package.json +1 -1
  29. package/packages/cli/package.json +1 -1
  30. package/packages/daemon/package.json +1 -1
  31. package/packages/daemon/src/api.js +124 -6
  32. package/packages/daemon/src/introducer.js +7 -2
  33. package/packages/daemon/src/process.js +11 -8
  34. package/packages/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
  35. package/packages/gui/dist/assets/index-AkOtskHS.css +1 -0
  36. package/packages/gui/dist/assets/index-B4uYLR57.js +8694 -0
  37. package/packages/gui/dist/index.html +3 -3
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/code-review.jsx +87 -39
  40. package/packages/gui/src/components/agents/diff-viewer.jsx +174 -70
  41. package/packages/gui/src/components/editor/ai-panel.jsx +199 -0
  42. package/packages/gui/src/components/editor/code-editor.jsx +81 -4
  43. package/packages/gui/src/components/editor/editor-toolbar.jsx +179 -0
  44. package/packages/gui/src/components/editor/file-tree.jsx +111 -4
  45. package/packages/gui/src/components/editor/inline-prompt.jsx +67 -0
  46. package/packages/gui/src/components/editor/quick-search.jsx +170 -0
  47. package/packages/gui/src/components/editor/selection-menu.jsx +88 -0
  48. package/packages/gui/src/components/editor/terminal.jsx +1 -0
  49. package/packages/gui/src/components/layout/activity-bar.jsx +5 -9
  50. package/packages/gui/src/components/layout/terminal-panel.jsx +8 -0
  51. package/packages/gui/src/components/ui/toast.jsx +13 -8
  52. package/packages/gui/src/stores/groove.js +70 -2
  53. package/packages/gui/src/views/agents.jsx +7 -7
  54. package/packages/gui/src/views/editor.jsx +219 -67
  55. package/node_modules/@groove-dev/gui/dist/assets/index-DcNgRadn.js +0 -8689
  56. package/node_modules/@groove-dev/gui/dist/assets/index-EY6WfKWH.css +0 -1
  57. package/packages/gui/dist/assets/index-DcNgRadn.js +0 -8689
  58. package/packages/gui/dist/assets/index-EY6WfKWH.css +0 -1
@@ -6,7 +6,7 @@ import { api } from '../../lib/api';
6
6
  import {
7
7
  ChevronRight, ChevronDown, File, Folder, FolderOpen,
8
8
  Plus, FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
9
- ChevronsDownUp, PanelLeftClose,
9
+ ChevronsDownUp, PanelLeftClose, GitBranch, Activity,
10
10
  } from 'lucide-react';
11
11
  import { ScrollArea } from '../ui/scroll-area';
12
12
 
@@ -101,13 +101,20 @@ function InlineInput({ defaultValue = '', placeholder, onSubmit, onCancel, depth
101
101
 
102
102
  // ── Tree Node ────────────────────────────────────────────────
103
103
 
104
- function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expanded, onContextMenu, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir }) {
104
+ function GitDot({ status }) {
105
+ if (!status) return null;
106
+ const color = status === 'A' || status === '?' ? 'bg-success' : status === 'D' ? 'bg-danger' : 'bg-warning';
107
+ return <span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', color)} />;
108
+ }
109
+
110
+ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expanded, onContextMenu, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir, gitStatusMap }) {
105
111
  const isDir = entry.type === 'dir';
106
112
  const isActive = activePath === entry.path;
107
113
  const isOpen = expanded.has(entry.path);
108
114
  const indent = depth * 16 + 8;
109
115
  const isDragging = dragState?.draggingPath === entry.path;
110
116
  const isDragOver = isDir && dragState?.dragOverPath === entry.path;
117
+ const fileGitStatus = !isDir ? gitStatusMap?.[entry.path] : null;
111
118
 
112
119
  function handleContextMenu(e) {
113
120
  e.preventDefault();
@@ -150,12 +157,13 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
150
157
  <File size={14} className={cn('flex-shrink-0', getFileColor(entry.name))} />
151
158
  </>
152
159
  )}
153
- <span className="truncate">{entry.name}</span>
160
+ <span className="truncate flex-1">{entry.name}</span>
161
+ {fileGitStatus && <GitDot status={fileGitStatus} />}
154
162
  </button>
155
163
  );
156
164
  }
157
165
 
158
- function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir, onContextMenu, inlineInput, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir }) {
166
+ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir, onContextMenu, inlineInput, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir, gitStatusMap }) {
159
167
  const entries = treeCache[dirPath] || [];
160
168
 
161
169
  useEffect(() => {
@@ -201,6 +209,7 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
201
209
  onDragEndEntry={onDragEndEntry}
202
210
  onSetDragOver={onSetDragOver}
203
211
  onDropOnDir={onDropOnDir}
212
+ gitStatusMap={gitStatusMap}
204
213
  />
205
214
  )}
206
215
  {entry.type === 'dir' && (
@@ -220,6 +229,7 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
220
229
  onDragEndEntry={onDragEndEntry}
221
230
  onSetDragOver={onSetDragOver}
222
231
  onDropOnDir={onDropOnDir}
232
+ gitStatusMap={gitStatusMap}
223
233
  />
224
234
  )}
225
235
  </div>
@@ -230,23 +240,64 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
230
240
 
231
241
  // ── Main FileTree ────────────────────────────────────────────
232
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
+
233
262
  export function FileTree({ rootDir, onCollapse }) {
234
263
  const treeCache = useGrooveStore((s) => s.editorTreeCache);
235
264
  const activeFile = useGrooveStore((s) => s.editorActiveFile);
236
265
  const openFile = useGrooveStore((s) => s.openFile);
237
266
  const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
238
267
  const addToast = useGrooveStore((s) => s.addToast);
268
+ const agents = useGrooveStore((s) => s.agents);
269
+ const editorSelectedAgent = useGrooveStore((s) => s.editorSelectedAgent);
239
270
 
240
271
  const [expanded, setExpanded] = useState(new Set(['']));
241
272
  const [filter, setFilter] = useState('');
242
273
  const [contextMenu, setContextMenu] = useState(null);
243
274
  const [inlineInput, setInlineInput] = useState(null);
244
275
  const [dragState, setDragState] = useState({ draggingPath: null, dragOverPath: null });
276
+ const [gitChanges, setGitChanges] = useState([]);
277
+ const [agentFiles, setAgentFiles] = useState([]);
245
278
 
246
279
  useEffect(() => {
247
280
  fetchTreeDir('');
248
281
  }, [fetchTreeDir, rootDir]);
249
282
 
283
+ useEffect(() => {
284
+ api.get('/files/git-status').then((data) => {
285
+ setGitChanges(data.entries || []);
286
+ }).catch(() => setGitChanges([]));
287
+ }, [rootDir]);
288
+
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
+ const gitStatusMap = {};
297
+ for (const entry of gitChanges) {
298
+ gitStatusMap[entry.path] = entry.status;
299
+ }
300
+
250
301
  function onDirToggle(path) {
251
302
  setExpanded((prev) => {
252
303
  const next = new Set(prev);
@@ -468,6 +519,60 @@ export function FileTree({ rootDir, onCollapse }) {
468
519
 
469
520
  {/* Tree */}
470
521
  <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 */}
471
576
  <div
472
577
  className="py-1"
473
578
  onDragOver={(e) => { if (!dragState.draggingPath) return; e.preventDefault(); setDragOverDir(null); }}
@@ -507,6 +612,7 @@ export function FileTree({ rootDir, onCollapse }) {
507
612
  onDragEndEntry={handleDragEndEntry}
508
613
  onSetDragOver={setDragOverDir}
509
614
  onDropOnDir={handleDropOnDir}
615
+ gitStatusMap={gitStatusMap}
510
616
  />
511
617
  )}
512
618
  {entry.type === 'dir' && (
@@ -526,6 +632,7 @@ export function FileTree({ rootDir, onCollapse }) {
526
632
  onDragEndEntry={handleDragEndEntry}
527
633
  onSetDragOver={setDragOverDir}
528
634
  onDropOnDir={handleDropOnDir}
635
+ gitStatusMap={gitStatusMap}
529
636
  />
530
637
  )}
531
638
  </div>
@@ -0,0 +1,67 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useRef, useEffect } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { Sparkles, Send, X } from 'lucide-react';
6
+
7
+ export function InlinePrompt({ line, coords, onClose, filePath }) {
8
+ const [input, setInput] = useState('');
9
+ const [sending, setSending] = useState(false);
10
+ const inputRef = useRef(null);
11
+ const agentId = useGrooveStore((s) => s.editorSelectedAgent);
12
+ const instructAgent = useGrooveStore((s) => s.instructAgent);
13
+
14
+ useEffect(() => {
15
+ setTimeout(() => inputRef.current?.focus(), 50);
16
+ }, []);
17
+
18
+ async function handleSend() {
19
+ const text = input.trim();
20
+ if (!text || sending || !agentId) return;
21
+ setSending(true);
22
+ const fileName = filePath?.split('/').pop() || 'file';
23
+ const prompt = `[Inline prompt at ${fileName}:${line}] ${text}`;
24
+ try {
25
+ await instructAgent(agentId, prompt);
26
+ } catch { /* toast handles */ }
27
+ setSending(false);
28
+ onClose();
29
+ }
30
+
31
+ function handleKeyDown(e) {
32
+ if (e.key === 'Escape') { e.preventDefault(); onClose(); }
33
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); }
34
+ }
35
+
36
+ const top = coords?.top ? Math.min(coords.top + 24, window.innerHeight - 60) : 200;
37
+
38
+ return (
39
+ <div
40
+ className="absolute left-8 right-8 z-30 flex items-center gap-2 px-3 py-2 bg-surface-2 border border-accent/30 rounded-lg shadow-lg"
41
+ style={{ top }}
42
+ >
43
+ <Sparkles size={14} className="text-accent flex-shrink-0" />
44
+ <input
45
+ ref={inputRef}
46
+ value={input}
47
+ onChange={(e) => setInput(e.target.value)}
48
+ onKeyDown={handleKeyDown}
49
+ placeholder={agentId ? 'Ask AI to edit at this line...' : 'Select an agent first'}
50
+ disabled={!agentId}
51
+ className="flex-1 bg-transparent text-xs text-text-0 font-sans placeholder:text-text-4 focus:outline-none disabled:opacity-50"
52
+ />
53
+ {input.trim() && (
54
+ <button
55
+ onClick={handleSend}
56
+ disabled={sending || !agentId}
57
+ className="p-1 text-accent hover:text-accent/80 cursor-pointer disabled:opacity-50"
58
+ >
59
+ <Send size={12} />
60
+ </button>
61
+ )}
62
+ <button onClick={onClose} className="p-1 text-text-4 hover:text-text-1 cursor-pointer">
63
+ <X size={12} />
64
+ </button>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,170 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { File, Folder, Search, X } from 'lucide-react';
6
+ import { api } from '../../lib/api';
7
+
8
+ const FILE_COLORS = {
9
+ js: 'text-text-2', jsx: 'text-text-2', ts: 'text-text-2', tsx: 'text-text-2',
10
+ css: 'text-text-3', html: 'text-text-3', json: 'text-text-3',
11
+ md: 'text-text-3', py: 'text-text-2', rs: 'text-text-3',
12
+ };
13
+
14
+ function getFileColor(name) {
15
+ const ext = name.split('.').pop()?.toLowerCase();
16
+ return FILE_COLORS[ext] || 'text-text-3';
17
+ }
18
+
19
+ export function QuickSearch() {
20
+ const open = useGrooveStore((s) => s.editorQuickSearchOpen);
21
+ const setOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
22
+ const openFile = useGrooveStore((s) => s.openFile);
23
+ const setActiveView = (v) => useGrooveStore.setState({ activeView: v });
24
+
25
+ const [query, setQuery] = useState('');
26
+ const [results, setResults] = useState([]);
27
+ const [loading, setLoading] = useState(false);
28
+ const [selectedIndex, setSelectedIndex] = useState(0);
29
+ const inputRef = useRef(null);
30
+ const debounceRef = useRef(null);
31
+
32
+ useEffect(() => {
33
+ if (open) {
34
+ setQuery('');
35
+ setResults([]);
36
+ setSelectedIndex(0);
37
+ setTimeout(() => inputRef.current?.focus(), 50);
38
+ }
39
+ }, [open]);
40
+
41
+ useEffect(() => {
42
+ function handleKey(e) {
43
+ if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
44
+ e.preventDefault();
45
+ setOpen(!open);
46
+ }
47
+ }
48
+ document.addEventListener('keydown', handleKey);
49
+ return () => document.removeEventListener('keydown', handleKey);
50
+ }, [open, setOpen]);
51
+
52
+ const search = useCallback(async (q) => {
53
+ if (!q.trim()) { setResults([]); return; }
54
+ setLoading(true);
55
+ try {
56
+ const data = await api.get(`/files/search?q=${encodeURIComponent(q)}`);
57
+ setResults(data.results || data.files || []);
58
+ setSelectedIndex(0);
59
+ } catch {
60
+ setResults([]);
61
+ }
62
+ setLoading(false);
63
+ }, []);
64
+
65
+ function handleChange(e) {
66
+ const val = e.target.value;
67
+ setQuery(val);
68
+ clearTimeout(debounceRef.current);
69
+ debounceRef.current = setTimeout(() => search(val), 200);
70
+ }
71
+
72
+ function handleSelect(path) {
73
+ setOpen(false);
74
+ const state = useGrooveStore.getState();
75
+ if (state.activeView !== 'editor') setActiveView('editor');
76
+ openFile(path);
77
+ }
78
+
79
+ function handleKeyDown(e) {
80
+ if (e.key === 'Escape') {
81
+ e.preventDefault();
82
+ setOpen(false);
83
+ }
84
+ if (e.key === 'ArrowDown') {
85
+ e.preventDefault();
86
+ setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
87
+ }
88
+ if (e.key === 'ArrowUp') {
89
+ e.preventDefault();
90
+ setSelectedIndex((i) => Math.max(i - 1, 0));
91
+ }
92
+ if (e.key === 'Enter' && results.length > 0) {
93
+ e.preventDefault();
94
+ const item = results[selectedIndex];
95
+ if (item) handleSelect(item.path || item);
96
+ }
97
+ }
98
+
99
+ if (!open) return null;
100
+
101
+ return (
102
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]" onClick={() => setOpen(false)}>
103
+ <div
104
+ className="w-[520px] bg-surface-2 border border-border rounded-xl shadow-2xl overflow-hidden"
105
+ onClick={(e) => e.stopPropagation()}
106
+ >
107
+ {/* Search input */}
108
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-border-subtle">
109
+ <Search size={14} className="text-text-4 flex-shrink-0" />
110
+ <input
111
+ ref={inputRef}
112
+ value={query}
113
+ onChange={handleChange}
114
+ onKeyDown={handleKeyDown}
115
+ placeholder="Search files by name..."
116
+ className="flex-1 bg-transparent text-sm text-text-0 font-sans placeholder:text-text-4 focus:outline-none"
117
+ />
118
+ {query && (
119
+ <button onClick={() => { setQuery(''); setResults([]); }} className="text-text-4 hover:text-text-1 cursor-pointer">
120
+ <X size={12} />
121
+ </button>
122
+ )}
123
+ </div>
124
+
125
+ {/* Results */}
126
+ <div className="max-h-[320px] overflow-y-auto">
127
+ {loading && (
128
+ <div className="px-4 py-6 text-center text-xs text-text-4 font-sans">Searching...</div>
129
+ )}
130
+ {!loading && query && results.length === 0 && (
131
+ <div className="px-4 py-6 text-center text-xs text-text-4 font-sans">No files found</div>
132
+ )}
133
+ {!loading && results.map((item, i) => {
134
+ const path = item.path || item;
135
+ const name = path.split('/').pop();
136
+ const dir = path.split('/').slice(0, -1).join('/');
137
+ const isDir = item.type === 'dir';
138
+
139
+ return (
140
+ <button
141
+ key={path}
142
+ onClick={() => handleSelect(path)}
143
+ className={cn(
144
+ 'w-full flex items-center gap-2.5 px-4 py-2 text-left cursor-pointer transition-colors',
145
+ i === selectedIndex ? 'bg-accent/10' : 'hover:bg-surface-4',
146
+ )}
147
+ >
148
+ {isDir
149
+ ? <Folder size={14} className="text-accent flex-shrink-0" />
150
+ : <File size={14} className={cn('flex-shrink-0', getFileColor(name))} />
151
+ }
152
+ <div className="flex-1 min-w-0">
153
+ <span className="text-xs font-sans text-text-0">{name}</span>
154
+ {dir && <span className="text-2xs text-text-4 font-sans ml-2 truncate">{dir}</span>}
155
+ </div>
156
+ </button>
157
+ );
158
+ })}
159
+ </div>
160
+
161
+ {/* Footer */}
162
+ <div className="flex items-center gap-3 px-4 py-2 border-t border-border-subtle text-2xs text-text-4 font-sans">
163
+ <span><kbd className="px-1 py-0.5 rounded bg-surface-4 text-text-3 font-mono">↑↓</kbd> navigate</span>
164
+ <span><kbd className="px-1 py-0.5 rounded bg-surface-4 text-text-3 font-mono">↵</kbd> open</span>
165
+ <span><kbd className="px-1 py-0.5 rounded bg-surface-4 text-text-3 font-mono">esc</kbd> close</span>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,88 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useEffect, useRef } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { Bot, BookOpen, Wrench, Bug, TestTube2 } from 'lucide-react';
6
+
7
+ const ACTIONS = [
8
+ { id: 'ask', label: 'Ask Agent', icon: Bot, instruction: 'Analyze this code and answer questions about it' },
9
+ { id: 'explain', label: 'Explain Code', icon: BookOpen, instruction: 'Explain what this code does in detail' },
10
+ { id: 'refactor', label: 'Refactor', icon: Wrench, instruction: 'Refactor this code for better readability and maintainability' },
11
+ { id: 'fix', label: 'Fix Bug', icon: Bug, instruction: 'Find and fix any bugs in this code' },
12
+ { id: 'test', label: 'Generate Tests', icon: TestTube2, instruction: 'Generate comprehensive tests for this code' },
13
+ ];
14
+
15
+ export function SelectionMenu({ x, y, filePath, lineStart, lineEnd, selectedCode, onClose }) {
16
+ const ref = useRef(null);
17
+ const agentId = useGrooveStore((s) => s.editorSelectedAgent);
18
+ const sendCodeToAgent = useGrooveStore((s) => s.sendCodeToAgent);
19
+ const toggleAiPanel = useGrooveStore((s) => s.toggleAiPanel);
20
+ const aiPanelOpen = useGrooveStore((s) => s.editorAiPanelOpen);
21
+
22
+ useEffect(() => {
23
+ function handleClick(e) {
24
+ if (ref.current && !ref.current.contains(e.target)) onClose();
25
+ }
26
+ function handleKey(e) {
27
+ if (e.key === 'Escape') onClose();
28
+ }
29
+ document.addEventListener('mousedown', handleClick);
30
+ document.addEventListener('keydown', handleKey);
31
+ return () => {
32
+ document.removeEventListener('mousedown', handleClick);
33
+ document.removeEventListener('keydown', handleKey);
34
+ };
35
+ }, [onClose]);
36
+
37
+ // Viewport boundary correction
38
+ useEffect(() => {
39
+ if (!ref.current) return;
40
+ const rect = ref.current.getBoundingClientRect();
41
+ const el = ref.current;
42
+ if (rect.right > window.innerWidth - 8) {
43
+ el.style.left = `${window.innerWidth - rect.width - 8}px`;
44
+ }
45
+ if (rect.bottom > window.innerHeight - 8) {
46
+ el.style.top = `${window.innerHeight - rect.height - 8}px`;
47
+ }
48
+ });
49
+
50
+ function handleAction(action) {
51
+ if (!agentId) return;
52
+ sendCodeToAgent(agentId, action.instruction, filePath, lineStart, lineEnd, selectedCode);
53
+ if (!aiPanelOpen) toggleAiPanel();
54
+ onClose();
55
+ }
56
+
57
+ if (!agentId) {
58
+ return (
59
+ <div
60
+ ref={ref}
61
+ className="fixed z-50 py-2 px-3 bg-surface-2 border border-border rounded-lg shadow-xl"
62
+ style={{ left: x, top: y }}
63
+ >
64
+ <p className="text-2xs text-text-4 font-sans">Select an agent in the toolbar to use AI features</p>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div
71
+ ref={ref}
72
+ className="fixed z-50 min-w-[180px] py-1 bg-surface-2 border border-border rounded-lg shadow-xl"
73
+ style={{ left: x, top: y }}
74
+ >
75
+ <div className="px-3 py-1 text-2xs text-text-4 font-sans font-medium">AI Actions</div>
76
+ {ACTIONS.map((action) => (
77
+ <button
78
+ key={action.id}
79
+ onClick={() => handleAction(action)}
80
+ className="w-full flex items-center gap-2.5 px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-5 cursor-pointer transition-colors text-left"
81
+ >
82
+ <action.icon size={12} className="text-accent flex-shrink-0" />
83
+ {action.label}
84
+ </button>
85
+ ))}
86
+ </div>
87
+ );
88
+ }
@@ -216,6 +216,7 @@ export function TerminalManager() {
216
216
  onRenameTab={renameTab}
217
217
  onToggleFullHeight={() => setFullHeight(true)}
218
218
  onMinimize={() => setFullHeight(false)}
219
+ onClose={() => setTerminalVisible(false)}
219
220
  >
220
221
  {tabs.map((tab) => (
221
222
  <TerminalInstance key={tab.id} tabId={tab.id} visible={tab.id === activeTab} registerKill={registerKill} />
@@ -1,20 +1,16 @@
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 } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Users, Newspaper, Settings, Globe, Eye } 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: 'chat', icon: MessageCircle, label: 'Chat' },
11
- { id: 'editor', icon: Code2, label: 'Editor' },
9
+ { id: 'agents', icon: Network, label: 'Agents' },
10
+ { id: 'editor', icon: Code2, label: 'Editor' },
12
11
  { id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
13
- { id: 'teams', icon: Users, label: 'Teams' },
14
- { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
15
- { id: 'toys', icon: Gamepad2, label: 'Toys' },
16
- { id: 'models', icon: Box, label: 'Models' },
17
- { id: 'model-lab', icon: FlaskConical, label: 'Model Lab' },
12
+ { id: 'teams', icon: Users, label: 'Teams' },
13
+ { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
18
14
  ];
19
15
 
20
16
  const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };
@@ -16,6 +16,7 @@ export function TerminalPanel({
16
16
  onCloseTab,
17
17
  onToggleFullHeight,
18
18
  onMinimize,
19
+ onClose,
19
20
  onRenameTab,
20
21
  }) {
21
22
  const dragging = useRef(false);
@@ -134,6 +135,13 @@ export function TerminalPanel({
134
135
  <Maximize2 size={12} />
135
136
  </button>
136
137
  )}
138
+ <button
139
+ onClick={onClose}
140
+ className="p-1.5 rounded text-text-3 hover:text-text-0 hover:bg-surface-5 cursor-pointer transition-colors"
141
+ title="Close terminal"
142
+ >
143
+ <X size={12} />
144
+ </button>
137
145
  </div>
138
146
  </div>
139
147
 
@@ -37,6 +37,10 @@ function ToastItem({ toast }) {
37
37
  const removeToast = useGrooveStore((s) => s.removeToast);
38
38
  const Icon = ICONS[toast.type] || Info;
39
39
  const duration = toast.persistent ? 0 : (toast.duration ?? DURATIONS[toast.type]);
40
+ const allActions = [
41
+ ...(toast.actions || []),
42
+ ...(toast.action?.url || toast.action?.onClick ? [toast.action] : []),
43
+ ];
40
44
 
41
45
  useEffect(() => {
42
46
  if (!duration) return;
@@ -52,7 +56,7 @@ function ToastItem({ toast }) {
52
56
  exit={{ opacity: 0, x: 80, scale: 0.95 }}
53
57
  transition={{ duration: 0.2 }}
54
58
  className={cn(
55
- 'w-80 border border-border bg-surface-1 shadow-xl',
59
+ 'min-w-80 max-w-md border border-border bg-surface-1 shadow-xl',
56
60
  'border-l-2 flex items-center gap-3 px-4 py-3',
57
61
  BORDER_COLORS[toast.type],
58
62
  )}
@@ -64,22 +68,23 @@ function ToastItem({ toast }) {
64
68
  <p className="text-xs text-text-3 font-sans mt-0.5">{toast.detail}</p>
65
69
  )}
66
70
  </div>
67
- {(toast.action?.url || toast.action?.onClick) && (
71
+ {allActions.length > 0 && allActions.map((act, i) => (
68
72
  <button
73
+ key={i}
69
74
  onClick={(e) => {
70
75
  e.stopPropagation();
71
- if (toast.action.onClick) {
72
- toast.action.onClick();
73
- } else if (toast.action.url) {
74
- try { window.open(toast.action.url, '_blank', 'noopener'); } catch {}
76
+ if (act.onClick) {
77
+ act.onClick();
78
+ } else if (act.url) {
79
+ try { window.open(act.url, '_blank', 'noopener'); } catch {}
75
80
  }
76
81
  removeToast(toast.id);
77
82
  }}
78
83
  className="text-xs font-medium text-accent hover:text-accent-hover bg-surface-5 hover:bg-surface-6 px-3 py-1.5 rounded transition-colors cursor-pointer flex-shrink-0 whitespace-nowrap"
79
84
  >
80
- {toast.action.label || 'Open'}
85
+ {act.label || 'Open'}
81
86
  </button>
82
- )}
87
+ ))}
83
88
  <button
84
89
  onClick={(e) => { e.stopPropagation(); removeToast(toast.id); }}
85
90
  className="p-1.5 text-text-4 hover:text-text-1 hover:bg-surface-5 rounded transition-colors cursor-pointer flex-shrink-0 z-10"