groove-dev 0.27.7 → 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 (127) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/src/api.js +496 -44
  3. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
  4. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  7. package/node_modules/@groove-dev/daemon/src/process.js +128 -104
  8. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  9. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  10. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  11. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  13. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  14. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  15. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  20. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
  28. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  29. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  32. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  33. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  34. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  36. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  37. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  38. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  39. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  41. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  42. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  43. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  44. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  46. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  47. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  48. package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
  49. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
  50. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  51. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  52. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  53. package/package.json +7 -2
  54. package/packages/daemon/src/api.js +496 -44
  55. package/packages/daemon/src/gateways/manager.js +25 -12
  56. package/packages/daemon/src/index.js +7 -0
  57. package/packages/daemon/src/introducer.js +72 -4
  58. package/packages/daemon/src/journalist.js +66 -11
  59. package/packages/daemon/src/process.js +128 -104
  60. package/packages/daemon/src/registry.js +1 -1
  61. package/packages/daemon/src/repo-import.js +541 -0
  62. package/packages/daemon/src/rotator.js +28 -1
  63. package/packages/daemon/src/supervisor.js +2 -1
  64. package/packages/daemon/src/tunnel-manager.js +504 -0
  65. package/packages/daemon/src/validate.js +13 -0
  66. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  67. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  68. package/packages/gui/dist/index.html +2 -2
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  71. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  72. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  75. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  76. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  77. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  78. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  80. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  81. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  82. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  83. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  84. package/packages/gui/src/app.css +14 -0
  85. package/packages/gui/src/app.jsx +13 -0
  86. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  87. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  88. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  89. package/packages/gui/src/components/agents/agent-node.jsx +16 -17
  90. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  91. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  92. package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
  93. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  94. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  95. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  96. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  97. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  98. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  99. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  100. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  101. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  102. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  103. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  104. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  105. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  106. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  107. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  108. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  109. package/packages/gui/src/components/ui/toast.jsx +1 -1
  110. package/packages/gui/src/lib/edition.js +4 -0
  111. package/packages/gui/src/lib/electron.js +17 -0
  112. package/packages/gui/src/lib/status.js +1 -0
  113. package/packages/gui/src/stores/groove.js +150 -6
  114. package/packages/gui/src/views/dashboard.jsx +39 -40
  115. package/packages/gui/src/views/marketplace.jsx +82 -0
  116. package/packages/gui/src/views/settings.jsx +66 -0
  117. package/packages/gui/vite.config.js +3 -0
  118. package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  119. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
  120. package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  121. package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
  122. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  123. package/test-slack.mjs +0 -28
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  126. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  127. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -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
+ }
@@ -0,0 +1,192 @@
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 { FolderBrowser } from '../agents/folder-browser';
6
+ import { cn } from '../../lib/cn';
7
+ import { FolderSearch } from 'lucide-react';
8
+
9
+ export function ServerDialog({ open, onOpenChange, server, onSave }) {
10
+ const [name, setName] = useState('');
11
+ const [host, setHost] = useState('');
12
+ const [user, setUser] = useState('');
13
+ const [sshPort, setSshPort] = useState(22);
14
+ const [sshKeyPath, setSshKeyPath] = useState('');
15
+ const [autoStart, setAutoStart] = useState(false);
16
+ const [autoConnect, setAutoConnect] = useState(false);
17
+ const [keyBrowserOpen, setKeyBrowserOpen] = useState(false);
18
+ const [saving, setSaving] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (open) {
22
+ if (server) {
23
+ setName(server.name || '');
24
+ setHost(server.host || '');
25
+ setUser(server.user || '');
26
+ setSshPort(server.port || 22);
27
+ setSshKeyPath(server.sshKeyPath || '');
28
+ setAutoStart(server.autoStart || false);
29
+ setAutoConnect(server.autoConnect || false);
30
+ } else {
31
+ setName('');
32
+ setHost('');
33
+ setUser('');
34
+ setSshPort(22);
35
+ setSshKeyPath('');
36
+ setAutoStart(false);
37
+ setAutoConnect(false);
38
+ }
39
+ }
40
+ }, [open, server]);
41
+
42
+ async function handleSave() {
43
+ if (!name.trim() || !host.trim() || !user.trim()) return;
44
+ setSaving(true);
45
+ try {
46
+ const data = {
47
+ name: name.trim(),
48
+ host: host.trim(),
49
+ user: user.trim(),
50
+ port: sshPort,
51
+ sshKeyPath: sshKeyPath.trim(),
52
+ autoStart,
53
+ autoConnect,
54
+ };
55
+ if (server?.id) data.id = server.id;
56
+ await onSave(data);
57
+ onOpenChange(false);
58
+ } catch {}
59
+ setSaving(false);
60
+ }
61
+
62
+ return (
63
+ <Dialog open={open} onOpenChange={onOpenChange}>
64
+ <DialogContent
65
+ title={server ? `Edit ${server.name}` : 'Add Remote Server'}
66
+ description="Configure SSH connection to a remote server"
67
+ className="max-w-[460px]"
68
+ >
69
+ <div className="px-5 py-4 space-y-4">
70
+ {/* Name */}
71
+ <div>
72
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Name</label>
73
+ <input
74
+ value={name}
75
+ onChange={(e) => setName(e.target.value)}
76
+ placeholder="api-vps"
77
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
78
+ autoFocus
79
+ />
80
+ </div>
81
+
82
+ {/* Host */}
83
+ <div>
84
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Host</label>
85
+ <input
86
+ value={host}
87
+ onChange={(e) => setHost(e.target.value)}
88
+ placeholder="165.22.180.45 or hostname"
89
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
90
+ />
91
+ </div>
92
+
93
+ {/* User + SSH Port */}
94
+ <div className="flex gap-3">
95
+ <div className="flex-1">
96
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">User</label>
97
+ <input
98
+ value={user}
99
+ onChange={(e) => setUser(e.target.value)}
100
+ placeholder="root"
101
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
102
+ />
103
+ </div>
104
+ <div className="w-24">
105
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">SSH Port</label>
106
+ <input
107
+ value={sshPort}
108
+ onChange={(e) => setSshPort(Number(e.target.value) || 22)}
109
+ type="number"
110
+ className="w-full h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
111
+ />
112
+ </div>
113
+ </div>
114
+
115
+ {/* SSH Key */}
116
+ <div>
117
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">SSH Key</label>
118
+ <div className="flex items-center gap-1.5">
119
+ <input
120
+ value={sshKeyPath}
121
+ onChange={(e) => setSshKeyPath(e.target.value)}
122
+ placeholder="~/.ssh/id_ed25519"
123
+ className="flex-1 h-9 px-3 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
124
+ />
125
+ <Button
126
+ variant="secondary"
127
+ size="sm"
128
+ onClick={() => setKeyBrowserOpen(true)}
129
+ className="h-9 px-2.5 flex-shrink-0"
130
+ >
131
+ <FolderSearch size={13} />
132
+ </Button>
133
+ </div>
134
+ </div>
135
+
136
+ {/* Toggles */}
137
+ <div className="space-y-3 pt-1">
138
+ <label className="flex items-center justify-between cursor-pointer">
139
+ <span className="text-xs text-text-2 font-sans">Auto-start daemon on connect</span>
140
+ <ToggleSwitch value={autoStart} onChange={setAutoStart} />
141
+ </label>
142
+ <label className="flex items-center justify-between cursor-pointer">
143
+ <span className="text-xs text-text-2 font-sans">Auto-connect on Groove launch</span>
144
+ <ToggleSwitch value={autoConnect} onChange={setAutoConnect} />
145
+ </label>
146
+ </div>
147
+
148
+ {/* Actions */}
149
+ <div className="flex justify-end gap-2 pt-2">
150
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} className="h-8 text-xs px-4 text-text-3">
151
+ Cancel
152
+ </Button>
153
+ <Button
154
+ variant="primary"
155
+ size="sm"
156
+ onClick={handleSave}
157
+ disabled={!name.trim() || !host.trim() || !user.trim() || saving}
158
+ className="h-8 text-xs px-4"
159
+ >
160
+ {saving ? 'Saving...' : 'Save'}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+
165
+ {/* File browser for SSH key */}
166
+ <FolderBrowser
167
+ open={keyBrowserOpen}
168
+ onOpenChange={setKeyBrowserOpen}
169
+ currentPath={sshKeyPath || '~/.ssh'}
170
+ onSelect={(path) => setSshKeyPath(path)}
171
+ />
172
+ </DialogContent>
173
+ </Dialog>
174
+ );
175
+ }
176
+
177
+ function ToggleSwitch({ value, onChange }) {
178
+ return (
179
+ <button
180
+ onClick={() => onChange(!value)}
181
+ className={cn(
182
+ 'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer',
183
+ value ? 'bg-accent' : 'bg-surface-5',
184
+ )}
185
+ >
186
+ <div className={cn(
187
+ 'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
188
+ value ? 'translate-x-4' : 'translate-x-0',
189
+ )} />
190
+ </button>
191
+ );
192
+ }
@@ -0,0 +1,63 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { AnimatePresence, motion } from 'framer-motion';
3
+ import { ShieldCheck, ShieldX, AlertTriangle } from 'lucide-react';
4
+ import { Button } from '../ui/button';
5
+ import { useGrooveStore } from '../../stores/groove';
6
+
7
+ export function ApprovalModal() {
8
+ const pendingApprovals = useGrooveStore((s) => s.pendingApprovals);
9
+ const approveRequest = useGrooveStore((s) => s.approveRequest);
10
+ const rejectRequest = useGrooveStore((s) => s.rejectRequest);
11
+
12
+ if (!pendingApprovals?.length) return null;
13
+
14
+ return (
15
+ <div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-50 w-full max-w-md flex flex-col gap-2 px-4">
16
+ <AnimatePresence>
17
+ {pendingApprovals.map((approval) => (
18
+ <motion.div
19
+ key={approval.id}
20
+ initial={{ y: 20, opacity: 0 }}
21
+ animate={{ y: 0, opacity: 1 }}
22
+ exit={{ y: 20, opacity: 0 }}
23
+ transition={{ duration: 0.2 }}
24
+ className="rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden"
25
+ >
26
+ <div className="px-4 py-3 flex items-start gap-3">
27
+ <AlertTriangle size={16} className="text-warning shrink-0 mt-0.5" />
28
+ <div className="flex-1 min-w-0">
29
+ <p className="text-sm font-semibold text-text-0 font-sans truncate">
30
+ {approval.agentName || 'Agent'} needs approval
31
+ </p>
32
+ {approval.action?.description && (
33
+ <p className="text-2xs text-text-3 font-sans mt-0.5 line-clamp-2">
34
+ {approval.action.description}
35
+ </p>
36
+ )}
37
+ </div>
38
+ </div>
39
+ <div className="px-4 py-2.5 border-t border-border-subtle flex items-center justify-end gap-2">
40
+ <Button
41
+ size="sm"
42
+ variant="ghost"
43
+ className="text-danger hover:bg-danger/10"
44
+ onClick={() => rejectRequest(approval.id)}
45
+ >
46
+ <ShieldX size={14} className="mr-1" />
47
+ Reject
48
+ </Button>
49
+ <Button
50
+ size="sm"
51
+ variant="accent"
52
+ onClick={() => approveRequest(approval.id)}
53
+ >
54
+ <ShieldCheck size={14} className="mr-1" />
55
+ Approve
56
+ </Button>
57
+ </div>
58
+ </motion.div>
59
+ ))}
60
+ </AnimatePresence>
61
+ </div>
62
+ );
63
+ }
@@ -78,7 +78,7 @@ export function ToastContainer() {
78
78
  const toasts = useGrooveStore((s) => s.toasts);
79
79
 
80
80
  return (
81
- <div className="fixed bottom-4 right-4 z-[100] flex flex-col-reverse gap-2">
81
+ <div className="fixed bottom-10 left-[60px] z-[100] flex flex-col-reverse gap-2">
82
82
  <AnimatePresence mode="popLayout">
83
83
  {toasts.slice(-3).map((toast) => (
84
84
  <ToastItem key={toast.id} toast={toast} />
@@ -0,0 +1,4 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export const isPro = __GROOVE_EDITION__ === 'pro';
4
+ export const edition = __GROOVE_EDITION__;
@@ -0,0 +1,17 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export function isElectron() {
4
+ return !!(window.groove || navigator.userAgent.includes('Electron'));
5
+ }
6
+
7
+ export function getPlatform() {
8
+ return window.groove?.platform || 'browser';
9
+ }
10
+
11
+ export function openExternal(url) {
12
+ if (window.groove) {
13
+ window.groove.openExternal(url);
14
+ } else {
15
+ window.open(url, '_blank');
16
+ }
17
+ }
@@ -42,6 +42,7 @@ export const ROLE_COLORS = {
42
42
  analyst: { bg: 'rgba(198, 120, 221, 0.12)', text: '#c678dd', border: '#c678dd' },
43
43
  creative: { bg: 'rgba(229, 192, 123, 0.12)', text: '#e5c07b', border: '#e5c07b' },
44
44
  slides: { bg: 'rgba(209, 154, 102, 0.12)', text: '#d19a66', border: '#d19a66' },
45
+ chat: { bg: 'rgba(198, 120, 221, 0.12)', text: '#c678dd', border: '#c678dd' },
45
46
  };
46
47
 
47
48
  export function roleColor(role) {