groove-dev 0.27.117 → 0.27.119
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/TRAINING_DATA_v4.md +6 -6
- package/moe-training/client/domain-tagger.js +3 -3
- package/moe-training/client/trajectory-capture.js +7 -0
- package/moe-training/client/transmission-queue.js +6 -0
- package/moe-training/test/shared/envelope-schema.test.js +3 -3
- 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 +13 -4
- package/node_modules/@groove-dev/daemon/src/index.js +4 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +70 -39
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
- package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
- package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -5
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +4 -0
- package/node_modules/moe-training/client/domain-tagger.js +3 -3
- package/node_modules/moe-training/client/trajectory-capture.js +7 -0
- package/node_modules/moe-training/client/transmission-queue.js +6 -0
- package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
- 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 +13 -4
- package/packages/daemon/src/index.js +4 -0
- package/packages/daemon/src/teams.js +70 -39
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
- package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/packages/gui/src/components/editor/file-tree.jsx +80 -3
- package/packages/gui/src/components/settings/quick-connect.jsx +29 -7
- package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
- package/packages/gui/src/components/ui/data-sharing-modal.jsx +151 -0
- package/packages/gui/src/stores/groove.js +29 -5
- package/packages/gui/src/views/agents.jsx +47 -11
- package/packages/gui/src/views/teams.jsx +4 -0
|
@@ -7,6 +7,7 @@ import { AppShell } from './components/layout/app-shell';
|
|
|
7
7
|
import { SetupWizard } from './components/onboarding/setup-wizard';
|
|
8
8
|
import { useKeyboard } from './lib/hooks/use-keyboard';
|
|
9
9
|
import { UpgradeModal } from './components/pro/upgrade-modal';
|
|
10
|
+
import { DataSharingModal } from './components/ui/data-sharing-modal';
|
|
10
11
|
import { WelcomeSplash } from './components/layout/welcome-splash';
|
|
11
12
|
import { FolderBrowser } from './components/agents/folder-browser';
|
|
12
13
|
|
|
@@ -182,6 +183,7 @@ export default function App() {
|
|
|
182
183
|
<ErrorBoundary>
|
|
183
184
|
<ViewRouter />
|
|
184
185
|
<UpgradeModal />
|
|
186
|
+
<DataSharingModal />
|
|
185
187
|
</ErrorBoundary>
|
|
186
188
|
);
|
|
187
189
|
}
|
|
@@ -106,10 +106,12 @@ function ContextMenu({ x, y, items, onClose }) {
|
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextMenu }) {
|
|
109
|
+
function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextMenu, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir }) {
|
|
110
110
|
const isDir = entry.type === 'dir';
|
|
111
111
|
const isExpanded = expandedDirs.has(entry.path);
|
|
112
112
|
const fileColor = isDir ? 'text-accent' : getFileColor(entry.name);
|
|
113
|
+
const isDragging = dragState?.draggingPath === entry.path;
|
|
114
|
+
const isDragOver = isDir && dragState?.dragOverPath === entry.path;
|
|
113
115
|
|
|
114
116
|
function handleCtxMenu(e) {
|
|
115
117
|
e.preventDefault();
|
|
@@ -120,12 +122,23 @@ function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextM
|
|
|
120
122
|
return (
|
|
121
123
|
<>
|
|
122
124
|
<button
|
|
125
|
+
draggable
|
|
126
|
+
onDragStart={(e) => {
|
|
127
|
+
e.dataTransfer.setData('application/json', JSON.stringify({ path: entry.path, name: entry.name, type: entry.type }));
|
|
128
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
129
|
+
onDragStartEntry(entry.path);
|
|
130
|
+
}}
|
|
131
|
+
onDragEnd={onDragEndEntry}
|
|
132
|
+
onDragOver={isDir ? (e) => { e.preventDefault(); e.stopPropagation(); onSetDragOver(entry.path); } : undefined}
|
|
133
|
+
onDrop={isDir ? (e) => onDropOnDir(entry.path, e) : undefined}
|
|
123
134
|
onClick={() => isDir ? onToggleDir(entry.path) : onOpen(entry.path)}
|
|
124
135
|
onDoubleClick={handleCtxMenu}
|
|
125
136
|
onContextMenu={handleCtxMenu}
|
|
126
137
|
className={cn(
|
|
127
138
|
'w-full flex items-center gap-1.5 py-1 text-xs font-sans cursor-pointer',
|
|
128
139
|
'hover:bg-surface-4/50 transition-colors text-left',
|
|
140
|
+
isDragging && 'opacity-50',
|
|
141
|
+
isDragOver && 'bg-accent/15 ring-1 ring-accent/50 rounded',
|
|
129
142
|
)}
|
|
130
143
|
style={{ paddingLeft: depth * 14 + 8 }}
|
|
131
144
|
>
|
|
@@ -151,6 +164,11 @@ function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir, onContextM
|
|
|
151
164
|
expandedDirs={expandedDirs}
|
|
152
165
|
onToggleDir={onToggleDir}
|
|
153
166
|
onContextMenu={onContextMenu}
|
|
167
|
+
dragState={dragState}
|
|
168
|
+
onDragStartEntry={onDragStartEntry}
|
|
169
|
+
onDragEndEntry={onDragEndEntry}
|
|
170
|
+
onSetDragOver={onSetDragOver}
|
|
171
|
+
onDropOnDir={onDropOnDir}
|
|
154
172
|
/>
|
|
155
173
|
))}
|
|
156
174
|
</>
|
|
@@ -176,6 +194,7 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
176
194
|
const [touchedFiles, setTouchedFiles] = useState([]);
|
|
177
195
|
const [inlineInput, setInlineInput] = useState(null);
|
|
178
196
|
const [contextMenu, setContextMenu] = useState(null);
|
|
197
|
+
const [dragState, setDragState] = useState({ draggingPath: null, dragOverPath: null });
|
|
179
198
|
const fetchedRef = useRef(new Set());
|
|
180
199
|
|
|
181
200
|
useEffect(() => {
|
|
@@ -302,6 +321,44 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
302
321
|
setExpandedDirs(new Set());
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
function handleDragStartEntry(path) {
|
|
325
|
+
setDragState({ draggingPath: path, dragOverPath: null });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function handleDragEndEntry() {
|
|
329
|
+
setDragState({ draggingPath: null, dragOverPath: null });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function setDragOverDir(path) {
|
|
333
|
+
setDragState(prev => prev.dragOverPath === path ? prev : { ...prev, dragOverPath: path });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handleDropOnDir(targetDirPath, e) {
|
|
337
|
+
e.preventDefault();
|
|
338
|
+
e.stopPropagation();
|
|
339
|
+
setDragState({ draggingPath: null, dragOverPath: null });
|
|
340
|
+
|
|
341
|
+
let data;
|
|
342
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
343
|
+
if (!data?.path) return;
|
|
344
|
+
|
|
345
|
+
if (data.type === 'dir' && (targetDirPath === data.path || targetDirPath.startsWith(data.path + '/'))) {
|
|
346
|
+
addToast('error', 'Cannot move a folder into itself');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const sourceDir = parentDir(data.path);
|
|
350
|
+
if (sourceDir === targetDirPath) return;
|
|
351
|
+
|
|
352
|
+
const newPath = targetDirPath ? `${targetDirPath}/${data.name}` : data.name;
|
|
353
|
+
try {
|
|
354
|
+
await api.post('/files/rename', { oldPath: data.path, newPath });
|
|
355
|
+
addToast('success', `Moved ${data.name} to ${targetDirPath || '/'}`);
|
|
356
|
+
handleRefresh();
|
|
357
|
+
} catch (err) {
|
|
358
|
+
addToast('error', 'Move failed', err.message);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
305
362
|
function toRelativePath(absPath) {
|
|
306
363
|
if (!absPath || !absPath.startsWith('/')) return absPath;
|
|
307
364
|
if (workingDir && absPath.startsWith(workingDir + '/')) {
|
|
@@ -484,7 +541,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
484
541
|
No files in scope
|
|
485
542
|
</div>
|
|
486
543
|
) : (
|
|
487
|
-
<div
|
|
544
|
+
<div
|
|
545
|
+
className="px-1"
|
|
546
|
+
onDragOver={(e) => { if (!dragState.draggingPath) return; e.preventDefault(); setDragOverDir(null); }}
|
|
547
|
+
onDrop={(e) => handleDropOnDir('', e)}
|
|
548
|
+
>
|
|
488
549
|
<div className="flex items-center gap-1.5 px-2 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider">
|
|
489
550
|
<Folder size={10} />
|
|
490
551
|
Scope
|
|
@@ -507,6 +568,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
507
568
|
expandedDirs={expandedDirs}
|
|
508
569
|
onToggleDir={handleToggleDir}
|
|
509
570
|
onContextMenu={handleContextMenu}
|
|
571
|
+
dragState={dragState}
|
|
572
|
+
onDragStartEntry={handleDragStartEntry}
|
|
573
|
+
onDragEndEntry={handleDragEndEntry}
|
|
574
|
+
onSetDragOver={setDragOverDir}
|
|
575
|
+
onDropOnDir={handleDropOnDir}
|
|
510
576
|
/>
|
|
511
577
|
)
|
|
512
578
|
))}
|
|
@@ -101,11 +101,13 @@ 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 }) {
|
|
104
|
+
function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expanded, onContextMenu, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir }) {
|
|
105
105
|
const isDir = entry.type === 'dir';
|
|
106
106
|
const isActive = activePath === entry.path;
|
|
107
107
|
const isOpen = expanded.has(entry.path);
|
|
108
108
|
const indent = depth * 16 + 8;
|
|
109
|
+
const isDragging = dragState?.draggingPath === entry.path;
|
|
110
|
+
const isDragOver = isDir && dragState?.dragOverPath === entry.path;
|
|
109
111
|
|
|
110
112
|
function handleContextMenu(e) {
|
|
111
113
|
e.preventDefault();
|
|
@@ -115,6 +117,15 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
|
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<button
|
|
120
|
+
draggable
|
|
121
|
+
onDragStart={(e) => {
|
|
122
|
+
e.dataTransfer.setData('application/json', JSON.stringify({ path: entry.path, name: entry.name, type: entry.type }));
|
|
123
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
124
|
+
onDragStartEntry(entry.path);
|
|
125
|
+
}}
|
|
126
|
+
onDragEnd={onDragEndEntry}
|
|
127
|
+
onDragOver={isDir ? (e) => { e.preventDefault(); e.stopPropagation(); onSetDragOver(entry.path); } : undefined}
|
|
128
|
+
onDrop={isDir ? (e) => onDropOnDir(entry.path, e) : undefined}
|
|
118
129
|
onClick={() => isDir ? onDirToggle(entry.path) : onFileClick(entry.path)}
|
|
119
130
|
onDoubleClick={handleContextMenu}
|
|
120
131
|
onContextMenu={handleContextMenu}
|
|
@@ -123,6 +134,8 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
|
|
|
123
134
|
'hover:bg-surface-5 transition-colors text-left select-none',
|
|
124
135
|
isActive && 'bg-accent/10 text-text-0',
|
|
125
136
|
!isActive && 'text-text-1',
|
|
137
|
+
isDragging && 'opacity-50',
|
|
138
|
+
isDragOver && 'bg-accent/15 ring-1 ring-accent/50 rounded',
|
|
126
139
|
)}
|
|
127
140
|
style={{ paddingLeft: indent }}
|
|
128
141
|
>
|
|
@@ -142,7 +155,7 @@ function TreeNode({ entry, depth = 0, activePath, onFileClick, onDirToggle, expa
|
|
|
142
155
|
);
|
|
143
156
|
}
|
|
144
157
|
|
|
145
|
-
function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir, onContextMenu, inlineInput }) {
|
|
158
|
+
function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggle, treeCache, fetchTreeDir, onContextMenu, inlineInput, dragState, onDragStartEntry, onDragEndEntry, onSetDragOver, onDropOnDir }) {
|
|
146
159
|
const entries = treeCache[dirPath] || [];
|
|
147
160
|
|
|
148
161
|
useEffect(() => {
|
|
@@ -183,6 +196,11 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
183
196
|
onDirToggle={onDirToggle}
|
|
184
197
|
expanded={expanded}
|
|
185
198
|
onContextMenu={onContextMenu}
|
|
199
|
+
dragState={dragState}
|
|
200
|
+
onDragStartEntry={onDragStartEntry}
|
|
201
|
+
onDragEndEntry={onDragEndEntry}
|
|
202
|
+
onSetDragOver={onSetDragOver}
|
|
203
|
+
onDropOnDir={onDropOnDir}
|
|
186
204
|
/>
|
|
187
205
|
)}
|
|
188
206
|
{entry.type === 'dir' && (
|
|
@@ -197,6 +215,11 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
197
215
|
fetchTreeDir={fetchTreeDir}
|
|
198
216
|
onContextMenu={onContextMenu}
|
|
199
217
|
inlineInput={inlineInput}
|
|
218
|
+
dragState={dragState}
|
|
219
|
+
onDragStartEntry={onDragStartEntry}
|
|
220
|
+
onDragEndEntry={onDragEndEntry}
|
|
221
|
+
onSetDragOver={onSetDragOver}
|
|
222
|
+
onDropOnDir={onDropOnDir}
|
|
200
223
|
/>
|
|
201
224
|
)}
|
|
202
225
|
</div>
|
|
@@ -218,6 +241,7 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
218
241
|
const [filter, setFilter] = useState('');
|
|
219
242
|
const [contextMenu, setContextMenu] = useState(null);
|
|
220
243
|
const [inlineInput, setInlineInput] = useState(null);
|
|
244
|
+
const [dragState, setDragState] = useState({ draggingPath: null, dragOverPath: null });
|
|
221
245
|
|
|
222
246
|
useEffect(() => {
|
|
223
247
|
fetchTreeDir('');
|
|
@@ -236,6 +260,45 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
236
260
|
setExpanded(new Set(['']));
|
|
237
261
|
}
|
|
238
262
|
|
|
263
|
+
function handleDragStartEntry(path) {
|
|
264
|
+
setDragState({ draggingPath: path, dragOverPath: null });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handleDragEndEntry() {
|
|
268
|
+
setDragState({ draggingPath: null, dragOverPath: null });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function setDragOverDir(path) {
|
|
272
|
+
setDragState(prev => prev.dragOverPath === path ? prev : { ...prev, dragOverPath: path });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function handleDropOnDir(targetDirPath, e) {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
setDragState({ draggingPath: null, dragOverPath: null });
|
|
279
|
+
|
|
280
|
+
let data;
|
|
281
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
282
|
+
if (!data?.path) return;
|
|
283
|
+
|
|
284
|
+
if (data.type === 'dir' && (targetDirPath === data.path || targetDirPath.startsWith(data.path + '/'))) {
|
|
285
|
+
addToast('error', 'Cannot move a folder into itself');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const sourceDir = parentDir(data.path);
|
|
289
|
+
if (sourceDir === targetDirPath) return;
|
|
290
|
+
|
|
291
|
+
const newPath = targetDirPath ? `${targetDirPath}/${data.name}` : data.name;
|
|
292
|
+
try {
|
|
293
|
+
await api.post('/files/rename', { oldPath: data.path, newPath });
|
|
294
|
+
fetchTreeDir(sourceDir);
|
|
295
|
+
fetchTreeDir(targetDirPath);
|
|
296
|
+
addToast('success', `Moved ${data.name} to ${targetDirPath || '/'}`);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
addToast('error', 'Move failed', err.message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
239
302
|
function handleContextMenu(e, entry) {
|
|
240
303
|
setContextMenu({ x: e.clientX, y: e.clientY, entry });
|
|
241
304
|
}
|
|
@@ -405,7 +468,11 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
405
468
|
|
|
406
469
|
{/* Tree */}
|
|
407
470
|
<ScrollArea className="flex-1">
|
|
408
|
-
<div
|
|
471
|
+
<div
|
|
472
|
+
className="py-1"
|
|
473
|
+
onDragOver={(e) => { if (!dragState.draggingPath) return; e.preventDefault(); setDragOverDir(null); }}
|
|
474
|
+
onDrop={(e) => handleDropOnDir('', e)}
|
|
475
|
+
>
|
|
409
476
|
{/* Inline input at root level */}
|
|
410
477
|
{inlineInput?.parentPath === '' && (
|
|
411
478
|
<InlineInput
|
|
@@ -435,6 +502,11 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
435
502
|
onDirToggle={onDirToggle}
|
|
436
503
|
expanded={expanded}
|
|
437
504
|
onContextMenu={handleContextMenu}
|
|
505
|
+
dragState={dragState}
|
|
506
|
+
onDragStartEntry={handleDragStartEntry}
|
|
507
|
+
onDragEndEntry={handleDragEndEntry}
|
|
508
|
+
onSetDragOver={setDragOverDir}
|
|
509
|
+
onDropOnDir={handleDropOnDir}
|
|
438
510
|
/>
|
|
439
511
|
)}
|
|
440
512
|
{entry.type === 'dir' && (
|
|
@@ -449,6 +521,11 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
449
521
|
fetchTreeDir={fetchTreeDir}
|
|
450
522
|
onContextMenu={handleContextMenu}
|
|
451
523
|
inlineInput={inlineInput}
|
|
524
|
+
dragState={dragState}
|
|
525
|
+
onDragStartEntry={handleDragStartEntry}
|
|
526
|
+
onDragEndEntry={handleDragEndEntry}
|
|
527
|
+
onSetDragOver={setDragOverDir}
|
|
528
|
+
onDropOnDir={handleDropOnDir}
|
|
452
529
|
/>
|
|
453
530
|
)}
|
|
454
531
|
</div>
|
|
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
6
6
|
import {
|
|
7
|
-
Server, Radio, ExternalLink, Loader2, X, Plus, ArrowLeft, Unplug, ArrowUpCircle,
|
|
7
|
+
Server, Radio, ExternalLink, Loader2, X, Plus, ArrowLeft, Unplug, ArrowUpCircle, Trash2,
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { StatusDot } from '../ui/status-dot';
|
|
10
10
|
import { Button } from '../ui/button';
|
|
@@ -202,14 +202,36 @@ export function QuickConnect() {
|
|
|
202
202
|
>
|
|
203
203
|
<Unplug size={12} />
|
|
204
204
|
</button>
|
|
205
|
+
<button
|
|
206
|
+
onClick={(e) => {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
useGrooveStore.getState().deleteTunnel(server.id);
|
|
209
|
+
}}
|
|
210
|
+
className="p-1 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
211
|
+
title="Delete connection"
|
|
212
|
+
>
|
|
213
|
+
<Trash2 size={12} />
|
|
214
|
+
</button>
|
|
205
215
|
</>
|
|
206
216
|
) : (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
<>
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => handleConnect(server.id)}
|
|
220
|
+
className="text-2xs text-text-3 font-sans hover:text-text-1 cursor-pointer transition-colors"
|
|
221
|
+
>
|
|
222
|
+
Connect
|
|
223
|
+
</button>
|
|
224
|
+
<button
|
|
225
|
+
onClick={(e) => {
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
useGrooveStore.getState().deleteTunnel(server.id);
|
|
228
|
+
}}
|
|
229
|
+
className="p-1 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
230
|
+
title="Delete connection"
|
|
231
|
+
>
|
|
232
|
+
<Trash2 size={12} />
|
|
233
|
+
</button>
|
|
234
|
+
</>
|
|
213
235
|
)}
|
|
214
236
|
</div>
|
|
215
237
|
</div>
|
|
@@ -4,7 +4,7 @@ import { Dialog, DialogContent } from '../ui/dialog';
|
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
5
|
import { Archive, Trash2, AlertTriangle } from 'lucide-react';
|
|
6
6
|
|
|
7
|
-
export function TeamRemovalDialog({ team, open, onOpenChange, onArchive, onDeletePermanently }) {
|
|
7
|
+
export function TeamRemovalDialog({ team, open, onOpenChange, onArchive, onDeletePermanently, mode }) {
|
|
8
8
|
const [confirmName, setConfirmName] = useState('');
|
|
9
9
|
const [showConfirmInput, setShowConfirmInput] = useState(false);
|
|
10
10
|
|
|
@@ -36,7 +36,9 @@ export function TeamRemovalDialog({ team, open, onOpenChange, onArchive, onDelet
|
|
|
36
36
|
<div className="min-w-0 flex-1">
|
|
37
37
|
<div className="text-sm font-semibold text-text-0 font-sans">Archive</div>
|
|
38
38
|
<p className="text-xs text-text-3 font-sans mt-0.5">
|
|
39
|
-
|
|
39
|
+
{mode === 'production'
|
|
40
|
+
? 'Team metadata will be archived. Your files remain in the project directory.'
|
|
41
|
+
: 'Files are preserved. You can restore the team later.'}
|
|
40
42
|
</p>
|
|
41
43
|
</div>
|
|
42
44
|
</button>
|
|
@@ -53,7 +55,9 @@ export function TeamRemovalDialog({ team, open, onOpenChange, onArchive, onDelet
|
|
|
53
55
|
<div className="min-w-0 flex-1">
|
|
54
56
|
<div className="text-sm font-semibold text-danger font-sans">Delete Permanently</div>
|
|
55
57
|
<p className="text-xs text-text-3 font-sans mt-0.5">
|
|
56
|
-
|
|
58
|
+
{mode === 'production'
|
|
59
|
+
? 'Team will be removed. Your files remain in the project directory.'
|
|
60
|
+
: 'All files in this team will be permanently deleted.'}
|
|
57
61
|
</p>
|
|
58
62
|
</div>
|
|
59
63
|
</button>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Dialog, DialogContent } from './dialog';
|
|
4
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
5
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
6
|
+
import { Sparkles, Share2, Cpu, Gift, Shield, Check, X } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export function DataSharingModal() {
|
|
9
|
+
const open = useGrooveStore((s) => s.dataSharingModalOpen);
|
|
10
|
+
const setTrainingOptIn = useGrooveStore((s) => s.setTrainingOptIn);
|
|
11
|
+
const dismissDataSharingModal = useGrooveStore((s) => s.dismissDataSharingModal);
|
|
12
|
+
const [dontShowAgain, setDontShowAgain] = useState(false);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Dialog open={open}>
|
|
16
|
+
<DialogContent
|
|
17
|
+
className="max-w-2xl"
|
|
18
|
+
description="Review how your data helps build open source AI"
|
|
19
|
+
onInteractOutside={(e) => e.preventDefault()}
|
|
20
|
+
onEscapeKeyDown={(e) => e.preventDefault()}
|
|
21
|
+
>
|
|
22
|
+
{/* Hero */}
|
|
23
|
+
<div className="relative bg-gradient-to-br from-accent/5 to-transparent px-6 pt-8 pb-6 text-center">
|
|
24
|
+
<div className="flex justify-center mb-3">
|
|
25
|
+
<div className="w-14 h-14 rounded-xl bg-accent/10 flex items-center justify-center">
|
|
26
|
+
<Sparkles size={32} className="text-accent" />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<DialogPrimitive.Title className="text-xl font-bold text-text-0 font-sans">
|
|
30
|
+
Help Build Open Source Intelligence
|
|
31
|
+
</DialogPrimitive.Title>
|
|
32
|
+
<p className="text-sm text-text-2 font-sans mt-2 max-w-md mx-auto">
|
|
33
|
+
Your usage data trains a free, local MoE model that every Groove user gets to use — including you.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* Value Proposition */}
|
|
38
|
+
<div className="px-6 pt-5 pb-1">
|
|
39
|
+
<div className="grid grid-cols-3 gap-3">
|
|
40
|
+
{[
|
|
41
|
+
{ icon: Share2, title: 'You Share', desc: 'Anonymized agent session data: tool calls, error patterns, task flows' },
|
|
42
|
+
{ icon: Cpu, title: 'We Train', desc: 'A Groove-specific Mixture of Experts model built on real multi-agent workflows' },
|
|
43
|
+
{ icon: Gift, title: 'Everyone Wins', desc: 'Free, local, open source model for all Groove users. More data = smarter agents' },
|
|
44
|
+
].map(({ icon: Icon, title, desc }) => (
|
|
45
|
+
<div key={title} className="rounded-lg border border-border-subtle bg-surface-2/50 p-3 text-center">
|
|
46
|
+
<div className="flex justify-center mb-2">
|
|
47
|
+
<Icon size={18} className="text-accent" />
|
|
48
|
+
</div>
|
|
49
|
+
<div className="text-xs font-semibold text-text-0 font-sans mb-1">{title}</div>
|
|
50
|
+
<div className="text-2xs text-text-2 font-sans leading-relaxed">{desc}</div>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* What We Collect */}
|
|
57
|
+
<div className="px-6 pt-4">
|
|
58
|
+
<div className="text-xs font-semibold uppercase text-text-3 tracking-wider font-sans mb-2.5">What We Collect</div>
|
|
59
|
+
<div className="space-y-1.5">
|
|
60
|
+
{[
|
|
61
|
+
'Agent tool calling patterns',
|
|
62
|
+
'Error and recovery sequences',
|
|
63
|
+
'Task complexity and coordination events',
|
|
64
|
+
'Model and provider usage metadata',
|
|
65
|
+
'Session duration and outcomes',
|
|
66
|
+
].map((item) => (
|
|
67
|
+
<div key={item} className="flex items-center gap-2">
|
|
68
|
+
<Check size={12} className="text-accent flex-shrink-0" />
|
|
69
|
+
<span className="text-xs text-text-1 font-sans">{item}</span>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
<p className="text-xs text-text-0 font-sans font-medium mt-3">
|
|
74
|
+
That's it. Groove orchestration data only.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* What We Never Collect */}
|
|
79
|
+
<div className="px-6 pt-4">
|
|
80
|
+
<div className="rounded-lg border border-border-subtle bg-surface-2/30 p-4">
|
|
81
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
82
|
+
<Shield size={14} className="text-text-2" />
|
|
83
|
+
<span className="text-xs font-semibold uppercase text-text-3 tracking-wider font-sans">What We Never Collect</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="space-y-1.5">
|
|
86
|
+
{[
|
|
87
|
+
'Your source code or file contents',
|
|
88
|
+
'API keys, passwords, or credentials',
|
|
89
|
+
'Personal information — emails, names, file paths',
|
|
90
|
+
'Anything that could identify your IP or projects',
|
|
91
|
+
].map((item) => (
|
|
92
|
+
<div key={item} className="flex items-center gap-2">
|
|
93
|
+
<X size={12} className="text-danger flex-shrink-0" />
|
|
94
|
+
<span className="text-xs text-text-1 font-sans">{item}</span>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-xs text-text-0 font-sans font-medium mt-3">
|
|
99
|
+
13 categories of PII are automatically scrubbed before any data leaves your machine.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Mission Statement */}
|
|
105
|
+
<div className="px-6 pt-4">
|
|
106
|
+
<div className="border-l-2 border-accent/30 pl-3">
|
|
107
|
+
<p className="text-xs text-text-3 italic leading-relaxed font-sans">
|
|
108
|
+
We believe in open source, decentralized intelligence. Not walled gardens. Not data hoarding. Every contribution makes the model better for everyone. We need your help to get there.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* CTA */}
|
|
114
|
+
<div className="border-t border-border-subtle mt-5 pt-4 pb-1 px-6">
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => setTrainingOptIn(true)}
|
|
118
|
+
className="w-full h-10 rounded-lg bg-accent text-white font-semibold text-sm hover:bg-accent/90 transition-colors cursor-pointer flex items-center justify-center gap-2"
|
|
119
|
+
>
|
|
120
|
+
<Sparkles size={15} />
|
|
121
|
+
Turn On Sharing
|
|
122
|
+
</button>
|
|
123
|
+
<div className="text-center mt-2.5">
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => dismissDataSharingModal(dontShowAgain)}
|
|
127
|
+
className="text-xs text-text-3 hover:text-text-1 transition-colors font-sans cursor-pointer"
|
|
128
|
+
>
|
|
129
|
+
Maybe Later
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex items-center justify-center gap-2 mt-3">
|
|
133
|
+
<input
|
|
134
|
+
type="checkbox"
|
|
135
|
+
id="data-sharing-dismiss"
|
|
136
|
+
checked={dontShowAgain}
|
|
137
|
+
onChange={(e) => setDontShowAgain(e.target.checked)}
|
|
138
|
+
className="w-3.5 h-3.5 rounded border-border accent-accent cursor-pointer"
|
|
139
|
+
/>
|
|
140
|
+
<label htmlFor="data-sharing-dismiss" className="text-2xs text-text-3 font-sans cursor-pointer select-none">
|
|
141
|
+
Don't show this again
|
|
142
|
+
</label>
|
|
143
|
+
</div>
|
|
144
|
+
<p className="text-center text-2xs text-text-4 font-sans mt-2 mb-1">
|
|
145
|
+
You can always enable this later in Settings
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</DialogContent>
|
|
149
|
+
</Dialog>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -65,7 +65,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
65
65
|
previewIterating: false,
|
|
66
66
|
|
|
67
67
|
// ── Team Launch Config (set during planner spawn, cascades to team) ──
|
|
68
|
-
teamLaunchConfig: null, // { provider, model, reasoningEffort, temperature, verbosity }
|
|
68
|
+
teamLaunchConfig: null, // { provider, model, reasoningEffort, temperature, verbosity, mode }
|
|
69
69
|
|
|
70
70
|
// ── Team Builder ──────────────────────────────────────────
|
|
71
71
|
teamBuilderOpen: false,
|
|
@@ -141,6 +141,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
141
141
|
// ── Training Data ──────────────────────────────────────────
|
|
142
142
|
trainingOptIn: false,
|
|
143
143
|
trainingStats: null,
|
|
144
|
+
dataSharingDismissed: false,
|
|
145
|
+
dataSharingModalOpen: false,
|
|
144
146
|
|
|
145
147
|
// ── Marketplace Auth ───────────────────────────────────────
|
|
146
148
|
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
@@ -224,6 +226,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
224
226
|
get().fetchBetaStatus();
|
|
225
227
|
get().fetchNetworkInstallStatus();
|
|
226
228
|
get().fetchTrainingStatus();
|
|
229
|
+
api.get('/config').then((cfg) => {
|
|
230
|
+
if (cfg?.dataSharingDismissed) set({ dataSharingDismissed: true });
|
|
231
|
+
}).catch(() => {});
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
const st = get();
|
|
234
|
+
if (!st.trainingOptIn && !st.dataSharingDismissed) {
|
|
235
|
+
set({ dataSharingModalOpen: true });
|
|
236
|
+
}
|
|
237
|
+
}, 1500);
|
|
227
238
|
get().fetchActivePreviews();
|
|
228
239
|
ws.send(JSON.stringify({ type: 'editor:watchdir', path: '' }));
|
|
229
240
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
@@ -940,10 +951,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
940
951
|
}
|
|
941
952
|
|
|
942
953
|
case 'training:status': {
|
|
943
|
-
|
|
954
|
+
const updates = {
|
|
944
955
|
trainingOptIn: msg.data?.optedIn ?? false,
|
|
945
956
|
trainingStats: msg.data,
|
|
946
|
-
}
|
|
957
|
+
};
|
|
958
|
+
if (msg.data?.optedIn) updates.dataSharingModalOpen = false;
|
|
959
|
+
set(updates);
|
|
947
960
|
break;
|
|
948
961
|
}
|
|
949
962
|
|
|
@@ -1148,10 +1161,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1148
1161
|
localStorage.setItem('groove:activeTeamId', id);
|
|
1149
1162
|
},
|
|
1150
1163
|
|
|
1151
|
-
async createTeam(name, workingDir) {
|
|
1164
|
+
async createTeam(name, workingDir, mode) {
|
|
1152
1165
|
try {
|
|
1153
1166
|
const body = { name };
|
|
1154
1167
|
if (workingDir) body.workingDir = workingDir;
|
|
1168
|
+
if (mode) body.mode = mode;
|
|
1155
1169
|
const team = await api.post('/teams', body);
|
|
1156
1170
|
// Only set activeTeamId — the WS team:created handler adds to the teams array
|
|
1157
1171
|
set({ activeTeamId: team.id });
|
|
@@ -1664,6 +1678,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1664
1678
|
...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
|
|
1665
1679
|
...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
|
|
1666
1680
|
...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
|
|
1681
|
+
...(tlc?.mode && { mode: tlc.mode }),
|
|
1667
1682
|
};
|
|
1668
1683
|
const result = await api.post('/recommended-team/launch', body);
|
|
1669
1684
|
const totalOk = (result.launched || 0) + (result.reused || 0);
|
|
@@ -2759,7 +2774,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2759
2774
|
async setTrainingOptIn(enabled) {
|
|
2760
2775
|
try {
|
|
2761
2776
|
await api.post('/training/opt-in', { enabled });
|
|
2762
|
-
set({ trainingOptIn: enabled });
|
|
2777
|
+
set({ trainingOptIn: enabled, dataSharingModalOpen: false });
|
|
2763
2778
|
if (!enabled) set({ trainingStats: null });
|
|
2764
2779
|
} catch (e) {
|
|
2765
2780
|
get().addToast('error', 'Failed to update training preference', e.body?.detail || e.message);
|
|
@@ -2773,6 +2788,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2773
2788
|
} catch { /* endpoint may not exist on older daemons */ }
|
|
2774
2789
|
},
|
|
2775
2790
|
|
|
2791
|
+
async dismissDataSharingModal(permanent) {
|
|
2792
|
+
if (permanent) {
|
|
2793
|
+
try { await api.patch('/config', { dataSharingDismissed: true }); } catch {}
|
|
2794
|
+
set({ dataSharingDismissed: true, dataSharingModalOpen: false });
|
|
2795
|
+
} else {
|
|
2796
|
+
set({ dataSharingModalOpen: false });
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
|
|
2776
2800
|
// ── Network (Early Access) ────────────────────────────────
|
|
2777
2801
|
|
|
2778
2802
|
async fetchBetaStatus() {
|