@geminilight/mindos 0.5.9 → 0.5.10
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 +1 -1
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/globals.css +10 -2
- package/app/app/login/page.tsx +1 -1
- package/app/app/view/[...path]/ViewPageClient.tsx +6 -1
- package/app/app/view/[...path]/not-found.tsx +1 -1
- package/app/components/AskModal.tsx +4 -4
- package/app/components/Breadcrumb.tsx +2 -2
- package/app/components/DirView.tsx +6 -6
- package/app/components/FileTree.tsx +2 -2
- package/app/components/HomeContent.tsx +7 -7
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/SearchModal.tsx +1 -1
- package/app/components/SettingsModal.tsx +2 -2
- package/app/components/SetupWizard.tsx +1 -1400
- package/app/components/Sidebar.tsx +4 -4
- package/app/components/SidebarLayout.tsx +9 -0
- package/app/components/SyncStatusBar.tsx +3 -3
- package/app/components/TableOfContents.tsx +1 -1
- package/app/components/UpdateBanner.tsx +1 -1
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MentionPopover.tsx +4 -4
- package/app/components/ask/MessageList.tsx +1 -1
- package/app/components/ask/SessionHistory.tsx +2 -2
- package/app/components/renderers/config/ConfigRenderer.tsx +1 -1
- package/app/components/renderers/csv/BoardView.tsx +2 -2
- package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
- package/app/components/renderers/csv/GalleryView.tsx +1 -1
- package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +27 -23
- package/app/components/settings/PluginsTab.tsx +4 -4
- package/app/components/settings/Primitives.tsx +1 -1
- package/app/components/settings/SyncTab.tsx +8 -8
- package/app/components/setup/StepAI.tsx +67 -0
- package/app/components/setup/StepAgents.tsx +237 -0
- package/app/components/setup/StepDots.tsx +39 -0
- package/app/components/setup/StepKB.tsx +237 -0
- package/app/components/setup/StepPorts.tsx +121 -0
- package/app/components/setup/StepReview.tsx +211 -0
- package/app/components/setup/StepSecurity.tsx +78 -0
- package/app/components/setup/constants.tsx +13 -0
- package/app/components/setup/index.tsx +464 -0
- package/app/components/setup/types.ts +53 -0
- package/app/lib/i18n.ts +4 -4
- package/package.json +1 -1
- package/skills/project-wiki/SKILL.md +92 -63
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { Sparkles, Loader2, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import type { SetupState, PortStatus, AgentEntry, AgentInstallStatus } from './types';
|
|
7
|
+
import { TOTAL_STEPS, STEP_KB, STEP_PORTS, STEP_AGENTS } from './constants';
|
|
8
|
+
import StepKB from './StepKB';
|
|
9
|
+
import StepAI from './StepAI';
|
|
10
|
+
import StepPorts from './StepPorts';
|
|
11
|
+
import StepSecurity from './StepSecurity';
|
|
12
|
+
import StepAgents from './StepAgents';
|
|
13
|
+
import StepReview from './StepReview';
|
|
14
|
+
import StepDots from './StepDots';
|
|
15
|
+
|
|
16
|
+
// ─── Helpers (shared by handleComplete + retryAgent) ─────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Build a single agent's install payload */
|
|
19
|
+
function buildAgentPayload(
|
|
20
|
+
key: string,
|
|
21
|
+
agents: AgentEntry[],
|
|
22
|
+
transport: 'auto' | 'stdio' | 'http',
|
|
23
|
+
scope: 'global' | 'project',
|
|
24
|
+
): { key: string; scope: string; transport: string } {
|
|
25
|
+
const agent = agents.find(a => a.key === key);
|
|
26
|
+
const effectiveTransport = transport === 'auto'
|
|
27
|
+
? (agent?.preferredTransport || 'stdio')
|
|
28
|
+
: transport;
|
|
29
|
+
return { key, scope, transport: effectiveTransport };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Parse a single install API result into AgentInstallStatus */
|
|
33
|
+
function parseInstallResult(
|
|
34
|
+
r: { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string },
|
|
35
|
+
): AgentInstallStatus {
|
|
36
|
+
return {
|
|
37
|
+
state: r.status === 'ok' ? 'ok' : 'error',
|
|
38
|
+
message: r.message,
|
|
39
|
+
transport: r.transport,
|
|
40
|
+
verified: r.verified,
|
|
41
|
+
verifyError: r.verifyError,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Phase runners (pure async, no setState — results consumed by caller) ────
|
|
46
|
+
|
|
47
|
+
/** Phase 1: Save setup config. Returns whether restart is needed. Throws on failure. */
|
|
48
|
+
async function saveConfig(state: SetupState): Promise<boolean> {
|
|
49
|
+
const payload = {
|
|
50
|
+
mindRoot: state.mindRoot,
|
|
51
|
+
template: state.template || undefined,
|
|
52
|
+
port: state.webPort,
|
|
53
|
+
mcpPort: state.mcpPort,
|
|
54
|
+
authToken: state.authToken,
|
|
55
|
+
webPassword: state.webPassword,
|
|
56
|
+
ai: state.provider === 'skip' ? undefined : {
|
|
57
|
+
provider: state.provider,
|
|
58
|
+
providers: {
|
|
59
|
+
anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
|
|
60
|
+
openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const res = await fetch('/api/setup', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
});
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
71
|
+
return !!data.needsRestart;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Phase 2: Install selected agents. Returns status map. */
|
|
75
|
+
async function installAgents(
|
|
76
|
+
keys: string[],
|
|
77
|
+
agents: AgentEntry[],
|
|
78
|
+
transport: 'auto' | 'stdio' | 'http',
|
|
79
|
+
scope: 'global' | 'project',
|
|
80
|
+
mcpPort: number,
|
|
81
|
+
authToken: string,
|
|
82
|
+
): Promise<Record<string, AgentInstallStatus>> {
|
|
83
|
+
const agentsPayload = keys.map(k => buildAgentPayload(k, agents, transport, scope));
|
|
84
|
+
const res = await fetch('/api/mcp/install', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
agents: agentsPayload,
|
|
89
|
+
transport,
|
|
90
|
+
url: `http://localhost:${mcpPort}/mcp`,
|
|
91
|
+
token: authToken || undefined,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
const updated: Record<string, AgentInstallStatus> = {};
|
|
96
|
+
if (data.results) {
|
|
97
|
+
for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
|
|
98
|
+
updated[r.agent] = parseInstallResult(r);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return updated;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Phase 3: Install skill to agents. Returns result. */
|
|
105
|
+
async function installSkill(
|
|
106
|
+
template: string,
|
|
107
|
+
agentKeys: string[],
|
|
108
|
+
): Promise<{ ok?: boolean; skill?: string; error?: string }> {
|
|
109
|
+
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
110
|
+
const res = await fetch('/api/mcp/install-skill', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ skill: skillName, agents: agentKeys }),
|
|
114
|
+
});
|
|
115
|
+
return await res.json();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export default function SetupWizard() {
|
|
121
|
+
const { t } = useLocale();
|
|
122
|
+
const s = t.setup;
|
|
123
|
+
|
|
124
|
+
const [step, setStep] = useState(0);
|
|
125
|
+
const [state, setState] = useState<SetupState>({
|
|
126
|
+
mindRoot: '~/MindOS/mind',
|
|
127
|
+
template: 'en',
|
|
128
|
+
provider: 'anthropic',
|
|
129
|
+
anthropicKey: '',
|
|
130
|
+
anthropicModel: 'claude-sonnet-4-6',
|
|
131
|
+
openaiKey: '',
|
|
132
|
+
openaiModel: 'gpt-5.4',
|
|
133
|
+
openaiBaseUrl: '',
|
|
134
|
+
webPort: 3000,
|
|
135
|
+
mcpPort: 8787,
|
|
136
|
+
authToken: '',
|
|
137
|
+
webPassword: '',
|
|
138
|
+
});
|
|
139
|
+
const [homeDir, setHomeDir] = useState('~');
|
|
140
|
+
const [tokenCopied, setTokenCopied] = useState(false);
|
|
141
|
+
const [submitting, setSubmitting] = useState(false);
|
|
142
|
+
const [completed, setCompleted] = useState(false);
|
|
143
|
+
const [error, setError] = useState('');
|
|
144
|
+
const [needsRestart, setNeedsRestart] = useState(false);
|
|
145
|
+
|
|
146
|
+
const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
147
|
+
const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
148
|
+
|
|
149
|
+
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
150
|
+
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
151
|
+
const [agentsLoaded, setAgentsLoaded] = useState(false);
|
|
152
|
+
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
153
|
+
const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
154
|
+
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
155
|
+
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
156
|
+
const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
|
|
157
|
+
const [setupPhase, setSetupPhase] = useState<'review' | 'saving' | 'agents' | 'skill' | 'done'>('review');
|
|
158
|
+
|
|
159
|
+
// Load existing config as defaults on mount, generate token if none exists
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
fetch('/api/setup')
|
|
162
|
+
.then(r => r.json())
|
|
163
|
+
.then(data => {
|
|
164
|
+
if (data.homeDir) setHomeDir(data.homeDir);
|
|
165
|
+
setState(prev => ({
|
|
166
|
+
...prev,
|
|
167
|
+
mindRoot: data.mindRoot || prev.mindRoot,
|
|
168
|
+
webPort: typeof data.port === 'number' ? data.port : prev.webPort,
|
|
169
|
+
mcpPort: typeof data.mcpPort === 'number' ? data.mcpPort : prev.mcpPort,
|
|
170
|
+
authToken: data.authToken || prev.authToken,
|
|
171
|
+
webPassword: data.webPassword || prev.webPassword,
|
|
172
|
+
provider: (data.provider === 'anthropic' || data.provider === 'openai') ? data.provider : prev.provider,
|
|
173
|
+
anthropicModel: data.anthropicModel || prev.anthropicModel,
|
|
174
|
+
openaiModel: data.openaiModel || prev.openaiModel,
|
|
175
|
+
openaiBaseUrl: data.openaiBaseUrl ?? prev.openaiBaseUrl,
|
|
176
|
+
}));
|
|
177
|
+
// Generate a new token only if none exists yet
|
|
178
|
+
if (!data.authToken) {
|
|
179
|
+
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
180
|
+
.then(r => r.json())
|
|
181
|
+
.then(tokenData => { if (tokenData.token) setState(p => ({ ...p, authToken: tokenData.token })); })
|
|
182
|
+
.catch(e => console.warn('[SetupWizard] Token generation failed:', e));
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch(e => {
|
|
186
|
+
console.warn('[SetupWizard] Failed to load config, generating token as fallback:', e);
|
|
187
|
+
// Fallback: generate token on failure
|
|
188
|
+
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
189
|
+
.then(r => r.json())
|
|
190
|
+
.then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
|
|
191
|
+
.catch(e2 => console.warn('[SetupWizard] Fallback token generation also failed:', e2));
|
|
192
|
+
});
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
// Auto-check ports when entering Step 3
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (step === STEP_PORTS) {
|
|
198
|
+
checkPort(state.webPort, 'web');
|
|
199
|
+
checkPort(state.mcpPort, 'mcp');
|
|
200
|
+
}
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [step]);
|
|
203
|
+
|
|
204
|
+
// Load agents when entering Step 5
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (step === STEP_AGENTS && !agentsLoaded && !agentsLoading) {
|
|
207
|
+
setAgentsLoading(true);
|
|
208
|
+
fetch('/api/mcp/agents')
|
|
209
|
+
.then(r => r.json())
|
|
210
|
+
.then(data => {
|
|
211
|
+
if (data.agents) {
|
|
212
|
+
setAgents(data.agents);
|
|
213
|
+
setSelectedAgents(new Set(
|
|
214
|
+
(data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
setAgentsLoaded(true);
|
|
218
|
+
})
|
|
219
|
+
.catch(e => { console.warn('[SetupWizard] Failed to load agents:', e); setAgentsLoaded(true); })
|
|
220
|
+
.finally(() => setAgentsLoading(false));
|
|
221
|
+
}
|
|
222
|
+
}, [step, agentsLoaded, agentsLoading]);
|
|
223
|
+
|
|
224
|
+
const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
|
|
225
|
+
setState(prev => ({ ...prev, [key]: val }));
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
const generateToken = useCallback(async (seed?: string) => {
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch('/api/setup/generate-token', {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
233
|
+
body: JSON.stringify({ seed: seed || undefined }),
|
|
234
|
+
});
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
if (data.token) setState(prev => ({ ...prev, authToken: data.token }));
|
|
237
|
+
} catch (e) { console.warn('[SetupWizard] generateToken failed:', e); }
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const copyToken = useCallback(() => {
|
|
241
|
+
navigator.clipboard.writeText(state.authToken).catch(() => { /* clipboard unavailable in insecure context */ });
|
|
242
|
+
setTokenCopied(true);
|
|
243
|
+
setTimeout(() => setTokenCopied(false), 2000);
|
|
244
|
+
}, [state.authToken]);
|
|
245
|
+
|
|
246
|
+
const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
|
|
247
|
+
if (port < 1024 || port > 65535) return;
|
|
248
|
+
const setStatus = which === 'web' ? setWebPortStatus : setMcpPortStatus;
|
|
249
|
+
setStatus({ checking: true, available: null, isSelf: false, suggestion: null });
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch('/api/setup/check-port', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({ port }),
|
|
255
|
+
});
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
setStatus({ checking: false, available: data.available ?? null, isSelf: !!data.isSelf, suggestion: data.suggestion ?? null });
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.warn('[SetupWizard] checkPort failed:', e);
|
|
260
|
+
setStatus({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
261
|
+
}
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
const portConflict = state.webPort === state.mcpPort;
|
|
265
|
+
|
|
266
|
+
const canNext = () => {
|
|
267
|
+
if (step === STEP_KB) return state.mindRoot.trim().length > 0;
|
|
268
|
+
if (step === STEP_PORTS) {
|
|
269
|
+
if (portConflict) return false;
|
|
270
|
+
if (webPortStatus.checking || mcpPortStatus.checking) return false;
|
|
271
|
+
if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
|
|
272
|
+
return (
|
|
273
|
+
state.webPort >= 1024 && state.webPort <= 65535 &&
|
|
274
|
+
state.mcpPort >= 1024 && state.mcpPort <= 65535
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const handleComplete = async () => {
|
|
281
|
+
setSubmitting(true);
|
|
282
|
+
setError('');
|
|
283
|
+
const agentKeys = Array.from(selectedAgents);
|
|
284
|
+
|
|
285
|
+
// Phase 1: Save config
|
|
286
|
+
setSetupPhase('saving');
|
|
287
|
+
let restartNeeded = false;
|
|
288
|
+
try {
|
|
289
|
+
restartNeeded = await saveConfig(state);
|
|
290
|
+
if (restartNeeded) setNeedsRestart(true);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
293
|
+
setSetupPhase('review');
|
|
294
|
+
setSubmitting(false);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Phase 2: Install agents
|
|
299
|
+
setSetupPhase('agents');
|
|
300
|
+
if (agentKeys.length > 0) {
|
|
301
|
+
const initialStatuses: Record<string, AgentInstallStatus> = {};
|
|
302
|
+
for (const key of agentKeys) initialStatuses[key] = { state: 'installing' };
|
|
303
|
+
setAgentStatuses(initialStatuses);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const statuses = await installAgents(agentKeys, agents, agentTransport, agentScope, state.mcpPort, state.authToken);
|
|
307
|
+
setAgentStatuses(statuses);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.warn('[SetupWizard] agent batch install failed:', e);
|
|
310
|
+
const errStatuses: Record<string, AgentInstallStatus> = {};
|
|
311
|
+
for (const key of agentKeys) errStatuses[key] = { state: 'error' };
|
|
312
|
+
setAgentStatuses(errStatuses);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Phase 3: Install skill
|
|
317
|
+
setSetupPhase('skill');
|
|
318
|
+
try {
|
|
319
|
+
const skillData = await installSkill(state.template, agentKeys);
|
|
320
|
+
setSkillInstallResult(skillData);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.warn('[SetupWizard] skill install failed:', e);
|
|
323
|
+
setSkillInstallResult({ error: 'Failed to install skill' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setSubmitting(false);
|
|
327
|
+
setCompleted(true);
|
|
328
|
+
setSetupPhase('done');
|
|
329
|
+
|
|
330
|
+
if (restartNeeded) {
|
|
331
|
+
// Config changed requiring restart — stay on page, show restart block
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
window.location.href = '/?welcome=1';
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const retryAgent = useCallback(async (key: string) => {
|
|
338
|
+
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
|
|
339
|
+
try {
|
|
340
|
+
const payload = buildAgentPayload(key, agents, agentTransport, agentScope);
|
|
341
|
+
const res = await fetch('/api/mcp/install', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify({
|
|
345
|
+
agents: [payload],
|
|
346
|
+
transport: agentTransport,
|
|
347
|
+
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
348
|
+
token: state.authToken || undefined,
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
351
|
+
const data = await res.json();
|
|
352
|
+
if (data.results?.[0]) {
|
|
353
|
+
const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
|
|
354
|
+
setAgentStatuses(prev => ({ ...prev, [key]: parseInstallResult(r) }));
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.warn('[SetupWizard] retryAgent failed:', e);
|
|
358
|
+
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
|
|
359
|
+
}
|
|
360
|
+
}, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
364
|
+
role="dialog" aria-modal="true" aria-labelledby="setup-title"
|
|
365
|
+
style={{ background: 'var(--background)' }}>
|
|
366
|
+
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
367
|
+
<div className="text-center mb-8">
|
|
368
|
+
<div className="inline-flex items-center gap-2 mb-2">
|
|
369
|
+
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
370
|
+
<h1 id="setup-title" className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
|
|
371
|
+
MindOS
|
|
372
|
+
</h1>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div className="flex justify-center">
|
|
377
|
+
<StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} disabled={submitting || completed} />
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
381
|
+
{s.stepTitles[step]}
|
|
382
|
+
</h2>
|
|
383
|
+
|
|
384
|
+
{step === 0 && <StepKB state={state} update={update} t={t} homeDir={homeDir} />}
|
|
385
|
+
{step === 1 && <StepAI state={state} update={update} s={s} />}
|
|
386
|
+
{step === 2 && (
|
|
387
|
+
<StepPorts
|
|
388
|
+
state={state} update={update}
|
|
389
|
+
webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
|
|
390
|
+
setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
|
|
391
|
+
checkPort={checkPort} portConflict={portConflict} s={s}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
{step === 3 && (
|
|
395
|
+
<StepSecurity
|
|
396
|
+
authToken={state.authToken} tokenCopied={tokenCopied}
|
|
397
|
+
onCopy={copyToken} onGenerate={generateToken}
|
|
398
|
+
webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
|
|
399
|
+
s={s}
|
|
400
|
+
/>
|
|
401
|
+
)}
|
|
402
|
+
{step === 4 && (
|
|
403
|
+
<StepAgents
|
|
404
|
+
agents={agents} agentsLoading={agentsLoading}
|
|
405
|
+
selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
|
|
406
|
+
agentTransport={agentTransport} setAgentTransport={setAgentTransport}
|
|
407
|
+
agentScope={agentScope} setAgentScope={setAgentScope}
|
|
408
|
+
agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
|
|
409
|
+
template={state.template}
|
|
410
|
+
/>
|
|
411
|
+
)}
|
|
412
|
+
{step === 5 && (
|
|
413
|
+
<StepReview
|
|
414
|
+
state={state} selectedAgents={selectedAgents}
|
|
415
|
+
agentStatuses={agentStatuses} onRetryAgent={retryAgent}
|
|
416
|
+
error={error} needsRestart={needsRestart}
|
|
417
|
+
s={s}
|
|
418
|
+
skillInstallResult={skillInstallResult}
|
|
419
|
+
setupPhase={setupPhase}
|
|
420
|
+
/>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{/* Navigation */}
|
|
424
|
+
<div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => setStep(step - 1)}
|
|
427
|
+
disabled={step === 0 || submitting || completed}
|
|
428
|
+
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
429
|
+
style={{ color: 'var(--foreground)' }}>
|
|
430
|
+
<ChevronLeft size={14} /> {s.back}
|
|
431
|
+
</button>
|
|
432
|
+
|
|
433
|
+
{step < TOTAL_STEPS - 1 ? (
|
|
434
|
+
<button
|
|
435
|
+
onClick={() => setStep(step + 1)}
|
|
436
|
+
disabled={!canNext()}
|
|
437
|
+
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
438
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
439
|
+
{s.next} <ChevronRight size={14} />
|
|
440
|
+
</button>
|
|
441
|
+
) : completed ? (
|
|
442
|
+
// After completing: show Done link (no restart needed) or nothing (RestartBlock handles it)
|
|
443
|
+
!needsRestart ? (
|
|
444
|
+
<a href="/?welcome=1"
|
|
445
|
+
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
|
|
446
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
447
|
+
{s.completeDone} →
|
|
448
|
+
</a>
|
|
449
|
+
) : null
|
|
450
|
+
) : (
|
|
451
|
+
<button
|
|
452
|
+
onClick={handleComplete}
|
|
453
|
+
disabled={submitting}
|
|
454
|
+
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
455
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}>
|
|
456
|
+
{submitting && <Loader2 size={14} className="animate-spin" />}
|
|
457
|
+
{submitting ? s.completing : s.complete}
|
|
458
|
+
</button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Messages } from '@/lib/i18n';
|
|
2
|
+
|
|
3
|
+
// ─── i18n type aliases ───────────────────────────────────────────────────────
|
|
4
|
+
export type SetupMessages = Messages['setup'];
|
|
5
|
+
export type McpMessages = Messages['settings']['mcp'];
|
|
6
|
+
|
|
7
|
+
// ─── Template ────────────────────────────────────────────────────────────────
|
|
8
|
+
export type Template = 'en' | 'zh' | 'empty' | '';
|
|
9
|
+
|
|
10
|
+
// ─── Setup state ─────────────────────────────────────────────────────────────
|
|
11
|
+
export interface SetupState {
|
|
12
|
+
mindRoot: string;
|
|
13
|
+
template: Template;
|
|
14
|
+
provider: 'anthropic' | 'openai' | 'skip';
|
|
15
|
+
anthropicKey: string;
|
|
16
|
+
anthropicModel: string;
|
|
17
|
+
openaiKey: string;
|
|
18
|
+
openaiModel: string;
|
|
19
|
+
openaiBaseUrl: string;
|
|
20
|
+
webPort: number;
|
|
21
|
+
mcpPort: number;
|
|
22
|
+
authToken: string;
|
|
23
|
+
webPassword: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Port check ──────────────────────────────────────────────────────────────
|
|
27
|
+
export interface PortStatus {
|
|
28
|
+
checking: boolean;
|
|
29
|
+
available: boolean | null;
|
|
30
|
+
isSelf: boolean;
|
|
31
|
+
suggestion: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Agent types ─────────────────────────────────────────────────────────────
|
|
35
|
+
export interface AgentEntry {
|
|
36
|
+
key: string;
|
|
37
|
+
name: string;
|
|
38
|
+
present: boolean;
|
|
39
|
+
installed: boolean;
|
|
40
|
+
hasProjectScope: boolean;
|
|
41
|
+
hasGlobalScope: boolean;
|
|
42
|
+
preferredTransport: 'stdio' | 'http';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
46
|
+
|
|
47
|
+
export interface AgentInstallStatus {
|
|
48
|
+
state: AgentInstallState;
|
|
49
|
+
message?: string;
|
|
50
|
+
transport?: string;
|
|
51
|
+
verified?: boolean;
|
|
52
|
+
verifyError?: string;
|
|
53
|
+
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -230,7 +230,8 @@ export const messages = {
|
|
|
230
230
|
skillLangZh: '中文',
|
|
231
231
|
selectDetected: 'Select Detected',
|
|
232
232
|
clearSelection: 'Clear',
|
|
233
|
-
},
|
|
233
|
+
},
|
|
234
|
+
save: 'Save',
|
|
234
235
|
saved: 'Saved',
|
|
235
236
|
saveFailed: 'Save failed',
|
|
236
237
|
reconfigure: 'Reconfigure',
|
|
@@ -286,7 +287,6 @@ export const messages = {
|
|
|
286
287
|
kbPathHint: 'Absolute path to your notes directory.',
|
|
287
288
|
kbPathDefault: '~/MindOS/mind',
|
|
288
289
|
kbPathUseDefault: (path: string) => `Use ${path}`,
|
|
289
|
-
kbPathExists: (n: number) => `Directory already has ${n} file(s) — template will not be applied.`,
|
|
290
290
|
kbPathHasFiles: (n: number) => `This directory already contains ${n} file${n > 1 ? 's' : ''}. You can skip the template or merge (existing files won't be overwritten).`,
|
|
291
291
|
kbTemplateSkip: 'Skip template',
|
|
292
292
|
kbTemplateMerge: 'Choose a template to merge',
|
|
@@ -625,7 +625,8 @@ export const messages = {
|
|
|
625
625
|
skillLangZh: '中文',
|
|
626
626
|
selectDetected: '选择已检测',
|
|
627
627
|
clearSelection: '清除',
|
|
628
|
-
},
|
|
628
|
+
},
|
|
629
|
+
save: '保存',
|
|
629
630
|
saved: '已保存',
|
|
630
631
|
saveFailed: '保存失败',
|
|
631
632
|
reconfigure: '重新配置',
|
|
@@ -681,7 +682,6 @@ export const messages = {
|
|
|
681
682
|
kbPathHint: '笔记目录的绝对路径。',
|
|
682
683
|
kbPathDefault: '~/MindOS/mind',
|
|
683
684
|
kbPathUseDefault: (path: string) => `使用 ${path}`,
|
|
684
|
-
kbPathExists: (n: number) => `目录已有 ${n} 个文件 — 将不会应用模板。`,
|
|
685
685
|
kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
|
|
686
686
|
kbTemplateSkip: '跳过模板',
|
|
687
687
|
kbTemplateMerge: '选择模板合并',
|
package/package.json
CHANGED