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,819 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { AnimatePresence, motion } from 'framer-motion';
5
+ import { useGrooveStore } from '../../stores/groove';
6
+ import { isElectron, selectFolder, setProjectDir } from '../../lib/electron';
7
+ import { cn } from '../../lib/cn';
8
+ import { ProviderCard } from './provider-card';
9
+ import { FolderBrowser } from '../agents/folder-browser';
10
+ import { Badge } from '../ui/badge';
11
+ import {
12
+ ChevronRight, ChevronLeft, Eye, EyeOff, Check, Sparkles, ArrowRight, FolderOpen,
13
+ } from 'lucide-react';
14
+
15
+ // ── Provider definitions ────────────────────────────────────
16
+
17
+ const PROVIDERS = [
18
+ {
19
+ id: 'claude-code',
20
+ name: 'Claude Code',
21
+ subtitle: 'by Anthropic',
22
+ models: ['Opus 4.6', 'Sonnet 4.6', 'Haiku 4.5'],
23
+ authType: 'Subscription or API key',
24
+ authModes: ['subscription', 'apikey'],
25
+ recommended: true,
26
+ letter: 'C',
27
+ gradientFrom: 'bg-purple/20 text-purple',
28
+ keyPlaceholder: 'sk-ant-...',
29
+ keyLabel: 'Anthropic API Key',
30
+ },
31
+ {
32
+ id: 'codex',
33
+ name: 'Codex',
34
+ subtitle: 'by OpenAI',
35
+ models: ['GPT-5.4 Pro', 'Standard', 'Mini', 'Nano'],
36
+ authType: 'API key or ChatGPT Plus',
37
+ authModes: ['apikey', 'chatgpt-plus'],
38
+ recommended: false,
39
+ letter: 'X',
40
+ gradientFrom: 'bg-success/20 text-success',
41
+ keyPlaceholder: 'sk-...',
42
+ keyLabel: 'OpenAI API Key',
43
+ },
44
+ {
45
+ id: 'gemini',
46
+ name: 'Gemini CLI',
47
+ subtitle: 'by Google',
48
+ models: ['Gemini 3.1 Pro', '3 Flash'],
49
+ authType: 'API key',
50
+ authModes: ['apikey'],
51
+ recommended: false,
52
+ letter: 'G',
53
+ gradientFrom: 'bg-info/20 text-info',
54
+ keyPlaceholder: 'AIza...',
55
+ keyLabel: 'Gemini API Key',
56
+ },
57
+ ];
58
+
59
+ // ── Animation variants ──────────────────────────────────────
60
+
61
+ const slideVariants = {
62
+ enter: (dir) => ({ x: dir > 0 ? 80 : -80, opacity: 0 }),
63
+ center: { x: 0, opacity: 1 },
64
+ exit: (dir) => ({ x: dir < 0 ? 80 : -80, opacity: 0 }),
65
+ };
66
+
67
+ const transition = { duration: 0.25, ease: [0.4, 0, 0.2, 1] };
68
+
69
+ // ── Step indicator ──────────────────────────────────────────
70
+
71
+ function StepDots({ current, total }) {
72
+ return (
73
+ <div className="flex items-center gap-2.5">
74
+ {Array.from({ length: total }, (_, i) => (
75
+ <div
76
+ key={i}
77
+ className={cn(
78
+ 'h-2 rounded-full transition-all duration-300',
79
+ i === current ? 'w-7 bg-accent' : i < current ? 'w-2 bg-accent/50' : 'w-2 bg-surface-5',
80
+ )}
81
+ />
82
+ ))}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ // ── Step 1: Welcome ─────────────────────────────────────────
88
+
89
+ function WelcomeStep({ onNext, onSkip }) {
90
+ return (
91
+ <div className="flex flex-col items-center justify-center text-center gap-6 max-w-lg mx-auto">
92
+ <motion.img
93
+ src="/favicon.png"
94
+ alt="Groove"
95
+ className="w-20 h-20"
96
+ initial={{ scale: 0.5, opacity: 0 }}
97
+ animate={{ scale: 1, opacity: 1 }}
98
+ transition={{ duration: 0.4, ease: 'easeOut' }}
99
+ />
100
+ <div className="space-y-2">
101
+ <h1 className="text-2xl font-bold text-text-0 font-sans">Welcome to Groove</h1>
102
+ <p className="text-sm text-text-2">Your AI coding team, ready in minutes</p>
103
+ </div>
104
+ <p className="text-xs text-text-3 leading-relaxed max-w-sm">
105
+ Let's set up your AI providers so you can start spawning agents.
106
+ This only takes a moment.
107
+ </p>
108
+ <button
109
+ type="button"
110
+ onClick={onNext}
111
+ className="mt-4 h-11 px-8 rounded-full bg-accent text-surface-0 font-semibold text-sm hover:bg-accent/80 transition-colors duration-100 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 flex items-center gap-2"
112
+ autoFocus
113
+ >
114
+ Get Started
115
+ <ArrowRight className="w-4 h-4" />
116
+ </button>
117
+ <button
118
+ type="button"
119
+ onClick={onSkip}
120
+ className="text-xs text-text-4 hover:text-text-2 transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent rounded px-2 py-1"
121
+ >
122
+ Skip setup
123
+ </button>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // ── Step 2: Project Folder ──────────────────────────────────
129
+
130
+ function ProjectFolderStep({ selectedDir, onSelectDir }) {
131
+ const [browsing, setBrowsing] = useState(false);
132
+
133
+ async function handleBrowse() {
134
+ const dir = await selectFolder({
135
+ title: 'Choose your project folder',
136
+ defaultPath: selectedDir || undefined,
137
+ });
138
+ if (dir) {
139
+ onSelectDir(dir);
140
+ } else if (!window.groove?.folders?.select) {
141
+ setBrowsing(true);
142
+ }
143
+ }
144
+
145
+ return (
146
+ <div className="flex flex-col items-center max-w-lg mx-auto w-full text-center">
147
+ <div className="mb-8">
148
+ <h2 className="text-2xl font-bold text-text-0 mb-2">Choose your project folder</h2>
149
+ <p className="text-sm text-text-2">
150
+ Pick the root directory where your code lives. Groove will manage agents from here.
151
+ </p>
152
+ </div>
153
+
154
+ <div className="w-full bg-surface-2 border border-border-subtle rounded-lg p-5 mb-6">
155
+ <div className="flex items-center gap-3">
156
+ <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center">
157
+ <FolderOpen className="w-6 h-6 text-accent" />
158
+ </div>
159
+ <div className="flex-1 min-w-0 text-left">
160
+ <p className="text-xs text-text-3 mb-0.5">Working directory</p>
161
+ <p className="text-sm font-mono text-text-0 truncate">
162
+ {selectedDir || 'No folder selected'}
163
+ </p>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <button
169
+ type="button"
170
+ onClick={handleBrowse}
171
+ className="h-10 px-6 rounded-lg bg-accent text-surface-0 font-medium text-sm hover:bg-accent/80 transition-colors cursor-pointer flex items-center gap-2"
172
+ >
173
+ <FolderOpen className="w-4 h-4" />
174
+ {selectedDir ? 'Change Folder' : 'Select Folder'}
175
+ </button>
176
+
177
+ <p className="mt-4 text-2xs text-text-4">
178
+ You can change this anytime in Settings.
179
+ </p>
180
+
181
+ {browsing && (
182
+ <FolderBrowser
183
+ open={browsing}
184
+ onOpenChange={setBrowsing}
185
+ currentPath={selectedDir || '/'}
186
+ onSelect={(dir) => { onSelectDir(dir); setBrowsing(false); }}
187
+ />
188
+ )}
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // ── Step 3: Install Providers ───────────────────────────────
194
+
195
+ function InstallStep({ providerStatus, selected, onInstall, installing, statusChecking }) {
196
+ const installedCount = PROVIDERS.filter((p) => providerStatus[p.id]?.installed).length;
197
+ const allInstalled = installedCount === PROVIDERS.length;
198
+
199
+ return (
200
+ <div className="flex flex-col items-center max-w-4xl mx-auto w-full">
201
+ <div className="text-center mb-10">
202
+ <h2 className="text-2xl font-bold text-text-0 mb-2">Choose your AI providers</h2>
203
+ <p className="text-sm text-text-2">Install the coding tools you want to use. You can always add more later.</p>
204
+ </div>
205
+
206
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full mb-8">
207
+ {PROVIDERS.map((p) => (
208
+ <ProviderCard
209
+ key={p.id}
210
+ {...p}
211
+ installed={providerStatus[p.id]?.installed}
212
+ installing={installing[p.id]}
213
+ failed={providerStatus[p.id]?.failed}
214
+ selected={selected.includes(p.id)}
215
+ onInstall={onInstall}
216
+ statusChecking={statusChecking}
217
+ />
218
+ ))}
219
+ </div>
220
+
221
+ <p className={cn(
222
+ 'text-xs text-center',
223
+ allInstalled ? 'text-success' : installedCount > 0 ? 'text-text-2' : 'text-text-4',
224
+ )}>
225
+ {allInstalled
226
+ ? 'All providers installed — you\'re ready to go!'
227
+ : installedCount > 0
228
+ ? `${installedCount} of ${PROVIDERS.length} providers installed`
229
+ : 'Click Install to set up a provider'}
230
+ </p>
231
+ </div>
232
+ );
233
+ }
234
+
235
+ // ── Step 4: Authentication ──────────────────────────────────
236
+
237
+ function AuthCard({ provider, providerStatus, onSaveKey, onSubscriptionLogin }) {
238
+ const [authMode, setAuthMode] = useState(
239
+ provider.authModes.includes('subscription') ? 'subscription' : 'apikey',
240
+ );
241
+ const [key, setKey] = useState('');
242
+ const [showKey, setShowKey] = useState(false);
243
+ const [saving, setSaving] = useState(false);
244
+ const [saved, setSaved] = useState(providerStatus?.authenticated || false);
245
+
246
+ const handleSave = async () => {
247
+ if (!key.trim()) return;
248
+ setSaving(true);
249
+ try {
250
+ await onSaveKey(provider.id, key.trim());
251
+ setSaved(true);
252
+ } catch {
253
+ // toast handled upstream
254
+ } finally {
255
+ setSaving(false);
256
+ }
257
+ };
258
+
259
+ const maskedKey = providerStatus?.maskedKey || (saved && key ? `${key.slice(0, 6)}${'•'.repeat(20)}` : null);
260
+
261
+ return (
262
+ <motion.div
263
+ initial={{ opacity: 0, y: 8 }}
264
+ animate={{ opacity: 1, y: 0 }}
265
+ transition={{ duration: 0.2 }}
266
+ className="bg-surface-2 border border-border-subtle rounded-md p-5"
267
+ >
268
+ <div className="flex items-center gap-3 mb-4">
269
+ <div className={cn('w-10 h-10 rounded-md flex items-center justify-center text-sm font-bold font-mono', provider.gradientFrom)}>
270
+ {provider.letter}
271
+ </div>
272
+ <div>
273
+ <h3 className="text-sm font-semibold text-text-0">{provider.name}</h3>
274
+ <p className="text-2xs text-text-3">{provider.subtitle}</p>
275
+ </div>
276
+ {(saved || providerStatus?.authenticated) && (
277
+ <Badge variant="success" className="ml-auto">Connected</Badge>
278
+ )}
279
+ </div>
280
+
281
+ {provider.authModes.length > 1 && (
282
+ <div className="flex gap-1 mb-4 bg-surface-3 p-0.5 rounded-md">
283
+ {provider.authModes.map((mode) => (
284
+ <button
285
+ key={mode}
286
+ type="button"
287
+ onClick={() => setAuthMode(mode)}
288
+ className={cn(
289
+ 'flex-1 h-7 rounded text-xs font-medium transition-colors duration-100 cursor-pointer',
290
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent',
291
+ authMode === mode ? 'bg-surface-5 text-text-0' : 'text-text-3 hover:text-text-1',
292
+ )}
293
+ >
294
+ {mode === 'subscription' ? 'Subscription' : mode === 'chatgpt-plus' ? 'ChatGPT Plus' : 'API Key'}
295
+ </button>
296
+ ))}
297
+ </div>
298
+ )}
299
+
300
+ {authMode === 'subscription' && (
301
+ <div className="space-y-3">
302
+ <p className="text-xs text-text-2">Sign in with your Claude subscription</p>
303
+ <button
304
+ type="button"
305
+ onClick={() => onSubscriptionLogin(provider.id)}
306
+ className="h-8 px-4 rounded-md bg-purple/15 text-purple border border-purple/20 text-xs font-medium hover:bg-purple/25 transition-colors duration-100 cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-purple"
307
+ >
308
+ Sign In
309
+ </button>
310
+ {providerStatus?.authenticated && (
311
+ <p className="text-xs text-success flex items-center gap-1.5">
312
+ <Check className="w-3.5 h-3.5" /> Connected
313
+ </p>
314
+ )}
315
+ </div>
316
+ )}
317
+
318
+ {authMode === 'chatgpt-plus' && (
319
+ <div className="space-y-3">
320
+ <p className="text-xs text-text-2">
321
+ Run <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">codex login</code> in your terminal to authenticate with ChatGPT Plus.
322
+ </p>
323
+ {providerStatus?.authenticated && (
324
+ <p className="text-xs text-success flex items-center gap-1.5">
325
+ <Check className="w-3.5 h-3.5" /> Connected
326
+ </p>
327
+ )}
328
+ </div>
329
+ )}
330
+
331
+ {authMode === 'apikey' && (
332
+ <div className="space-y-3">
333
+ {saved || providerStatus?.authenticated ? (
334
+ <div className="flex items-center gap-2">
335
+ <div className="flex-1 h-8 rounded-md bg-surface-1 border border-border px-3 flex items-center">
336
+ <span className="text-xs text-text-3 font-mono truncate">{maskedKey || '••••••••••••••'}</span>
337
+ </div>
338
+ <button
339
+ type="button"
340
+ onClick={() => { setSaved(false); setKey(''); }}
341
+ className="h-8 px-3 rounded-md text-xs text-text-3 hover:text-text-1 bg-surface-4 hover:bg-surface-5 transition-colors duration-100 cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
342
+ >
343
+ Change
344
+ </button>
345
+ </div>
346
+ ) : (
347
+ <div className="flex gap-2">
348
+ <div className="relative flex-1">
349
+ <input
350
+ type={showKey ? 'text' : 'password'}
351
+ value={key}
352
+ onChange={(e) => setKey(e.target.value)}
353
+ placeholder={provider.keyPlaceholder}
354
+ className="h-8 w-full rounded-md px-3 pr-8 text-sm bg-surface-1 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors duration-100 font-mono"
355
+ onKeyDown={(e) => e.key === 'Enter' && handleSave()}
356
+ aria-label={provider.keyLabel}
357
+ />
358
+ <button
359
+ type="button"
360
+ onClick={() => setShowKey(!showKey)}
361
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer"
362
+ aria-label={showKey ? 'Hide key' : 'Show key'}
363
+ >
364
+ {showKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
365
+ </button>
366
+ </div>
367
+ <button
368
+ type="button"
369
+ onClick={handleSave}
370
+ disabled={!key.trim() || saving}
371
+ className="h-8 px-4 rounded-md bg-accent text-surface-0 text-xs font-medium hover:bg-accent/80 transition-colors duration-100 cursor-pointer disabled:opacity-40 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
372
+ >
373
+ {saving ? 'Saving...' : 'Save'}
374
+ </button>
375
+ </div>
376
+ )}
377
+ </div>
378
+ )}
379
+ </motion.div>
380
+ );
381
+ }
382
+
383
+ function AuthStep({ providerStatus, installedIds, onSaveKey, onSubscriptionLogin }) {
384
+ const installed = PROVIDERS.filter((p) => installedIds.includes(p.id) || providerStatus[p.id]?.installed);
385
+ const hasAuthenticated = installed.some((p) => providerStatus[p.id]?.authenticated);
386
+
387
+ return (
388
+ <div className="flex flex-col items-center max-w-2xl mx-auto w-full">
389
+ <div className="text-center mb-8">
390
+ <h2 className="text-2xl font-bold text-text-0 mb-2">Connect your accounts</h2>
391
+ <p className="text-sm text-text-2">Add credentials for your installed providers.</p>
392
+ </div>
393
+
394
+ <div className="flex flex-col gap-4 w-full mb-6">
395
+ {installed.map((p) => (
396
+ <AuthCard
397
+ key={p.id}
398
+ provider={p}
399
+ providerStatus={providerStatus[p.id] || {}}
400
+ onSaveKey={onSaveKey}
401
+ onSubscriptionLogin={onSubscriptionLogin}
402
+ />
403
+ ))}
404
+ </div>
405
+
406
+ {installed.length === 0 && (
407
+ <p className="text-sm text-text-3 text-center">No providers installed yet. Go back to install one.</p>
408
+ )}
409
+ </div>
410
+ );
411
+ }
412
+
413
+ // ── Step 5: Default Model ───────────────────────────────────
414
+
415
+ function DefaultModelStep({ providerStatus, installedIds, defaultProvider, defaultModel, onSetDefault }) {
416
+ const authenticated = PROVIDERS.filter(
417
+ (p) => (installedIds.includes(p.id) || providerStatus[p.id]?.installed) && providerStatus[p.id]?.authenticated,
418
+ );
419
+
420
+ return (
421
+ <div className="flex flex-col items-center max-w-2xl mx-auto w-full">
422
+ <div className="text-center mb-8">
423
+ <h2 className="text-2xl font-bold text-text-0 mb-2">Set your default</h2>
424
+ <p className="text-sm text-text-2">Choose which provider and model to use by default. You can switch per-agent anytime.</p>
425
+ </div>
426
+
427
+ <div className="flex flex-col gap-3 w-full max-w-md mb-6">
428
+ {authenticated.map((p) => (
429
+ <button
430
+ key={p.id}
431
+ type="button"
432
+ onClick={() => onSetDefault(p.id, p.models[0])}
433
+ className={cn(
434
+ 'flex items-center gap-4 p-4 rounded-md border transition-all duration-200 text-left cursor-pointer',
435
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent',
436
+ defaultProvider === p.id
437
+ ? 'bg-accent/8 border-accent ring-1 ring-accent/30'
438
+ : 'bg-surface-2 border-border-subtle hover:bg-surface-3',
439
+ )}
440
+ >
441
+ <div className={cn('w-10 h-10 rounded-md flex items-center justify-center text-sm font-bold font-mono shrink-0', p.gradientFrom)}>
442
+ {p.letter}
443
+ </div>
444
+ <div className="flex-1 min-w-0">
445
+ <h3 className="text-sm font-semibold text-text-0">{p.name}</h3>
446
+ <p className="text-2xs text-text-3">{p.subtitle}</p>
447
+ </div>
448
+ <div className={cn(
449
+ 'w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors',
450
+ defaultProvider === p.id ? 'border-accent bg-accent' : 'border-border',
451
+ )}>
452
+ {defaultProvider === p.id && (
453
+ <div className="w-2 h-2 rounded-full bg-surface-0" />
454
+ )}
455
+ </div>
456
+ </button>
457
+ ))}
458
+
459
+ {authenticated.length === 0 && (
460
+ <p className="text-sm text-text-3 text-center">No authenticated providers. Go back to connect one.</p>
461
+ )}
462
+ </div>
463
+
464
+ {defaultProvider && (
465
+ <div className="w-full max-w-md">
466
+ <label className="text-xs font-medium text-text-2 mb-2 block">Model</label>
467
+ <div className="flex flex-wrap gap-2">
468
+ {PROVIDERS.find((p) => p.id === defaultProvider)?.models.map((m) => (
469
+ <button
470
+ key={m}
471
+ type="button"
472
+ onClick={() => onSetDefault(defaultProvider, m)}
473
+ className={cn(
474
+ 'h-7 px-3 rounded-full text-xs font-medium transition-colors duration-100 cursor-pointer',
475
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent',
476
+ defaultModel === m
477
+ ? 'bg-accent text-surface-0'
478
+ : 'bg-surface-4 text-text-2 hover:bg-surface-5 hover:text-text-0',
479
+ )}
480
+ >
481
+ {m}
482
+ </button>
483
+ ))}
484
+ </div>
485
+ </div>
486
+ )}
487
+ </div>
488
+ );
489
+ }
490
+
491
+ // ── Step 6: Done ────────────────────────────────────────────
492
+
493
+ function DoneStep({ providerStatus, defaultProvider, defaultModel, onFinish }) {
494
+ const installedCount = PROVIDERS.filter((p) => providerStatus[p.id]?.installed).length;
495
+ const defName = PROVIDERS.find((p) => p.id === defaultProvider)?.name;
496
+
497
+ return (
498
+ <div className="flex flex-col items-center justify-center text-center gap-6 max-w-lg mx-auto">
499
+ <motion.div
500
+ initial={{ scale: 0 }}
501
+ animate={{ scale: 1 }}
502
+ transition={{ type: 'spring', stiffness: 300, damping: 20 }}
503
+ className="w-20 h-20 rounded-full bg-success/15 flex items-center justify-center"
504
+ >
505
+ <Check className="w-10 h-10 text-success" strokeWidth={2.5} />
506
+ </motion.div>
507
+
508
+ <div className="space-y-2">
509
+ <h1 className="text-2xl font-bold text-text-0 font-sans">You're all set!</h1>
510
+ <p className="text-sm text-text-2">
511
+ {installedCount} provider{installedCount !== 1 ? 's' : ''} installed
512
+ {defName ? `, default: ${defName}` : ''}
513
+ {defaultModel ? ` (${defaultModel})` : ''}
514
+ </p>
515
+ </div>
516
+
517
+ <motion.div
518
+ className="flex gap-3 mt-4"
519
+ initial={{ opacity: 0, y: 8 }}
520
+ animate={{ opacity: 1, y: 0 }}
521
+ transition={{ delay: 0.3 }}
522
+ >
523
+ <button
524
+ type="button"
525
+ onClick={onFinish}
526
+ className="h-11 px-8 rounded-full bg-accent text-surface-0 font-semibold text-sm hover:bg-accent/80 transition-colors duration-100 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 flex items-center gap-2"
527
+ autoFocus
528
+ >
529
+ <Sparkles className="w-4 h-4" />
530
+ Start Building
531
+ </button>
532
+ </motion.div>
533
+ </div>
534
+ );
535
+ }
536
+
537
+ // ── Main Wizard ─────────────────────────────────────────────
538
+
539
+ const TOTAL_STEPS = 6;
540
+
541
+ export function SetupWizard() {
542
+ const dismissOnboarding = useGrooveStore((s) => s.dismissOnboarding);
543
+ const installProvider = useGrooveStore((s) => s.installProvider);
544
+ const setDefaultProvider = useGrooveStore((s) => s.setDefaultProvider);
545
+ const addToast = useGrooveStore((s) => s.addToast);
546
+
547
+ const [step, setStep] = useState(0);
548
+ const [direction, setDirection] = useState(1);
549
+ const [selected, setSelected] = useState(['claude-code']);
550
+ const [installing, setInstalling] = useState({});
551
+ const [providerStatus, setProviderStatus] = useState({});
552
+ const [defaultProv, setDefaultProv] = useState(null);
553
+ const [defaultMod, setDefaultMod] = useState(null);
554
+ const [statusChecking, setStatusChecking] = useState(true);
555
+ const [projectDir, setProjectDir] = useState(null);
556
+ const containerRef = useRef(null);
557
+
558
+ useEffect(() => {
559
+ let cancelled = false;
560
+
561
+ async function fetchOnce() {
562
+ try {
563
+ const data = await fetch('/api/onboarding/status').then((r) => r.ok ? r.json() : null);
564
+ if (data?.providers) {
565
+ const status = {};
566
+ const installedIds = [];
567
+ for (const p of data.providers) {
568
+ const authed = p.authStatus === 'authenticated' || p.authStatus === 'key-set';
569
+ status[p.id] = { installed: p.installed, authenticated: authed };
570
+ if (p.installed) installedIds.push(p.id);
571
+ }
572
+ return { status, installedIds, workingDir: data.config?.defaultWorkingDir || null };
573
+ }
574
+ } catch { /* fallback */ }
575
+ try {
576
+ const data = await fetch('/api/providers').then((r) => r.ok ? r.json() : null);
577
+ if (data) {
578
+ const status = {};
579
+ const installedIds = [];
580
+ const list = Array.isArray(data) ? data : data.providers || [];
581
+ for (const p of list) {
582
+ const isInstalled = p.installed || p.authStatus === 'authenticated' || p.authStatus === 'key-set' || false;
583
+ status[p.id] = { installed: isInstalled, authenticated: p.authenticated || false };
584
+ if (isInstalled) installedIds.push(p.id);
585
+ }
586
+ return { status, installedIds };
587
+ }
588
+ } catch { /* ignore */ }
589
+ return null;
590
+ }
591
+
592
+ async function fetchStatus() {
593
+ setStatusChecking(true);
594
+ let result = await fetchOnce();
595
+ if (!cancelled && result && result.installedIds.length === 0) {
596
+ for (let retry = 0; retry < 2; retry++) {
597
+ await new Promise((r) => setTimeout(r, 2000));
598
+ if (cancelled) return;
599
+ result = await fetchOnce();
600
+ if (result && result.installedIds.length > 0) break;
601
+ }
602
+ }
603
+ if (!cancelled && result) {
604
+ setProviderStatus(result.status);
605
+ if (result.installedIds.length > 0) {
606
+ setSelected((prev) => {
607
+ const merged = new Set([...prev, ...result.installedIds]);
608
+ return [...merged];
609
+ });
610
+ }
611
+ if (result.workingDir && !projectDir) {
612
+ setProjectDir(result.workingDir);
613
+ }
614
+ }
615
+ if (!cancelled) setStatusChecking(false);
616
+ }
617
+ fetchStatus();
618
+ return () => { cancelled = true; };
619
+ }, [step]);
620
+
621
+ const goNext = useCallback(() => {
622
+ if (step < TOTAL_STEPS - 1) {
623
+ setDirection(1);
624
+ setStep((s) => s + 1);
625
+ }
626
+ }, [step]);
627
+
628
+ const goBack = useCallback(() => {
629
+ if (step > 0) {
630
+ setDirection(-1);
631
+ setStep((s) => s - 1);
632
+ }
633
+ }, [step]);
634
+
635
+ const handleSkip = useCallback(() => {
636
+ dismissOnboarding();
637
+ }, [dismissOnboarding]);
638
+
639
+ const handleFinish = useCallback(async () => {
640
+ if (projectDir) {
641
+ try {
642
+ await setProjectDir(projectDir);
643
+ } catch {
644
+ addToast('error', 'Failed to set project directory');
645
+ }
646
+ }
647
+ dismissOnboarding();
648
+ }, [dismissOnboarding, projectDir, addToast]);
649
+
650
+ const handleInstall = useCallback(async (id) => {
651
+ setInstalling((prev) => ({ ...prev, [id]: true }));
652
+ try {
653
+ await installProvider(id);
654
+ setProviderStatus((prev) => ({ ...prev, [id]: { ...prev[id], installed: true } }));
655
+ setSelected((s) => s.includes(id) ? s : [...s, id]);
656
+ } catch {
657
+ setProviderStatus((prev) => ({ ...prev, [id]: { ...prev[id], failed: true } }));
658
+ } finally {
659
+ setInstalling((prev) => ({ ...prev, [id]: false }));
660
+ }
661
+ }, [installProvider]);
662
+
663
+ const handleSaveKey = useCallback(async (providerId, key) => {
664
+ try {
665
+ const res = await fetch(`/api/credentials/${encodeURIComponent(providerId)}`, {
666
+ method: 'POST',
667
+ headers: { 'Content-Type': 'application/json' },
668
+ body: JSON.stringify({ key }),
669
+ });
670
+ if (!res.ok) throw new Error('Failed to save key');
671
+ setProviderStatus((prev) => ({
672
+ ...prev,
673
+ [providerId]: { ...prev[providerId], authenticated: true, maskedKey: `${key.slice(0, 6)}${'•'.repeat(20)}` },
674
+ }));
675
+ addToast('success', 'API key saved');
676
+ } catch (err) {
677
+ addToast('error', 'Failed to save key', err.message);
678
+ throw err;
679
+ }
680
+ }, [addToast]);
681
+
682
+ const handleSubscriptionLogin = useCallback((providerId) => {
683
+ if (isElectron() && window.groove?.auth?.login) {
684
+ window.groove.auth.login();
685
+ } else {
686
+ fetch('/api/auth/login-url')
687
+ .then((r) => r.json())
688
+ .then((d) => { if (d.url) window.open(d.url, '_blank'); })
689
+ .catch(() => addToast('error', 'Failed to start login'));
690
+ }
691
+ }, [addToast]);
692
+
693
+ const handleSetDefault = useCallback(async (provider, model) => {
694
+ setDefaultProv(provider);
695
+ setDefaultMod(model);
696
+ try {
697
+ await setDefaultProvider(provider, model);
698
+ } catch { /* toast upstream */ }
699
+ }, [setDefaultProvider]);
700
+
701
+ const hasInstalled = PROVIDERS.some((p) => providerStatus[p.id]?.installed);
702
+ const hasAuthenticated = PROVIDERS.some((p) => providerStatus[p.id]?.authenticated);
703
+ const canContinue =
704
+ step === 0 ? true :
705
+ step === 1 ? true :
706
+ step === 2 ? hasInstalled :
707
+ step === 3 ? hasAuthenticated :
708
+ step === 4 ? !!defaultProv :
709
+ false;
710
+
711
+ useEffect(() => {
712
+ const handleKeyDown = (e) => {
713
+ if (e.key === 'Enter' && step < TOTAL_STEPS - 1 && canContinue) goNext();
714
+ if (e.key === 'Escape') handleSkip();
715
+ };
716
+ window.addEventListener('keydown', handleKeyDown);
717
+ return () => window.removeEventListener('keydown', handleKeyDown);
718
+ }, [step, canContinue, goNext, handleSkip]);
719
+
720
+ const stepContent = [
721
+ <WelcomeStep key="welcome" onNext={goNext} onSkip={handleSkip} />,
722
+ <ProjectFolderStep key="folder" selectedDir={projectDir} onSelectDir={setProjectDir} />,
723
+ <InstallStep
724
+ key="install"
725
+ providerStatus={providerStatus}
726
+ selected={selected}
727
+ onInstall={handleInstall}
728
+ installing={installing}
729
+ statusChecking={statusChecking}
730
+ />,
731
+ <AuthStep
732
+ key="auth"
733
+ providerStatus={providerStatus}
734
+ installedIds={selected}
735
+ onSaveKey={handleSaveKey}
736
+ onSubscriptionLogin={handleSubscriptionLogin}
737
+ />,
738
+ <DefaultModelStep
739
+ key="default"
740
+ providerStatus={providerStatus}
741
+ installedIds={selected}
742
+ defaultProvider={defaultProv}
743
+ defaultModel={defaultMod}
744
+ onSetDefault={handleSetDefault}
745
+ />,
746
+ <DoneStep
747
+ key="done"
748
+ providerStatus={providerStatus}
749
+ defaultProvider={defaultProv}
750
+ defaultModel={defaultMod}
751
+ onFinish={handleFinish}
752
+ />,
753
+ ];
754
+
755
+ return (
756
+ <div
757
+ ref={containerRef}
758
+ className="fixed inset-0 z-50 bg-gradient-to-b from-surface-0 to-surface-1 flex flex-col font-sans overflow-hidden"
759
+ >
760
+ {/* Title bar drag region for Electron */}
761
+ {isElectron() && <div className="h-8 w-full electron-drag shrink-0" />}
762
+
763
+ {/* Top bar */}
764
+ <div className="flex items-center justify-between px-8 py-4 shrink-0">
765
+ <StepDots current={step} total={TOTAL_STEPS} />
766
+ {step > 0 && step < TOTAL_STEPS - 1 && (
767
+ <button
768
+ type="button"
769
+ onClick={handleSkip}
770
+ className="text-xs text-text-4 hover:text-text-2 transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent rounded px-2 py-1"
771
+ >
772
+ Skip setup
773
+ </button>
774
+ )}
775
+ </div>
776
+
777
+ {/* Step content */}
778
+ <div className="flex-1 flex items-center justify-center px-8 py-4 overflow-y-auto">
779
+ <AnimatePresence mode="wait" custom={direction}>
780
+ <motion.div
781
+ key={step}
782
+ custom={direction}
783
+ variants={slideVariants}
784
+ initial="enter"
785
+ animate="center"
786
+ exit="exit"
787
+ transition={transition}
788
+ className="w-full"
789
+ >
790
+ {stepContent[step]}
791
+ </motion.div>
792
+ </AnimatePresence>
793
+ </div>
794
+
795
+ {/* Bottom navigation */}
796
+ {step > 0 && step < TOTAL_STEPS - 1 && (
797
+ <div className="flex items-center justify-between px-8 py-8 shrink-0">
798
+ <button
799
+ type="button"
800
+ onClick={goBack}
801
+ className="h-10 px-6 rounded-md text-sm text-text-2 hover:text-text-0 bg-surface-3 hover:bg-surface-4 transition-colors duration-100 cursor-pointer flex items-center gap-1.5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
802
+ >
803
+ <ChevronLeft className="w-4 h-4" />
804
+ Back
805
+ </button>
806
+ <button
807
+ type="button"
808
+ onClick={goNext}
809
+ disabled={!canContinue}
810
+ className="h-10 px-8 rounded-md text-sm font-medium bg-accent text-surface-0 hover:bg-accent/80 transition-colors duration-100 cursor-pointer disabled:opacity-40 disabled:pointer-events-none flex items-center gap-1.5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
811
+ >
812
+ {step === 4 ? 'Finish Setup' : 'Continue'}
813
+ <ChevronRight className="w-4 h-4" />
814
+ </button>
815
+ </div>
816
+ )}
817
+ </div>
818
+ );
819
+ }