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.
Files changed (48) hide show
  1. package/TRAINING_DATA_v4.md +6 -6
  2. package/moe-training/client/domain-tagger.js +3 -3
  3. package/moe-training/client/trajectory-capture.js +7 -0
  4. package/moe-training/client/transmission-queue.js +6 -0
  5. package/moe-training/test/shared/envelope-schema.test.js +3 -3
  6. package/node_modules/@groove-dev/cli/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/package.json +1 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +13 -4
  9. package/node_modules/@groove-dev/daemon/src/index.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/teams.js +70 -39
  11. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
  18. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +29 -7
  20. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  21. package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -5
  23. package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
  24. package/node_modules/@groove-dev/gui/src/views/teams.jsx +4 -0
  25. package/node_modules/moe-training/client/domain-tagger.js +3 -3
  26. package/node_modules/moe-training/client/trajectory-capture.js +7 -0
  27. package/node_modules/moe-training/client/transmission-queue.js +6 -0
  28. package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
  29. package/package.json +1 -1
  30. package/packages/cli/package.json +1 -1
  31. package/packages/daemon/package.json +1 -1
  32. package/packages/daemon/src/api.js +13 -4
  33. package/packages/daemon/src/index.js +4 -0
  34. package/packages/daemon/src/teams.js +70 -39
  35. package/packages/daemon/src/validate.js +10 -0
  36. package/packages/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
  37. package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  38. package/packages/gui/dist/index.html +2 -2
  39. package/packages/gui/package.json +1 -1
  40. package/packages/gui/src/app.jsx +2 -0
  41. package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
  42. package/packages/gui/src/components/editor/file-tree.jsx +80 -3
  43. package/packages/gui/src/components/settings/quick-connect.jsx +29 -7
  44. package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  45. package/packages/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  46. package/packages/gui/src/stores/groove.js +29 -5
  47. package/packages/gui/src/views/agents.jsx +47 -11
  48. package/packages/gui/src/views/teams.jsx +4 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.117",
3
+ "version": "0.27.119",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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 className="px-1">
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 className="py-1">
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
- <button
208
- onClick={() => handleConnect(server.id)}
209
- className="text-2xs text-text-3 font-sans hover:text-text-1 cursor-pointer transition-colors"
210
- >
211
- Connect
212
- </button>
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
- Files are preserved. You can restore the team later.
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
- All files in this team will be permanently deleted.
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&apos;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&apos;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
- set({
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() {