groove-dev 0.27.8 → 0.27.11

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 (124) hide show
  1. package/node_modules/@groove-dev/daemon/src/api.js +460 -25
  2. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  3. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  5. package/node_modules/@groove-dev/daemon/src/process.js +67 -7
  6. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  8. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  9. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  10. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  11. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  12. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  13. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  18. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  23. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  24. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
  25. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  27. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  28. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  29. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  30. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  32. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  33. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  34. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  35. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  36. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  37. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  38. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  39. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  40. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  41. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  43. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  44. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  45. package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
  46. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
  47. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  48. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  49. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  50. package/package.json +7 -2
  51. package/packages/daemon/src/api.js +460 -25
  52. package/packages/daemon/src/index.js +7 -0
  53. package/packages/daemon/src/introducer.js +72 -4
  54. package/packages/daemon/src/journalist.js +66 -11
  55. package/packages/daemon/src/process.js +67 -7
  56. package/packages/daemon/src/registry.js +1 -1
  57. package/packages/daemon/src/repo-import.js +541 -0
  58. package/packages/daemon/src/rotator.js +28 -1
  59. package/packages/daemon/src/supervisor.js +2 -1
  60. package/packages/daemon/src/tunnel-manager.js +504 -0
  61. package/packages/daemon/src/validate.js +13 -0
  62. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  63. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  64. package/packages/gui/dist/index.html +2 -2
  65. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  69. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  71. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  72. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  73. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  74. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  75. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  76. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  77. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  78. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  80. package/packages/gui/src/app.css +14 -0
  81. package/packages/gui/src/app.jsx +13 -0
  82. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  83. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  84. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  85. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  86. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  87. package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
  88. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  89. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  90. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  91. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  92. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  93. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  94. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  95. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  96. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  97. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  98. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  99. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  100. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  101. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  102. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  103. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  104. package/packages/gui/src/components/ui/toast.jsx +1 -1
  105. package/packages/gui/src/lib/edition.js +4 -0
  106. package/packages/gui/src/lib/electron.js +17 -0
  107. package/packages/gui/src/lib/status.js +1 -0
  108. package/packages/gui/src/stores/groove.js +139 -6
  109. package/packages/gui/src/views/dashboard.jsx +38 -39
  110. package/packages/gui/src/views/marketplace.jsx +82 -0
  111. package/packages/gui/src/views/settings.jsx +66 -0
  112. package/packages/gui/vite.config.js +3 -0
  113. package/integrations/FEDERATION_PLAN.md +0 -583
  114. package/integrations/VOICE_PLAN.md +0 -232
  115. package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
  116. package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
  117. package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
  118. package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
  119. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  120. package/test-slack.mjs +0 -28
  121. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  122. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  123. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -0,0 +1,68 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { AlertTriangle } from 'lucide-react';
4
+ import { Dialog, DialogContent } from '../ui/dialog';
5
+ import { Button } from '../ui/button';
6
+
7
+ export function RepoNukeDialog({ repo, open, onClose, onConfirm }) {
8
+ const [deleteFiles, setDeleteFiles] = useState(true);
9
+
10
+ if (!repo) return null;
11
+
12
+ const agents = repo.agents?.length || 0;
13
+ const processes = repo.processes?.length || 0;
14
+ const credentials = repo.credentialKeys?.length || 0;
15
+ const fileCount = repo.fileCount || 0;
16
+
17
+ return (
18
+ <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
19
+ <DialogContent title={`Nuke ${repo.repoName || repo.name}?`} description="Confirm destructive removal of imported repo">
20
+ <div className="px-5 py-4 space-y-4">
21
+ <div className="flex items-start gap-2">
22
+ <AlertTriangle size={16} className="text-danger flex-shrink-0 mt-0.5" />
23
+ <p className="text-sm text-text-1 font-sans">This cannot be undone.</p>
24
+ </div>
25
+
26
+ <div className="space-y-1.5 text-xs text-text-2 font-sans">
27
+ {agents > 0 && <div className="flex items-center gap-2">
28
+ <span className="text-success">✓</span> Kill {agents} agent{agents !== 1 ? 's' : ''}
29
+ </div>}
30
+ {processes > 0 && <div className="flex items-center gap-2">
31
+ <span className="text-success">✓</span> Stop {processes} process{processes !== 1 ? 'es' : ''}
32
+ </div>}
33
+ {credentials > 0 && <div className="flex items-center gap-2">
34
+ <span className="text-success">✓</span> Remove {credentials} credential{credentials !== 1 ? 's' : ''}
35
+ </div>}
36
+ <div className="flex items-center gap-2">
37
+ <span className="text-success">✓</span> Delete team &quot;{repo.teamId || repo.repoName || repo.name}&quot;
38
+ </div>
39
+ <div className="flex items-center gap-2">
40
+ <span className="text-success">✓</span> Clean all .groove state
41
+ </div>
42
+ </div>
43
+
44
+ <label className="flex items-center gap-2 cursor-pointer">
45
+ <input
46
+ type="checkbox"
47
+ checked={deleteFiles}
48
+ onChange={(e) => setDeleteFiles(e.target.checked)}
49
+ className="accent-[var(--color-danger)]"
50
+ />
51
+ <span className="text-xs text-text-1 font-sans">
52
+ Delete repo files{fileCount > 0 ? ` (${fileCount} files)` : ''}
53
+ </span>
54
+ </label>
55
+
56
+ <div className="flex items-center gap-2 pt-1">
57
+ <Button variant="danger" size="sm" onClick={() => onConfirm(deleteFiles)}>
58
+ Nuke Everything
59
+ </Button>
60
+ <Button variant="ghost" size="sm" onClick={onClose}>
61
+ Cancel
62
+ </Button>
63
+ </div>
64
+ </div>
65
+ </DialogContent>
66
+ </Dialog>
67
+ );
68
+ }
@@ -0,0 +1,22 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useGrooveStore } from '../../stores/groove';
3
+ import { UpgradeCard } from './upgrade-card';
4
+
5
+ export function ProGate({ feature, description, children }) {
6
+ const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
7
+ const user = useGrooveStore((s) => s.marketplaceUser);
8
+
9
+ if (__GROOVE_EDITION__ !== 'pro') {
10
+ return <UpgradeCard feature={feature} description={description} variant="community" />;
11
+ }
12
+
13
+ if (!authenticated) {
14
+ return <UpgradeCard feature={feature} description={description} variant="sign-in" />;
15
+ }
16
+
17
+ if (!user?.subscription?.active) {
18
+ return <UpgradeCard feature={feature} description={description} variant="subscribe" />;
19
+ }
20
+
21
+ return children;
22
+ }
@@ -0,0 +1,48 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { Lock, Download, LogIn, Sparkles } from 'lucide-react';
3
+ import { openExternal } from '../../lib/electron';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+
6
+ const VARIANTS = {
7
+ community: {
8
+ heading: 'Get Groove Desktop',
9
+ cta: 'Download',
10
+ icon: Download,
11
+ action: () => openExternal('https://groovedev.ai/download'),
12
+ },
13
+ 'sign-in': {
14
+ heading: 'Sign in to unlock',
15
+ cta: 'Sign in',
16
+ icon: LogIn,
17
+ action: () => useGrooveStore.getState().marketplaceLogin(),
18
+ },
19
+ subscribe: {
20
+ heading: 'Upgrade to Pro',
21
+ cta: 'Subscribe',
22
+ icon: Sparkles,
23
+ action: () => openExternal('https://groovedev.ai/pro'),
24
+ },
25
+ };
26
+
27
+ export function UpgradeCard({ feature, description, variant = 'community' }) {
28
+ const v = VARIANTS[variant] || VARIANTS.community;
29
+ const CtaIcon = v.icon;
30
+
31
+ return (
32
+ <div className="rounded-lg border border-border-subtle bg-surface-1/50 px-5 py-6 text-center">
33
+ <div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-purple/10">
34
+ <Lock size={18} className="text-purple" />
35
+ </div>
36
+ <h3 className="text-sm font-semibold text-text-1 font-sans">{v.heading}</h3>
37
+ <p className="mt-1.5 text-2xs text-text-3 font-sans">{feature}</p>
38
+ <p className="mt-1 text-2xs text-text-4 font-sans max-w-xs mx-auto">{description}</p>
39
+ <button
40
+ onClick={v.action}
41
+ className="mt-4 inline-flex items-center gap-1.5 h-7 px-4 rounded-full bg-purple/15 text-purple text-xs font-semibold font-sans hover:bg-purple/25 transition-colors cursor-pointer"
42
+ >
43
+ <CtaIcon size={13} />
44
+ {v.cta}
45
+ </button>
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,129 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { AnimatePresence, motion } from 'framer-motion';
6
+ import {
7
+ Server, Radio, ExternalLink, Loader2, X, Plus, Settings,
8
+ } from 'lucide-react';
9
+ import { StatusDot } from '../ui/status-dot';
10
+
11
+ export function QuickConnect() {
12
+ const open = useGrooveStore((s) => s.quickConnectOpen);
13
+ const toggle = useGrooveStore((s) => s.toggleQuickConnect);
14
+ const savedTunnels = useGrooveStore((s) => s.savedTunnels);
15
+ const [connectingId, setConnectingId] = useState(null);
16
+
17
+ if (!open) return null;
18
+
19
+ async function handleConnect(id) {
20
+ setConnectingId(id);
21
+ try {
22
+ await useGrooveStore.getState().connectTunnel(id);
23
+ toggle();
24
+ } catch {}
25
+ setConnectingId(null);
26
+ }
27
+
28
+ function handleOpenRemote(server) {
29
+ const port = server.localPort;
30
+ const name = encodeURIComponent(server.name);
31
+ window.open(`http://localhost:${port}?instance=${name}`, '_blank');
32
+ toggle();
33
+ }
34
+
35
+ return (
36
+ <>
37
+ <div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" onClick={toggle} />
38
+
39
+ <AnimatePresence>
40
+ <motion.div
41
+ initial={{ opacity: 0, y: -20, scale: 0.96 }}
42
+ animate={{ opacity: 1, y: 0, scale: 1 }}
43
+ exit={{ opacity: 0, y: -10, scale: 0.98 }}
44
+ transition={{ duration: 0.15 }}
45
+ className="fixed top-[20%] left-1/2 -translate-x-1/2 z-50 w-[400px] bg-surface-1 border border-border rounded-lg shadow-2xl overflow-hidden"
46
+ >
47
+ {/* Header */}
48
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle">
49
+ <div className="flex items-center gap-2">
50
+ <Radio size={15} className="text-accent" />
51
+ <span className="text-sm font-semibold text-text-0 font-sans">Quick Connect</span>
52
+ </div>
53
+ <button onClick={toggle} className="p-1 text-text-4 hover:text-text-1 cursor-pointer transition-colors">
54
+ <X size={14} />
55
+ </button>
56
+ </div>
57
+
58
+ {/* Server list */}
59
+ <div className="overflow-y-auto max-h-[320px] py-1">
60
+ {savedTunnels.length === 0 ? (
61
+ <div className="px-4 py-8 text-center">
62
+ <Server size={24} className="text-text-4 mx-auto mb-2" />
63
+ <p className="text-sm text-text-3 font-sans">No saved servers</p>
64
+ <p className="text-2xs text-text-4 font-sans mt-1">Add one in Settings to get started.</p>
65
+ <button
66
+ onClick={() => {
67
+ toggle();
68
+ useGrooveStore.getState().setActiveView('settings');
69
+ }}
70
+ className="mt-3 inline-flex items-center gap-1.5 text-xs text-accent hover:text-accent/80 font-sans cursor-pointer transition-colors"
71
+ >
72
+ <Settings size={12} /> Go to Settings
73
+ </button>
74
+ </div>
75
+ ) : (
76
+ savedTunnels.map((server) => (
77
+ <button
78
+ key={server.id}
79
+ onClick={() => server.active ? handleOpenRemote(server) : handleConnect(server.id)}
80
+ disabled={connectingId === server.id}
81
+ className={cn(
82
+ 'w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors',
83
+ 'hover:bg-surface-5',
84
+ connectingId === server.id && 'opacity-60 pointer-events-none',
85
+ )}
86
+ >
87
+ <Server size={15} className={server.active ? 'text-success' : 'text-text-4'} />
88
+ <div className="flex-1 min-w-0">
89
+ <div className="flex items-center gap-2">
90
+ <span className="text-sm font-medium text-text-0 font-sans truncate">{server.name}</span>
91
+ {server.active && <StatusDot status="running" size="sm" />}
92
+ </div>
93
+ <span className="text-2xs text-text-4 font-mono">{server.user}@{server.host}</span>
94
+ </div>
95
+ <div className="flex-shrink-0">
96
+ {connectingId === server.id ? (
97
+ <Loader2 size={14} className="text-text-3 animate-spin" />
98
+ ) : server.active ? (
99
+ <span className="flex items-center gap-1 text-2xs text-success font-sans">
100
+ <ExternalLink size={11} /> Open
101
+ </span>
102
+ ) : (
103
+ <span className="text-2xs text-text-3 font-sans">Connect</span>
104
+ )}
105
+ </div>
106
+ </button>
107
+ ))
108
+ )}
109
+ </div>
110
+
111
+ {/* Footer */}
112
+ {savedTunnels.length > 0 && (
113
+ <div className="px-4 py-2 border-t border-border-subtle">
114
+ <button
115
+ onClick={() => {
116
+ toggle();
117
+ useGrooveStore.getState().setActiveView('settings');
118
+ }}
119
+ className="flex items-center gap-1.5 text-2xs text-text-4 hover:text-text-2 font-sans cursor-pointer transition-colors"
120
+ >
121
+ <Plus size={10} /> Manage servers in Settings
122
+ </button>
123
+ </div>
124
+ )}
125
+ </motion.div>
126
+ </AnimatePresence>
127
+ </>
128
+ );
129
+ }
@@ -0,0 +1,243 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { Badge } from '../ui/badge';
4
+ import { StatusDot } from '../ui/status-dot';
5
+ import { Button } from '../ui/button';
6
+ import { useGrooveStore } from '../../stores/groove';
7
+ import { fmtUptime } from '../../lib/format';
8
+ import { cn } from '../../lib/cn';
9
+ import {
10
+ Plug, PlugZap, Pencil, Trash2, Loader2, Check, X, AlertTriangle,
11
+ ExternalLink, Download, Play,
12
+ } from 'lucide-react';
13
+
14
+ export function RemoteServerCard({ server, onEdit, onDelete, onConnect, onDisconnect, onTest }) {
15
+ const [testResult, setTestResult] = useState(null);
16
+ const [testLoading, setTestLoading] = useState(false);
17
+ const [connecting, setConnecting] = useState(false);
18
+ const [connectStep, setConnectStep] = useState(null);
19
+
20
+ // Listen for tunnel.status WebSocket events for progress updates
21
+ useEffect(() => {
22
+ function handleWs(e) {
23
+ try {
24
+ const msg = JSON.parse(e.data);
25
+ if (msg.type === 'tunnel.status' && msg.data?.id === server.id) {
26
+ setConnectStep(msg.data.step);
27
+ }
28
+ } catch {}
29
+ }
30
+ const ws = useGrooveStore.getState().ws;
31
+ if (ws) ws.addEventListener('message', handleWs);
32
+ return () => { if (ws) ws.removeEventListener('message', handleWs); };
33
+ }, [server.id]);
34
+
35
+ async function handleTest() {
36
+ setTestLoading(true);
37
+ setTestResult(null);
38
+ try {
39
+ const result = await onTest();
40
+ setTestResult(result);
41
+ } catch (err) {
42
+ setTestResult({ error: err.message || 'Test failed' });
43
+ }
44
+ setTestLoading(false);
45
+ }
46
+
47
+ async function handleConnect() {
48
+ setConnecting(true);
49
+ setConnectStep(null);
50
+ setTestResult(null);
51
+ try {
52
+ await onConnect();
53
+ setConnectStep(null);
54
+ } catch (err) {
55
+ const tr = err?.testResult || err?.body?.testResult;
56
+ if (tr) {
57
+ setTestResult(tr);
58
+ } else {
59
+ setTestResult({ error: err?.body?.error || err?.message || 'Connection failed' });
60
+ }
61
+ setConnectStep(null);
62
+ }
63
+ setConnecting(false);
64
+ }
65
+
66
+ async function handleDisconnect() {
67
+ setConnecting(true);
68
+ try {
69
+ await onDisconnect();
70
+ } catch {}
71
+ setConnecting(false);
72
+ }
73
+
74
+ function handleOpenRemote() {
75
+ const port = server.localPort;
76
+ const name = encodeURIComponent(server.name);
77
+ window.open(`http://localhost:${port}?instance=${name}`, '_blank');
78
+ }
79
+
80
+ const connectLabel = connectStep === 'installing'
81
+ ? 'Installing Groove...'
82
+ : connectStep === 'starting'
83
+ ? 'Starting daemon...'
84
+ : connecting
85
+ ? 'Connecting...'
86
+ : 'Connect';
87
+
88
+ const uptimeSeconds = server.active && server.startedAt
89
+ ? Math.floor((Date.now() - new Date(server.startedAt).getTime()) / 1000)
90
+ : 0;
91
+
92
+ return (
93
+ <div className={cn(
94
+ 'rounded-lg border bg-surface-2 p-4',
95
+ server.active ? 'border-success/40' : 'border-border-subtle',
96
+ )}>
97
+ {/* Top row: name + status */}
98
+ <div className="flex items-center justify-between mb-1.5">
99
+ <span className="text-[13px] font-semibold text-text-0 font-sans">{server.name}</span>
100
+ {server.active ? (
101
+ <Badge variant="success" className="text-2xs gap-1">
102
+ <StatusDot status="running" size="sm" /> Connected
103
+ </Badge>
104
+ ) : (
105
+ <Badge variant="default" className="text-2xs">Disconnected</Badge>
106
+ )}
107
+ </div>
108
+
109
+ {/* Connection string */}
110
+ <div className="text-xs text-text-3 font-mono mb-1">
111
+ {server.user}@{server.host}:{server.port || 22}
112
+ </div>
113
+
114
+ {/* SSH key path */}
115
+ {server.sshKeyPath && (
116
+ <div className="text-2xs text-text-4 font-mono truncate mb-2">
117
+ Key: {server.sshKeyPath}
118
+ </div>
119
+ )}
120
+
121
+ {/* Active connection stats */}
122
+ {server.active && (
123
+ <div className="flex items-center gap-3 text-2xs text-text-3 font-sans mb-2">
124
+ {uptimeSeconds > 0 && <span>Uptime: {fmtUptime(uptimeSeconds)}</span>}
125
+ {server.latencyMs != null && <span>Latency: {server.latencyMs}ms</span>}
126
+ {server.localPort && <span>Port: {server.localPort}</span>}
127
+ </div>
128
+ )}
129
+
130
+ {/* Connected instance explanation */}
131
+ {server.active && (
132
+ <div className="text-2xs text-text-4 bg-surface-1 rounded px-2.5 py-1.5 mb-3">
133
+ Separate Groove instance on your remote server. Local teams are not affected.
134
+ </div>
135
+ )}
136
+
137
+ {/* Action buttons */}
138
+ <div className="flex items-center gap-2">
139
+ {server.active ? (
140
+ <>
141
+ <Button
142
+ variant="primary"
143
+ size="sm"
144
+ onClick={handleOpenRemote}
145
+ className="h-7 text-2xs gap-1"
146
+ >
147
+ <ExternalLink size={11} />
148
+ Open Remote GUI
149
+ </Button>
150
+ <Button
151
+ variant="ghost"
152
+ size="sm"
153
+ onClick={handleDisconnect}
154
+ disabled={connecting}
155
+ className="h-7 text-2xs text-danger hover:text-danger gap-1"
156
+ >
157
+ <Plug size={11} />
158
+ {connecting ? 'Disconnecting...' : 'Disconnect'}
159
+ </Button>
160
+ </>
161
+ ) : (
162
+ <>
163
+ <Button
164
+ variant="primary"
165
+ size="sm"
166
+ onClick={handleConnect}
167
+ disabled={connecting}
168
+ className="h-7 text-2xs gap-1"
169
+ >
170
+ {connecting ? <Loader2 size={11} className="animate-spin" /> : <PlugZap size={11} />}
171
+ {connectLabel}
172
+ </Button>
173
+ <Button
174
+ variant="ghost"
175
+ size="sm"
176
+ onClick={handleTest}
177
+ disabled={testLoading || connecting}
178
+ className="h-7 text-2xs text-text-3 gap-1"
179
+ >
180
+ {testLoading ? <Loader2 size={11} className="animate-spin" /> : <PlugZap size={11} />}
181
+ Test
182
+ </Button>
183
+ </>
184
+ )}
185
+ <div className="flex-1" />
186
+ {!server.active && (
187
+ <>
188
+ <button
189
+ onClick={() => onEdit(server)}
190
+ className="p-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
191
+ title="Edit"
192
+ >
193
+ <Pencil size={12} />
194
+ </button>
195
+ <button
196
+ onClick={() => onDelete(server.id)}
197
+ className="p-1.5 text-text-4 hover:text-danger cursor-pointer transition-colors"
198
+ title="Delete"
199
+ >
200
+ <Trash2 size={12} />
201
+ </button>
202
+ </>
203
+ )}
204
+ </div>
205
+
206
+ {/* Inline test result */}
207
+ {testResult && !connecting && (
208
+ <div className={cn(
209
+ 'mt-2 px-3 py-2 rounded-md text-2xs font-sans flex items-start gap-2',
210
+ testResult.error
211
+ ? 'bg-danger/8 border border-danger/20 text-danger'
212
+ : testResult.reachable
213
+ ? 'bg-success/8 border border-success/20 text-success'
214
+ : 'bg-warning/8 border border-warning/20 text-warning',
215
+ )}>
216
+ {testResult.error ? (
217
+ <><X size={11} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
218
+ ) : testResult.reachable ? (
219
+ <>
220
+ <Check size={11} className="mt-0.5 flex-shrink-0" />
221
+ <span>
222
+ {testResult.daemonRunning
223
+ ? 'Connected. Groove running.'
224
+ : testResult.grooveInstalled
225
+ ? 'Connected. Groove installed but stopped.'
226
+ : 'Connected. Groove not installed.'}
227
+ {!testResult.daemonRunning && ' Click Connect to set up automatically.'}
228
+ </span>
229
+ </>
230
+ ) : (
231
+ <><AlertTriangle size={11} className="mt-0.5 flex-shrink-0" /> Host unreachable</>
232
+ )}
233
+ <button
234
+ onClick={() => setTestResult(null)}
235
+ className="ml-auto text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0"
236
+ >
237
+ <X size={10} />
238
+ </button>
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ }