groove-dev 0.27.168 → 0.27.171
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/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/index.js +15 -2
- package/node_modules/@groove-dev/daemon/src/process.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +7 -1
- package/node_modules/@groove-dev/daemon/src/registry.js +3 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
- package/node_modules/@groove-dev/daemon/src/state.js +7 -2
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
- package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BsCp-oqa.js +1030 -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/agent-feed.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
- package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +30 -1
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +64 -42
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/index.js +15 -2
- package/packages/daemon/src/process.js +1 -1
- package/packages/daemon/src/providers/claude-code.js +7 -1
- package/packages/daemon/src/registry.js +3 -0
- package/packages/daemon/src/routes/files.js +18 -5
- package/packages/daemon/src/state.js +7 -2
- package/packages/daemon/src/tunnel-manager.js +16 -6
- package/packages/daemon/src/validate.js +1 -0
- package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/packages/gui/dist/assets/index-BsCp-oqa.js +1030 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +6 -0
- package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
- package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/packages/gui/src/components/agents/spawn-wizard.jsx +30 -1
- package/packages/gui/src/components/editor/terminal.jsx +64 -42
- package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
- package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/packages/gui/src/views/agents.jsx +2 -11
- package/packages/gui/src/views/editor.jsx +63 -2
- package/packages/gui/src/views/settings.jsx +2 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BJVNpGIp.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CSMIQsrG.js +0 -1025
- package/packages/gui/dist/assets/index-BJVNpGIp.css +0 -1
- package/packages/gui/dist/assets/index-CSMIQsrG.js +0 -1025
- package/ssh/error.png +0 -0
- package/terminal/Screenshot_2026-05-19_at_12.20.15_PM.png +0 -0
|
@@ -75,7 +75,10 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
75
75
|
onSelectionChange?.(text || '');
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// ── Spawn logic ──────────────────────────────────────────────
|
|
78
79
|
let spawnAttempts = 0;
|
|
80
|
+
let outputReady = false;
|
|
81
|
+
|
|
79
82
|
function trySpawn() {
|
|
80
83
|
spawnAttempts++;
|
|
81
84
|
const ws = useGrooveStore.getState().ws;
|
|
@@ -85,7 +88,6 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const requestId = `spawn-${++spawnSeq}`;
|
|
88
|
-
const initialCols = term.cols;
|
|
89
91
|
ws.send(JSON.stringify({ type: 'terminal:spawn', cols: term.cols, rows: term.rows, requestId }));
|
|
90
92
|
|
|
91
93
|
function onMessage(event) {
|
|
@@ -94,28 +96,31 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
94
96
|
|
|
95
97
|
if (msg.type === 'terminal:spawned' && msg.requestId === requestId && !termIdRef.current) {
|
|
96
98
|
termIdRef.current = msg.id;
|
|
99
|
+
// Give the shell time to start and the layout to fully settle,
|
|
100
|
+
// then correct dimensions, wipe any garbled output, and redraw.
|
|
97
101
|
setTimeout(() => {
|
|
98
|
-
try {
|
|
102
|
+
try { fitAddon.fit(); } catch {}
|
|
99
103
|
const c = term.cols, r = term.rows;
|
|
100
104
|
if (c > 1 && r > 1 && termIdRef.current) {
|
|
101
105
|
const w = useGrooveStore.getState().ws;
|
|
102
106
|
if (w?.readyState === WebSocket.OPEN) {
|
|
103
107
|
w.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
|
|
104
108
|
lastSizeRef.current = { cols: c, rows: r };
|
|
105
|
-
if (c !== initialCols) {
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
term.clear();
|
|
108
|
-
if (w.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
109
|
-
w.send(JSON.stringify({ type: 'terminal:input', id: termIdRef.current, data: '\x0c' }));
|
|
110
|
-
}
|
|
111
|
-
}, 80);
|
|
112
|
-
}
|
|
113
109
|
}
|
|
114
110
|
}
|
|
115
|
-
|
|
111
|
+
// Wipe xterm so garbled output from wrong-sized PTY is never visible
|
|
112
|
+
term.reset();
|
|
113
|
+
outputReady = true;
|
|
114
|
+
// Ask the shell to clear screen and redraw its prompt
|
|
115
|
+
const w2 = useGrooveStore.getState().ws;
|
|
116
|
+
if (w2?.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
117
|
+
w2.send(JSON.stringify({ type: 'terminal:input', id: termIdRef.current, data: '\x0c' }));
|
|
118
|
+
}
|
|
119
|
+
}, 300);
|
|
116
120
|
} else if (msg.type === 'terminal:output' && msg.id === termIdRef.current) {
|
|
117
|
-
term.write(msg.data);
|
|
121
|
+
if (outputReady) term.write(msg.data);
|
|
118
122
|
} else if (msg.type === 'terminal:exit' && msg.id === termIdRef.current) {
|
|
123
|
+
outputReady = true;
|
|
119
124
|
term.write('\r\n\x1b[90m[session ended]\x1b[0m\r\n');
|
|
120
125
|
termIdRef.current = null;
|
|
121
126
|
}
|
|
@@ -131,59 +136,86 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
131
136
|
}
|
|
132
137
|
});
|
|
133
138
|
|
|
139
|
+
// Debounce resize messages so rapid drag doesn't flood
|
|
140
|
+
// the PTY with SIGWINCHs that cause staircase redraws
|
|
141
|
+
let resizeTimer = null;
|
|
134
142
|
term.onResize(({ cols, rows }) => {
|
|
135
143
|
if (cols === lastSizeRef.current.cols && rows === lastSizeRef.current.rows) return;
|
|
136
144
|
if (cols < 2 || rows < 2) return;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
lastSizeRef.current = { cols, rows };
|
|
146
|
+
clearTimeout(resizeTimer);
|
|
147
|
+
resizeTimer = setTimeout(() => {
|
|
148
|
+
const { cols: c, rows: r } = lastSizeRef.current;
|
|
149
|
+
const ws = useGrooveStore.getState().ws;
|
|
150
|
+
if (ws?.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
151
|
+
ws.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
|
|
152
|
+
}
|
|
153
|
+
}, 150);
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
// ── Deferred spawn — wait for container to be visible & stable ──
|
|
145
158
|
let hasSpawned = false;
|
|
159
|
+
let settleTimer = null;
|
|
160
|
+
|
|
146
161
|
function tryInitSpawn() {
|
|
147
162
|
if (hasSpawned) return;
|
|
148
|
-
|
|
163
|
+
const el = containerRef.current;
|
|
164
|
+
// Container must have meaningful pixel dimensions (not hidden/mid-layout)
|
|
165
|
+
if (!el || el.offsetWidth < 200 || el.offsetHeight < 50) {
|
|
166
|
+
clearTimeout(settleTimer);
|
|
167
|
+
settleTimer = null;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Already waiting for settle
|
|
171
|
+
if (settleTimer) return;
|
|
172
|
+
// Wait 200ms for layout to fully stabilize before spawning
|
|
173
|
+
settleTimer = setTimeout(() => {
|
|
174
|
+
if (hasSpawned) return;
|
|
175
|
+
const el2 = containerRef.current;
|
|
176
|
+
if (!el2 || el2.offsetWidth < 200 || el2.offsetHeight < 50) {
|
|
177
|
+
settleTimer = null;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
149
180
|
hasSpawned = true;
|
|
181
|
+
try { fitAddon.fit(); } catch {}
|
|
150
182
|
trySpawn();
|
|
151
|
-
}
|
|
183
|
+
}, 200);
|
|
152
184
|
}
|
|
153
185
|
|
|
154
|
-
requestAnimationFrame(
|
|
155
|
-
try { fitAddon.fit(); } catch {}
|
|
156
|
-
requestAnimationFrame(() => {
|
|
157
|
-
try { fitAddon.fit(); } catch {}
|
|
158
|
-
tryInitSpawn();
|
|
159
|
-
});
|
|
160
|
-
});
|
|
186
|
+
requestAnimationFrame(tryInitSpawn);
|
|
161
187
|
|
|
162
|
-
|
|
188
|
+
// Absolute fallback — if observer never fires, spawn after 5s
|
|
189
|
+
const fallback = setTimeout(() => {
|
|
163
190
|
if (!hasSpawned) {
|
|
164
|
-
try { fitAddon.fit(); } catch {}
|
|
165
191
|
hasSpawned = true;
|
|
192
|
+
try { fitAddon.fit(); } catch {}
|
|
166
193
|
trySpawn();
|
|
167
194
|
}
|
|
168
|
-
},
|
|
195
|
+
}, 5000);
|
|
169
196
|
|
|
170
197
|
const observer = new ResizeObserver(() => {
|
|
171
198
|
if (!visibleRef.current) return;
|
|
172
199
|
requestAnimationFrame(() => {
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
if (!hasSpawned) {
|
|
201
|
+
tryInitSpawn();
|
|
202
|
+
} else {
|
|
203
|
+
try { fitAddon.fit(); } catch {}
|
|
204
|
+
}
|
|
175
205
|
});
|
|
176
206
|
});
|
|
177
207
|
observer.observe(containerRef.current);
|
|
178
208
|
|
|
179
209
|
return () => {
|
|
180
|
-
clearTimeout(
|
|
210
|
+
clearTimeout(settleTimer);
|
|
211
|
+
clearTimeout(fallback);
|
|
181
212
|
observer.disconnect();
|
|
182
213
|
if (handlerRef.current) {
|
|
183
214
|
handlerRef.current.ws.removeEventListener('message', handlerRef.current.handler);
|
|
184
215
|
}
|
|
185
216
|
term.dispose();
|
|
186
217
|
fitRef.current = null;
|
|
218
|
+
termRef.current = null;
|
|
187
219
|
mountedRef.current = false;
|
|
188
220
|
};
|
|
189
221
|
}, []);
|
|
@@ -193,16 +225,6 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
193
225
|
if (visible && fitRef.current) {
|
|
194
226
|
requestAnimationFrame(() => {
|
|
195
227
|
try { fitRef.current.fit(); } catch {}
|
|
196
|
-
if (termRef.current && termIdRef.current) {
|
|
197
|
-
const c = termRef.current.cols, r = termRef.current.rows;
|
|
198
|
-
if (c > 1 && r > 1) {
|
|
199
|
-
const ws = useGrooveStore.getState().ws;
|
|
200
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
201
|
-
ws.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
|
|
202
|
-
lastSizeRef.current = { cols: c, rows: r };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
228
|
});
|
|
207
229
|
}
|
|
208
230
|
}, [visible]);
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useRef, useCallback, useState } from 'react';
|
|
2
|
+
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
3
3
|
import { LayoutList } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { FleetPane } from './fleet-pane';
|
|
6
|
+
import { RecommendedTeamCard } from '../agents/recommended-team-card';
|
|
6
7
|
|
|
7
8
|
export function FleetContent() {
|
|
9
|
+
const allAgents = useGrooveStore((s) => s.agents);
|
|
10
|
+
const checkRecommendedTeam = useGrooveStore((s) => s.checkRecommendedTeam);
|
|
11
|
+
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const hasPlanner = allAgents.some((a) => a.role === 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
15
|
+
if (!hasPlanner) return;
|
|
16
|
+
const interval = setInterval(() => checkRecommendedTeam(), 5000);
|
|
17
|
+
return () => clearInterval(interval);
|
|
18
|
+
}, [allAgents, checkRecommendedTeam]);
|
|
8
19
|
const rawSelected = useGrooveStore((s) => s.fleetSelectedAgents);
|
|
9
20
|
const lastSelectedRef = useRef(rawSelected);
|
|
10
21
|
if (rawSelected[0] || rawSelected[1]) lastSelectedRef.current = rawSelected;
|
|
@@ -67,7 +78,7 @@ export function FleetContent() {
|
|
|
67
78
|
if (!selected[0] && !selected[1]) {
|
|
68
79
|
return (
|
|
69
80
|
<div
|
|
70
|
-
className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3"
|
|
81
|
+
className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 relative"
|
|
71
82
|
onDragOver={(e) => handleDragOver(e, 'left')}
|
|
72
83
|
onDragLeave={handleDragLeave}
|
|
73
84
|
onDrop={(e) => handleDrop(e, 0)}
|
|
@@ -75,13 +86,15 @@ export function FleetContent() {
|
|
|
75
86
|
<LayoutList size={32} strokeWidth={1} className="text-text-4" />
|
|
76
87
|
<p className="text-sm font-sans">Select an agent or drag one here</p>
|
|
77
88
|
<p className="text-xs font-sans text-text-4">Cmd+Click or drag to open side-by-side</p>
|
|
89
|
+
<RecommendedTeamCard />
|
|
78
90
|
</div>
|
|
79
91
|
);
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
if (!splitMode || !selected[1]) {
|
|
83
95
|
return (
|
|
84
|
-
<div className="flex-1 flex min-w-0 min-h-0">
|
|
96
|
+
<div className="flex-1 flex min-w-0 min-h-0 relative">
|
|
97
|
+
<RecommendedTeamCard />
|
|
85
98
|
<div className="flex-1 min-w-0 min-h-0">
|
|
86
99
|
<FleetPane agentId={selected[0]} paneIndex={0} />
|
|
87
100
|
</div>
|
|
@@ -107,7 +120,8 @@ export function FleetContent() {
|
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
return (
|
|
110
|
-
<div className="flex-1 flex min-w-0 min-h-0">
|
|
123
|
+
<div className="flex-1 flex min-w-0 min-h-0 relative">
|
|
124
|
+
<RecommendedTeamCard />
|
|
111
125
|
<div
|
|
112
126
|
ref={leftRef}
|
|
113
127
|
className={`min-w-0 min-h-0 ${dropTarget === 'left' ? 'ring-2 ring-inset ring-accent/30' : ''}`}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useRef, useCallback, useMemo, useState } from 'react';
|
|
3
|
-
import { Search, X, ChevronRight, Plus, Trash2 } from 'lucide-react';
|
|
3
|
+
import { Search, X, ChevronRight, Plus, Trash2, Pencil, Users, Check } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { FleetAgentRow } from './fleet-agent-row';
|
|
@@ -21,9 +21,16 @@ export function FleetSidebar({ width }) {
|
|
|
21
21
|
const toggleCollapsed = useGrooveStore((s) => s.fleetToggleTeamCollapsed);
|
|
22
22
|
const setSidebarWidth = useGrooveStore((s) => s.fleetSetSidebarWidth);
|
|
23
23
|
const deleteTeam = useGrooveStore((s) => s.deleteTeam);
|
|
24
|
+
const createTeam = useGrooveStore((s) => s.createTeam);
|
|
25
|
+
const renameTeam = useGrooveStore((s) => s.renameTeam);
|
|
24
26
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
27
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
25
28
|
|
|
26
29
|
const [confirmDeleteTeam, setConfirmDeleteTeam] = useState(null);
|
|
30
|
+
const [renamingTeamId, setRenamingTeamId] = useState(null);
|
|
31
|
+
const [renameValue, setRenameValue] = useState('');
|
|
32
|
+
const [creatingTeam, setCreatingTeam] = useState(false);
|
|
33
|
+
const [newTeamName, setNewTeamName] = useState('');
|
|
27
34
|
|
|
28
35
|
const dragging = useRef(false);
|
|
29
36
|
const startX = useRef(0);
|
|
@@ -99,6 +106,31 @@ export function FleetSidebar({ width }) {
|
|
|
99
106
|
openDetail({ type: 'spawn', presetTeamId: teamId });
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
async function handleCreateTeam() {
|
|
110
|
+
const name = newTeamName.trim();
|
|
111
|
+
if (!name) return;
|
|
112
|
+
try {
|
|
113
|
+
await createTeam(name);
|
|
114
|
+
setNewTeamName('');
|
|
115
|
+
setCreatingTeam(false);
|
|
116
|
+
} catch { /* toast handles */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleRename(teamId) {
|
|
120
|
+
const name = renameValue.trim();
|
|
121
|
+
if (!name) { setRenamingTeamId(null); return; }
|
|
122
|
+
try {
|
|
123
|
+
await renameTeam(teamId, name);
|
|
124
|
+
} catch { /* toast handles */ }
|
|
125
|
+
setRenamingTeamId(null);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function startRename(e, team) {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
setRenamingTeamId(team.id);
|
|
131
|
+
setRenameValue(team.name);
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
return (
|
|
103
135
|
<div
|
|
104
136
|
className="flex-shrink-0 flex flex-col bg-surface-1 border-r border-border relative h-full"
|
|
@@ -141,49 +173,73 @@ export function FleetSidebar({ width }) {
|
|
|
141
173
|
'w-full flex items-center gap-1 px-2 py-1.5 rounded-md hover:bg-surface-2 transition-colors group',
|
|
142
174
|
isConfirming && 'bg-danger/10 hover:bg-danger/20',
|
|
143
175
|
)}>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
{renamingTeamId === team.id ? (
|
|
177
|
+
<div className="flex items-center gap-1.5 flex-1 min-w-0 pl-1">
|
|
178
|
+
<input
|
|
179
|
+
value={renameValue}
|
|
180
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
181
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleRename(team.id); if (e.key === 'Escape') setRenamingTeamId(null); }}
|
|
182
|
+
autoFocus
|
|
183
|
+
className="flex-1 min-w-0 h-6 px-1.5 text-xs bg-surface-3 border border-accent/40 rounded text-text-0 font-sans focus:outline-none"
|
|
184
|
+
/>
|
|
185
|
+
<button onClick={() => handleRename(team.id)} className="p-0.5 text-accent cursor-pointer"><Check size={12} /></button>
|
|
186
|
+
<button onClick={() => setRenamingTeamId(null)} className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"><X size={12} /></button>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
<button
|
|
191
|
+
onClick={() => toggleCollapsed(team.id)}
|
|
192
|
+
onDoubleClick={(e) => startRename(e, team)}
|
|
193
|
+
className="flex items-center gap-1.5 flex-1 min-w-0 cursor-pointer"
|
|
194
|
+
>
|
|
195
|
+
<ChevronRight
|
|
196
|
+
size={14}
|
|
197
|
+
className={cn(
|
|
198
|
+
'text-text-4 transition-transform flex-shrink-0',
|
|
199
|
+
!isCollapsed && 'rotate-90',
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
<span className={cn(
|
|
203
|
+
'text-xs font-medium font-sans truncate text-left',
|
|
204
|
+
isConfirming ? 'text-danger' : 'text-text-1',
|
|
205
|
+
)}>
|
|
206
|
+
{isConfirming ? 'Click again to delete' : team.name}
|
|
207
|
+
</span>
|
|
208
|
+
</button>
|
|
209
|
+
|
|
210
|
+
{/* Hover actions + meta — stacked in same space */}
|
|
211
|
+
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
212
|
+
<button
|
|
213
|
+
onClick={(e) => startRename(e, team)}
|
|
214
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 rounded text-text-4 hover:text-accent transition-opacity cursor-pointer"
|
|
215
|
+
title="Rename team"
|
|
216
|
+
>
|
|
217
|
+
<Pencil size={11} />
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
onClick={(e) => handleSpawnToTeam(e, team.id)}
|
|
221
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 rounded text-text-4 hover:text-accent transition-opacity cursor-pointer"
|
|
222
|
+
title="Spawn agent to team"
|
|
223
|
+
>
|
|
224
|
+
<Plus size={14} />
|
|
225
|
+
</button>
|
|
226
|
+
<button
|
|
227
|
+
onClick={(e) => handleDeleteTeam(e, team.id)}
|
|
228
|
+
className={cn(
|
|
229
|
+
'opacity-0 group-hover:opacity-100 p-0.5 rounded transition-opacity cursor-pointer',
|
|
230
|
+
isConfirming ? 'text-danger' : 'text-text-4 hover:text-danger',
|
|
231
|
+
)}
|
|
232
|
+
title="Delete team"
|
|
233
|
+
>
|
|
234
|
+
<Trash2 size={12} />
|
|
235
|
+
</button>
|
|
236
|
+
<span className="group-hover:opacity-0 text-2xs text-text-4 font-mono transition-opacity">
|
|
237
|
+
{allTeamAgents.length}
|
|
238
|
+
</span>
|
|
239
|
+
<span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', teamStatusDot(allTeamAgents))} />
|
|
240
|
+
</div>
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
187
243
|
</div>
|
|
188
244
|
|
|
189
245
|
{/* Agent rows */}
|
|
@@ -206,6 +262,31 @@ export function FleetSidebar({ width }) {
|
|
|
206
262
|
)}
|
|
207
263
|
</div>
|
|
208
264
|
|
|
265
|
+
{/* Create team */}
|
|
266
|
+
<div className="px-2.5 py-2 border-t border-border-subtle flex-shrink-0">
|
|
267
|
+
{creatingTeam ? (
|
|
268
|
+
<div className="flex items-center gap-1.5">
|
|
269
|
+
<input
|
|
270
|
+
value={newTeamName}
|
|
271
|
+
onChange={(e) => setNewTeamName(e.target.value)}
|
|
272
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleCreateTeam(); if (e.key === 'Escape') { setCreatingTeam(false); setNewTeamName(''); } }}
|
|
273
|
+
placeholder="Team name..."
|
|
274
|
+
autoFocus
|
|
275
|
+
className="flex-1 min-w-0 h-7 px-2 text-xs bg-surface-3 border border-accent/40 rounded text-text-0 font-sans placeholder:text-text-4 focus:outline-none"
|
|
276
|
+
/>
|
|
277
|
+
<button onClick={handleCreateTeam} disabled={!newTeamName.trim()} className="p-1 text-accent cursor-pointer disabled:opacity-30"><Check size={13} /></button>
|
|
278
|
+
<button onClick={() => { setCreatingTeam(false); setNewTeamName(''); }} className="p-1 text-text-4 hover:text-text-1 cursor-pointer"><X size={13} /></button>
|
|
279
|
+
</div>
|
|
280
|
+
) : (
|
|
281
|
+
<button
|
|
282
|
+
onClick={() => setCreatingTeam(true)}
|
|
283
|
+
className="flex items-center gap-1.5 text-xs text-text-3 hover:text-accent font-sans font-medium cursor-pointer transition-colors"
|
|
284
|
+
>
|
|
285
|
+
<Users size={12} /> New Team
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
209
290
|
{/* Resize handle */}
|
|
210
291
|
<div
|
|
211
292
|
className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
@@ -140,13 +140,13 @@ export function BreadcrumbBar({
|
|
|
140
140
|
const [instanceName, setInstanceName] = useState(null);
|
|
141
141
|
|
|
142
142
|
useEffect(() => {
|
|
143
|
-
|
|
143
|
+
const urlParam = new URLSearchParams(window.location.search).get('instance');
|
|
144
|
+
if (urlParam) {
|
|
145
|
+
setInstanceName(urlParam);
|
|
146
|
+
} else if (window.groove?.getInstanceInfo) {
|
|
144
147
|
window.groove.getInstanceInfo().then(info => {
|
|
145
148
|
if (info?.name) setInstanceName(info.name);
|
|
146
149
|
});
|
|
147
|
-
} else {
|
|
148
|
-
const param = new URLSearchParams(window.location.search).get('instance');
|
|
149
|
-
if (param) setInstanceName(param);
|
|
150
150
|
}
|
|
151
151
|
}, []);
|
|
152
152
|
|
|
@@ -17,12 +17,14 @@ export function QuickConnect() {
|
|
|
17
17
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
18
18
|
const tunnelStep = useGrooveStore((s) => s.tunnelConnectStep);
|
|
19
19
|
const [connectingId, setConnectingId] = useState(null);
|
|
20
|
+
const [openingServer, setOpeningServer] = useState(null);
|
|
20
21
|
const [showWizard, setShowWizard] = useState(false);
|
|
21
22
|
const wizardTunnelId = useRef(null);
|
|
22
23
|
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
if (open) {
|
|
25
26
|
setShowWizard(false);
|
|
27
|
+
setOpeningServer(null);
|
|
26
28
|
useGrooveStore.getState().fetchTunnels();
|
|
27
29
|
}
|
|
28
30
|
}, [open]);
|
|
@@ -34,14 +36,15 @@ export function QuickConnect() {
|
|
|
34
36
|
try {
|
|
35
37
|
await useGrooveStore.getState().connectTunnel(id);
|
|
36
38
|
const tunnel = savedTunnels.find((t) => t.id === id);
|
|
39
|
+
setConnectingId(null);
|
|
40
|
+
setOpeningServer({ name: tunnel?.name || 'Remote' });
|
|
37
41
|
if (tunnel?.host) {
|
|
38
42
|
addToast('info', `Add ${tunnel.host} to Federation Whitelist?`, '', {
|
|
39
43
|
label: 'Add',
|
|
40
44
|
onClick: () => useGrooveStore.getState().addToWhitelist(tunnel.host),
|
|
41
45
|
});
|
|
42
46
|
}
|
|
43
|
-
|
|
44
|
-
toggle();
|
|
47
|
+
setTimeout(() => { setOpeningServer(null); toggle(); }, 4000);
|
|
45
48
|
return;
|
|
46
49
|
} catch (err) {
|
|
47
50
|
const detail = err?.message || 'Unknown error';
|
|
@@ -90,32 +93,55 @@ export function QuickConnect() {
|
|
|
90
93
|
exit={{ opacity: 0, y: -10, scale: 0.98 }}
|
|
91
94
|
transition={{ duration: 0.15 }}
|
|
92
95
|
className={cn(
|
|
93
|
-
'fixed top-
|
|
94
|
-
showWizard ? 'w-[
|
|
96
|
+
'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-[#24282f] border border-[#2c313a] rounded-lg shadow-2xl overflow-hidden',
|
|
97
|
+
showWizard ? 'w-[680px]' : 'w-[480px]',
|
|
95
98
|
)}
|
|
96
99
|
>
|
|
97
100
|
{/* Header */}
|
|
98
|
-
<div className="flex items-center justify-between px-
|
|
99
|
-
<div className="flex items-center gap-
|
|
101
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-[#2c313a]">
|
|
102
|
+
<div className="flex items-center gap-3">
|
|
100
103
|
{showWizard && (
|
|
101
104
|
<button
|
|
102
105
|
onClick={() => setShowWizard(false)}
|
|
103
|
-
className="p-1 -ml-1 text-
|
|
106
|
+
className="p-1.5 -ml-1 text-[#6e7681] hover:text-[#e6e8ed] cursor-pointer transition-colors"
|
|
104
107
|
>
|
|
105
|
-
<ArrowLeft size={
|
|
108
|
+
<ArrowLeft size={16} />
|
|
106
109
|
</button>
|
|
107
110
|
)}
|
|
108
|
-
<Radio size={
|
|
109
|
-
<span className="text-
|
|
111
|
+
<Radio size={17} className="text-[#33afbc]" />
|
|
112
|
+
<span className="text-base font-semibold text-[#e6e8ed]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif", letterSpacing: '-0.2px' }}>
|
|
110
113
|
{showWizard ? (wizardTunnelId.current ? 'Connection Setup' : 'Add Connection') : 'Quick Connect'}
|
|
111
114
|
</span>
|
|
112
115
|
</div>
|
|
113
|
-
<button onClick={handleClose} className="p-1 text-
|
|
114
|
-
<X size={
|
|
116
|
+
<button onClick={handleClose} className="p-1.5 text-[#6e7681] hover:text-[#e6e8ed] cursor-pointer transition-colors">
|
|
117
|
+
<X size={16} />
|
|
115
118
|
</button>
|
|
116
119
|
</div>
|
|
117
120
|
|
|
118
|
-
{
|
|
121
|
+
{openingServer ? (
|
|
122
|
+
<div className="px-6 py-12 text-center">
|
|
123
|
+
<div className="relative w-14 h-14 mx-auto mb-5">
|
|
124
|
+
<span className="absolute inset-0 rounded-full border-2 border-[#33afbc]/20 animate-ping" style={{ animationDuration: '2s' }} />
|
|
125
|
+
<span className="absolute inset-0 rounded-full border-2 border-transparent border-t-[#33afbc] animate-spin" style={{ animationDuration: '1s' }} />
|
|
126
|
+
<span className="absolute inset-[6px] rounded-full bg-[#33afbc]/8 flex items-center justify-center">
|
|
127
|
+
<Server size={18} className="text-[#33afbc]" />
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<p className="text-base font-semibold text-[#e6e8ed] mb-1.5" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}>
|
|
131
|
+
Opening {openingServer.name}
|
|
132
|
+
</p>
|
|
133
|
+
<p className="text-sm text-[#6e7681]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}>
|
|
134
|
+
Loading remote dashboard...
|
|
135
|
+
</p>
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => { setOpeningServer(null); toggle(); }}
|
|
138
|
+
className="mt-6 text-xs text-[#6e7681] hover:text-[#e6e8ed] cursor-pointer transition-colors"
|
|
139
|
+
style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}
|
|
140
|
+
>
|
|
141
|
+
Dismiss
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
) : showWizard ? (
|
|
119
145
|
<SSHWizard
|
|
120
146
|
server={wizardTunnelId.current ? savedTunnels.find((t) => t.id === wizardTunnelId.current) || null : null}
|
|
121
147
|
onSave={async (data) => {
|
|
@@ -145,45 +171,49 @@ export function QuickConnect() {
|
|
|
145
171
|
) : (
|
|
146
172
|
<>
|
|
147
173
|
{/* Server list */}
|
|
148
|
-
<div className="overflow-y-auto max-h-[
|
|
174
|
+
<div className="overflow-y-auto max-h-[400px] py-2">
|
|
149
175
|
{savedTunnels.length === 0 ? (
|
|
150
|
-
<div className="px-
|
|
151
|
-
<Server size={
|
|
152
|
-
<p className="text-
|
|
153
|
-
<p className="text-
|
|
154
|
-
<
|
|
155
|
-
variant="primary"
|
|
156
|
-
size="sm"
|
|
176
|
+
<div className="px-6 py-10 text-center">
|
|
177
|
+
<Server size={32} className="text-[#6e7681] mx-auto mb-3" />
|
|
178
|
+
<p className="text-base font-semibold text-[#e6e8ed]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}>No saved servers</p>
|
|
179
|
+
<p className="text-xs text-[#6e7681] mt-1.5" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}>Add a connection to get started.</p>
|
|
180
|
+
<button
|
|
157
181
|
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
158
|
-
className="
|
|
182
|
+
className="inline-flex items-center gap-1.5 h-9 px-5 mt-4 rounded bg-[#33afbc] text-[#0a0c10] text-sm font-semibold cursor-pointer transition-opacity hover:opacity-90"
|
|
183
|
+
style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}
|
|
159
184
|
>
|
|
160
|
-
<Plus size={
|
|
161
|
-
</
|
|
185
|
+
<Plus size={14} /> Add Connection
|
|
186
|
+
</button>
|
|
162
187
|
</div>
|
|
163
188
|
) : (
|
|
164
189
|
savedTunnels.map((server) => (
|
|
165
190
|
<div
|
|
166
191
|
key={server.id}
|
|
167
192
|
className={cn(
|
|
168
|
-
'w-full flex items-center gap-
|
|
169
|
-
'hover:bg-
|
|
193
|
+
'w-full flex items-center gap-4 px-5 py-3.5 transition-colors',
|
|
194
|
+
'hover:bg-[#2c313a]',
|
|
170
195
|
connectingId === server.id && 'opacity-60 pointer-events-none',
|
|
171
196
|
)}
|
|
172
197
|
>
|
|
173
|
-
<
|
|
198
|
+
<div className={cn(
|
|
199
|
+
'w-10 h-10 rounded flex items-center justify-center flex-shrink-0',
|
|
200
|
+
server.active ? 'bg-[#33afbc]/10' : 'bg-[rgba(255,255,255,0.04)]',
|
|
201
|
+
)}>
|
|
202
|
+
<Server size={18} className={server.active ? 'text-[#33afbc]' : 'text-[#8b95a5]'} />
|
|
203
|
+
</div>
|
|
174
204
|
<button
|
|
175
205
|
onClick={() => server.active ? handleOpenRemote(server) : handleConnect(server.id)}
|
|
176
206
|
disabled={connectingId === server.id}
|
|
177
207
|
className="flex-1 min-w-0 text-left cursor-pointer"
|
|
178
208
|
>
|
|
179
209
|
<div className="flex items-center gap-2">
|
|
180
|
-
<span className="text-sm font-
|
|
210
|
+
<span className="text-sm font-semibold text-[#e6e8ed] truncate" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif", letterSpacing: '-0.2px' }}>{server.name}</span>
|
|
181
211
|
{server.active && <StatusDot status="running" size="sm" />}
|
|
182
212
|
{server.remoteVersion && (
|
|
183
|
-
<span className="text-
|
|
213
|
+
<span className="text-xs text-[#6e7681] ml-1" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace" }}>v{server.remoteVersion}</span>
|
|
184
214
|
)}
|
|
185
215
|
</div>
|
|
186
|
-
<span className="text-
|
|
216
|
+
<span className="text-xs text-[#6e7681]" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace" }}>{server.user}@{server.host}</span>
|
|
187
217
|
</button>
|
|
188
218
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
189
219
|
{connectingId === server.id ? (
|
|
@@ -267,12 +297,13 @@ export function QuickConnect() {
|
|
|
267
297
|
</div>
|
|
268
298
|
|
|
269
299
|
{/* Footer with Add button */}
|
|
270
|
-
<div className="px-
|
|
300
|
+
<div className="px-5 py-3.5 border-t border-[#2c313a]">
|
|
271
301
|
<button
|
|
272
302
|
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
273
|
-
className="flex items-center gap-
|
|
303
|
+
className="flex items-center gap-2 text-sm text-[#33afbc] hover:opacity-80 font-semibold cursor-pointer transition-opacity"
|
|
304
|
+
style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" }}
|
|
274
305
|
>
|
|
275
|
-
<Plus size={
|
|
306
|
+
<Plus size={14} /> Add new connection
|
|
276
307
|
</button>
|
|
277
308
|
</div>
|
|
278
309
|
</>
|