groove-dev 0.27.112 → 0.27.115
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +3 -0
- package/moe-training/client/parsers/codex.js +3 -3
- package/moe-training/client/parsers/gemini.js +2 -2
- package/moe-training/client/step-classifier.js +2 -2
- package/moe-training/test/client/step-classifier.test.js +63 -7
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +75 -15
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +36 -10
- package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
- 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/components/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -8
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
- package/node_modules/moe-training/client/parsers/codex.js +3 -3
- package/node_modules/moe-training/client/parsers/gemini.js +2 -2
- package/node_modules/moe-training/client/step-classifier.js +2 -2
- package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +43 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +75 -15
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +36 -10
- package/packages/daemon/src/teams.js +100 -6
- package/packages/daemon/src/tunnel-manager.js +75 -43
- package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BKCiOUDb.js} +593 -593
- package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/stores/groove.js +57 -8
- package/packages/gui/src/views/agents.jsx +31 -3
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +106 -3
- package/TRAINING_DATA_v2.md +0 -9
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -39,6 +39,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
39
39
|
|
|
40
40
|
// ── Teams ─────────────────────────────────────────────────
|
|
41
41
|
teams: [],
|
|
42
|
+
archivedTeams: [],
|
|
42
43
|
activeTeamId: localStorage.getItem('groove:activeTeamId') || null,
|
|
43
44
|
|
|
44
45
|
// ── Gateways ──────────────────────────────────────────────
|
|
@@ -171,7 +172,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
171
172
|
|
|
172
173
|
// ── Tunnels ────────────────────────────────────────────────
|
|
173
174
|
savedTunnels: [],
|
|
174
|
-
activeTunnelId: null,
|
|
175
175
|
|
|
176
176
|
// ── GitHub Repo Import ────────────────────────────────────
|
|
177
177
|
importedRepos: [],
|
|
@@ -224,6 +224,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
224
224
|
get().fetchBetaStatus();
|
|
225
225
|
get().fetchNetworkInstallStatus();
|
|
226
226
|
get().fetchTrainingStatus();
|
|
227
|
+
get().fetchActivePreviews();
|
|
228
|
+
ws.send(JSON.stringify({ type: 'editor:watchdir', path: '' }));
|
|
227
229
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
228
230
|
if (window.groove?.auth?.onSubscriptionStatus) {
|
|
229
231
|
window.groove.auth.onSubscriptionStatus((data) => {
|
|
@@ -611,7 +613,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
611
613
|
case 'file:changed': {
|
|
612
614
|
const savedAt = get().editorRecentSaves[msg.path];
|
|
613
615
|
if (savedAt && Date.now() - savedAt < 2000) break;
|
|
614
|
-
set((s) => ({ editorChangedFiles: { ...s.editorChangedFiles, [msg.path]: msg.timestamp } }));
|
|
615
616
|
// Auto-capture workspace snapshot for diff viewer
|
|
616
617
|
if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
|
|
617
618
|
const existing = get().editorFiles[msg.path];
|
|
@@ -619,6 +620,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
619
620
|
get().captureSnapshot(msg.path, existing.content);
|
|
620
621
|
}
|
|
621
622
|
}
|
|
623
|
+
if (get().editorFiles[msg.path]) {
|
|
624
|
+
get().reloadFile(msg.path);
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
case 'file:tree-changed': {
|
|
630
|
+
get().fetchTreeDir(msg.path || '');
|
|
622
631
|
break;
|
|
623
632
|
}
|
|
624
633
|
|
|
@@ -694,12 +703,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
694
703
|
break;
|
|
695
704
|
|
|
696
705
|
case 'tunnel.connected':
|
|
697
|
-
set({ activeTunnelId: msg.data?.id || null });
|
|
698
706
|
get().fetchTunnels();
|
|
699
707
|
break;
|
|
700
708
|
|
|
701
709
|
case 'tunnel.disconnected':
|
|
702
|
-
set({ activeTunnelId: null });
|
|
703
710
|
get().fetchTunnels();
|
|
704
711
|
break;
|
|
705
712
|
|
|
@@ -1164,8 +1171,9 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1164
1171
|
// WS team:deleted handler removes from array and switches activeTeamId.
|
|
1165
1172
|
// Deleting the default team regenerates a fresh one server-side; the
|
|
1166
1173
|
// team:created event arrives separately so the list stays populated.
|
|
1167
|
-
const wiped = team?.isDefault ? 'wiped' : '
|
|
1168
|
-
get().addToast('
|
|
1174
|
+
const wiped = team?.isDefault ? 'wiped' : 'archived';
|
|
1175
|
+
get().addToast('success', `Team "${team?.name}" ${wiped}`, wiped === 'archived' ? 'Files preserved — restore anytime from Archived Teams' : undefined);
|
|
1176
|
+
get().fetchArchivedTeams();
|
|
1169
1177
|
} catch (err) {
|
|
1170
1178
|
get().addToast('error', 'Failed to delete team', err.message);
|
|
1171
1179
|
}
|
|
@@ -1179,6 +1187,33 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1179
1187
|
try { localStorage.setItem('groove:teamOrder', JSON.stringify(teams.map((t) => t.id))); } catch {}
|
|
1180
1188
|
},
|
|
1181
1189
|
|
|
1190
|
+
async fetchArchivedTeams() {
|
|
1191
|
+
try {
|
|
1192
|
+
const data = await api.get('/teams/archived');
|
|
1193
|
+
set({ archivedTeams: data.archived || data.teams || [] });
|
|
1194
|
+
} catch { /* endpoint may not exist yet */ }
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
async restoreTeam(archivedId) {
|
|
1198
|
+
try {
|
|
1199
|
+
await api.post(`/teams/archived/${encodeURIComponent(archivedId)}/restore`);
|
|
1200
|
+
get().addToast('success', 'Team restored');
|
|
1201
|
+
get().fetchArchivedTeams();
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
get().addToast('error', 'Failed to restore team', err.message);
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
|
|
1207
|
+
async purgeTeam(archivedId) {
|
|
1208
|
+
try {
|
|
1209
|
+
await api.delete(`/teams/archived/${encodeURIComponent(archivedId)}`);
|
|
1210
|
+
get().addToast('info', 'Archived team permanently deleted');
|
|
1211
|
+
get().fetchArchivedTeams();
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
get().addToast('error', 'Failed to purge team', err.message);
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1182
1217
|
async cloneTeam(id) {
|
|
1183
1218
|
const team = get().teams.find((t) => t.id === id);
|
|
1184
1219
|
if (!team) return;
|
|
@@ -1261,6 +1296,20 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1261
1296
|
|
|
1262
1297
|
// ── Preview ──────────────────────────────────────────────
|
|
1263
1298
|
|
|
1299
|
+
async fetchActivePreviews() {
|
|
1300
|
+
try {
|
|
1301
|
+
const data = await api.get('/preview');
|
|
1302
|
+
const previews = data.previews || [];
|
|
1303
|
+
if (previews.length > 0) {
|
|
1304
|
+
const p = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
|
|
1305
|
+
set({
|
|
1306
|
+
previewState: { url: `/api/preview/${p.teamId}/proxy/`, teamId: p.teamId, kind: p.kind, deviceSize: 'desktop', screenshotMode: false },
|
|
1307
|
+
showPreviewInAgents: true,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
} catch {}
|
|
1311
|
+
},
|
|
1312
|
+
|
|
1264
1313
|
openPreview(url, teamId, kind) {
|
|
1265
1314
|
set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], showPreviewInAgents: true });
|
|
1266
1315
|
},
|
|
@@ -1853,7 +1902,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1853
1902
|
|
|
1854
1903
|
async connectTunnel(id) {
|
|
1855
1904
|
const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
|
|
1856
|
-
set({ activeTunnelId: id });
|
|
1857
1905
|
get().fetchTunnels();
|
|
1858
1906
|
if (result.localPort && result.name) {
|
|
1859
1907
|
if (window.groove?.remote?.openWindow) {
|
|
@@ -1872,7 +1920,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1872
1920
|
async disconnectTunnel(id) {
|
|
1873
1921
|
const tunnel = get().savedTunnels.find(t => t.id === id);
|
|
1874
1922
|
await api.post(`/tunnels/${encodeURIComponent(id)}/disconnect`);
|
|
1875
|
-
set({ activeTunnelId: null });
|
|
1876
1923
|
get().fetchTunnels();
|
|
1877
1924
|
if (tunnel?.localPort && window.groove?.remote?.closeByPort) {
|
|
1878
1925
|
window.groove.remote.closeByPort(tunnel.localPort);
|
|
@@ -2495,6 +2542,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2495
2542
|
try {
|
|
2496
2543
|
const data = await api.get(`/files/tree?path=${encodeURIComponent(dirPath)}`);
|
|
2497
2544
|
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: data.entries || [] } }));
|
|
2545
|
+
const ws = get().ws;
|
|
2546
|
+
if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watchdir', path: dirPath }));
|
|
2498
2547
|
} catch (err) {
|
|
2499
2548
|
console.error('[file-tree] fetchTreeDir failed for', dirPath, err.message);
|
|
2500
2549
|
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: [] } }));
|
|
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
|
|
|
10
10
|
import { cn } from '../lib/cn';
|
|
11
11
|
import { Button } from '../components/ui/button';
|
|
12
12
|
import { Badge } from '../components/ui/badge';
|
|
13
|
-
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers } from 'lucide-react';
|
|
13
|
+
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Archive } from 'lucide-react';
|
|
14
14
|
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';
|
|
@@ -88,11 +88,13 @@ export function TeamTabBar() {
|
|
|
88
88
|
const renameTeam = useGrooveStore((s) => s.renameTeam);
|
|
89
89
|
const cloneTeam = useGrooveStore((s) => s.cloneTeam);
|
|
90
90
|
const reorderTeams = useGrooveStore((s) => s.reorderTeams);
|
|
91
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
91
92
|
|
|
92
93
|
const [creating, setCreating] = useState(false);
|
|
93
94
|
const [newName, setNewName] = useState('');
|
|
94
95
|
const [renamingId, setRenamingId] = useState(null);
|
|
95
96
|
const [renameValue, setRenameValue] = useState('');
|
|
97
|
+
const [archiveConfirm, setArchiveConfirm] = useState(null);
|
|
96
98
|
const submitting = useRef(false);
|
|
97
99
|
const [dragId, setDragId] = useState(null);
|
|
98
100
|
const [dragOverId, setDragOverId] = useState(null);
|
|
@@ -247,8 +249,15 @@ export function TeamTabBar() {
|
|
|
247
249
|
<Copy size={12} /> Clone
|
|
248
250
|
</ContextMenuItem>
|
|
249
251
|
<ContextMenuSeparator />
|
|
250
|
-
<ContextMenuItem danger onSelect={() =>
|
|
251
|
-
|
|
252
|
+
<ContextMenuItem danger onSelect={() => {
|
|
253
|
+
const teamAgents = agents.filter((a) => a.teamId === team.id);
|
|
254
|
+
if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
|
|
255
|
+
addToast('error', 'Stop running agents first');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setArchiveConfirm(team);
|
|
259
|
+
}}>
|
|
260
|
+
<Trash2 size={12} /> {team.isDefault ? 'Wipe' : 'Archive'}
|
|
252
261
|
</ContextMenuItem>
|
|
253
262
|
</ContextMenuContent>
|
|
254
263
|
</ContextMenu>
|
|
@@ -295,6 +304,25 @@ export function TeamTabBar() {
|
|
|
295
304
|
<ChevronRight size={14} />
|
|
296
305
|
</button>
|
|
297
306
|
)}
|
|
307
|
+
|
|
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>
|
|
298
326
|
</div>
|
|
299
327
|
);
|
|
300
328
|
}
|
|
@@ -8,8 +8,7 @@ import { MediaViewer, isMediaFile } from '../components/editor/media-viewer';
|
|
|
8
8
|
import { EditorStatusBar } from '../components/editor/editor-status-bar';
|
|
9
9
|
import { GotoLine } from '../components/editor/goto-line';
|
|
10
10
|
import { Breadcrumbs } from '../components/editor/breadcrumbs';
|
|
11
|
-
import { Code2,
|
|
12
|
-
import { Button } from '../components/ui/button';
|
|
11
|
+
import { Code2, Eye, FileCode, PanelLeftOpen } from 'lucide-react';
|
|
13
12
|
import { api } from '../lib/api';
|
|
14
13
|
import { cn } from '../lib/cn';
|
|
15
14
|
|
|
@@ -25,11 +24,8 @@ const SIDEBAR_MAX = 400;
|
|
|
25
24
|
export default function EditorView() {
|
|
26
25
|
const activeFile = useGrooveStore((s) => s.editorActiveFile);
|
|
27
26
|
const files = useGrooveStore((s) => s.editorFiles);
|
|
28
|
-
const changedFiles = useGrooveStore((s) => s.editorChangedFiles);
|
|
29
27
|
const updateFileContent = useGrooveStore((s) => s.updateFileContent);
|
|
30
28
|
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
31
|
-
const reloadFile = useGrooveStore((s) => s.reloadFile);
|
|
32
|
-
const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
|
|
33
29
|
const sidebarWidth = useGrooveStore((s) => s.editorSidebarWidth);
|
|
34
30
|
const setSidebarWidth = useGrooveStore((s) => s.setEditorSidebarWidth);
|
|
35
31
|
|
|
@@ -104,7 +100,6 @@ export default function EditorView() {
|
|
|
104
100
|
const file = activeFile ? files[activeFile] : null;
|
|
105
101
|
const isMedia = activeFile && isMediaFile(activeFile);
|
|
106
102
|
const isHtml = activeFile && isHtmlFile(activeFile);
|
|
107
|
-
const hasExternalChange = activeFile && changedFiles[activeFile];
|
|
108
103
|
|
|
109
104
|
return (
|
|
110
105
|
<div className="flex h-full">
|
|
@@ -151,20 +146,6 @@ export default function EditorView() {
|
|
|
151
146
|
/>
|
|
152
147
|
)}
|
|
153
148
|
|
|
154
|
-
{/* External change banner */}
|
|
155
|
-
{hasExternalChange && (
|
|
156
|
-
<div className="absolute top-0 left-0 right-0 z-10 flex items-center gap-2 px-4 py-2 bg-warning/10 border-b border-warning/20">
|
|
157
|
-
<AlertTriangle size={14} className="text-warning" />
|
|
158
|
-
<span className="text-xs text-warning font-sans flex-1">File modified externally</span>
|
|
159
|
-
<Button variant="ghost" size="sm" onClick={() => reloadFile(activeFile)}>
|
|
160
|
-
<RefreshCw size={12} /> Reload
|
|
161
|
-
</Button>
|
|
162
|
-
<Button variant="ghost" size="sm" onClick={() => dismissFileChange(activeFile)}>
|
|
163
|
-
<X size={12} /> Dismiss
|
|
164
|
-
</Button>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
149
|
{/* Editor / Media / Empty */}
|
|
169
150
|
{!activeFile && (
|
|
170
151
|
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans">
|
|
@@ -5,6 +5,7 @@ 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';
|
|
8
9
|
import { api } from '../lib/api';
|
|
9
10
|
import { useToast } from '../lib/hooks/use-toast';
|
|
10
11
|
import { fmtNum, fmtDollar, timeAgo, fmtUptime } from '../lib/format';
|
|
@@ -12,6 +13,7 @@ import { cn } from '../lib/cn';
|
|
|
12
13
|
import {
|
|
13
14
|
Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX,
|
|
14
15
|
Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
|
|
16
|
+
Archive, RotateCcw, ChevronRight,
|
|
15
17
|
} from 'lucide-react';
|
|
16
18
|
|
|
17
19
|
// ── Team Dashboard ────────────────────────────────────────────
|
|
@@ -21,8 +23,18 @@ function TeamsDashboard() {
|
|
|
21
23
|
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
22
24
|
const deleteTeam = useGrooveStore((s) => s.deleteTeam);
|
|
23
25
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
26
|
+
const archivedTeams = useGrooveStore((s) => s.archivedTeams);
|
|
27
|
+
const fetchArchivedTeams = useGrooveStore((s) => s.fetchArchivedTeams);
|
|
28
|
+
const restoreTeam = useGrooveStore((s) => s.restoreTeam);
|
|
29
|
+
const purgeTeam = useGrooveStore((s) => s.purgeTeam);
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
const [archiveConfirm, setArchiveConfirm] = useState(null);
|
|
32
|
+
const [purgeConfirm, setPurgeConfirm] = useState(null);
|
|
33
|
+
const [archivedOpen, setArchivedOpen] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => { fetchArchivedTeams(); }, []);
|
|
36
|
+
|
|
37
|
+
if (teams.length === 0 && archivedTeams.length === 0) {
|
|
26
38
|
return (
|
|
27
39
|
<div className="flex-1 flex items-center justify-center">
|
|
28
40
|
<div className="text-center space-y-2">
|
|
@@ -74,10 +86,10 @@ function TeamsDashboard() {
|
|
|
74
86
|
addToast('error', 'Stop running agents first');
|
|
75
87
|
return;
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
setArchiveConfirm(team);
|
|
78
90
|
}}
|
|
79
91
|
className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
|
|
80
|
-
title="
|
|
92
|
+
title="Archive team"
|
|
81
93
|
>
|
|
82
94
|
<Trash2 size={13} />
|
|
83
95
|
</button>
|
|
@@ -112,6 +124,97 @@ function TeamsDashboard() {
|
|
|
112
124
|
);
|
|
113
125
|
})}
|
|
114
126
|
</div>
|
|
127
|
+
|
|
128
|
+
{/* Archived Teams */}
|
|
129
|
+
{archivedTeams.length > 0 && (
|
|
130
|
+
<div className="border-t border-border-subtle">
|
|
131
|
+
<button
|
|
132
|
+
onClick={() => setArchivedOpen(!archivedOpen)}
|
|
133
|
+
className="w-full flex items-center gap-2 px-5 py-3 text-left cursor-pointer hover:bg-surface-5/30 transition-colors"
|
|
134
|
+
>
|
|
135
|
+
<ChevronRight
|
|
136
|
+
size={12}
|
|
137
|
+
className={cn('text-text-4 transition-transform duration-200', archivedOpen && 'rotate-90')}
|
|
138
|
+
/>
|
|
139
|
+
<Archive size={13} className="text-text-3" />
|
|
140
|
+
<span className="text-xs font-semibold text-text-2 font-sans uppercase tracking-wider flex-1">
|
|
141
|
+
Archived Teams
|
|
142
|
+
</span>
|
|
143
|
+
<span className="text-2xs font-mono text-text-4 bg-surface-4 px-1.5 py-0.5 rounded">
|
|
144
|
+
{archivedTeams.length}
|
|
145
|
+
</span>
|
|
146
|
+
</button>
|
|
147
|
+
{archivedOpen && (
|
|
148
|
+
<div className="px-4 pb-4 space-y-2">
|
|
149
|
+
{archivedTeams.map((at) => (
|
|
150
|
+
<div key={at.id} className="flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-0 border border-border-subtle">
|
|
151
|
+
<Archive size={13} className="text-text-4 flex-shrink-0" />
|
|
152
|
+
<div className="flex-1 min-w-0">
|
|
153
|
+
<span className="text-xs font-semibold text-text-1 font-sans">{at.originalName || at.name}</span>
|
|
154
|
+
{(at.deletedAt || at.archivedAt) && (
|
|
155
|
+
<div className="text-2xs text-text-4 font-mono mt-0.5">Archived {timeAgo(at.deletedAt || at.archivedAt)}</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => restoreTeam(at.id)}
|
|
160
|
+
className="p-1.5 text-text-3 hover:text-accent rounded transition-colors cursor-pointer"
|
|
161
|
+
title="Restore team"
|
|
162
|
+
>
|
|
163
|
+
<RotateCcw size={13} />
|
|
164
|
+
</button>
|
|
165
|
+
<button
|
|
166
|
+
onClick={() => setPurgeConfirm(at)}
|
|
167
|
+
className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
|
|
168
|
+
title="Permanently delete"
|
|
169
|
+
>
|
|
170
|
+
<Trash2 size={13} />
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
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>
|
|
198
|
+
|
|
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>
|
|
115
218
|
</div>
|
|
116
219
|
);
|
|
117
220
|
}
|
|
@@ -57,15 +57,15 @@ export class CodexParser {
|
|
|
57
57
|
if (item.type === 'command_execution') {
|
|
58
58
|
const rawOutput = item.aggregated_output || '';
|
|
59
59
|
if (item.exit_code !== 0) {
|
|
60
|
-
return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
60
|
+
return { type: 'error', is_error: true, content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
61
61
|
}
|
|
62
62
|
const obs = truncateObservation(rawOutput);
|
|
63
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
63
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
64
64
|
}
|
|
65
65
|
if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
|
|
66
66
|
const rawOutput = item.output || item.content || '';
|
|
67
67
|
const obs = truncateObservation(rawOutput);
|
|
68
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -64,11 +64,11 @@ export class GeminiParser {
|
|
|
64
64
|
const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
|
|
65
65
|
const rawText = contentParts.map((p) => p.text || '').join('');
|
|
66
66
|
const obs = truncateObservation(rawText);
|
|
67
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
67
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
case 'error': {
|
|
71
|
-
return { type: 'error', content: jsonEvent.message || 'Unknown error' };
|
|
71
|
+
return { type: 'error', is_error: true, content: jsonEvent.message || 'Unknown error' };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
case 'agent_end': {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
const ERROR_SIGNAL_RE =
|
|
3
|
+
const ERROR_SIGNAL_RE = /(?:^|\n)\s*(?:error[ :\[]\S|Error[ :\[]\S|ERROR[ :\[]\S)|(?:exit code [1-9]|non-zero exit|ENOENT|EACCES|EPERM|Command failed)|(?:(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError):\s)|(?:Cannot find module|Module not found|ModuleNotFoundError|ImportError:)|(?:FATAL|PANIC|Traceback \(most recent)|(?:error TS\d{4}:)/;
|
|
4
4
|
const FIX_SIGNAL_RE = /\b(?:fix|correcting|I see the issue|let me fix|the (?:issue|problem|bug) (?:is|was)|instead I should|my mistake)\b/i;
|
|
5
5
|
|
|
6
6
|
const CORRECTION_RE = /\b(?:no[,. ](?:that|not|don't|wrong)|that'?s (?:not|wrong|incorrect)|don'?t do that|stop (?:doing|that)|instead (?:of|do)|undo|revert|go back|try (?:again|differently)|you (?:broke|missed|forgot))\b/i;
|
|
@@ -54,7 +54,7 @@ export class StepClassifier {
|
|
|
54
54
|
|
|
55
55
|
const content = step.content || '';
|
|
56
56
|
|
|
57
|
-
if (
|
|
57
|
+
if (step.type === 'observation' && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
|
|
58
58
|
step.type = 'error';
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
|
|
|
132
132
|
assert.equal(StepClassifier.countUserInterventions(steps), 0);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it('reclassifies action
|
|
135
|
+
it('never reclassifies action to error', () => {
|
|
136
136
|
const classifier = new StepClassifier();
|
|
137
137
|
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
138
138
|
const result = classifier.onStep(step);
|
|
139
|
-
assert.equal(result.type, '
|
|
139
|
+
assert.equal(result.type, 'action');
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it('reclassifies observation with error content to error', () => {
|
|
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
|
|
|
215
215
|
assert.equal(result.type, 'error');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('
|
|
218
|
+
it('does not reclassify observation containing bare word "error" in source code', () => {
|
|
219
219
|
const classifier = new StepClassifier();
|
|
220
|
-
const step = { type: '
|
|
220
|
+
const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
|
|
221
221
|
const result = classifier.onStep(step);
|
|
222
|
-
assert.equal(result.type, '
|
|
222
|
+
assert.equal(result.type, 'observation');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('
|
|
225
|
+
it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
|
|
226
226
|
const classifier = new StepClassifier();
|
|
227
|
-
const step = { type: '
|
|
227
|
+
const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
|
|
228
|
+
const result = classifier.onStep(step);
|
|
229
|
+
assert.equal(result.type, 'observation');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('does not reclassify observation reading a file that mentions exceptions', () => {
|
|
233
|
+
const classifier = new StepClassifier();
|
|
234
|
+
const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
|
|
235
|
+
const result = classifier.onStep(step);
|
|
236
|
+
assert.equal(result.type, 'observation');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('reclassifies observation with real TypeScript build error', () => {
|
|
240
|
+
const classifier = new StepClassifier();
|
|
241
|
+
const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
|
|
242
|
+
const result = classifier.onStep(step);
|
|
243
|
+
assert.equal(result.type, 'error');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reclassifies observation with Python traceback', () => {
|
|
247
|
+
const classifier = new StepClassifier();
|
|
248
|
+
const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
|
|
249
|
+
const result = classifier.onStep(step);
|
|
250
|
+
assert.equal(result.type, 'error');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('reclassifies observation with actual TypeError message', () => {
|
|
254
|
+
const classifier = new StepClassifier();
|
|
255
|
+
const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
|
|
256
|
+
const result = classifier.onStep(step);
|
|
257
|
+
assert.equal(result.type, 'error');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('reclassifies observation with exit code failure', () => {
|
|
261
|
+
const classifier = new StepClassifier();
|
|
262
|
+
const step = { type: 'observation', content: 'Process exited with exit code 1' };
|
|
263
|
+
const result = classifier.onStep(step);
|
|
264
|
+
assert.equal(result.type, 'error');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('reclassifies observation with ModuleNotFoundError', () => {
|
|
268
|
+
const classifier = new StepClassifier();
|
|
269
|
+
const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
|
|
228
270
|
const result = classifier.onStep(step);
|
|
229
271
|
assert.equal(result.type, 'error');
|
|
230
272
|
});
|
|
273
|
+
|
|
274
|
+
it('preserves action type even with error keywords', () => {
|
|
275
|
+
const classifier = new StepClassifier();
|
|
276
|
+
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
277
|
+
const result = classifier.onStep(step);
|
|
278
|
+
assert.equal(result.type, 'action');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('preserves action type regardless of is_error flag', () => {
|
|
282
|
+
const classifier = new StepClassifier();
|
|
283
|
+
const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
|
|
284
|
+
const result = classifier.onStep(step);
|
|
285
|
+
assert.equal(result.type, 'action');
|
|
286
|
+
});
|
|
231
287
|
});
|
|
232
288
|
|
|
233
289
|
describe('StepClassifier.classifyIntent', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.115",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, 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, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// GROOVE CLI — team commands
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { createInterface } from 'readline';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { apiCall } from '../client.js';
|
|
6
7
|
|
|
8
|
+
function confirm(prompt) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim().toLowerCase() === 'y'); });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export async function teamCreate(name) {
|
|
8
16
|
try {
|
|
9
17
|
const team = await apiCall('POST', '/api/teams', { name });
|
|
@@ -37,9 +45,43 @@ export async function teamList() {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export async function teamDelete(id) {
|
|
48
|
+
const ok = await confirm(` This will archive the team directory. Continue? [y/N] `);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
try {
|
|
41
54
|
await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
|
|
42
|
-
console.log(chalk.green(`
|
|
55
|
+
console.log(chalk.green(` Archived team "${id}"`) + chalk.dim(' — restore with `groove team restore <id>`'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(chalk.red(' Failed:'), err.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function teamArchived() {
|
|
63
|
+
try {
|
|
64
|
+
const { archived } = await apiCall('GET', '/api/teams/archived');
|
|
65
|
+
if (archived.length === 0) {
|
|
66
|
+
console.log(chalk.dim(' No archived teams.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.bold(`\n Archived Teams (${archived.length})\n`));
|
|
70
|
+
for (const t of archived) {
|
|
71
|
+
const date = t.deletedAt ? new Date(t.deletedAt).toLocaleDateString() : 'unknown';
|
|
72
|
+
console.log(` ${chalk.bold(t.originalName || t.id)} — archive-id: ${t.id} — deleted ${date} — ${t.agentCount} agent(s)`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
} catch {
|
|
76
|
+
console.error(chalk.red(' Cannot connect to daemon.'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function teamRestore(id) {
|
|
82
|
+
try {
|
|
83
|
+
const team = await apiCall('POST', `/api/teams/archived/${encodeURIComponent(id)}/restore`);
|
|
84
|
+
console.log(chalk.green(` Restored team "${team.name}"`) + ` (new id: ${team.id})`);
|
|
43
85
|
} catch (err) {
|
|
44
86
|
console.error(chalk.red(' Failed:'), err.message);
|
|
45
87
|
process.exit(1);
|