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.
- package/CLAUDE.md +34 -2
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +124 -6
- package/node_modules/@groove-dev/daemon/src/introducer.js +7 -2
- package/node_modules/@groove-dev/daemon/src/process.js +11 -8
- package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +8694 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -3
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +87 -39
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +174 -70
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +199 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +81 -4
- package/node_modules/@groove-dev/gui/src/components/editor/editor-toolbar.jsx +179 -0
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +111 -4
- package/node_modules/@groove-dev/gui/src/components/editor/inline-prompt.jsx +67 -0
- package/node_modules/@groove-dev/gui/src/components/editor/quick-search.jsx +170 -0
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +88 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -9
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +8 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +13 -8
- package/node_modules/@groove-dev/gui/src/stores/groove.js +70 -2
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -7
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +219 -67
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +124 -6
- package/packages/daemon/src/introducer.js +7 -2
- package/packages/daemon/src/process.js +11 -8
- package/packages/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
- package/packages/gui/dist/assets/index-AkOtskHS.css +1 -0
- package/packages/gui/dist/assets/index-B4uYLR57.js +8694 -0
- package/packages/gui/dist/index.html +3 -3
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/code-review.jsx +87 -39
- package/packages/gui/src/components/agents/diff-viewer.jsx +174 -70
- package/packages/gui/src/components/editor/ai-panel.jsx +199 -0
- package/packages/gui/src/components/editor/code-editor.jsx +81 -4
- package/packages/gui/src/components/editor/editor-toolbar.jsx +179 -0
- package/packages/gui/src/components/editor/file-tree.jsx +111 -4
- package/packages/gui/src/components/editor/inline-prompt.jsx +67 -0
- package/packages/gui/src/components/editor/quick-search.jsx +170 -0
- package/packages/gui/src/components/editor/selection-menu.jsx +88 -0
- package/packages/gui/src/components/editor/terminal.jsx +1 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +5 -9
- package/packages/gui/src/components/layout/terminal-panel.jsx +8 -0
- package/packages/gui/src/components/ui/toast.jsx +13 -8
- package/packages/gui/src/stores/groove.js +70 -2
- package/packages/gui/src/views/agents.jsx +7 -7
- package/packages/gui/src/views/editor.jsx +219 -67
- package/node_modules/@groove-dev/gui/dist/assets/index-DcNgRadn.js +0 -8689
- package/node_modules/@groove-dev/gui/dist/assets/index-EY6WfKWH.css +0 -1
- package/packages/gui/dist/assets/index-DcNgRadn.js +0 -8689
- 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
|
|
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,
|
|
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,
|
|
10
|
-
{ id: '
|
|
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,
|
|
14
|
-
{ id: 'marketplace', icon: Puzzle,
|
|
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
|
-
{
|
|
71
|
+
{allActions.length > 0 && allActions.map((act, i) => (
|
|
68
72
|
<button
|
|
73
|
+
key={i}
|
|
69
74
|
onClick={(e) => {
|
|
70
75
|
e.stopPropagation();
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
} else if (
|
|
74
|
-
try { window.open(
|
|
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
|
-
{
|
|
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"
|