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.
- package/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +587 -68
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +1 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +373 -58
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +32 -132
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- 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
|
+
}
|