groove-dev 0.27.115 → 0.27.116

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 (32) hide show
  1. package/TRAINING_DATA_v4.md +6 -3
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/cli/src/commands/team.js +27 -12
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/api.js +3 -2
  6. package/node_modules/@groove-dev/daemon/src/process.js +254 -208
  7. package/node_modules/@groove-dev/daemon/src/teams.js +53 -24
  8. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +3 -2
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-D4Q72afD.css → index-DdN9RVnC.css} +1 -1
  10. package/node_modules/@groove-dev/gui/dist/assets/{index-BKCiOUDb.js → index-fq--PD7_.js} +1724 -1724
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  14. package/node_modules/@groove-dev/gui/src/stores/groove.js +15 -4
  15. package/node_modules/@groove-dev/gui/src/views/agents.jsx +10 -19
  16. package/node_modules/@groove-dev/gui/src/views/teams.jsx +17 -41
  17. package/package.json +1 -1
  18. package/packages/cli/package.json +1 -1
  19. package/packages/cli/src/commands/team.js +27 -12
  20. package/packages/daemon/package.json +1 -1
  21. package/packages/daemon/src/api.js +3 -2
  22. package/packages/daemon/src/process.js +254 -208
  23. package/packages/daemon/src/teams.js +53 -24
  24. package/packages/daemon/src/tunnel-manager.js +3 -2
  25. package/packages/gui/dist/assets/{index-D4Q72afD.css → index-DdN9RVnC.css} +1 -1
  26. package/packages/gui/dist/assets/{index-BKCiOUDb.js → index-fq--PD7_.js} +1724 -1724
  27. package/packages/gui/dist/index.html +2 -2
  28. package/packages/gui/package.json +1 -1
  29. package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  30. package/packages/gui/src/stores/groove.js +15 -4
  31. package/packages/gui/src/views/agents.jsx +10 -19
  32. package/packages/gui/src/views/teams.jsx +17 -41
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-BKCiOUDb.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-fq--PD7_.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-D4Q72afD.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-DdN9RVnC.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.115",
3
+ "version": "0.27.116",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,156 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { Dialog, DialogContent } from '../ui/dialog';
4
+ import { Button } from '../ui/button';
5
+ import { Archive, Trash2, AlertTriangle } from 'lucide-react';
6
+
7
+ export function TeamRemovalDialog({ team, open, onOpenChange, onArchive, onDeletePermanently }) {
8
+ const [confirmName, setConfirmName] = useState('');
9
+ const [showConfirmInput, setShowConfirmInput] = useState(false);
10
+
11
+ useEffect(() => {
12
+ if (!open) {
13
+ setConfirmName('');
14
+ setShowConfirmInput(false);
15
+ }
16
+ }, [open]);
17
+
18
+ const nameMatch = confirmName === (team?.name || '');
19
+
20
+ return (
21
+ <Dialog open={open} onOpenChange={onOpenChange}>
22
+ <DialogContent title={`Remove Team: ${team?.name || ''}`} description="Choose how to remove this team">
23
+ <div className="px-5 py-4 space-y-4">
24
+ <p className="text-sm text-text-1 font-sans">
25
+ What would you like to do with this team and its files?
26
+ </p>
27
+
28
+ {/* Archive option */}
29
+ <button
30
+ onClick={() => { onArchive(team?.id); onOpenChange(false); }}
31
+ className="w-full flex items-start gap-3 p-3.5 rounded-lg border border-border-subtle bg-surface-0 hover:border-accent/30 hover:bg-surface-2 transition-all cursor-pointer text-left group"
32
+ >
33
+ <div className="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center flex-shrink-0 group-hover:bg-accent/20 transition-colors">
34
+ <Archive size={16} className="text-accent" />
35
+ </div>
36
+ <div className="min-w-0 flex-1">
37
+ <div className="text-sm font-semibold text-text-0 font-sans">Archive</div>
38
+ <p className="text-xs text-text-3 font-sans mt-0.5">
39
+ Files are preserved. You can restore the team later.
40
+ </p>
41
+ </div>
42
+ </button>
43
+
44
+ {/* Delete Permanently option */}
45
+ <div className="rounded-lg border border-danger/20 bg-danger/5 overflow-hidden">
46
+ <button
47
+ onClick={() => setShowConfirmInput(true)}
48
+ className="w-full flex items-start gap-3 p-3.5 cursor-pointer text-left group"
49
+ >
50
+ <div className="w-8 h-8 rounded-md bg-danger/10 flex items-center justify-center flex-shrink-0 group-hover:bg-danger/20 transition-colors">
51
+ <Trash2 size={16} className="text-danger" />
52
+ </div>
53
+ <div className="min-w-0 flex-1">
54
+ <div className="text-sm font-semibold text-danger font-sans">Delete Permanently</div>
55
+ <p className="text-xs text-text-3 font-sans mt-0.5">
56
+ All files in this team will be permanently deleted.
57
+ </p>
58
+ </div>
59
+ </button>
60
+
61
+ {showConfirmInput && (
62
+ <div className="px-3.5 pb-3.5 space-y-2">
63
+ <div className="flex items-center gap-1.5 text-2xs text-warning font-sans">
64
+ <AlertTriangle size={11} />
65
+ <span>Type <span className="font-mono font-semibold text-text-0">{team?.name}</span> to confirm</span>
66
+ </div>
67
+ <input
68
+ type="text"
69
+ value={confirmName}
70
+ onChange={(e) => setConfirmName(e.target.value)}
71
+ placeholder={team?.name}
72
+ className="w-full h-8 px-3 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-danger"
73
+ autoFocus
74
+ spellCheck={false}
75
+ onKeyDown={(e) => {
76
+ if (e.key === 'Enter' && nameMatch) {
77
+ onDeletePermanently(team?.id);
78
+ onOpenChange(false);
79
+ }
80
+ if (e.key === 'Escape') setShowConfirmInput(false);
81
+ }}
82
+ />
83
+ <Button
84
+ variant="danger"
85
+ size="sm"
86
+ disabled={!nameMatch}
87
+ onClick={() => { onDeletePermanently(team?.id); onOpenChange(false); }}
88
+ className="w-full"
89
+ >
90
+ <Trash2 size={12} /> Delete Forever
91
+ </Button>
92
+ </div>
93
+ )}
94
+ </div>
95
+ </div>
96
+
97
+ <div className="px-5 py-3 border-t border-border-subtle flex justify-end">
98
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button>
99
+ </div>
100
+ </DialogContent>
101
+ </Dialog>
102
+ );
103
+ }
104
+
105
+ export function PurgeConfirmDialog({ team, open, onOpenChange, onPurge }) {
106
+ const [confirmName, setConfirmName] = useState('');
107
+
108
+ useEffect(() => {
109
+ if (!open) setConfirmName('');
110
+ }, [open]);
111
+
112
+ const displayName = team?.originalName || team?.name || '';
113
+ const nameMatch = confirmName === displayName;
114
+
115
+ return (
116
+ <Dialog open={open} onOpenChange={onOpenChange}>
117
+ <DialogContent title="Permanently Delete" description="Confirm permanent deletion">
118
+ <div className="px-5 py-4 space-y-3">
119
+ <p className="text-sm text-text-1 font-sans">
120
+ Permanently delete <span className="font-semibold text-text-0">{displayName}</span>?
121
+ </p>
122
+ <p className="text-xs text-danger font-sans">
123
+ This cannot be undone. All team files will be permanently removed.
124
+ </p>
125
+ <div className="space-y-2 pt-1">
126
+ <div className="flex items-center gap-1.5 text-2xs text-warning font-sans">
127
+ <AlertTriangle size={11} />
128
+ <span>Type <span className="font-mono font-semibold text-text-0">{displayName}</span> to confirm</span>
129
+ </div>
130
+ <input
131
+ type="text"
132
+ value={confirmName}
133
+ onChange={(e) => setConfirmName(e.target.value)}
134
+ placeholder={displayName}
135
+ className="w-full h-8 px-3 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-danger"
136
+ autoFocus
137
+ spellCheck={false}
138
+ onKeyDown={(e) => {
139
+ if (e.key === 'Enter' && nameMatch) {
140
+ onPurge(team?.id);
141
+ onOpenChange(false);
142
+ }
143
+ }}
144
+ />
145
+ </div>
146
+ </div>
147
+ <div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-2">
148
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button>
149
+ <Button variant="danger" size="sm" disabled={!nameMatch} onClick={() => { onPurge(team?.id); onOpenChange(false); }}>
150
+ <Trash2 size={12} /> Delete Forever
151
+ </Button>
152
+ </div>
153
+ </DialogContent>
154
+ </Dialog>
155
+ );
156
+ }
@@ -1164,21 +1164,32 @@ export const useGrooveStore = create((set, get) => ({
1164
1164
  }
1165
1165
  },
1166
1166
 
1167
- async deleteTeam(id) {
1167
+ async archiveTeam(id) {
1168
1168
  const team = get().teams.find((t) => t.id === id);
1169
1169
  try {
1170
1170
  await api.delete(`/teams/${encodeURIComponent(id)}`);
1171
- // WS team:deleted handler removes from array and switches activeTeamId.
1172
- // Deleting the default team regenerates a fresh one server-side; the
1173
- // team:created event arrives separately so the list stays populated.
1174
1171
  const wiped = team?.isDefault ? 'wiped' : 'archived';
1175
1172
  get().addToast('success', `Team "${team?.name}" ${wiped}`, wiped === 'archived' ? 'Files preserved — restore anytime from Archived Teams' : undefined);
1176
1173
  get().fetchArchivedTeams();
1174
+ } catch (err) {
1175
+ get().addToast('error', 'Failed to archive team', err.message);
1176
+ }
1177
+ },
1178
+
1179
+ async deleteTeamPermanently(id) {
1180
+ const team = get().teams.find((t) => t.id === id);
1181
+ try {
1182
+ await api.delete(`/teams/${encodeURIComponent(id)}?permanent=true`);
1183
+ get().addToast('success', `Team "${team?.name}" permanently deleted`);
1177
1184
  } catch (err) {
1178
1185
  get().addToast('error', 'Failed to delete team', err.message);
1179
1186
  }
1180
1187
  },
1181
1188
 
1189
+ async deleteTeam(id) {
1190
+ return get().archiveTeam(id);
1191
+ },
1192
+
1182
1193
  reorderTeams(fromIndex, toIndex) {
1183
1194
  const teams = [...get().teams];
1184
1195
  const [moved] = teams.splice(fromIndex, 1);
@@ -15,6 +15,7 @@ import { PreviewWorkspace } from '../components/preview/preview-workspace';
15
15
  import { WorkspaceMode } from '../components/agents/workspace-mode';
16
16
  import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
17
17
  import { Dialog, DialogContent } from '../components/ui/dialog';
18
+ import { TeamRemovalDialog } from '../components/teams/team-removal-dialog';
18
19
  import { Select, SelectTrigger, SelectContent, SelectItem } from '../components/ui/select';
19
20
  import { ScrollArea } from '../components/ui/scroll-area';
20
21
  import { Tooltip } from '../components/ui/tooltip';
@@ -84,7 +85,8 @@ export function TeamTabBar() {
84
85
  const agents = useGrooveStore((s) => s.agents);
85
86
  const switchTeam = useGrooveStore((s) => s.switchTeam);
86
87
  const createTeam = useGrooveStore((s) => s.createTeam);
87
- const deleteTeam = useGrooveStore((s) => s.deleteTeam);
88
+ const archiveTeam = useGrooveStore((s) => s.archiveTeam);
89
+ const deleteTeamPermanently = useGrooveStore((s) => s.deleteTeamPermanently);
88
90
  const renameTeam = useGrooveStore((s) => s.renameTeam);
89
91
  const cloneTeam = useGrooveStore((s) => s.cloneTeam);
90
92
  const reorderTeams = useGrooveStore((s) => s.reorderTeams);
@@ -305,24 +307,13 @@ export function TeamTabBar() {
305
307
  </button>
306
308
  )}
307
309
 
308
- <Dialog open={!!archiveConfirm} onOpenChange={(open) => !open && setArchiveConfirm(null)}>
309
- <DialogContent title="Archive Team" description="Confirm team archival">
310
- <div className="px-5 py-4 space-y-3">
311
- <p className="text-sm text-text-1 font-sans">
312
- This will archive <span className="font-semibold text-text-0">{archiveConfirm?.name}</span>.
313
- </p>
314
- <p className="text-xs text-text-2 font-sans">
315
- All files will be preserved and can be restored later from the Archived Teams section.
316
- </p>
317
- </div>
318
- <div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-2">
319
- <Button variant="ghost" size="sm" onClick={() => setArchiveConfirm(null)}>Cancel</Button>
320
- <Button variant="danger" size="sm" onClick={() => { deleteTeam(archiveConfirm.id); setArchiveConfirm(null); }}>
321
- <Archive size={12} /> Archive
322
- </Button>
323
- </div>
324
- </DialogContent>
325
- </Dialog>
310
+ <TeamRemovalDialog
311
+ team={archiveConfirm}
312
+ open={!!archiveConfirm}
313
+ onOpenChange={(open) => !open && setArchiveConfirm(null)}
314
+ onArchive={archiveTeam}
315
+ onDeletePermanently={deleteTeamPermanently}
316
+ />
326
317
  </div>
327
318
  );
328
319
  }
@@ -5,7 +5,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/tabs'
5
5
  import { Button } from '../components/ui/button';
6
6
  import { Badge } from '../components/ui/badge';
7
7
  import { StatusDot } from '../components/ui/status-dot';
8
- import { Dialog, DialogContent } from '../components/ui/dialog';
9
8
  import { api } from '../lib/api';
10
9
  import { useToast } from '../lib/hooks/use-toast';
11
10
  import { fmtNum, fmtDollar, timeAgo, fmtUptime } from '../lib/format';
@@ -15,13 +14,15 @@ import {
15
14
  Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
16
15
  Archive, RotateCcw, ChevronRight,
17
16
  } from 'lucide-react';
17
+ import { TeamRemovalDialog, PurgeConfirmDialog } from '../components/teams/team-removal-dialog';
18
18
 
19
19
  // ── Team Dashboard ────────────────────────────────────────────
20
20
  function TeamsDashboard() {
21
21
  const teams = useGrooveStore((s) => s.teams);
22
22
  const agents = useGrooveStore((s) => s.agents);
23
23
  const activeTeamId = useGrooveStore((s) => s.activeTeamId);
24
- const deleteTeam = useGrooveStore((s) => s.deleteTeam);
24
+ const archiveTeam = useGrooveStore((s) => s.archiveTeam);
25
+ const deleteTeamPermanently = useGrooveStore((s) => s.deleteTeamPermanently);
25
26
  const addToast = useGrooveStore((s) => s.addToast);
26
27
  const archivedTeams = useGrooveStore((s) => s.archivedTeams);
27
28
  const fetchArchivedTeams = useGrooveStore((s) => s.fetchArchivedTeams);
@@ -82,7 +83,7 @@ function TeamsDashboard() {
82
83
  </div>
83
84
  <button
84
85
  onClick={() => {
85
- if (teamAgents.some((a) => a.status === 'running')) {
86
+ if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
86
87
  addToast('error', 'Stop running agents first');
87
88
  return;
88
89
  }
@@ -176,45 +177,20 @@ function TeamsDashboard() {
176
177
  </div>
177
178
  )}
178
179
 
179
- {/* Archive confirmation dialog */}
180
- <Dialog open={!!archiveConfirm} onOpenChange={(open) => !open && setArchiveConfirm(null)}>
181
- <DialogContent title="Archive Team" description="Confirm team archival">
182
- <div className="px-5 py-4 space-y-3">
183
- <p className="text-sm text-text-1 font-sans">
184
- This will archive <span className="font-semibold text-text-0">{archiveConfirm?.name}</span>.
185
- </p>
186
- <p className="text-xs text-text-2 font-sans">
187
- All files will be preserved and can be restored later from the Archived Teams section.
188
- </p>
189
- </div>
190
- <div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-2">
191
- <Button variant="ghost" size="sm" onClick={() => setArchiveConfirm(null)}>Cancel</Button>
192
- <Button variant="danger" size="sm" onClick={() => { deleteTeam(archiveConfirm.id); setArchiveConfirm(null); }}>
193
- <Archive size={12} /> Archive
194
- </Button>
195
- </div>
196
- </DialogContent>
197
- </Dialog>
180
+ <TeamRemovalDialog
181
+ team={archiveConfirm}
182
+ open={!!archiveConfirm}
183
+ onOpenChange={(open) => !open && setArchiveConfirm(null)}
184
+ onArchive={archiveTeam}
185
+ onDeletePermanently={deleteTeamPermanently}
186
+ />
198
187
 
199
- {/* Purge confirmation dialog */}
200
- <Dialog open={!!purgeConfirm} onOpenChange={(open) => !open && setPurgeConfirm(null)}>
201
- <DialogContent title="Permanently Delete" description="Confirm permanent deletion">
202
- <div className="px-5 py-4 space-y-3">
203
- <p className="text-sm text-text-1 font-sans">
204
- Permanently delete <span className="font-semibold text-text-0">{purgeConfirm?.originalName || purgeConfirm?.name}</span>?
205
- </p>
206
- <p className="text-xs text-danger font-sans">
207
- This cannot be undone. All team files will be permanently removed.
208
- </p>
209
- </div>
210
- <div className="px-5 py-4 border-t border-border-subtle flex justify-end gap-2">
211
- <Button variant="ghost" size="sm" onClick={() => setPurgeConfirm(null)}>Cancel</Button>
212
- <Button variant="danger" size="sm" onClick={() => { purgeTeam(purgeConfirm.id); setPurgeConfirm(null); }}>
213
- <Trash2 size={12} /> Delete Forever
214
- </Button>
215
- </div>
216
- </DialogContent>
217
- </Dialog>
188
+ <PurgeConfirmDialog
189
+ team={purgeConfirm}
190
+ open={!!purgeConfirm}
191
+ onOpenChange={(open) => !open && setPurgeConfirm(null)}
192
+ onPurge={purgeTeam}
193
+ />
218
194
  </div>
219
195
  );
220
196
  }