groove-dev 0.27.14 → 0.27.17

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 (169) hide show
  1. package/README.md +37 -1
  2. package/developerID_application.cer +0 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +587 -68
  4. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  5. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  6. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  7. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  10. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  11. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  12. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  13. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  14. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  15. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  16. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  17. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  19. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  21. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  22. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  23. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  24. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  25. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  26. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  27. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  28. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  30. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  31. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  32. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  33. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  34. package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
  35. package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  37. package/node_modules/@groove-dev/gui/index.html +1 -0
  38. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  39. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  43. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  44. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  45. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  46. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  48. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  49. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  50. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
  51. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  52. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  53. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  54. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  55. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  56. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  57. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  58. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  59. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  60. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  61. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  62. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  66. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  67. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  68. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  71. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  72. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  74. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  75. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  76. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  77. package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
  78. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  79. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  80. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  81. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  82. package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
  83. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  84. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  85. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  86. package/package.json +1 -1
  87. package/packages/daemon/src/api.js +587 -68
  88. package/packages/daemon/src/classifier.js +24 -0
  89. package/packages/daemon/src/credentials.js +12 -2
  90. package/packages/daemon/src/federation/ambassador.js +204 -0
  91. package/packages/daemon/src/federation/connection.js +359 -0
  92. package/packages/daemon/src/federation/contracts.js +112 -0
  93. package/packages/daemon/src/federation/whitelist.js +190 -0
  94. package/packages/daemon/src/federation.js +166 -7
  95. package/packages/daemon/src/index.js +172 -19
  96. package/packages/daemon/src/introducer.js +52 -7
  97. package/packages/daemon/src/journalist.js +46 -1
  98. package/packages/daemon/src/memory.js +36 -16
  99. package/packages/daemon/src/process.js +140 -23
  100. package/packages/daemon/src/providers/base.js +1 -0
  101. package/packages/daemon/src/providers/claude-code.js +1 -0
  102. package/packages/daemon/src/providers/codex.js +124 -28
  103. package/packages/daemon/src/providers/gemini.js +104 -17
  104. package/packages/daemon/src/providers/index.js +17 -0
  105. package/packages/daemon/src/registry.js +10 -1
  106. package/packages/daemon/src/rotator.js +93 -30
  107. package/packages/daemon/src/skills.js +33 -3
  108. package/packages/daemon/src/terminal-pty.js +9 -1
  109. package/packages/daemon/src/tool-executor.js +11 -5
  110. package/packages/daemon/src/toys.js +69 -0
  111. package/packages/daemon/src/tunnel-manager.js +24 -5
  112. package/packages/daemon/templates/toys-catalog.json +242 -0
  113. package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
  114. package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
  115. package/packages/gui/dist/index.html +3 -2
  116. package/packages/gui/index.html +1 -0
  117. package/packages/gui/src/app.css +7 -0
  118. package/packages/gui/src/app.jsx +37 -10
  119. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  120. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  121. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  122. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  123. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  124. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  125. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  126. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  127. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  128. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  129. package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
  130. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  131. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  132. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  133. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  134. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  135. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  136. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  137. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  138. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  139. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  140. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  141. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  142. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  143. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  144. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  145. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  146. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  147. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  148. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  149. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  150. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  151. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  152. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  153. package/packages/gui/src/components/ui/toast.jsx +2 -2
  154. package/packages/gui/src/lib/electron.js +15 -0
  155. package/packages/gui/src/lib/format.js +1 -0
  156. package/packages/gui/src/stores/groove.js +373 -58
  157. package/packages/gui/src/views/agents.jsx +148 -42
  158. package/packages/gui/src/views/editor.jsx +92 -2
  159. package/packages/gui/src/views/federation.jsx +37 -0
  160. package/packages/gui/src/views/marketplace.jsx +2 -42
  161. package/packages/gui/src/views/settings.jsx +32 -132
  162. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  163. package/packages/gui/src/views/teams.jsx +3 -3
  164. package/packages/gui/src/views/toys.jsx +162 -0
  165. package/plans/chat-persistence-refactor.md +154 -0
  166. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  167. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  168. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  169. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -0,0 +1,549 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { Button } from '../ui/button';
4
+ import { StatusDot } from '../ui/status-dot';
5
+ import { FolderBrowser } from '../agents/folder-browser';
6
+ import { cn } from '../../lib/cn';
7
+ import {
8
+ FolderSearch, Check, X, AlertTriangle, Loader2,
9
+ ExternalLink, Server, KeyRound, Settings, Plug,
10
+ } from 'lucide-react';
11
+
12
+ const STEPS = [
13
+ { id: 'details', label: 'Server Details', icon: Server },
14
+ { id: 'auth', label: 'Authentication', icon: KeyRound },
15
+ { id: 'setup', label: 'Setup', icon: Settings },
16
+ { id: 'connected', label: 'Connected', icon: Plug },
17
+ ];
18
+
19
+ function StepIndicator({ steps, currentStep, completedSteps, onStepClick }) {
20
+ return (
21
+ <div className="flex items-center gap-1 mb-4">
22
+ {steps.map((step, i) => {
23
+ const isActive = currentStep === i;
24
+ const isCompleted = completedSteps.includes(i);
25
+ const isClickable = isCompleted || i < currentStep;
26
+ const Icon = step.icon;
27
+
28
+ return (
29
+ <div key={step.id} className="flex items-center gap-1 flex-1">
30
+ <button
31
+ onClick={() => isClickable && onStepClick(i)}
32
+ disabled={!isClickable}
33
+ className={cn(
34
+ 'flex items-center gap-1.5 px-2 py-1 rounded-md transition-colors text-2xs font-sans font-medium',
35
+ isActive
36
+ ? 'bg-accent/12 text-accent'
37
+ : isCompleted
38
+ ? 'text-success cursor-pointer hover:bg-surface-3'
39
+ : 'text-text-4',
40
+ isClickable && !isActive && 'cursor-pointer',
41
+ )}
42
+ >
43
+ <div className={cn(
44
+ 'w-5 h-5 rounded-full flex items-center justify-center text-2xs font-semibold border transition-colors',
45
+ isActive
46
+ ? 'border-accent bg-accent/15 text-accent'
47
+ : isCompleted
48
+ ? 'border-success/40 bg-success/10 text-success'
49
+ : 'border-border-subtle bg-surface-3 text-text-4',
50
+ )}>
51
+ {isCompleted ? <Check size={10} /> : i + 1}
52
+ </div>
53
+ <span className="hidden sm:inline">{step.label}</span>
54
+ </button>
55
+ {i < steps.length - 1 && (
56
+ <div className={cn(
57
+ 'flex-1 h-px mx-1',
58
+ isCompleted ? 'bg-success/30' : 'bg-border-subtle',
59
+ )} />
60
+ )}
61
+ </div>
62
+ );
63
+ })}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ function ToggleSwitch({ value, onChange }) {
69
+ return (
70
+ <button
71
+ onClick={() => onChange(!value)}
72
+ className={cn(
73
+ 'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer',
74
+ value ? 'bg-accent' : 'bg-surface-5',
75
+ )}
76
+ >
77
+ <div className={cn(
78
+ 'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
79
+ value ? 'translate-x-4' : 'translate-x-0',
80
+ )} />
81
+ </button>
82
+ );
83
+ }
84
+
85
+ function FieldCard({ icon: Icon, title, children }) {
86
+ return (
87
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5 flex flex-col gap-2">
88
+ <div className="flex items-center gap-2">
89
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
90
+ <Icon size={12} className="text-accent" />
91
+ </div>
92
+ <span className="text-[13px] font-medium text-text-0 font-sans leading-tight">{title}</span>
93
+ </div>
94
+ <div className="mt-1">{children}</div>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
100
+ const [step, setStep] = useState(0);
101
+ const [completedSteps, setCompletedSteps] = useState([]);
102
+
103
+ const [name, setName] = useState('');
104
+ const [host, setHost] = useState('');
105
+ const [user, setUser] = useState('');
106
+ const [sshPort, setSshPort] = useState(22);
107
+ const [sshKeyPath, setSshKeyPath] = useState('');
108
+ const [autoStart, setAutoStart] = useState(false);
109
+ const [autoConnect, setAutoConnect] = useState(false);
110
+ const [keyBrowserOpen, setKeyBrowserOpen] = useState(false);
111
+
112
+ const [testLoading, setTestLoading] = useState(false);
113
+ const [testResult, setTestResult] = useState(null);
114
+ const [saving, setSaving] = useState(false);
115
+ const [connecting, setConnecting] = useState(false);
116
+
117
+ useEffect(() => {
118
+ if (server) {
119
+ setName(server.name || '');
120
+ setHost(server.host || '');
121
+ setUser(server.user || '');
122
+ setSshPort(server.port || 22);
123
+ setSshKeyPath(server.sshKeyPath || '');
124
+ setAutoStart(server.autoStart || false);
125
+ setAutoConnect(server.autoConnect || false);
126
+ setCompletedSteps([0, 1]);
127
+ setStep(2);
128
+ } else {
129
+ setName('');
130
+ setHost('');
131
+ setUser('');
132
+ setSshPort(22);
133
+ setSshKeyPath('');
134
+ setAutoStart(false);
135
+ setAutoConnect(false);
136
+ setCompletedSteps([]);
137
+ setStep(0);
138
+ }
139
+ }, [server]);
140
+
141
+ function buildData() {
142
+ const data = {
143
+ name: name.trim(),
144
+ host: host.trim(),
145
+ user: user.trim(),
146
+ port: sshPort,
147
+ sshKeyPath: sshKeyPath.trim(),
148
+ autoStart,
149
+ autoConnect,
150
+ };
151
+ if (server?.id) data.id = server.id;
152
+ return data;
153
+ }
154
+
155
+ function canAdvanceStep0() {
156
+ return name.trim() && host.trim() && user.trim();
157
+ }
158
+
159
+ function handleNext() {
160
+ if (step === 0 && !canAdvanceStep0()) return;
161
+ setCompletedSteps((prev) => prev.includes(step) ? prev : [...prev, step]);
162
+ setStep((s) => Math.min(s + 1, STEPS.length - 1));
163
+ }
164
+
165
+ function handleBack() {
166
+ setStep((s) => Math.max(s - 1, 0));
167
+ }
168
+
169
+ async function handleTest() {
170
+ setTestLoading(true);
171
+ setTestResult(null);
172
+ try {
173
+ const data = buildData();
174
+ setSaving(true);
175
+ await onSave(data);
176
+ setSaving(false);
177
+ const result = await onTest();
178
+ setTestResult(result);
179
+ } catch (err) {
180
+ setTestResult({ error: err.message || 'Test failed' });
181
+ setSaving(false);
182
+ }
183
+ setTestLoading(false);
184
+ }
185
+
186
+ async function handleSaveAndSetup() {
187
+ setSaving(true);
188
+ try {
189
+ const data = buildData();
190
+ await onSave(data);
191
+ setCompletedSteps((prev) => prev.includes(step) ? prev : [...prev, step]);
192
+ setStep(2);
193
+ } catch (err) {
194
+ setTestResult({ error: err.message || 'Save failed' });
195
+ }
196
+ setSaving(false);
197
+ }
198
+
199
+ async function handleConnect() {
200
+ setConnecting(true);
201
+ try {
202
+ const data = buildData();
203
+ await onSave(data);
204
+ await onConnect();
205
+ setCompletedSteps((prev) => [...new Set([...prev, 2])]);
206
+ setStep(3);
207
+ } catch (err) {
208
+ setTestResult({ error: err?.body?.error || err?.message || 'Connection failed' });
209
+ }
210
+ setConnecting(false);
211
+ }
212
+
213
+ const inputCls = 'h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent';
214
+ const monoInputCls = 'h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent';
215
+
216
+ return (
217
+ <div className="p-4">
218
+ <StepIndicator
219
+ steps={STEPS}
220
+ currentStep={step}
221
+ completedSteps={completedSteps}
222
+ onStepClick={setStep}
223
+ />
224
+
225
+ {/* Step 0: Server Details */}
226
+ {step === 0 && (
227
+ <div className="grid grid-cols-2 gap-3">
228
+ <FieldCard icon={Server} title="Server Info">
229
+ <div className="space-y-2.5">
230
+ <div>
231
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">Name</label>
232
+ <input
233
+ value={name}
234
+ onChange={(e) => setName(e.target.value)}
235
+ placeholder="api-vps"
236
+ className={cn(inputCls, 'w-full')}
237
+ autoFocus
238
+ />
239
+ </div>
240
+ <div>
241
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">Host</label>
242
+ <input
243
+ value={host}
244
+ onChange={(e) => setHost(e.target.value)}
245
+ placeholder="165.22.180.45"
246
+ className={cn(monoInputCls, 'w-full')}
247
+ />
248
+ </div>
249
+ </div>
250
+ </FieldCard>
251
+
252
+ <FieldCard icon={Settings} title="Connection">
253
+ <div className="space-y-2.5">
254
+ <div>
255
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">User</label>
256
+ <input
257
+ value={user}
258
+ onChange={(e) => setUser(e.target.value)}
259
+ placeholder="root"
260
+ className={cn(monoInputCls, 'w-full')}
261
+ />
262
+ </div>
263
+ <div>
264
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">SSH Port</label>
265
+ <input
266
+ value={sshPort}
267
+ onChange={(e) => setSshPort(Number(e.target.value) || 22)}
268
+ type="number"
269
+ className={cn(monoInputCls, 'w-24')}
270
+ />
271
+ </div>
272
+ </div>
273
+ </FieldCard>
274
+ </div>
275
+ )}
276
+
277
+ {/* Step 1: Authentication */}
278
+ {step === 1 && (
279
+ <div className="grid grid-cols-2 gap-3">
280
+ <FieldCard icon={KeyRound} title="SSH Key">
281
+ <div className="space-y-2.5">
282
+ <div>
283
+ <label className="text-2xs font-semibold text-text-2 font-sans mb-1 block">Key Path</label>
284
+ <div className="flex items-center gap-1.5">
285
+ <input
286
+ value={sshKeyPath}
287
+ onChange={(e) => setSshKeyPath(e.target.value)}
288
+ placeholder="~/.ssh/id_ed25519"
289
+ className={cn(monoInputCls, 'flex-1 min-w-0')}
290
+ autoFocus
291
+ />
292
+ <Button
293
+ variant="secondary"
294
+ size="sm"
295
+ onClick={() => setKeyBrowserOpen(true)}
296
+ className="h-8 px-2 flex-shrink-0"
297
+ >
298
+ <FolderSearch size={12} />
299
+ </Button>
300
+ </div>
301
+ <p className="text-2xs text-text-4 font-sans mt-1">
302
+ Leave blank to use default SSH agent.
303
+ </p>
304
+ </div>
305
+ <Button
306
+ variant="secondary"
307
+ size="sm"
308
+ onClick={handleTest}
309
+ disabled={testLoading}
310
+ className="h-7 text-2xs gap-1.5"
311
+ >
312
+ {testLoading ? <Loader2 size={11} className="animate-spin" /> : <Plug size={11} />}
313
+ Test Connection
314
+ </Button>
315
+ </div>
316
+ </FieldCard>
317
+
318
+ <div className="space-y-3">
319
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5">
320
+ <div className="flex items-center gap-2 mb-2">
321
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
322
+ <Server size={12} className="text-accent" />
323
+ </div>
324
+ <span className="text-[13px] font-medium text-text-0 font-sans">Target</span>
325
+ </div>
326
+ <div className="space-y-1.5 text-2xs font-sans">
327
+ <div className="flex items-center justify-between">
328
+ <span className="text-text-3">Host</span>
329
+ <span className="text-text-1 font-mono">{host || '—'}</span>
330
+ </div>
331
+ <div className="flex items-center justify-between">
332
+ <span className="text-text-3">User</span>
333
+ <span className="text-text-1 font-mono">{user || '—'}</span>
334
+ </div>
335
+ <div className="flex items-center justify-between">
336
+ <span className="text-text-3">Port</span>
337
+ <span className="text-text-1 font-mono">{sshPort}</span>
338
+ </div>
339
+ </div>
340
+ </div>
341
+
342
+ {testResult && (
343
+ <div className={cn(
344
+ 'px-3 py-2.5 rounded-lg text-2xs font-sans flex items-start gap-2',
345
+ testResult.error
346
+ ? 'bg-danger/8 border border-danger/20 text-danger'
347
+ : testResult.reachable
348
+ ? 'bg-success/8 border border-success/20 text-success'
349
+ : 'bg-warning/8 border border-warning/20 text-warning',
350
+ )}>
351
+ {testResult.error ? (
352
+ <><X size={11} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
353
+ ) : testResult.reachable ? (
354
+ <><Check size={11} className="mt-0.5 flex-shrink-0" /> Server reachable</>
355
+ ) : (
356
+ <><AlertTriangle size={11} className="mt-0.5 flex-shrink-0" /> Host unreachable</>
357
+ )}
358
+ </div>
359
+ )}
360
+ </div>
361
+
362
+ <FolderBrowser
363
+ open={keyBrowserOpen}
364
+ onOpenChange={setKeyBrowserOpen}
365
+ currentPath={sshKeyPath || '~/.ssh'}
366
+ onSelect={(path) => setSshKeyPath(path)}
367
+ />
368
+ </div>
369
+ )}
370
+
371
+ {/* Step 2: Setup */}
372
+ {step === 2 && (
373
+ <div className="grid grid-cols-2 gap-3">
374
+ <FieldCard icon={Settings} title="Behavior">
375
+ <div className="space-y-3">
376
+ <label className="flex items-center justify-between cursor-pointer">
377
+ <div>
378
+ <span className="text-xs text-text-1 font-sans block">Auto-start daemon</span>
379
+ <span className="text-2xs text-text-4 font-sans">Start Groove on the remote when connecting</span>
380
+ </div>
381
+ <ToggleSwitch value={autoStart} onChange={setAutoStart} />
382
+ </label>
383
+ <label className="flex items-center justify-between cursor-pointer">
384
+ <div>
385
+ <span className="text-xs text-text-1 font-sans block">Auto-connect on launch</span>
386
+ <span className="text-2xs text-text-4 font-sans">Connect when Groove starts</span>
387
+ </div>
388
+ <ToggleSwitch value={autoConnect} onChange={setAutoConnect} />
389
+ </label>
390
+ </div>
391
+ </FieldCard>
392
+
393
+ {testResult && !testResult.error && (
394
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5">
395
+ <div className="flex items-center gap-2 mb-3">
396
+ <div className="w-6 h-6 rounded bg-success/10 flex items-center justify-center flex-shrink-0">
397
+ <Check size={12} className="text-success" />
398
+ </div>
399
+ <span className="text-[13px] font-medium text-text-0 font-sans">Test Results</span>
400
+ </div>
401
+ <div className="space-y-2">
402
+ <div className="flex items-center gap-2 text-2xs font-sans">
403
+ <StatusDot status={testResult.reachable ? 'running' : 'crashed'} size="sm" />
404
+ <span className="text-text-1">Reachable</span>
405
+ </div>
406
+ <div className="flex items-center gap-2 text-2xs font-sans">
407
+ <StatusDot status={testResult.grooveInstalled ? 'running' : 'stopped'} size="sm" />
408
+ <span className="text-text-1">Groove Installed</span>
409
+ </div>
410
+ <div className="flex items-center gap-2 text-2xs font-sans">
411
+ <StatusDot status={testResult.daemonRunning ? 'running' : 'stopped'} size="sm" />
412
+ <span className="text-text-1">Daemon Running</span>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ )}
417
+
418
+ {(!testResult || testResult.error) && (
419
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5">
420
+ <div className="flex items-center gap-2 mb-2">
421
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
422
+ <Server size={12} className="text-accent" />
423
+ </div>
424
+ <span className="text-[13px] font-medium text-text-0 font-sans">{name || 'Server'}</span>
425
+ </div>
426
+ <div className="space-y-1.5 text-2xs font-sans">
427
+ <div className="flex items-center justify-between">
428
+ <span className="text-text-3">Connection</span>
429
+ <span className="text-text-1 font-mono">{user}@{host}:{sshPort}</span>
430
+ </div>
431
+ {sshKeyPath && (
432
+ <div className="flex items-center justify-between">
433
+ <span className="text-text-3">SSH Key</span>
434
+ <span className="text-text-1 font-mono truncate max-w-40">{sshKeyPath}</span>
435
+ </div>
436
+ )}
437
+ </div>
438
+ </div>
439
+ )}
440
+ </div>
441
+ )}
442
+
443
+ {/* Step 3: Connected */}
444
+ {step === 3 && (
445
+ <div className="grid grid-cols-2 gap-3">
446
+ <div className="rounded-lg border border-success/30 bg-success/5 px-4 py-5 text-center">
447
+ <div className="w-10 h-10 rounded-full bg-success/15 flex items-center justify-center mx-auto mb-3">
448
+ <Check size={20} className="text-success" />
449
+ </div>
450
+ <h3 className="text-sm font-semibold text-text-0 font-sans mb-1">Connected</h3>
451
+ <p className="text-2xs text-text-3 font-sans">
452
+ Successfully connected to <span className="font-mono text-text-1">{name}</span>
453
+ </p>
454
+ <Button
455
+ variant="primary"
456
+ size="sm"
457
+ onClick={() => {
458
+ const port = server?.localPort;
459
+ const n = encodeURIComponent(name);
460
+ window.open(`http://localhost:${port}?instance=${n}`, '_blank');
461
+ }}
462
+ className="h-8 text-xs gap-1.5 mt-4"
463
+ >
464
+ <ExternalLink size={12} />
465
+ Open Remote GUI
466
+ </Button>
467
+ </div>
468
+
469
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5">
470
+ <div className="flex items-center gap-2 mb-3">
471
+ <div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
472
+ <Server size={12} className="text-accent" />
473
+ </div>
474
+ <span className="text-[13px] font-medium text-text-0 font-sans">Connection Info</span>
475
+ </div>
476
+ <div className="space-y-1.5 text-2xs font-sans">
477
+ <div className="flex items-center justify-between">
478
+ <span className="text-text-3">Connection</span>
479
+ <span className="text-text-1 font-mono">{user}@{host}:{sshPort}</span>
480
+ </div>
481
+ {sshKeyPath && (
482
+ <div className="flex items-center justify-between">
483
+ <span className="text-text-3">SSH Key</span>
484
+ <span className="text-text-1 font-mono truncate max-w-40">{sshKeyPath}</span>
485
+ </div>
486
+ )}
487
+ <div className="flex items-center justify-between">
488
+ <span className="text-text-3">Auto-start</span>
489
+ <span className="text-text-1">{autoStart ? 'On' : 'Off'}</span>
490
+ </div>
491
+ <div className="flex items-center justify-between">
492
+ <span className="text-text-3">Auto-connect</span>
493
+ <span className="text-text-1">{autoConnect ? 'On' : 'Off'}</span>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ )}
499
+
500
+ {/* Navigation footer */}
501
+ <div className="flex items-center justify-between mt-4">
502
+ <Button
503
+ variant="ghost"
504
+ size="sm"
505
+ onClick={step === 0 ? onCancel : step === 3 ? onCancel : handleBack}
506
+ className="h-8 text-xs px-4 text-text-3"
507
+ >
508
+ {step === 0 ? 'Cancel' : step === 3 ? 'Done' : 'Back'}
509
+ </Button>
510
+ {step < 3 && (
511
+ <div className="flex gap-2">
512
+ {step === 2 ? (
513
+ <Button
514
+ variant="primary"
515
+ size="sm"
516
+ onClick={handleConnect}
517
+ disabled={connecting || saving}
518
+ className="h-8 text-xs px-4 gap-1.5"
519
+ >
520
+ {connecting ? <Loader2 size={12} className="animate-spin" /> : <Plug size={12} />}
521
+ {connecting ? 'Connecting...' : 'Connect'}
522
+ </Button>
523
+ ) : step === 1 ? (
524
+ <Button
525
+ variant="primary"
526
+ size="sm"
527
+ onClick={handleSaveAndSetup}
528
+ disabled={saving}
529
+ className="h-8 text-xs px-4"
530
+ >
531
+ {saving ? 'Saving...' : 'Next'}
532
+ </Button>
533
+ ) : (
534
+ <Button
535
+ variant="primary"
536
+ size="sm"
537
+ onClick={handleNext}
538
+ disabled={!canAdvanceStep0()}
539
+ className="h-8 text-xs px-4"
540
+ >
541
+ Next
542
+ </Button>
543
+ )}
544
+ </div>
545
+ )}
546
+ </div>
547
+ </div>
548
+ );
549
+ }
@@ -0,0 +1,78 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { Card } from '../ui/card';
3
+ import { Badge } from '../ui/badge';
4
+ import * as Icons from 'lucide-react';
5
+
6
+ const DIFFICULTY_VARIANT = {
7
+ beginner: 'success',
8
+ intermediate: 'warning',
9
+ advanced: 'danger',
10
+ };
11
+
12
+ function resolveIcon(name) {
13
+ if (!name) return Icons.Box;
14
+ const pascal = name.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());
15
+ return Icons[pascal] || Icons[name] || Icons.Box;
16
+ }
17
+
18
+ export function ToyCard({ toy, onClick }) {
19
+ const Icon = resolveIcon(toy.icon);
20
+
21
+ return (
22
+ <Card
23
+ hover
24
+ className="group flex flex-col p-4 gap-3 transition-all duration-150 hover:scale-[1.02] hover:shadow-lg hover:shadow-black/20"
25
+ onClick={() => onClick(toy)}
26
+ >
27
+ {/* Icon + name + category */}
28
+ <div className="flex items-start gap-3">
29
+ <div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-accent/10 border border-accent/20 text-accent">
30
+ <Icon size={20} />
31
+ </div>
32
+ <div className="flex-1 min-w-0 pt-0.5">
33
+ <h3 className="text-sm font-semibold text-text-0 font-sans truncate pr-1">{toy.name}</h3>
34
+ </div>
35
+ <Badge variant="default" className="flex-shrink-0 mt-0.5">
36
+ {toy.category}
37
+ </Badge>
38
+ </div>
39
+
40
+ {/* Description — 2 lines max */}
41
+ <p className="text-xs text-text-2 font-sans leading-relaxed line-clamp-2">{toy.description}</p>
42
+
43
+ {/* Bottom badges */}
44
+ <div className="flex items-center gap-1.5 mt-auto flex-wrap">
45
+ {toy.custom && (
46
+ <Badge variant="accent">Custom</Badge>
47
+ )}
48
+ <Badge variant={DIFFICULTY_VARIANT[toy.difficulty] || 'default'}>
49
+ {toy.difficulty || 'beginner'}
50
+ </Badge>
51
+ <Badge variant={toy.authType === 'none' ? 'success' : 'warning'}>
52
+ {toy.authType === 'none' ? 'No Key Required' : 'API Key Required'}
53
+ </Badge>
54
+ </div>
55
+ </Card>
56
+ );
57
+ }
58
+
59
+ export function ToyCardSkeleton() {
60
+ return (
61
+ <Card className="p-4 space-y-3">
62
+ <div className="flex items-start gap-3">
63
+ <div className="w-10 h-10 rounded-lg bg-surface-4 animate-pulse" />
64
+ <div className="flex-1 space-y-2 pt-1">
65
+ <div className="h-3.5 w-24 bg-surface-4 rounded animate-pulse" />
66
+ </div>
67
+ </div>
68
+ <div className="space-y-1.5">
69
+ <div className="h-3 w-full bg-surface-4 rounded animate-pulse" />
70
+ <div className="h-3 w-3/4 bg-surface-4 rounded animate-pulse" />
71
+ </div>
72
+ <div className="flex gap-1.5">
73
+ <div className="h-5 w-16 bg-surface-4 rounded animate-pulse" />
74
+ <div className="h-5 w-24 bg-surface-4 rounded animate-pulse" />
75
+ </div>
76
+ </Card>
77
+ );
78
+ }