@geminilight/mindos 0.5.0 → 0.5.1

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.
@@ -1,5 +1,5 @@
1
1
  import type { Metadata } from 'next';
2
- import { Geist, Geist_Mono, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
2
+ import { Inter, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
3
3
  import './globals.css';
4
4
  import { getFileTree } from '@/lib/fs';
5
5
  import ShellLayout from '@/components/ShellLayout';
@@ -9,13 +9,14 @@ import ErrorBoundary from '@/components/ErrorBoundary';
9
9
  import RegisterSW from './register-sw';
10
10
  import UpdateBanner from '@/components/UpdateBanner';
11
11
 
12
- const geistSans = Geist({
12
+ const geistSans = Inter({
13
13
  variable: '--font-geist-sans',
14
14
  subsets: ['latin'],
15
15
  });
16
16
 
17
- const geistMono = Geist_Mono({
17
+ const geistMono = IBM_Plex_Mono({
18
18
  variable: '--font-geist-mono',
19
+ weight: ['400', '600'],
19
20
  subsets: ['latin'],
20
21
  });
21
22
 
@@ -28,7 +28,7 @@ interface SetupState {
28
28
 
29
29
  interface PortStatus {
30
30
  checking: boolean;
31
- available: boolean | null; // null = not yet checked
31
+ available: boolean | null;
32
32
  suggestion: number | null;
33
33
  }
34
34
 
@@ -40,7 +40,6 @@ interface AgentEntry {
40
40
  hasGlobalScope: boolean;
41
41
  }
42
42
 
43
- // Per-agent install tracking (live, in Step 5)
44
43
  type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
45
44
  interface AgentInstallStatus {
46
45
  state: AgentInstallState;
@@ -58,10 +57,8 @@ const STEP_KB = 0;
58
57
  const STEP_PORTS = 2;
59
58
  const STEP_AGENTS = 4;
60
59
 
61
- // -------------------------------------------------------------------
62
- // Step4Inner extracted so its local seed/showSeed state survives
63
- // parent re-renders (declaring inside SetupWizard would remount it)
64
- // -------------------------------------------------------------------
60
+ // ─── Step 4 (Security) ────────────────────────────────────────────────────────
61
+ // Extracted at module level so its local seed/showSeed state survives parent re-renders
65
62
  function Step4Inner({
66
63
  authToken, tokenCopied, onCopy, onGenerate, webPassword, onPasswordChange, s,
67
64
  }: {
@@ -120,9 +117,7 @@ function Step4Inner({
120
117
  );
121
118
  }
122
119
 
123
- // -------------------------------------------------------------------
124
- // PortField — input + inline availability badge + suggestion button
125
- // -------------------------------------------------------------------
120
+ // ─── PortField ────────────────────────────────────────────────────────────────
126
121
  function PortField({
127
122
  label, hint, value, onChange, status, onCheckPort, s,
128
123
  }: {
@@ -173,9 +168,370 @@ function PortField({
173
168
  );
174
169
  }
175
170
 
176
- // -------------------------------------------------------------------
177
- // Main component
178
- // -------------------------------------------------------------------
171
+ // ─── Step 1: Knowledge Base ───────────────────────────────────────────────────
172
+ function Step1({
173
+ state, update, t,
174
+ }: {
175
+ state: SetupState;
176
+ update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
177
+ t: ReturnType<typeof useLocale>['t'];
178
+ }) {
179
+ const s = t.setup;
180
+ return (
181
+ <div className="space-y-6">
182
+ <Field label={s.kbPath} hint={s.kbPathHint}>
183
+ <Input value={state.mindRoot} onChange={e => update('mindRoot', e.target.value)} placeholder={s.kbPathDefault} />
184
+ </Field>
185
+ <div>
186
+ <label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
187
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
188
+ {TEMPLATES.map(tpl => (
189
+ <button key={tpl.id} onClick={() => update('template', tpl.id)}
190
+ className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
191
+ style={{
192
+ background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
193
+ borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
194
+ }}>
195
+ <div className="flex items-center gap-2">
196
+ <span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
197
+ <span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
198
+ {t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
199
+ </span>
200
+ </div>
201
+ <div className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
202
+ style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
203
+ {tpl.dirs.map(d => <div key={d}>{d}</div>)}
204
+ </div>
205
+ </button>
206
+ ))}
207
+ </div>
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ // ─── Step 2: AI Provider ──────────────────────────────────────────────────────
214
+ function Step2({
215
+ state, update, s,
216
+ }: {
217
+ state: SetupState;
218
+ update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
219
+ s: ReturnType<typeof useLocale>['t']['setup'];
220
+ }) {
221
+ const providers = [
222
+ { id: 'anthropic' as const, icon: <Brain size={18} />, label: 'Anthropic', desc: 'Claude — claude-sonnet-4-6' },
223
+ { id: 'openai' as const, icon: <Zap size={18} />, label: 'OpenAI', desc: 'GPT or any OpenAI-compatible API' },
224
+ { id: 'skip' as const, icon: <SkipForward size={18} />, label: s.aiSkipTitle, desc: s.aiSkipDesc },
225
+ ];
226
+ return (
227
+ <div className="space-y-5">
228
+ <div className="grid grid-cols-1 gap-3">
229
+ {providers.map(p => (
230
+ <button key={p.id} onClick={() => update('provider', p.id)}
231
+ className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
232
+ style={{
233
+ background: state.provider === p.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
234
+ borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
235
+ }}>
236
+ <span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
237
+ {p.icon}
238
+ </span>
239
+ <div>
240
+ <p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
241
+ <p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
242
+ </div>
243
+ {state.provider === p.id && (
244
+ <CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
245
+ )}
246
+ </button>
247
+ ))}
248
+ </div>
249
+ {state.provider !== 'skip' && (
250
+ <div className="space-y-4 pt-2">
251
+ <Field label={s.apiKey}>
252
+ <ApiKeyInput
253
+ value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
254
+ onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
255
+ placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
256
+ />
257
+ </Field>
258
+ <Field label={s.model}>
259
+ <Input
260
+ value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
261
+ onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
262
+ />
263
+ </Field>
264
+ {state.provider === 'openai' && (
265
+ <Field label={s.baseUrl} hint={s.baseUrlHint}>
266
+ <Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
267
+ placeholder="https://api.openai.com/v1" />
268
+ </Field>
269
+ )}
270
+ </div>
271
+ )}
272
+ </div>
273
+ );
274
+ }
275
+
276
+ // ─── Step 3: Ports ────────────────────────────────────────────────────────────
277
+ function Step3({
278
+ state, update, webPortStatus, mcpPortStatus, setWebPortStatus, setMcpPortStatus, checkPort, portConflict, s,
279
+ }: {
280
+ state: SetupState;
281
+ update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
282
+ webPortStatus: PortStatus;
283
+ mcpPortStatus: PortStatus;
284
+ setWebPortStatus: (s: PortStatus) => void;
285
+ setMcpPortStatus: (s: PortStatus) => void;
286
+ checkPort: (port: number, which: 'web' | 'mcp') => void;
287
+ portConflict: boolean;
288
+ s: ReturnType<typeof useLocale>['t']['setup'];
289
+ }) {
290
+ return (
291
+ <div className="space-y-5">
292
+ <PortField
293
+ label={s.webPort} hint={s.portHint} value={state.webPort}
294
+ onChange={v => { update('webPort', v); setWebPortStatus({ checking: false, available: null, suggestion: null }); }}
295
+ status={webPortStatus}
296
+ onCheckPort={port => checkPort(port, 'web')}
297
+ s={s}
298
+ />
299
+ <PortField
300
+ label={s.mcpPort} hint={s.portHint} value={state.mcpPort}
301
+ onChange={v => { update('mcpPort', v); setMcpPortStatus({ checking: false, available: null, suggestion: null }); }}
302
+ status={mcpPortStatus}
303
+ onCheckPort={port => checkPort(port, 'mcp')}
304
+ s={s}
305
+ />
306
+ {portConflict && (
307
+ <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--amber)' }}>
308
+ <AlertTriangle size={12} /> {s.portConflict}
309
+ </p>
310
+ )}
311
+ {!portConflict && (webPortStatus.available === null || mcpPortStatus.available === null) && !webPortStatus.checking && !mcpPortStatus.checking && (
312
+ <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
313
+ )}
314
+ <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
315
+ <AlertTriangle size={12} /> {s.portRestartWarning}
316
+ </p>
317
+ </div>
318
+ );
319
+ }
320
+
321
+ // ─── Step 5: Agent Tools ──────────────────────────────────────────────────────
322
+ function Step5({
323
+ agents, agentsLoading, selectedAgents, setSelectedAgents,
324
+ agentTransport, setAgentTransport, agentScope, setAgentScope,
325
+ agentStatuses, s, settingsMcp,
326
+ }: {
327
+ agents: AgentEntry[];
328
+ agentsLoading: boolean;
329
+ selectedAgents: Set<string>;
330
+ setSelectedAgents: React.Dispatch<React.SetStateAction<Set<string>>>;
331
+ agentTransport: 'stdio' | 'http';
332
+ setAgentTransport: (v: 'stdio' | 'http') => void;
333
+ agentScope: 'global' | 'project';
334
+ setAgentScope: (v: 'global' | 'project') => void;
335
+ agentStatuses: Record<string, AgentInstallStatus>;
336
+ s: ReturnType<typeof useLocale>['t']['setup'];
337
+ settingsMcp: ReturnType<typeof useLocale>['t']['settings']['mcp'];
338
+ }) {
339
+ const toggleAgent = (key: string) => {
340
+ setSelectedAgents(prev => {
341
+ const next = new Set(prev);
342
+ if (next.has(key)) next.delete(key); else next.add(key);
343
+ return next;
344
+ });
345
+ };
346
+
347
+ const getStatusBadge = (key: string, installed: boolean) => {
348
+ const st = agentStatuses[key];
349
+ if (st) {
350
+ if (st.state === 'installing') return (
351
+ <span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--muted-foreground)' }}>
352
+ <Loader2 size={10} className="animate-spin" /> {s.agentInstalling}
353
+ </span>
354
+ );
355
+ if (st.state === 'ok') return (
356
+ <span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
357
+ style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
358
+ <CheckCircle2 size={10} /> {s.agentStatusOk}
359
+ </span>
360
+ );
361
+ if (st.state === 'error') return (
362
+ <span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
363
+ style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
364
+ <XCircle size={10} /> {s.agentStatusError}
365
+ {st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
366
+ </span>
367
+ );
368
+ }
369
+ if (installed) return (
370
+ <span className="text-[11px] px-1.5 py-0.5 rounded"
371
+ style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
372
+ {settingsMcp.installed}
373
+ </span>
374
+ );
375
+ return (
376
+ <span className="text-[11px] px-1.5 py-0.5 rounded"
377
+ style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
378
+ {s.agentNotInstalled}
379
+ </span>
380
+ );
381
+ };
382
+
383
+ return (
384
+ <div className="space-y-5">
385
+ <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
386
+ {agentsLoading ? (
387
+ <div className="flex items-center gap-2 py-4" style={{ color: 'var(--muted-foreground)' }}>
388
+ <Loader2 size={14} className="animate-spin" />
389
+ <span className="text-sm">{s.agentToolsLoading}</span>
390
+ </div>
391
+ ) : agents.length === 0 ? (
392
+ <p className="text-sm py-4 text-center" style={{ color: 'var(--muted-foreground)' }}>
393
+ {s.agentToolsEmpty}
394
+ </p>
395
+ ) : (
396
+ <>
397
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
398
+ {agents.map((agent, i) => (
399
+ <label key={agent.key}
400
+ className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
401
+ style={{
402
+ background: i % 2 === 0 ? 'var(--card)' : 'transparent',
403
+ borderTop: i > 0 ? '1px solid var(--border)' : undefined,
404
+ }}>
405
+ <input
406
+ type="checkbox"
407
+ checked={selectedAgents.has(agent.key)}
408
+ onChange={() => toggleAgent(agent.key)}
409
+ className="accent-amber-500"
410
+ disabled={agentStatuses[agent.key]?.state === 'installing'}
411
+ />
412
+ <span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
413
+ {getStatusBadge(agent.key, agent.installed)}
414
+ </label>
415
+ ))}
416
+ </div>
417
+ <div className="grid grid-cols-2 gap-4">
418
+ <Field label={s.agentTransport}>
419
+ <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
420
+ <option value="stdio">{settingsMcp.transportStdio}</option>
421
+ <option value="http">{settingsMcp.transportHttp}</option>
422
+ </Select>
423
+ </Field>
424
+ <Field label={s.agentScope}>
425
+ <Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
426
+ <option value="global">{settingsMcp.global}</option>
427
+ <option value="project">{settingsMcp.project}</option>
428
+ </Select>
429
+ </Field>
430
+ </div>
431
+ {selectedAgents.size === 0 && (
432
+ <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentNoneSelected}</p>
433
+ )}
434
+ </>
435
+ )}
436
+ </div>
437
+ );
438
+ }
439
+
440
+ // ─── Step 6: Review ───────────────────────────────────────────────────────────
441
+ function Step6({
442
+ state, selectedAgents, error, portChanged, maskKey, s,
443
+ }: {
444
+ state: SetupState;
445
+ selectedAgents: Set<string>;
446
+ error: string;
447
+ portChanged: boolean;
448
+ maskKey: (key: string) => string;
449
+ s: ReturnType<typeof useLocale>['t']['setup'];
450
+ }) {
451
+ const rows: [string, string][] = [
452
+ [s.kbPath, state.mindRoot],
453
+ [s.template, state.template || '—'],
454
+ [s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
455
+ ...(state.provider !== 'skip' ? [
456
+ [s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
457
+ [s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
458
+ ] : []),
459
+ [s.webPort, String(state.webPort)],
460
+ [s.mcpPort, String(state.mcpPort)],
461
+ [s.authToken, state.authToken || '—'],
462
+ [s.webPassword, state.webPassword ? '••••••••' : '(none)'],
463
+ [s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
464
+ ];
465
+
466
+ return (
467
+ <div className="space-y-5">
468
+ <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
469
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
470
+ {rows.map(([label, value], i) => (
471
+ <div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
472
+ style={{
473
+ background: i % 2 === 0 ? 'var(--card)' : 'transparent',
474
+ borderTop: i > 0 ? '1px solid var(--border)' : undefined,
475
+ }}>
476
+ <span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
477
+ <span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
478
+ </div>
479
+ ))}
480
+ </div>
481
+ {error && (
482
+ <div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
483
+ {s.completeFailed}: {error}
484
+ </div>
485
+ )}
486
+ {portChanged && (
487
+ <div className="space-y-3">
488
+ <div className="p-3 rounded-lg text-sm flex items-center gap-2"
489
+ style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
490
+ <AlertTriangle size={14} /> {s.portChanged}
491
+ </div>
492
+ <a href="/" className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
493
+ style={{ background: 'var(--amber)', color: 'white' }}>
494
+ {s.completeDone} &rarr;
495
+ </a>
496
+ </div>
497
+ )}
498
+ </div>
499
+ );
500
+ }
501
+
502
+ // ─── Step dots ────────────────────────────────────────────────────────────────
503
+ function StepDots({ step, setStep, stepTitles }: {
504
+ step: number;
505
+ setStep: (s: number) => void;
506
+ stepTitles: readonly string[];
507
+ }) {
508
+ return (
509
+ <div className="flex items-center gap-2 mb-8">
510
+ {stepTitles.map((title: string, i: number) => (
511
+ <div key={i} className="flex items-center gap-2">
512
+ {i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
513
+ <button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
514
+ <div
515
+ className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
516
+ style={{
517
+ background: i <= step ? 'var(--amber)' : 'var(--muted)',
518
+ color: i <= step ? 'white' : 'var(--muted-foreground)',
519
+ opacity: i <= step ? 1 : 0.5,
520
+ }}>
521
+ {i + 1}
522
+ </div>
523
+ <span className="text-xs hidden sm:inline"
524
+ style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
525
+ {title}
526
+ </span>
527
+ </button>
528
+ </div>
529
+ ))}
530
+ </div>
531
+ );
532
+ }
533
+
534
+ // ─── Main component ───────────────────────────────────────────────────────────
179
535
  export default function SetupWizard() {
180
536
  const { t } = useLocale();
181
537
  const s = t.setup;
@@ -200,17 +556,14 @@ export default function SetupWizard() {
200
556
  const [error, setError] = useState('');
201
557
  const [portChanged, setPortChanged] = useState(false);
202
558
 
203
- // Port availability
204
559
  const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
205
560
  const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
206
561
 
207
- // Agent Tools
208
562
  const [agents, setAgents] = useState<AgentEntry[]>([]);
209
563
  const [agentsLoading, setAgentsLoading] = useState(false);
210
564
  const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
211
565
  const [agentTransport, setAgentTransport] = useState<'stdio' | 'http'>('stdio');
212
566
  const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
213
- // Live per-agent install status (shown inline in Step 5 during/after submit)
214
567
  const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
215
568
 
216
569
  // Generate token on mount
@@ -288,11 +641,34 @@ export default function SetupWizard() {
288
641
  }
289
642
  }, []);
290
643
 
644
+ const maskKey = (key: string) => {
645
+ if (!key) return '(not set)';
646
+ if (key.length <= 8) return '•••';
647
+ return key.slice(0, 6) + '•••' + key.slice(-3);
648
+ };
649
+
650
+ const portConflict = state.webPort === state.mcpPort;
651
+
652
+ const canNext = () => {
653
+ if (step === STEP_KB) return state.mindRoot.trim().length > 0;
654
+ if (step === STEP_PORTS) {
655
+ if (portConflict) return false;
656
+ if (webPortStatus.checking || mcpPortStatus.checking) return false;
657
+ if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
658
+ return (
659
+ state.webPort >= 1024 && state.webPort <= 65535 &&
660
+ state.mcpPort >= 1024 && state.mcpPort <= 65535
661
+ );
662
+ }
663
+ return true;
664
+ };
665
+
291
666
  const handleComplete = async () => {
292
667
  setSubmitting(true);
293
668
  setError('');
669
+ let didPortChange = false;
294
670
 
295
- // 1. Save setup config first
671
+ // 1. Save setup config
296
672
  try {
297
673
  const payload = {
298
674
  mindRoot: state.mindRoot,
@@ -316,16 +692,16 @@ export default function SetupWizard() {
316
692
  });
317
693
  const data = await res.json();
318
694
  if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
319
- if (data.portChanged) setPortChanged(true);
695
+ didPortChange = !!data.portChanged;
696
+ if (didPortChange) setPortChanged(true);
320
697
  } catch (e) {
321
698
  setError(e instanceof Error ? e.message : String(e));
322
699
  setSubmitting(false);
323
700
  return;
324
701
  }
325
702
 
326
- // 2. Install agents after config saved — update statuses live
703
+ // 2. Install agents after config saved
327
704
  if (selectedAgents.size > 0) {
328
- // Mark all selected as "installing"
329
705
  const initialStatuses: Record<string, AgentInstallStatus> = {};
330
706
  for (const key of selectedAgents) initialStatuses[key] = { state: 'installing' };
331
707
  setAgentStatuses(initialStatuses);
@@ -346,15 +722,11 @@ export default function SetupWizard() {
346
722
  if (data.results) {
347
723
  const updated: Record<string, AgentInstallStatus> = {};
348
724
  for (const r of data.results as Array<{ agent: string; status: string; message?: string }>) {
349
- updated[r.agent] = {
350
- state: r.status === 'ok' ? 'ok' : 'error',
351
- message: r.message,
352
- };
725
+ updated[r.agent] = { state: r.status === 'ok' ? 'ok' : 'error', message: r.message };
353
726
  }
354
727
  setAgentStatuses(updated);
355
728
  }
356
729
  } catch {
357
- // Mark all as error
358
730
  const errStatuses: Record<string, AgentInstallStatus> = {};
359
731
  for (const key of selectedAgents) errStatuses[key] = { state: 'error' };
360
732
  setAgentStatuses(errStatuses);
@@ -362,396 +734,18 @@ export default function SetupWizard() {
362
734
  }
363
735
 
364
736
  setSubmitting(false);
365
- if (!portChanged) window.location.href = '/';
366
- };
367
-
368
- const portConflict = state.webPort === state.mcpPort;
369
737
 
370
- const canNext = () => {
371
- if (step === STEP_KB) return state.mindRoot.trim().length > 0;
372
- if (step === STEP_PORTS) {
373
- if (portConflict) return false;
374
- if (webPortStatus.checking || mcpPortStatus.checking) return false;
375
- if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
376
- return (
377
- state.webPort >= 1024 && state.webPort <= 65535 &&
378
- state.mcpPort >= 1024 && state.mcpPort <= 65535
379
- );
738
+ if (didPortChange) {
739
+ // Port changed stay on page, show restart hint
740
+ return;
380
741
  }
381
- return true;
382
- };
383
-
384
- const maskKey = (key: string) => {
385
- if (!key) return '(not set)';
386
- if (key.length <= 8) return '•••';
387
- return key.slice(0, 6) + '•••' + key.slice(-3);
388
- };
389
-
390
- // ----------------------------------------------------------------
391
- // Step dots
392
- // ----------------------------------------------------------------
393
- const StepDots = () => (
394
- <div className="flex items-center gap-2 mb-8">
395
- {s.stepTitles.map((title: string, i: number) => (
396
- <div key={i} className="flex items-center gap-2">
397
- {i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
398
- <button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
399
- <div
400
- className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
401
- style={{
402
- background: i <= step ? 'var(--amber)' : 'var(--muted)',
403
- color: i <= step ? 'white' : 'var(--muted-foreground)',
404
- opacity: i <= step ? 1 : 0.5,
405
- }}
406
- >
407
- {i + 1}
408
- </div>
409
- <span className="text-xs hidden sm:inline"
410
- style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
411
- {title}
412
- </span>
413
- </button>
414
- </div>
415
- ))}
416
- </div>
417
- );
418
-
419
- // ----------------------------------------------------------------
420
- // Step 1: Knowledge Base
421
- // ----------------------------------------------------------------
422
- const Step1 = () => (
423
- <div className="space-y-6">
424
- <Field label={s.kbPath} hint={s.kbPathHint}>
425
- <Input value={state.mindRoot} onChange={e => update('mindRoot', e.target.value)} placeholder={s.kbPathDefault} />
426
- </Field>
427
- <div>
428
- <label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
429
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
430
- {TEMPLATES.map(tpl => (
431
- <button key={tpl.id} onClick={() => update('template', tpl.id)}
432
- className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
433
- style={{
434
- background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
435
- borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
436
- }}>
437
- <div className="flex items-center gap-2">
438
- <span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
439
- <span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
440
- {t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
441
- </span>
442
- </div>
443
- <div className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
444
- style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
445
- {tpl.dirs.map(d => <div key={d}>{d}</div>)}
446
- </div>
447
- </button>
448
- ))}
449
- </div>
450
- </div>
451
- </div>
452
- );
453
-
454
- // ----------------------------------------------------------------
455
- // Step 2: AI Provider — card-based selection including skip
456
- // ----------------------------------------------------------------
457
- const PROVIDERS = [
458
- {
459
- id: 'anthropic' as const,
460
- icon: <Brain size={18} />,
461
- label: 'Anthropic',
462
- desc: 'Claude — claude-sonnet-4-6',
463
- },
464
- {
465
- id: 'openai' as const,
466
- icon: <Zap size={18} />,
467
- label: 'OpenAI',
468
- desc: 'GPT or any OpenAI-compatible API',
469
- },
470
- {
471
- id: 'skip' as const,
472
- icon: <SkipForward size={18} />,
473
- label: s.aiSkipTitle,
474
- desc: s.aiSkipDesc,
475
- },
476
- ];
477
-
478
- const Step2 = () => (
479
- <div className="space-y-5">
480
- <div className="grid grid-cols-1 gap-3">
481
- {PROVIDERS.map(p => (
482
- <button key={p.id} onClick={() => update('provider', p.id)}
483
- className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
484
- style={{
485
- background: state.provider === p.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
486
- borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
487
- }}>
488
- <span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
489
- {p.icon}
490
- </span>
491
- <div>
492
- <p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
493
- <p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
494
- </div>
495
- {state.provider === p.id && (
496
- <CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
497
- )}
498
- </button>
499
- ))}
500
- </div>
501
-
502
- {state.provider !== 'skip' && (
503
- <div className="space-y-4 pt-2">
504
- <Field label={s.apiKey}>
505
- <ApiKeyInput
506
- value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
507
- onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
508
- placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
509
- />
510
- </Field>
511
- <Field label={s.model}>
512
- <Input
513
- value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
514
- onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
515
- />
516
- </Field>
517
- {state.provider === 'openai' && (
518
- <Field label={s.baseUrl} hint={s.baseUrlHint}>
519
- <Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
520
- placeholder="https://api.openai.com/v1" />
521
- </Field>
522
- )}
523
- </div>
524
- )}
525
- </div>
526
- );
527
-
528
- // ----------------------------------------------------------------
529
- // Step 3: Ports
530
- // ----------------------------------------------------------------
531
- const Step3 = () => (
532
- <div className="space-y-5">
533
- <PortField
534
- label={s.webPort} hint={s.portHint} value={state.webPort}
535
- onChange={v => { update('webPort', v); setWebPortStatus({ checking: false, available: null, suggestion: null }); }}
536
- status={webPortStatus}
537
- onCheckPort={port => checkPort(port, 'web')}
538
- s={s}
539
- />
540
- <PortField
541
- label={s.mcpPort} hint={s.portHint} value={state.mcpPort}
542
- onChange={v => { update('mcpPort', v); setMcpPortStatus({ checking: false, available: null, suggestion: null }); }}
543
- status={mcpPortStatus}
544
- onCheckPort={port => checkPort(port, 'mcp')}
545
- s={s}
546
- />
547
- {portConflict && (
548
- <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--amber)' }}>
549
- <AlertTriangle size={12} /> {s.portConflict}
550
- </p>
551
- )}
552
- {!portConflict && (webPortStatus.available === null || mcpPortStatus.available === null) && !webPortStatus.checking && !mcpPortStatus.checking && (
553
- <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
554
- )}
555
- <p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
556
- <AlertTriangle size={12} /> {s.portRestartWarning}
557
- </p>
558
- </div>
559
- );
560
-
561
- // ----------------------------------------------------------------
562
- // Step 5: Agent Tools
563
- // ----------------------------------------------------------------
564
- const Step5 = () => {
565
- const toggleAgent = (key: string) => {
566
- setSelectedAgents(prev => {
567
- const next = new Set(prev);
568
- if (next.has(key)) next.delete(key); else next.add(key);
569
- return next;
570
- });
571
- };
572
-
573
- const getStatusBadge = (key: string, installed: boolean) => {
574
- const st = agentStatuses[key];
575
-
576
- // Show install result if we've run setup
577
- if (st) {
578
- if (st.state === 'installing') return (
579
- <span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--muted-foreground)' }}>
580
- <Loader2 size={10} className="animate-spin" /> {s.agentInstalling}
581
- </span>
582
- );
583
- if (st.state === 'ok') return (
584
- <span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
585
- style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
586
- <CheckCircle2 size={10} /> {s.agentStatusOk}
587
- </span>
588
- );
589
- if (st.state === 'error') return (
590
- <span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
591
- style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
592
- <XCircle size={10} /> {s.agentStatusError}
593
- {st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
594
- </span>
595
- );
596
- }
597
-
598
- // Show app install status (before setup runs)
599
- if (installed) return (
600
- <span className="text-[11px] px-1.5 py-0.5 rounded"
601
- style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
602
- {t.settings.mcp.installed}
603
- </span>
604
- );
605
- return (
606
- <span className="text-[11px] px-1.5 py-0.5 rounded"
607
- style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
608
- {s.agentNotInstalled}
609
- </span>
610
- );
611
- };
612
-
613
- return (
614
- <div className="space-y-5">
615
- <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
616
-
617
- {agentsLoading ? (
618
- <div className="flex items-center gap-2 py-4" style={{ color: 'var(--muted-foreground)' }}>
619
- <Loader2 size={14} className="animate-spin" />
620
- <span className="text-sm">{s.agentToolsLoading}</span>
621
- </div>
622
- ) : agents.length === 0 ? (
623
- <p className="text-sm py-4 text-center" style={{ color: 'var(--muted-foreground)' }}>
624
- {s.agentToolsEmpty}
625
- </p>
626
- ) : (
627
- <>
628
- <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
629
- {agents.map((agent, i) => (
630
- <label key={agent.key}
631
- className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
632
- style={{
633
- background: i % 2 === 0 ? 'var(--card)' : 'transparent',
634
- borderTop: i > 0 ? '1px solid var(--border)' : undefined,
635
- }}>
636
- <input
637
- type="checkbox"
638
- checked={selectedAgents.has(agent.key)}
639
- onChange={() => toggleAgent(agent.key)}
640
- className="accent-amber-500"
641
- disabled={agentStatuses[agent.key]?.state === 'installing'}
642
- />
643
- <span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
644
- {getStatusBadge(agent.key, agent.installed)}
645
- </label>
646
- ))}
647
- </div>
648
-
649
- <div className="grid grid-cols-2 gap-4">
650
- <Field label={s.agentTransport}>
651
- <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
652
- <option value="stdio">{t.settings.mcp.transportStdio}</option>
653
- <option value="http">{t.settings.mcp.transportHttp}</option>
654
- </Select>
655
- </Field>
656
- <Field label={s.agentScope}>
657
- <Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
658
- <option value="global">{t.settings.mcp.global}</option>
659
- <option value="project">{t.settings.mcp.project}</option>
660
- </Select>
661
- </Field>
662
- </div>
663
-
664
- {selectedAgents.size === 0 && (
665
- <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentNoneSelected}</p>
666
- )}
667
- </>
668
- )}
669
- </div>
670
- );
742
+ window.location.href = '/';
671
743
  };
672
744
 
673
- // ----------------------------------------------------------------
674
- // Step 6: Review
675
- // ----------------------------------------------------------------
676
- const Step6 = () => {
677
- const rows: [string, string][] = [
678
- [s.kbPath, state.mindRoot],
679
- [s.template, state.template || '—'],
680
- [s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
681
- ...(state.provider !== 'skip' ? [
682
- [s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
683
- [s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
684
- ] : []),
685
- [s.webPort, String(state.webPort)],
686
- [s.mcpPort, String(state.mcpPort)],
687
- [s.authToken, state.authToken || '—'],
688
- [s.webPassword, state.webPassword ? '••••••••' : '(none)'],
689
- [s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
690
- ];
691
-
692
- return (
693
- <div className="space-y-5">
694
- <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
695
- <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
696
- {rows.map(([label, value], i) => (
697
- <div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
698
- style={{
699
- background: i % 2 === 0 ? 'var(--card)' : 'transparent',
700
- borderTop: i > 0 ? '1px solid var(--border)' : undefined,
701
- }}>
702
- <span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
703
- <span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
704
- </div>
705
- ))}
706
- </div>
707
-
708
- {error && (
709
- <div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
710
- {s.completeFailed}: {error}
711
- </div>
712
- )}
713
-
714
- {portChanged && (
715
- <div className="space-y-3">
716
- <div className="p-3 rounded-lg text-sm flex items-center gap-2"
717
- style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
718
- <AlertTriangle size={14} /> {s.portChanged}
719
- </div>
720
- <a href="/" className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
721
- style={{ background: 'var(--amber)', color: 'white' }}>
722
- {s.completeDone} &rarr;
723
- </a>
724
- </div>
725
- )}
726
- </div>
727
- );
728
- };
729
-
730
- const steps = [
731
- Step1,
732
- Step2,
733
- Step3,
734
- () => (
735
- <Step4Inner
736
- authToken={state.authToken}
737
- tokenCopied={tokenCopied}
738
- onCopy={copyToken}
739
- onGenerate={generateToken}
740
- webPassword={state.webPassword}
741
- onPasswordChange={v => update('webPassword', v)}
742
- s={s}
743
- />
744
- ),
745
- Step5,
746
- Step6,
747
- ];
748
- const CurrentStep = steps[step];
749
-
750
745
  return (
751
746
  <div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
752
747
  style={{ background: 'var(--background)' }}>
753
748
  <div className="w-full max-w-xl mx-auto px-6 py-12">
754
- {/* Header */}
755
749
  <div className="text-center mb-8">
756
750
  <div className="inline-flex items-center gap-2 mb-2">
757
751
  <Sparkles size={18} style={{ color: 'var(--amber)' }} />
@@ -762,14 +756,47 @@ export default function SetupWizard() {
762
756
  </div>
763
757
 
764
758
  <div className="flex justify-center">
765
- <StepDots />
759
+ <StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} />
766
760
  </div>
767
761
 
768
762
  <h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
769
763
  {s.stepTitles[step]}
770
764
  </h2>
771
765
 
772
- <CurrentStep />
766
+ {step === 0 && <Step1 state={state} update={update} t={t} />}
767
+ {step === 1 && <Step2 state={state} update={update} s={s} />}
768
+ {step === 2 && (
769
+ <Step3
770
+ state={state} update={update}
771
+ webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
772
+ setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
773
+ checkPort={checkPort} portConflict={portConflict} s={s}
774
+ />
775
+ )}
776
+ {step === 3 && (
777
+ <Step4Inner
778
+ authToken={state.authToken} tokenCopied={tokenCopied}
779
+ onCopy={copyToken} onGenerate={generateToken}
780
+ webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
781
+ s={s}
782
+ />
783
+ )}
784
+ {step === 4 && (
785
+ <Step5
786
+ agents={agents} agentsLoading={agentsLoading}
787
+ selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
788
+ agentTransport={agentTransport} setAgentTransport={setAgentTransport}
789
+ agentScope={agentScope} setAgentScope={setAgentScope}
790
+ agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
791
+ />
792
+ )}
793
+ {step === 5 && (
794
+ <Step6
795
+ state={state} selectedAgents={selectedAgents}
796
+ error={error} portChanged={portChanged}
797
+ maskKey={maskKey} s={s}
798
+ />
799
+ )}
773
800
 
774
801
  {/* Navigation */}
775
802
  <div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",