groove-dev 0.27.169 → 0.27.172
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/routes/files.js +18 -5
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- 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/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/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/routes/files.js +18 -5
- package/packages/daemon/src/tunnel-manager.js +16 -6
- package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/packages/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- 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/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-BvXojcnr.css +0 -1
- package/packages/gui/dist/assets/index-BvXojcnr.css +0 -1
|
@@ -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
|
</>
|