groove-dev 0.27.113 → 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.
- package/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +6 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +27 -2
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/process.js +254 -208
- package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
- package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- 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/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +59 -2
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +27 -2
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/process.js +254 -208
- package/packages/daemon/src/teams.js +143 -20
- package/packages/daemon/src/tunnel-manager.js +78 -45
- package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- 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/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/packages/gui/src/stores/groove.js +57 -12
- package/packages/gui/src/views/agents.jsx +23 -4
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +84 -5
- package/TRAINING_DATA_v3.md +0 -11
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
- package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
- package/codex-test/offroad-nitro-racer/index.html +0 -21
- package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
- package/codex-test/offroad-nitro-racer/package.json +0 -15
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
- package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
- package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
- package/codex-test/offroad-nitro-racer/src/style.css +0 -291
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
- package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
- package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DdN9RVnC.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -157,13 +157,10 @@ export function WorkspaceMode() {
|
|
|
157
157
|
const editorFiles = useGrooveStore((s) => s.editorFiles);
|
|
158
158
|
const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
|
|
159
159
|
const editorOpenTabs = useGrooveStore((s) => s.editorOpenTabs);
|
|
160
|
-
const editorChangedFiles = useGrooveStore((s) => s.editorChangedFiles);
|
|
161
160
|
const setActiveFile = useGrooveStore((s) => s.setActiveFile);
|
|
162
161
|
const closeFile = useGrooveStore((s) => s.closeFile);
|
|
163
162
|
const updateFileContent = useGrooveStore((s) => s.updateFileContent);
|
|
164
163
|
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
165
|
-
const reloadFile = useGrooveStore((s) => s.reloadFile);
|
|
166
|
-
const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
|
|
167
164
|
|
|
168
165
|
const teamAgents = agents.filter((a) => a.teamId === activeTeamId);
|
|
169
166
|
const agent = teamAgents.find((a) => a.id === workspaceAgentId) || teamAgents[0];
|
|
@@ -207,7 +204,6 @@ export function WorkspaceMode() {
|
|
|
207
204
|
}
|
|
208
205
|
|
|
209
206
|
const file = editorActiveFile ? editorFiles[editorActiveFile] : null;
|
|
210
|
-
const hasExternalChange = editorActiveFile && editorChangedFiles[editorActiveFile];
|
|
211
207
|
const isMedia = editorActiveFile && isMediaFile(editorActiveFile);
|
|
212
208
|
|
|
213
209
|
return (
|
|
@@ -260,24 +256,6 @@ export function WorkspaceMode() {
|
|
|
260
256
|
/>
|
|
261
257
|
|
|
262
258
|
<div className="flex-1 relative min-h-0">
|
|
263
|
-
{hasExternalChange && (
|
|
264
|
-
<div className="absolute top-1 right-3 z-10 flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-2/90 border border-border-subtle backdrop-blur-sm">
|
|
265
|
-
<span className="text-2xs text-text-3 font-sans">Modified</span>
|
|
266
|
-
<button
|
|
267
|
-
onClick={() => reloadFile(editorActiveFile)}
|
|
268
|
-
className="text-2xs text-accent hover:text-accent/80 font-sans cursor-pointer"
|
|
269
|
-
>
|
|
270
|
-
Reload
|
|
271
|
-
</button>
|
|
272
|
-
<button
|
|
273
|
-
onClick={() => dismissFileChange(editorActiveFile)}
|
|
274
|
-
className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"
|
|
275
|
-
>
|
|
276
|
-
<X size={10} />
|
|
277
|
-
</button>
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
|
|
281
259
|
{!editorActiveFile && (
|
|
282
260
|
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans bg-surface-1">
|
|
283
261
|
<div className="text-center space-y-2">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug
|
|
2
|
+
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { StatusDot } from '../ui/status-dot';
|
|
5
5
|
import { Badge } from '../ui/badge';
|
|
@@ -23,7 +23,7 @@ export function StatusBar({
|
|
|
23
23
|
const updateProgress = useGrooveStore((s) => s.updateProgress);
|
|
24
24
|
const setUpdateModalOpen = useGrooveStore((s) => s.setUpdateModalOpen);
|
|
25
25
|
const navigate = useGrooveStore((s) => s.setActiveView);
|
|
26
|
-
const
|
|
26
|
+
const activeTunnels = savedTunnels.filter((t) => t.active);
|
|
27
27
|
const electron = isElectron();
|
|
28
28
|
|
|
29
29
|
return (
|
|
@@ -52,40 +52,7 @@ export function StatusBar({
|
|
|
52
52
|
{connected && agentCount > 0 && (
|
|
53
53
|
<span className="text-text-4">{runningCount}/{agentCount} agents</span>
|
|
54
54
|
)}
|
|
55
|
-
{
|
|
56
|
-
<div className="flex items-center gap-1">
|
|
57
|
-
<button
|
|
58
|
-
onClick={() => {
|
|
59
|
-
const port = activeTunnel.localPort;
|
|
60
|
-
const name = encodeURIComponent(activeTunnel.name);
|
|
61
|
-
openExternal(`http://localhost:${port}?instance=${name}`);
|
|
62
|
-
}}
|
|
63
|
-
className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
|
|
64
|
-
title="Open remote GUI"
|
|
65
|
-
>
|
|
66
|
-
<Radio size={10} className="text-success" />
|
|
67
|
-
<span>{activeTunnel.name}</span>
|
|
68
|
-
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
69
|
-
{activeTunnel.latencyMs != null && (
|
|
70
|
-
<span className="text-text-4">{activeTunnel.latencyMs}ms</span>
|
|
71
|
-
)}
|
|
72
|
-
</button>
|
|
73
|
-
<button
|
|
74
|
-
onClick={() => useGrooveStore.getState().addToWhitelist(activeTunnel.host)}
|
|
75
|
-
className="p-0.5 text-text-4 hover:text-accent cursor-pointer transition-colors rounded"
|
|
76
|
-
title="Add to Federation Whitelist"
|
|
77
|
-
>
|
|
78
|
-
<Shield size={10} />
|
|
79
|
-
</button>
|
|
80
|
-
<button
|
|
81
|
-
onClick={() => useGrooveStore.getState().disconnectTunnel(activeTunnel.id)}
|
|
82
|
-
className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
83
|
-
title="Disconnect"
|
|
84
|
-
>
|
|
85
|
-
<X size={10} />
|
|
86
|
-
</button>
|
|
87
|
-
</div>
|
|
88
|
-
) : tunneled ? (
|
|
55
|
+
{tunneled ? (
|
|
89
56
|
<button
|
|
90
57
|
onClick={() => window.groove?.remote?.close?.() || window.close()}
|
|
91
58
|
className="flex items-center gap-1.5 text-text-3 hover:text-danger cursor-pointer transition-colors"
|
|
@@ -94,15 +61,46 @@ export function StatusBar({
|
|
|
94
61
|
<Unplug size={10} />
|
|
95
62
|
<span>Disconnect</span>
|
|
96
63
|
</button>
|
|
97
|
-
) :
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
64
|
+
) : (
|
|
65
|
+
<>
|
|
66
|
+
{activeTunnels.map((tunnel) => (
|
|
67
|
+
<div key={tunnel.id} className="flex items-center gap-1">
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => {
|
|
70
|
+
const port = tunnel.localPort;
|
|
71
|
+
const name = encodeURIComponent(tunnel.name);
|
|
72
|
+
openExternal(`http://localhost:${port}?instance=${name}`);
|
|
73
|
+
}}
|
|
74
|
+
className="flex items-center gap-1.5 text-text-3 hover:text-text-1 cursor-pointer transition-colors"
|
|
75
|
+
title="Open remote GUI"
|
|
76
|
+
>
|
|
77
|
+
<Radio size={10} className="text-success" />
|
|
78
|
+
<span>{tunnel.name}</span>
|
|
79
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
80
|
+
{tunnel.latencyMs != null && (
|
|
81
|
+
<span className="text-text-4">{tunnel.latencyMs}ms</span>
|
|
82
|
+
)}
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => useGrooveStore.getState().disconnectTunnel(tunnel.id)}
|
|
86
|
+
className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
87
|
+
title="Disconnect"
|
|
88
|
+
>
|
|
89
|
+
<X size={10} />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
{savedTunnels.length > 0 && (
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => useGrooveStore.getState().toggleQuickConnect()}
|
|
96
|
+
className="flex items-center gap-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
|
|
97
|
+
title="Quick Connect to remote server"
|
|
98
|
+
>
|
|
99
|
+
<Plug size={10} />
|
|
100
|
+
<span>Connect</span>
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
</>
|
|
106
104
|
)}
|
|
107
105
|
{connected && (
|
|
108
106
|
<button
|
|
@@ -32,7 +32,8 @@ export function QuickConnect() {
|
|
|
32
32
|
onClick: () => useGrooveStore.getState().addToWhitelist(tunnel.host),
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
setConnectingId(null);
|
|
36
|
+
return;
|
|
36
37
|
} catch (err) {
|
|
37
38
|
let detail = err?.message || 'Unknown error';
|
|
38
39
|
if (detail.toLowerCase().includes('port forward')) {
|
|
@@ -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
|
+
}
|
|
@@ -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: [],
|
|
@@ -225,6 +225,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
225
225
|
get().fetchNetworkInstallStatus();
|
|
226
226
|
get().fetchTrainingStatus();
|
|
227
227
|
get().fetchActivePreviews();
|
|
228
|
+
ws.send(JSON.stringify({ type: 'editor:watchdir', path: '' }));
|
|
228
229
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
229
230
|
if (window.groove?.auth?.onSubscriptionStatus) {
|
|
230
231
|
window.groove.auth.onSubscriptionStatus((data) => {
|
|
@@ -612,7 +613,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
612
613
|
case 'file:changed': {
|
|
613
614
|
const savedAt = get().editorRecentSaves[msg.path];
|
|
614
615
|
if (savedAt && Date.now() - savedAt < 2000) break;
|
|
615
|
-
set((s) => ({ editorChangedFiles: { ...s.editorChangedFiles, [msg.path]: msg.timestamp } }));
|
|
616
616
|
// Auto-capture workspace snapshot for diff viewer
|
|
617
617
|
if (get().workspaceMode && msg.path && !get().workspaceSnapshots[msg.path]) {
|
|
618
618
|
const existing = get().editorFiles[msg.path];
|
|
@@ -620,6 +620,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
620
620
|
get().captureSnapshot(msg.path, existing.content);
|
|
621
621
|
}
|
|
622
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 || '');
|
|
623
631
|
break;
|
|
624
632
|
}
|
|
625
633
|
|
|
@@ -695,12 +703,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
695
703
|
break;
|
|
696
704
|
|
|
697
705
|
case 'tunnel.connected':
|
|
698
|
-
set({ activeTunnelId: msg.data?.id || null });
|
|
699
706
|
get().fetchTunnels();
|
|
700
707
|
break;
|
|
701
708
|
|
|
702
709
|
case 'tunnel.disconnected':
|
|
703
|
-
set({ activeTunnelId: null });
|
|
704
710
|
get().fetchTunnels();
|
|
705
711
|
break;
|
|
706
712
|
|
|
@@ -1158,20 +1164,32 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1158
1164
|
}
|
|
1159
1165
|
},
|
|
1160
1166
|
|
|
1161
|
-
async
|
|
1167
|
+
async archiveTeam(id) {
|
|
1162
1168
|
const team = get().teams.find((t) => t.id === id);
|
|
1163
1169
|
try {
|
|
1164
1170
|
await api.delete(`/teams/${encodeURIComponent(id)}`);
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
get().addToast('
|
|
1171
|
+
const wiped = team?.isDefault ? 'wiped' : 'archived';
|
|
1172
|
+
get().addToast('success', `Team "${team?.name}" ${wiped}`, wiped === 'archived' ? 'Files preserved — restore anytime from Archived Teams' : undefined);
|
|
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`);
|
|
1170
1184
|
} catch (err) {
|
|
1171
1185
|
get().addToast('error', 'Failed to delete team', err.message);
|
|
1172
1186
|
}
|
|
1173
1187
|
},
|
|
1174
1188
|
|
|
1189
|
+
async deleteTeam(id) {
|
|
1190
|
+
return get().archiveTeam(id);
|
|
1191
|
+
},
|
|
1192
|
+
|
|
1175
1193
|
reorderTeams(fromIndex, toIndex) {
|
|
1176
1194
|
const teams = [...get().teams];
|
|
1177
1195
|
const [moved] = teams.splice(fromIndex, 1);
|
|
@@ -1180,6 +1198,33 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1180
1198
|
try { localStorage.setItem('groove:teamOrder', JSON.stringify(teams.map((t) => t.id))); } catch {}
|
|
1181
1199
|
},
|
|
1182
1200
|
|
|
1201
|
+
async fetchArchivedTeams() {
|
|
1202
|
+
try {
|
|
1203
|
+
const data = await api.get('/teams/archived');
|
|
1204
|
+
set({ archivedTeams: data.archived || data.teams || [] });
|
|
1205
|
+
} catch { /* endpoint may not exist yet */ }
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
async restoreTeam(archivedId) {
|
|
1209
|
+
try {
|
|
1210
|
+
await api.post(`/teams/archived/${encodeURIComponent(archivedId)}/restore`);
|
|
1211
|
+
get().addToast('success', 'Team restored');
|
|
1212
|
+
get().fetchArchivedTeams();
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
get().addToast('error', 'Failed to restore team', err.message);
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
|
|
1218
|
+
async purgeTeam(archivedId) {
|
|
1219
|
+
try {
|
|
1220
|
+
await api.delete(`/teams/archived/${encodeURIComponent(archivedId)}`);
|
|
1221
|
+
get().addToast('info', 'Archived team permanently deleted');
|
|
1222
|
+
get().fetchArchivedTeams();
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
get().addToast('error', 'Failed to purge team', err.message);
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
|
|
1183
1228
|
async cloneTeam(id) {
|
|
1184
1229
|
const team = get().teams.find((t) => t.id === id);
|
|
1185
1230
|
if (!team) return;
|
|
@@ -1868,7 +1913,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1868
1913
|
|
|
1869
1914
|
async connectTunnel(id) {
|
|
1870
1915
|
const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
|
|
1871
|
-
set({ activeTunnelId: id });
|
|
1872
1916
|
get().fetchTunnels();
|
|
1873
1917
|
if (result.localPort && result.name) {
|
|
1874
1918
|
if (window.groove?.remote?.openWindow) {
|
|
@@ -1887,7 +1931,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1887
1931
|
async disconnectTunnel(id) {
|
|
1888
1932
|
const tunnel = get().savedTunnels.find(t => t.id === id);
|
|
1889
1933
|
await api.post(`/tunnels/${encodeURIComponent(id)}/disconnect`);
|
|
1890
|
-
set({ activeTunnelId: null });
|
|
1891
1934
|
get().fetchTunnels();
|
|
1892
1935
|
if (tunnel?.localPort && window.groove?.remote?.closeByPort) {
|
|
1893
1936
|
window.groove.remote.closeByPort(tunnel.localPort);
|
|
@@ -2510,6 +2553,8 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2510
2553
|
try {
|
|
2511
2554
|
const data = await api.get(`/files/tree?path=${encodeURIComponent(dirPath)}`);
|
|
2512
2555
|
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: data.entries || [] } }));
|
|
2556
|
+
const ws = get().ws;
|
|
2557
|
+
if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watchdir', path: dirPath }));
|
|
2513
2558
|
} catch (err) {
|
|
2514
2559
|
console.error('[file-tree] fetchTreeDir failed for', dirPath, err.message);
|
|
2515
2560
|
set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: [] } }));
|
|
@@ -10,11 +10,12 @@ 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';
|
|
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,15 +85,18 @@ 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
|
|
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);
|
|
93
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
91
94
|
|
|
92
95
|
const [creating, setCreating] = useState(false);
|
|
93
96
|
const [newName, setNewName] = useState('');
|
|
94
97
|
const [renamingId, setRenamingId] = useState(null);
|
|
95
98
|
const [renameValue, setRenameValue] = useState('');
|
|
99
|
+
const [archiveConfirm, setArchiveConfirm] = useState(null);
|
|
96
100
|
const submitting = useRef(false);
|
|
97
101
|
const [dragId, setDragId] = useState(null);
|
|
98
102
|
const [dragOverId, setDragOverId] = useState(null);
|
|
@@ -247,8 +251,15 @@ export function TeamTabBar() {
|
|
|
247
251
|
<Copy size={12} /> Clone
|
|
248
252
|
</ContextMenuItem>
|
|
249
253
|
<ContextMenuSeparator />
|
|
250
|
-
<ContextMenuItem danger onSelect={() =>
|
|
251
|
-
|
|
254
|
+
<ContextMenuItem danger onSelect={() => {
|
|
255
|
+
const teamAgents = agents.filter((a) => a.teamId === team.id);
|
|
256
|
+
if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
|
|
257
|
+
addToast('error', 'Stop running agents first');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setArchiveConfirm(team);
|
|
261
|
+
}}>
|
|
262
|
+
<Trash2 size={12} /> {team.isDefault ? 'Wipe' : 'Archive'}
|
|
252
263
|
</ContextMenuItem>
|
|
253
264
|
</ContextMenuContent>
|
|
254
265
|
</ContextMenu>
|
|
@@ -295,6 +306,14 @@ export function TeamTabBar() {
|
|
|
295
306
|
<ChevronRight size={14} />
|
|
296
307
|
</button>
|
|
297
308
|
)}
|
|
309
|
+
|
|
310
|
+
<TeamRemovalDialog
|
|
311
|
+
team={archiveConfirm}
|
|
312
|
+
open={!!archiveConfirm}
|
|
313
|
+
onOpenChange={(open) => !open && setArchiveConfirm(null)}
|
|
314
|
+
onArchive={archiveTeam}
|
|
315
|
+
onDeletePermanently={deleteTeamPermanently}
|
|
316
|
+
/>
|
|
298
317
|
</div>
|
|
299
318
|
);
|
|
300
319
|
}
|
|
@@ -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">
|