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,363 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useCallback } from 'react';
3
+ import {
4
+ Search, GitBranch, Star, ExternalLink, Loader2, Check,
5
+ FolderOpen, HardDrive, Package, PenLine, Users, ArrowLeft, Download,
6
+ } from 'lucide-react';
7
+ import { Button } from '../ui/button';
8
+ import { Badge } from '../ui/badge';
9
+ import { cn } from '../../lib/cn';
10
+ import { fmtNum } from '../../lib/format';
11
+ import { useGrooveStore } from '../../stores/groove';
12
+ import { useToast } from '../../lib/hooks/use-toast';
13
+
14
+ const GITHUB_RE = /github\.com\/([^/]+)\/([^/\s#?]+)/;
15
+
16
+ export function RepoImport() {
17
+ const [step, setStep] = useState('input');
18
+ const [url, setUrl] = useState('');
19
+ const [loading, setLoading] = useState(false);
20
+ const [preview, setPreview] = useState(null);
21
+ const [pathOption, setPathOption] = useState('standalone');
22
+ const [customPath, setCustomPath] = useState('');
23
+ const [createTeam, setCreateTeam] = useState(true);
24
+ const [teamName, setTeamName] = useState('');
25
+
26
+ const previewRepo = useGrooveStore((s) => s.previewRepo);
27
+ const importRepo = useGrooveStore((s) => s.importRepo);
28
+ const importInProgress = useGrooveStore((s) => s.importInProgress);
29
+ const toast = useToast();
30
+
31
+ const doPreview = useCallback(async (repoUrl) => {
32
+ const match = repoUrl.match(GITHUB_RE);
33
+ if (!match) return;
34
+ setLoading(true);
35
+ try {
36
+ const data = await previewRepo(repoUrl);
37
+ setPreview(data);
38
+ setTeamName(data.name || match[2]);
39
+ setStep('preview');
40
+ } catch (err) {
41
+ toast.error('Preview failed', err.message);
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ }, [previewRepo, toast]);
46
+
47
+ const handleUrlChange = useCallback((e) => {
48
+ const val = e.target.value;
49
+ setUrl(val);
50
+ if (GITHUB_RE.test(val) && step === 'input') {
51
+ doPreview(val);
52
+ }
53
+ }, [step, doPreview]);
54
+
55
+ const handleImport = useCallback(async () => {
56
+ if (!preview) return;
57
+ let targetPath;
58
+ if (pathOption === 'standalone') targetPath = `~/Projects/${preview.name}`;
59
+ else if (pathOption === 'subdirectory') targetPath = `./packages/${preview.name}`;
60
+ else targetPath = customPath;
61
+ try {
62
+ await importRepo(url, targetPath, createTeam, teamName);
63
+ toast.success(`Importing ${preview.name}`, 'Setup agent will handle the rest');
64
+ setStep('input');
65
+ setUrl('');
66
+ setPreview(null);
67
+ } catch (err) {
68
+ toast.error('Import failed', err.message);
69
+ }
70
+ }, [preview, pathOption, customPath, url, createTeam, teamName, importRepo, toast]);
71
+
72
+ // ── Step 1: URL Input ──────────────────────────────────
73
+ if (step === 'input') {
74
+ return (
75
+ <div className="relative">
76
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
77
+ <input
78
+ type="text"
79
+ value={url}
80
+ onChange={handleUrlChange}
81
+ placeholder="Paste a GitHub URL..."
82
+ className={cn(
83
+ 'w-full h-9 rounded-lg pl-9 pr-20 text-sm font-sans',
84
+ 'bg-surface-1 border border-border text-text-0',
85
+ 'placeholder:text-text-4',
86
+ 'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
87
+ 'transition-colors duration-100',
88
+ )}
89
+ />
90
+ <Button
91
+ variant="primary"
92
+ size="sm"
93
+ className="absolute right-1.5 top-1/2 -translate-y-1/2"
94
+ onClick={() => doPreview(url)}
95
+ disabled={!GITHUB_RE.test(url) || loading}
96
+ >
97
+ {loading ? <Loader2 size={12} className="animate-spin" /> : 'Preview'}
98
+ </Button>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ // ── Step 2: Preview ────────────────────────────────────
104
+ if (step === 'preview' && preview) {
105
+ return (
106
+ <div className="space-y-4">
107
+ <div className="rounded-lg border border-border-subtle bg-surface-2 p-5">
108
+ <div className="flex items-start gap-4">
109
+ <div className="w-12 h-12 rounded-xl bg-accent/8 flex items-center justify-center flex-shrink-0">
110
+ <GitBranch size={22} className="text-accent" />
111
+ </div>
112
+ <div className="flex-1 min-w-0">
113
+ <div className="flex items-center gap-2.5 mb-1">
114
+ <span className="text-lg font-bold text-text-0 font-sans">{preview.name}</span>
115
+ <span className="text-xs text-text-4 font-sans">{preview.owner}</span>
116
+ </div>
117
+ <div className="flex items-center gap-3 text-2xs text-text-3 font-sans">
118
+ {preview.language && <Badge variant="outline" className="text-2xs">{preview.language}</Badge>}
119
+ {preview.stars != null && (
120
+ <span className="flex items-center gap-1">
121
+ <Star size={10} className="text-warning" fill="currentColor" />
122
+ {fmtNum(preview.stars)}
123
+ </span>
124
+ )}
125
+ {preview.license && <span>{preview.license}</span>}
126
+ </div>
127
+ </div>
128
+ <div className="flex items-center gap-2 flex-shrink-0">
129
+ <Button variant="primary" size="sm" onClick={() => setStep('configure')} className="h-8 text-xs gap-1.5 px-4">
130
+ <FolderOpen size={13} />
131
+ Clone & Setup
132
+ </Button>
133
+ <Button
134
+ variant="ghost"
135
+ size="sm"
136
+ onClick={() => window.open(url.startsWith('http') ? url : `https://${url}`, '_blank')}
137
+ className="h-8 text-xs gap-1.5"
138
+ >
139
+ <ExternalLink size={12} />
140
+ GitHub
141
+ </Button>
142
+ <button
143
+ onClick={() => { setStep('input'); setPreview(null); }}
144
+ className="text-2xs text-text-4 font-sans hover:text-text-2 cursor-pointer bg-transparent border-0 ml-1"
145
+ >
146
+ Cancel
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ {preview.description && (
153
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-5 py-4">
154
+ <h4 className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2">About</h4>
155
+ <p className="text-sm text-text-1 font-sans leading-relaxed">{preview.description}</p>
156
+ </div>
157
+ )}
158
+
159
+ {preview.readmePreview && (
160
+ <div className="rounded-lg border border-border-subtle bg-surface-1 px-5 py-4">
161
+ <h4 className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-3">README</h4>
162
+ <div className="text-sm text-text-2 font-sans leading-relaxed whitespace-pre-wrap">{preview.readmePreview}</div>
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }
168
+
169
+ // ── Step 3: Configure ──────────────────────────────────
170
+ if (step === 'configure' && preview) {
171
+ const pathOptions = [
172
+ {
173
+ id: 'standalone',
174
+ icon: HardDrive,
175
+ title: 'Standalone project',
176
+ description: 'Clone to its own directory, separate from this workspace',
177
+ path: `~/Projects/${preview.name}`,
178
+ },
179
+ {
180
+ id: 'subdirectory',
181
+ icon: Package,
182
+ title: 'Workspace package',
183
+ description: 'Add as a package inside this project\'s monorepo',
184
+ path: `./packages/${preview.name}`,
185
+ },
186
+ {
187
+ id: 'custom',
188
+ icon: PenLine,
189
+ title: 'Custom location',
190
+ description: 'Choose your own path',
191
+ path: null,
192
+ },
193
+ ];
194
+
195
+ return (
196
+ <div className="rounded-xl border border-border-subtle bg-surface-2 overflow-hidden">
197
+ {/* Header */}
198
+ <div className="px-5 py-4 border-b border-border-subtle bg-surface-3/50">
199
+ <div className="flex items-center gap-3">
200
+ <div className="w-9 h-9 rounded-lg bg-accent/10 flex items-center justify-center flex-shrink-0">
201
+ <Download size={16} className="text-accent" />
202
+ </div>
203
+ <div>
204
+ <h3 className="text-sm font-semibold text-text-0 font-sans">
205
+ Clone {preview.name}
206
+ </h3>
207
+ <p className="text-2xs text-text-4 font-sans mt-0.5">
208
+ {preview.owner}/{preview.name} — configure where to install
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div className="px-5 py-4 space-y-5">
215
+ {/* Location picker */}
216
+ <div>
217
+ <label className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2.5 block">
218
+ Install location
219
+ </label>
220
+ <div className="space-y-2">
221
+ {pathOptions.map((opt) => {
222
+ const Icon = opt.icon;
223
+ const selected = pathOption === opt.id;
224
+ return (
225
+ <button
226
+ key={opt.id}
227
+ onClick={() => setPathOption(opt.id)}
228
+ className={cn(
229
+ 'w-full text-left rounded-lg border p-3.5 transition-all duration-150 cursor-pointer',
230
+ selected
231
+ ? 'border-accent bg-accent/5 ring-1 ring-accent/30'
232
+ : 'border-border-subtle bg-surface-1 hover:border-border hover:bg-surface-1/80',
233
+ )}
234
+ >
235
+ <div className="flex items-start gap-3">
236
+ <div className={cn(
237
+ 'w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 mt-0.5',
238
+ selected ? 'bg-accent/15 text-accent' : 'bg-surface-3 text-text-4',
239
+ )}>
240
+ <Icon size={15} />
241
+ </div>
242
+ <div className="flex-1 min-w-0">
243
+ <div className="flex items-center gap-2">
244
+ <span className={cn(
245
+ 'text-xs font-semibold font-sans',
246
+ selected ? 'text-text-0' : 'text-text-2',
247
+ )}>
248
+ {opt.title}
249
+ </span>
250
+ {selected && (
251
+ <div className="w-4 h-4 rounded-full bg-accent flex items-center justify-center">
252
+ <Check size={10} className="text-white" />
253
+ </div>
254
+ )}
255
+ </div>
256
+ <p className="text-2xs text-text-4 font-sans mt-0.5 leading-relaxed">
257
+ {opt.description}
258
+ </p>
259
+ {opt.path && selected && (
260
+ <code className="text-2xs text-accent/80 font-mono mt-1.5 block truncate">
261
+ {opt.path}
262
+ </code>
263
+ )}
264
+ </div>
265
+ </div>
266
+ </button>
267
+ );
268
+ })}
269
+ </div>
270
+
271
+ {/* Custom path input */}
272
+ {pathOption === 'custom' && (
273
+ <div className="mt-2.5 ml-11">
274
+ <input
275
+ value={customPath}
276
+ onChange={(e) => setCustomPath(e.target.value)}
277
+ placeholder="/path/to/clone"
278
+ autoFocus
279
+ className={cn(
280
+ 'w-full h-9 px-3 text-xs font-mono rounded-md',
281
+ 'bg-surface-0 border border-border text-text-0',
282
+ 'placeholder:text-text-4',
283
+ 'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
284
+ 'transition-colors',
285
+ )}
286
+ />
287
+ </div>
288
+ )}
289
+ </div>
290
+
291
+ {/* Team toggle */}
292
+ <div className="border-t border-border-subtle pt-4">
293
+ <div className="flex items-center justify-between">
294
+ <div className="flex items-center gap-2.5">
295
+ <div className="w-8 h-8 rounded-md bg-surface-3 flex items-center justify-center">
296
+ <Users size={14} className="text-text-4" />
297
+ </div>
298
+ <div>
299
+ <span className="text-xs font-semibold text-text-2 font-sans block">Create a team</span>
300
+ <span className="text-2xs text-text-4 font-sans">Organize agents working on this repo into their own team</span>
301
+ </div>
302
+ </div>
303
+ <button
304
+ onClick={() => setCreateTeam(!createTeam)}
305
+ className={cn(
306
+ 'w-9 h-5 rounded-full p-0.5 transition-colors cursor-pointer flex-shrink-0',
307
+ createTeam ? 'bg-accent' : 'bg-surface-5',
308
+ )}
309
+ >
310
+ <div className={cn(
311
+ 'w-4 h-4 rounded-full bg-white shadow-sm transition-transform',
312
+ createTeam ? 'translate-x-4' : 'translate-x-0',
313
+ )} />
314
+ </button>
315
+ </div>
316
+ {createTeam && (
317
+ <div className="mt-2.5 ml-11">
318
+ <input
319
+ value={teamName}
320
+ onChange={(e) => setTeamName(e.target.value)}
321
+ placeholder="Team name"
322
+ className={cn(
323
+ 'w-full h-9 px-3 text-xs font-sans rounded-md',
324
+ 'bg-surface-0 border border-border text-text-0',
325
+ 'placeholder:text-text-4',
326
+ 'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
327
+ 'transition-colors',
328
+ )}
329
+ />
330
+ </div>
331
+ )}
332
+ </div>
333
+ </div>
334
+
335
+ {/* Footer */}
336
+ <div className="px-5 py-3.5 border-t border-border-subtle bg-surface-3/30 flex items-center justify-between">
337
+ <button
338
+ onClick={() => setStep('preview')}
339
+ className="flex items-center gap-1.5 text-2xs text-text-4 font-sans hover:text-text-2 cursor-pointer bg-transparent border-0 transition-colors"
340
+ >
341
+ <ArrowLeft size={11} />
342
+ Back
343
+ </button>
344
+ <Button
345
+ variant="primary"
346
+ size="sm"
347
+ onClick={handleImport}
348
+ disabled={importInProgress || (pathOption === 'custom' && !customPath.trim())}
349
+ className="h-8 text-xs gap-1.5 px-5"
350
+ >
351
+ {importInProgress ? (
352
+ <><Loader2 size={12} className="animate-spin" /> Importing...</>
353
+ ) : (
354
+ <><Download size={12} /> Clone & Setup</>
355
+ )}
356
+ </Button>
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ return null;
363
+ }
@@ -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
+ }