@geminilight/mindos 0.5.8 → 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.
Files changed (65) hide show
  1. package/README.md +9 -10
  2. package/README_zh.md +8 -9
  3. package/app/app/api/mcp/agents/route.ts +7 -0
  4. package/app/app/api/mcp/install-skill/route.ts +6 -0
  5. package/app/app/api/setup/check-port/route.ts +27 -3
  6. package/app/app/api/setup/route.ts +2 -9
  7. package/app/app/api/skills/route.ts +1 -1
  8. package/app/app/globals.css +28 -4
  9. package/app/app/login/page.tsx +2 -2
  10. package/app/app/view/[...path]/ViewPageClient.tsx +15 -10
  11. package/app/app/view/[...path]/not-found.tsx +1 -1
  12. package/app/components/AskModal.tsx +5 -5
  13. package/app/components/Breadcrumb.tsx +2 -2
  14. package/app/components/DirView.tsx +6 -6
  15. package/app/components/FileTree.tsx +7 -7
  16. package/app/components/HomeContent.tsx +8 -8
  17. package/app/components/OnboardingView.tsx +1 -1
  18. package/app/components/SearchModal.tsx +1 -1
  19. package/app/components/SettingsModal.tsx +2 -2
  20. package/app/components/SetupWizard.tsx +1 -1258
  21. package/app/components/Sidebar.tsx +4 -4
  22. package/app/components/SidebarLayout.tsx +9 -0
  23. package/app/components/SyncStatusBar.tsx +6 -6
  24. package/app/components/TableOfContents.tsx +1 -1
  25. package/app/components/UpdateBanner.tsx +1 -1
  26. package/app/components/ask/FileChip.tsx +1 -1
  27. package/app/components/ask/MentionPopover.tsx +4 -4
  28. package/app/components/ask/MessageList.tsx +3 -3
  29. package/app/components/ask/SessionHistory.tsx +3 -3
  30. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
  31. package/app/components/renderers/config/ConfigRenderer.tsx +4 -4
  32. package/app/components/renderers/csv/BoardView.tsx +2 -2
  33. package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
  34. package/app/components/renderers/csv/GalleryView.tsx +1 -1
  35. package/app/components/renderers/csv/types.ts +1 -1
  36. package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
  37. package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
  38. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  39. package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
  40. package/app/components/renderers/workflow/WorkflowRenderer.tsx +4 -4
  41. package/app/components/settings/KnowledgeTab.tsx +1 -1
  42. package/app/components/settings/McpTab.tsx +93 -47
  43. package/app/components/settings/PluginsTab.tsx +4 -4
  44. package/app/components/settings/Primitives.tsx +4 -4
  45. package/app/components/settings/SyncTab.tsx +13 -13
  46. package/app/components/setup/StepAI.tsx +67 -0
  47. package/app/components/setup/StepAgents.tsx +237 -0
  48. package/app/components/setup/StepDots.tsx +39 -0
  49. package/app/components/setup/StepKB.tsx +237 -0
  50. package/app/components/setup/StepPorts.tsx +121 -0
  51. package/app/components/setup/StepReview.tsx +211 -0
  52. package/app/components/setup/StepSecurity.tsx +78 -0
  53. package/app/components/setup/constants.tsx +13 -0
  54. package/app/components/setup/index.tsx +464 -0
  55. package/app/components/setup/types.ts +53 -0
  56. package/app/lib/i18n.ts +52 -8
  57. package/app/lib/mcp-agents.ts +81 -0
  58. package/bin/lib/gateway.js +44 -4
  59. package/bin/lib/mcp-agents.js +81 -0
  60. package/bin/lib/mcp-install.js +34 -4
  61. package/package.json +3 -1
  62. package/scripts/setup.js +43 -6
  63. package/skills/project-wiki/SKILL.md +92 -63
  64. package/app/public/landing/index.html +0 -353
  65. package/app/public/landing/style.css +0 -216
@@ -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} &rarr;
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
@@ -228,7 +228,10 @@ export const messages = {
228
228
  skillLanguage: 'Skill Language',
229
229
  skillLangEn: 'English',
230
230
  skillLangZh: '中文',
231
- }, save: 'Save',
231
+ selectDetected: 'Select Detected',
232
+ clearSelection: 'Clear',
233
+ },
234
+ save: 'Save',
232
235
  saved: 'Saved',
233
236
  saveFailed: 'Save failed',
234
237
  reconfigure: 'Reconfigure',
@@ -283,7 +286,10 @@ export const messages = {
283
286
  kbPath: 'Knowledge base path',
284
287
  kbPathHint: 'Absolute path to your notes directory.',
285
288
  kbPathDefault: '~/MindOS/mind',
286
- kbPathExists: (n: number) => `Directory already has ${n} file(s) — template will not be applied.`,
289
+ kbPathUseDefault: (path: string) => `Use ${path}`,
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
+ kbTemplateSkip: 'Skip template',
292
+ kbTemplateMerge: 'Choose a template to merge',
287
293
  template: 'Starter template',
288
294
  templateSkip: 'Skip (directory already has files)',
289
295
  // Step 2
@@ -325,7 +331,16 @@ export const messages = {
325
331
  agentToolsLoading: 'Loading agents…',
326
332
  agentToolsEmpty: 'No supported agents detected.',
327
333
  agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
328
- agentSkipLater: 'Skip — configure later',
334
+ agentSkipLater: 'Skip — install agents later in Settings > MCP',
335
+ agentSelectDetected: 'Select detected agents',
336
+ agentNoneDetected: 'No agents detected on your system.',
337
+ agentShowMore: (n: number) => `Show ${n} more agents`,
338
+ agentAdvanced: 'Advanced options',
339
+ agentScopeGlobal: 'Install for all projects',
340
+ agentScopeProject: 'This project only',
341
+ badgeInstalled: 'Installed',
342
+ badgeDetected: 'Detected',
343
+ badgeNotFound: 'Not found',
329
344
  agentNotInstalled: 'not installed',
330
345
  agentDetected: 'detected',
331
346
  agentNotFound: 'not found',
@@ -338,6 +353,7 @@ export const messages = {
338
353
  agentUnverified: 'unverified',
339
354
  agentVerifyNote: 'stdio agents are verified after restart',
340
355
  // Skill auto-install
356
+ skillWhat: 'Skills teach AI agents how to use your knowledge base — like instructions for reading, writing, and organizing your notes.',
341
357
  skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
342
358
  skillLabel: 'Skill',
343
359
  skillInstalling: 'Installing skill…',
@@ -347,9 +363,15 @@ export const messages = {
347
363
  aiSkipTitle: 'Skip for now',
348
364
  aiSkipDesc: 'You can add an API key later in Settings → AI.',
349
365
  // Step 6 — Review
350
- reviewHint: 'Verify your settings before completing setup.',
366
+ reviewHint: 'Verify your settings, then press Complete Setup.',
351
367
  reviewInstallResults: 'Agent configuration results:',
368
+ phaseSaving: 'Saving configuration…',
369
+ phaseAgents: 'Configuring agents…',
370
+ phaseSkill: 'Installing skill…',
371
+ phaseDone: 'Setup complete!',
352
372
  retryAgent: 'Retry',
373
+ agentFailedCount: (n: number) => `${n} agent${n > 1 ? 's' : ''} failed`,
374
+ agentCountSummary: (n: number) => `${n} agent${n > 1 ? 's' : ''}`,
353
375
  agentFailureNote: 'Agent failures are non-blocking — you can enter MindOS and retry from Settings → MCP.',
354
376
  portAvailable: 'Available',
355
377
  portChanged: 'Port changed — please restart the server for it to take effect.',
@@ -601,7 +623,10 @@ export const messages = {
601
623
  skillLanguage: 'Skill 语言',
602
624
  skillLangEn: 'English',
603
625
  skillLangZh: '中文',
604
- }, save: '保存',
626
+ selectDetected: '选择已检测',
627
+ clearSelection: '清除',
628
+ },
629
+ save: '保存',
605
630
  saved: '已保存',
606
631
  saveFailed: '保存失败',
607
632
  reconfigure: '重新配置',
@@ -656,7 +681,10 @@ export const messages = {
656
681
  kbPath: '知识库路径',
657
682
  kbPathHint: '笔记目录的绝对路径。',
658
683
  kbPathDefault: '~/MindOS/mind',
659
- kbPathExists: (n: number) => `目录已有 ${n} 个文件 — 将不会应用模板。`,
684
+ kbPathUseDefault: (path: string) => `使用 ${path}`,
685
+ kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
686
+ kbTemplateSkip: '跳过模板',
687
+ kbTemplateMerge: '选择模板合并',
660
688
  template: '初始模板',
661
689
  templateSkip: '跳过(目录已有文件)',
662
690
  // Step 2
@@ -698,7 +726,16 @@ export const messages = {
698
726
  agentToolsLoading: '正在加载 Agent…',
699
727
  agentToolsEmpty: '未检测到受支持的 Agent。',
700
728
  agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
701
- agentSkipLater: '跳过 — 稍后配置',
729
+ agentSkipLater: '跳过 — 稍后在 设置 > MCP 中安装',
730
+ agentSelectDetected: '选择已检测到的 Agent',
731
+ agentNoneDetected: '未在系统中检测到已安装的 Agent。',
732
+ agentShowMore: (n: number) => `显示另外 ${n} 个 Agent`,
733
+ agentAdvanced: '高级选项',
734
+ agentScopeGlobal: '为所有项目安装',
735
+ agentScopeProject: '仅当前项目',
736
+ badgeInstalled: '已安装',
737
+ badgeDetected: '已检测到',
738
+ badgeNotFound: '未找到',
702
739
  agentNotInstalled: '未安装',
703
740
  agentDetected: '已检测到',
704
741
  agentNotFound: '未找到',
@@ -711,6 +748,7 @@ export const messages = {
711
748
  agentUnverified: '未验证',
712
749
  agentVerifyNote: 'stdio agent 需重启后验证',
713
750
  // Skill auto-install
751
+ skillWhat: 'Skill 教 AI Agent 如何使用你的知识库 — 包括笔记的读取、写入和整理规则。',
714
752
  skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
715
753
  skillLabel: 'Skill',
716
754
  skillInstalling: '正在安装 Skill…',
@@ -720,9 +758,15 @@ export const messages = {
720
758
  aiSkipTitle: '暂时跳过',
721
759
  aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
722
760
  // Step 6 — Review
723
- reviewHint: '完成设置前请确认以下信息。',
761
+ reviewHint: '确认设置无误后,点击完成。',
724
762
  reviewInstallResults: 'Agent 配置结果:',
763
+ phaseSaving: '正在保存配置…',
764
+ phaseAgents: '正在配置 Agent…',
765
+ phaseSkill: '正在安装 Skill…',
766
+ phaseDone: '设置完成!',
725
767
  retryAgent: '重试',
768
+ agentFailedCount: (n: number) => `${n} 个 Agent 配置失败`,
769
+ agentCountSummary: (n: number) => `${n} 个 Agent`,
726
770
  agentFailureNote: 'Agent 安装失败不影响进入 MindOS — 可稍后在 设置 → MCP 中重试。',
727
771
  portAvailable: '可用',
728
772
  portChanged: '端口已变更 — 请重启服务以使其生效。',