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.
Files changed (70) hide show
  1. package/CENTRAL_COMMAND_REBUILD.md +689 -0
  2. package/EMBEDDING_DIAGNOSTIC.md +197 -0
  3. package/TRAINING_DATA_v4.md +6 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +27 -2
  8. package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
  9. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  10. package/node_modules/@groove-dev/daemon/src/process.js +254 -208
  11. package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
  18. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
  20. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
  22. package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
  23. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
  24. package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/cli/src/commands/team.js +59 -2
  28. package/packages/daemon/package.json +1 -1
  29. package/packages/daemon/src/api.js +27 -2
  30. package/packages/daemon/src/filewatcher.js +45 -0
  31. package/packages/daemon/src/index.js +14 -2
  32. package/packages/daemon/src/process.js +254 -208
  33. package/packages/daemon/src/teams.js +143 -20
  34. package/packages/daemon/src/tunnel-manager.js +78 -45
  35. package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
  36. package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
  40. package/packages/gui/src/components/layout/status-bar.jsx +43 -45
  41. package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
  42. package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  43. package/packages/gui/src/stores/groove.js +57 -12
  44. package/packages/gui/src/views/agents.jsx +23 -4
  45. package/packages/gui/src/views/editor.jsx +1 -20
  46. package/packages/gui/src/views/teams.jsx +84 -5
  47. package/TRAINING_DATA_v3.md +0 -11
  48. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
  49. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
  50. package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
  51. package/codex-test/offroad-nitro-racer/index.html +0 -21
  52. package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
  53. package/codex-test/offroad-nitro-racer/package.json +0 -15
  54. package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
  55. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
  56. package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
  57. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
  58. package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
  59. package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
  60. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
  61. package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
  62. package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
  63. package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
  64. package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
  65. package/codex-test/offroad-nitro-racer/src/style.css +0 -291
  66. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
  67. package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
  68. package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
  69. package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
  70. 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-BYh6iHqL.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-DAlSbVyK.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.113",
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",
@@ -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, Shield } from 'lucide-react';
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 activeTunnel = savedTunnels.find((t) => t.active);
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
- {activeTunnel ? (
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
- ) : savedTunnels.length > 0 && (
98
- <button
99
- onClick={() => useGrooveStore.getState().toggleQuickConnect()}
100
- className="flex items-center gap-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
101
- title="Quick Connect to remote server"
102
- >
103
- <Plug size={10} />
104
- <span>Connect</span>
105
- </button>
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
- toggle();
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 deleteTeam(id) {
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
- // WS team:deleted handler removes from array and switches activeTeamId.
1166
- // Deleting the default team regenerates a fresh one server-side; the
1167
- // team:created event arrives separately so the list stays populated.
1168
- const wiped = team?.isDefault ? 'wiped' : 'deleted';
1169
- get().addToast('info', `Team "${team?.name}" ${wiped}`);
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 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);
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={() => deleteTeam(team.id)}>
251
- <Trash2 size={12} /> {team.isDefault ? 'Wipe' : 'Delete'}
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, AlertTriangle, RefreshCw, X, Eye, FileCode, PanelLeftOpen } from 'lucide-react';
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">