groove-dev 0.22.4 → 0.22.5
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/node_modules/@groove-dev/gui/dist/assets/index-DCuYr-nF.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-eDPQ15uG.js +572 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +3 -2
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +296 -22
- package/package.json +1 -1
- package/packages/gui/dist/assets/index-DCuYr-nF.css +1 -0
- package/packages/gui/dist/assets/index-eDPQ15uG.js +572 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/editor/code-editor.jsx +3 -2
- package/packages/gui/src/components/editor/file-tree.jsx +296 -22
- package/node_modules/@groove-dev/gui/dist/assets/index-BDyGhxDd.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BHDZqhzW.js +0 -562
- package/packages/gui/dist/assets/index-BDyGhxDd.css +0 -1
- package/packages/gui/dist/assets/index-BHDZqhzW.js +0 -562
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-eDPQ15uG.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DCuYr-nF.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -26,7 +26,8 @@ const LANGS = {
|
|
|
26
26
|
|
|
27
27
|
// Custom theme overrides to match our design tokens
|
|
28
28
|
const grooveTheme = EditorView.theme({
|
|
29
|
-
'&': { backgroundColor: '#24282f', color: '#bcc2cd', fontFamily: 'var(--font-mono)', fontSize: '13px' },
|
|
29
|
+
'&': { backgroundColor: '#24282f', color: '#bcc2cd', fontFamily: 'var(--font-mono)', fontSize: '13px', height: '100%' },
|
|
30
|
+
'.cm-scroller': { overflow: 'auto' },
|
|
30
31
|
'.cm-content': { caretColor: '#33afbc' },
|
|
31
32
|
'.cm-cursor': { borderLeftColor: '#33afbc' },
|
|
32
33
|
'.cm-gutters': { backgroundColor: '#24282f', borderRight: '1px solid #2c313a', color: '#505862' },
|
|
@@ -103,5 +104,5 @@ export function CodeEditor({ content, language, onChange, onSave }) {
|
|
|
103
104
|
view.dispatch({ effects: langCompartment.current.reconfigure(langExt()) });
|
|
104
105
|
}, [language]);
|
|
105
106
|
|
|
106
|
-
return <div ref={containerRef} className="w-full h-full" />;
|
|
107
|
+
return <div ref={containerRef} className="w-full h-full overflow-hidden" />;
|
|
107
108
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
|
-
import {
|
|
5
|
+
import { api } from '../../lib/api';
|
|
6
|
+
import {
|
|
7
|
+
ChevronRight, ChevronDown, File, Folder, FolderOpen,
|
|
8
|
+
Plus, FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
|
|
9
|
+
} from 'lucide-react';
|
|
6
10
|
import { ScrollArea } from '../ui/scroll-area';
|
|
7
11
|
|
|
8
12
|
const FILE_COLORS = {
|
|
@@ -18,15 +22,100 @@ function getFileColor(name) {
|
|
|
18
22
|
return FILE_COLORS[ext] || 'text-text-3';
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
// ── Context Menu ─────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function ContextMenu({ x, y, items, onClose }) {
|
|
28
|
+
const ref = useRef(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
function handleClick(e) {
|
|
32
|
+
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
33
|
+
}
|
|
34
|
+
document.addEventListener('mousedown', handleClick);
|
|
35
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
36
|
+
}, [onClose]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
ref={ref}
|
|
41
|
+
className="fixed z-50 min-w-[160px] py-1 bg-surface-2 border border-border rounded-lg shadow-xl"
|
|
42
|
+
style={{ left: x, top: y }}
|
|
43
|
+
>
|
|
44
|
+
{items.map((item, i) =>
|
|
45
|
+
item.separator ? (
|
|
46
|
+
<div key={i} className="h-px bg-border-subtle my-1" />
|
|
47
|
+
) : (
|
|
48
|
+
<button
|
|
49
|
+
key={i}
|
|
50
|
+
onClick={() => { item.action(); onClose(); }}
|
|
51
|
+
className={cn(
|
|
52
|
+
'w-full flex items-center gap-2.5 px-3 py-1.5 text-xs font-sans text-left cursor-pointer transition-colors',
|
|
53
|
+
item.danger
|
|
54
|
+
? 'text-danger hover:bg-danger/10'
|
|
55
|
+
: 'text-text-1 hover:bg-surface-5',
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{item.icon && <item.icon size={12} className={item.danger ? 'text-danger' : 'text-text-3'} />}
|
|
59
|
+
{item.label}
|
|
60
|
+
</button>
|
|
61
|
+
)
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Inline Input (for new file/folder/rename) ─────────────────
|
|
68
|
+
|
|
69
|
+
function InlineInput({ defaultValue = '', placeholder, onSubmit, onCancel, depth = 0 }) {
|
|
70
|
+
const [value, setValue] = useState(defaultValue);
|
|
71
|
+
const inputRef = useRef(null);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
inputRef.current?.focus();
|
|
75
|
+
if (defaultValue) inputRef.current?.select();
|
|
76
|
+
}, [defaultValue]);
|
|
77
|
+
|
|
78
|
+
function handleKeyDown(e) {
|
|
79
|
+
if (e.key === 'Enter') {
|
|
80
|
+
const name = value.trim();
|
|
81
|
+
if (name) onSubmit(name);
|
|
82
|
+
}
|
|
83
|
+
if (e.key === 'Escape') onCancel();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex items-center py-0.5" style={{ paddingLeft: depth * 16 + 8 }}>
|
|
88
|
+
<input
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
value={value}
|
|
91
|
+
onChange={(e) => setValue(e.target.value)}
|
|
92
|
+
onKeyDown={handleKeyDown}
|
|
93
|
+
onBlur={onCancel}
|
|
94
|
+
placeholder={placeholder}
|
|
95
|
+
className="w-full h-5 px-1.5 text-xs bg-surface-0 border border-accent rounded text-text-0 font-sans focus:outline-none"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Tree Node ────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expanded, onContextMenu }) {
|
|
22
104
|
const isDir = entry.type === 'dir';
|
|
23
105
|
const isActive = activePath === entry.path;
|
|
24
106
|
const isOpen = expanded.has(entry.path);
|
|
25
107
|
const indent = depth * 16 + 8;
|
|
26
108
|
|
|
109
|
+
function handleContextMenu(e) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
onContextMenu(e, entry);
|
|
113
|
+
}
|
|
114
|
+
|
|
27
115
|
return (
|
|
28
116
|
<button
|
|
29
117
|
onClick={() => isDir ? onDirToggle(entry.path) : onFileClick(entry.path)}
|
|
118
|
+
onContextMenu={handleContextMenu}
|
|
30
119
|
className={cn(
|
|
31
120
|
'w-full flex items-center gap-1.5 py-1 text-xs font-sans cursor-pointer',
|
|
32
121
|
'hover:bg-surface-5 transition-colors text-left select-none',
|
|
@@ -51,7 +140,7 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
|
|
|
51
140
|
);
|
|
52
141
|
}
|
|
53
142
|
|
|
54
|
-
function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir }) {
|
|
143
|
+
function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir, onContextMenu, inlineInput }) {
|
|
55
144
|
const entries = treeCache[dirPath] || [];
|
|
56
145
|
|
|
57
146
|
useEffect(() => {
|
|
@@ -64,16 +153,36 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
64
153
|
|
|
65
154
|
return (
|
|
66
155
|
<>
|
|
156
|
+
{/* Inline input for new file/folder in this directory */}
|
|
157
|
+
{inlineInput?.parentPath === dirPath && (
|
|
158
|
+
<InlineInput
|
|
159
|
+
placeholder={inlineInput.type === 'file' ? 'filename.ext' : 'folder-name'}
|
|
160
|
+
onSubmit={inlineInput.onSubmit}
|
|
161
|
+
onCancel={inlineInput.onCancel}
|
|
162
|
+
depth={depth}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
67
165
|
{entries.map((entry) => (
|
|
68
166
|
<div key={entry.path}>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
167
|
+
{/* Rename input */}
|
|
168
|
+
{inlineInput?.renamePath === entry.path ? (
|
|
169
|
+
<InlineInput
|
|
170
|
+
defaultValue={entry.name}
|
|
171
|
+
onSubmit={inlineInput.onSubmit}
|
|
172
|
+
onCancel={inlineInput.onCancel}
|
|
173
|
+
depth={depth}
|
|
174
|
+
/>
|
|
175
|
+
) : (
|
|
176
|
+
<TreeNode
|
|
177
|
+
entry={entry}
|
|
178
|
+
depth={depth}
|
|
179
|
+
activePath={activePath}
|
|
180
|
+
onFileClick={onFileClick}
|
|
181
|
+
onDirToggle={onDirToggle}
|
|
182
|
+
expanded={expanded}
|
|
183
|
+
onContextMenu={onContextMenu}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
77
186
|
{entry.type === 'dir' && (
|
|
78
187
|
<TreeDir
|
|
79
188
|
dirPath={entry.path}
|
|
@@ -84,6 +193,8 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
84
193
|
onDirToggle={onDirToggle}
|
|
85
194
|
treeCache={treeCache}
|
|
86
195
|
fetchTreeDir={fetchTreeDir}
|
|
196
|
+
onContextMenu={onContextMenu}
|
|
197
|
+
inlineInput={inlineInput}
|
|
87
198
|
/>
|
|
88
199
|
)}
|
|
89
200
|
</div>
|
|
@@ -92,16 +203,20 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
92
203
|
);
|
|
93
204
|
}
|
|
94
205
|
|
|
206
|
+
// ── Main FileTree ────────────────────────────────────────────
|
|
207
|
+
|
|
95
208
|
export function FileTree({ rootDir }) {
|
|
96
209
|
const treeCache = useGrooveStore((s) => s.editorTreeCache);
|
|
97
210
|
const activeFile = useGrooveStore((s) => s.editorActiveFile);
|
|
98
211
|
const openFile = useGrooveStore((s) => s.openFile);
|
|
99
212
|
const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
|
|
213
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
100
214
|
|
|
101
215
|
const [expanded, setExpanded] = useState(new Set(['']));
|
|
102
216
|
const [filter, setFilter] = useState('');
|
|
217
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
218
|
+
const [inlineInput, setInlineInput] = useState(null);
|
|
103
219
|
|
|
104
|
-
// Load root on mount
|
|
105
220
|
useEffect(() => {
|
|
106
221
|
fetchTreeDir('');
|
|
107
222
|
}, [fetchTreeDir, rootDir]);
|
|
@@ -115,10 +230,123 @@ export function FileTree({ rootDir }) {
|
|
|
115
230
|
});
|
|
116
231
|
}
|
|
117
232
|
|
|
233
|
+
function handleContextMenu(e, entry) {
|
|
234
|
+
setContextMenu({ x: e.clientX, y: e.clientY, entry });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Empty area context menu (right-click on blank space)
|
|
238
|
+
function handleRootContextMenu(e) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
setContextMenu({ x: e.clientX, y: e.clientY, entry: { type: 'dir', path: '', name: 'root' } });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parentDir(path) {
|
|
244
|
+
const parts = path.split('/');
|
|
245
|
+
parts.pop();
|
|
246
|
+
return parts.join('/');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function handleNewFile(dirPath) {
|
|
250
|
+
setExpanded((prev) => new Set([...prev, dirPath]));
|
|
251
|
+
setInlineInput({
|
|
252
|
+
type: 'file',
|
|
253
|
+
parentPath: dirPath,
|
|
254
|
+
onSubmit: async (name) => {
|
|
255
|
+
const path = dirPath ? `${dirPath}/${name}` : name;
|
|
256
|
+
try {
|
|
257
|
+
await api.post('/files/create', { path, content: '' });
|
|
258
|
+
fetchTreeDir(dirPath);
|
|
259
|
+
openFile(path);
|
|
260
|
+
addToast('success', `Created ${name}`);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
addToast('error', 'Create failed', err.message);
|
|
263
|
+
}
|
|
264
|
+
setInlineInput(null);
|
|
265
|
+
},
|
|
266
|
+
onCancel: () => setInlineInput(null),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function handleNewFolder(dirPath) {
|
|
271
|
+
setExpanded((prev) => new Set([...prev, dirPath]));
|
|
272
|
+
setInlineInput({
|
|
273
|
+
type: 'folder',
|
|
274
|
+
parentPath: dirPath,
|
|
275
|
+
onSubmit: async (name) => {
|
|
276
|
+
const path = dirPath ? `${dirPath}/${name}` : name;
|
|
277
|
+
try {
|
|
278
|
+
await api.post('/files/mkdir', { path });
|
|
279
|
+
fetchTreeDir(dirPath);
|
|
280
|
+
setExpanded((prev) => new Set([...prev, path]));
|
|
281
|
+
addToast('success', `Created ${name}/`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
addToast('error', 'Create folder failed', err.message);
|
|
284
|
+
}
|
|
285
|
+
setInlineInput(null);
|
|
286
|
+
},
|
|
287
|
+
onCancel: () => setInlineInput(null),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function handleRename(entry) {
|
|
292
|
+
setInlineInput({
|
|
293
|
+
type: 'rename',
|
|
294
|
+
renamePath: entry.path,
|
|
295
|
+
onSubmit: async (newName) => {
|
|
296
|
+
const dir = parentDir(entry.path);
|
|
297
|
+
const newPath = dir ? `${dir}/${newName}` : newName;
|
|
298
|
+
try {
|
|
299
|
+
await api.post('/files/rename', { oldPath: entry.path, newPath });
|
|
300
|
+
fetchTreeDir(dir);
|
|
301
|
+
addToast('success', `Renamed to ${newName}`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
addToast('error', 'Rename failed', err.message);
|
|
304
|
+
}
|
|
305
|
+
setInlineInput(null);
|
|
306
|
+
},
|
|
307
|
+
onCancel: () => setInlineInput(null),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function handleDelete(entry) {
|
|
312
|
+
const label = entry.type === 'dir' ? `folder "${entry.name}" and all contents` : `"${entry.name}"`;
|
|
313
|
+
if (!window.confirm(`Delete ${label}?`)) return;
|
|
314
|
+
try {
|
|
315
|
+
await api.delete(`/files/delete?path=${encodeURIComponent(entry.path)}`);
|
|
316
|
+
fetchTreeDir(parentDir(entry.path));
|
|
317
|
+
addToast('success', `Deleted ${entry.name}`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
addToast('error', 'Delete failed', err.message);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildContextMenuItems(entry) {
|
|
324
|
+
const isDir = entry.type === 'dir';
|
|
325
|
+
const items = [];
|
|
326
|
+
|
|
327
|
+
if (isDir) {
|
|
328
|
+
items.push({ icon: FilePlus, label: 'New File', action: () => handleNewFile(entry.path) });
|
|
329
|
+
items.push({ icon: FolderPlus, label: 'New Folder', action: () => handleNewFolder(entry.path) });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (entry.name !== 'root') {
|
|
333
|
+
if (items.length > 0) items.push({ separator: true });
|
|
334
|
+
items.push({ icon: Pencil, label: 'Rename', action: () => handleRename(entry) });
|
|
335
|
+
items.push({ icon: Trash2, label: 'Delete', danger: true, action: () => handleDelete(entry) });
|
|
336
|
+
} else {
|
|
337
|
+
// Root context — only new file/folder
|
|
338
|
+
items.length = 0;
|
|
339
|
+
items.push({ icon: FilePlus, label: 'New File', action: () => handleNewFile('') });
|
|
340
|
+
items.push({ icon: FolderPlus, label: 'New Folder', action: () => handleNewFolder('') });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return items;
|
|
344
|
+
}
|
|
345
|
+
|
|
118
346
|
const rootEntries = treeCache[''] || [];
|
|
119
347
|
|
|
120
348
|
return (
|
|
121
|
-
<div className="flex flex-col h-full bg-surface-1">
|
|
349
|
+
<div className="flex flex-col h-full bg-surface-1" onContextMenu={handleRootContextMenu}>
|
|
122
350
|
{/* Toolbar */}
|
|
123
351
|
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border-subtle">
|
|
124
352
|
<div className="flex-1 relative">
|
|
@@ -130,9 +358,24 @@ export function FileTree({ rootDir }) {
|
|
|
130
358
|
className="w-full h-6 pl-6 pr-2 text-xs bg-surface-0 border border-border-subtle rounded text-text-1 placeholder:text-text-4 focus:outline-none focus:border-accent font-sans"
|
|
131
359
|
/>
|
|
132
360
|
</div>
|
|
361
|
+
<button
|
|
362
|
+
onClick={() => handleNewFile('')}
|
|
363
|
+
className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
|
|
364
|
+
title="New file"
|
|
365
|
+
>
|
|
366
|
+
<FilePlus size={12} />
|
|
367
|
+
</button>
|
|
368
|
+
<button
|
|
369
|
+
onClick={() => handleNewFolder('')}
|
|
370
|
+
className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
|
|
371
|
+
title="New folder"
|
|
372
|
+
>
|
|
373
|
+
<FolderPlus size={12} />
|
|
374
|
+
</button>
|
|
133
375
|
<button
|
|
134
376
|
onClick={() => fetchTreeDir('')}
|
|
135
377
|
className="p-1 text-text-4 hover:text-text-1 transition-colors cursor-pointer"
|
|
378
|
+
title="Refresh"
|
|
136
379
|
>
|
|
137
380
|
<RefreshCw size={12} />
|
|
138
381
|
</button>
|
|
@@ -141,18 +384,37 @@ export function FileTree({ rootDir }) {
|
|
|
141
384
|
{/* Tree */}
|
|
142
385
|
<ScrollArea className="flex-1">
|
|
143
386
|
<div className="py-1">
|
|
387
|
+
{/* Inline input at root level */}
|
|
388
|
+
{inlineInput?.parentPath === '' && (
|
|
389
|
+
<InlineInput
|
|
390
|
+
placeholder={inlineInput.type === 'file' ? 'filename.ext' : 'folder-name'}
|
|
391
|
+
onSubmit={inlineInput.onSubmit}
|
|
392
|
+
onCancel={inlineInput.onCancel}
|
|
393
|
+
depth={0}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
144
396
|
{rootEntries
|
|
145
397
|
.filter((e) => !filter || e.name.toLowerCase().includes(filter.toLowerCase()))
|
|
146
398
|
.map((entry) => (
|
|
147
399
|
<div key={entry.path}>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
400
|
+
{inlineInput?.renamePath === entry.path ? (
|
|
401
|
+
<InlineInput
|
|
402
|
+
defaultValue={entry.name}
|
|
403
|
+
onSubmit={inlineInput.onSubmit}
|
|
404
|
+
onCancel={inlineInput.onCancel}
|
|
405
|
+
depth={0}
|
|
406
|
+
/>
|
|
407
|
+
) : (
|
|
408
|
+
<TreeNode
|
|
409
|
+
entry={entry}
|
|
410
|
+
depth={0}
|
|
411
|
+
activePath={activeFile}
|
|
412
|
+
onFileClick={openFile}
|
|
413
|
+
onDirToggle={onDirToggle}
|
|
414
|
+
expanded={expanded}
|
|
415
|
+
onContextMenu={handleContextMenu}
|
|
416
|
+
/>
|
|
417
|
+
)}
|
|
156
418
|
{entry.type === 'dir' && (
|
|
157
419
|
<TreeDir
|
|
158
420
|
dirPath={entry.path}
|
|
@@ -163,12 +425,24 @@ export function FileTree({ rootDir }) {
|
|
|
163
425
|
onDirToggle={onDirToggle}
|
|
164
426
|
treeCache={treeCache}
|
|
165
427
|
fetchTreeDir={fetchTreeDir}
|
|
428
|
+
onContextMenu={handleContextMenu}
|
|
429
|
+
inlineInput={inlineInput}
|
|
166
430
|
/>
|
|
167
431
|
)}
|
|
168
432
|
</div>
|
|
169
433
|
))}
|
|
170
434
|
</div>
|
|
171
435
|
</ScrollArea>
|
|
436
|
+
|
|
437
|
+
{/* Context menu */}
|
|
438
|
+
{contextMenu && (
|
|
439
|
+
<ContextMenu
|
|
440
|
+
x={contextMenu.x}
|
|
441
|
+
y={contextMenu.y}
|
|
442
|
+
items={buildContextMenuItems(contextMenu.entry)}
|
|
443
|
+
onClose={() => setContextMenu(null)}
|
|
444
|
+
/>
|
|
445
|
+
)}
|
|
172
446
|
</div>
|
|
173
447
|
);
|
|
174
448
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.5",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|