@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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/app/app/api/skills/route.ts +1 -1
  3. package/app/app/globals.css +10 -2
  4. package/app/app/login/page.tsx +1 -1
  5. package/app/app/view/[...path]/ViewPageClient.tsx +6 -1
  6. package/app/app/view/[...path]/not-found.tsx +1 -1
  7. package/app/components/AskModal.tsx +4 -4
  8. package/app/components/Breadcrumb.tsx +2 -2
  9. package/app/components/DirView.tsx +6 -6
  10. package/app/components/FileTree.tsx +2 -2
  11. package/app/components/HomeContent.tsx +7 -7
  12. package/app/components/OnboardingView.tsx +1 -1
  13. package/app/components/SearchModal.tsx +1 -1
  14. package/app/components/SettingsModal.tsx +2 -2
  15. package/app/components/SetupWizard.tsx +1 -1400
  16. package/app/components/Sidebar.tsx +4 -4
  17. package/app/components/SidebarLayout.tsx +9 -0
  18. package/app/components/SyncStatusBar.tsx +3 -3
  19. package/app/components/TableOfContents.tsx +1 -1
  20. package/app/components/UpdateBanner.tsx +1 -1
  21. package/app/components/ask/FileChip.tsx +1 -1
  22. package/app/components/ask/MentionPopover.tsx +4 -4
  23. package/app/components/ask/MessageList.tsx +1 -1
  24. package/app/components/ask/SessionHistory.tsx +2 -2
  25. package/app/components/renderers/config/ConfigRenderer.tsx +1 -1
  26. package/app/components/renderers/csv/BoardView.tsx +2 -2
  27. package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
  28. package/app/components/renderers/csv/GalleryView.tsx +1 -1
  29. package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
  30. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  31. package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
  32. package/app/components/settings/KnowledgeTab.tsx +1 -1
  33. package/app/components/settings/McpTab.tsx +27 -23
  34. package/app/components/settings/PluginsTab.tsx +4 -4
  35. package/app/components/settings/Primitives.tsx +1 -1
  36. package/app/components/settings/SyncTab.tsx +8 -8
  37. package/app/components/setup/StepAI.tsx +67 -0
  38. package/app/components/setup/StepAgents.tsx +237 -0
  39. package/app/components/setup/StepDots.tsx +39 -0
  40. package/app/components/setup/StepKB.tsx +237 -0
  41. package/app/components/setup/StepPorts.tsx +121 -0
  42. package/app/components/setup/StepReview.tsx +211 -0
  43. package/app/components/setup/StepSecurity.tsx +78 -0
  44. package/app/components/setup/constants.tsx +13 -0
  45. package/app/components/setup/index.tsx +464 -0
  46. package/app/components/setup/types.ts +53 -0
  47. package/app/lib/i18n.ts +4 -4
  48. package/package.json +1 -1
  49. 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} &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
@@ -230,7 +230,8 @@ export const messages = {
230
230
  skillLangZh: '中文',
231
231
  selectDetected: 'Select Detected',
232
232
  clearSelection: 'Clear',
233
- }, save: 'Save',
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
- }, save: '保存',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",