@geminilight/mindos 0.5.8 → 0.5.9

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 (34) hide show
  1. package/README.md +8 -9
  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/globals.css +18 -2
  8. package/app/app/login/page.tsx +1 -1
  9. package/app/app/view/[...path]/ViewPageClient.tsx +9 -9
  10. package/app/components/AskModal.tsx +1 -1
  11. package/app/components/FileTree.tsx +5 -5
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/SetupWizard.tsx +283 -141
  14. package/app/components/SyncStatusBar.tsx +3 -3
  15. package/app/components/ask/MessageList.tsx +2 -2
  16. package/app/components/ask/SessionHistory.tsx +1 -1
  17. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
  18. package/app/components/renderers/config/ConfigRenderer.tsx +3 -3
  19. package/app/components/renderers/csv/types.ts +1 -1
  20. package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
  21. package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
  22. package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
  23. package/app/components/settings/McpTab.tsx +66 -24
  24. package/app/components/settings/Primitives.tsx +3 -3
  25. package/app/components/settings/SyncTab.tsx +5 -5
  26. package/app/lib/i18n.ts +48 -4
  27. package/app/lib/mcp-agents.ts +81 -0
  28. package/bin/lib/gateway.js +44 -4
  29. package/bin/lib/mcp-agents.js +81 -0
  30. package/bin/lib/mcp-install.js +34 -4
  31. package/package.json +3 -1
  32. package/scripts/setup.js +43 -6
  33. package/app/public/landing/index.html +0 -353
  34. package/app/public/landing/style.css +0 -216
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
4
4
  import {
5
5
  Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw,
6
6
  Loader2, ChevronLeft, ChevronRight, AlertTriangle, CheckCircle2,
7
- XCircle, Zap, Brain, SkipForward, Info,
7
+ XCircle, Zap, Brain, SkipForward, Info, ChevronDown,
8
8
  } from 'lucide-react';
9
9
  import { useLocale } from '@/lib/LocaleContext';
10
10
  import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
@@ -147,13 +147,30 @@ function PortField({
147
147
  onCheckPort: (port: number) => void;
148
148
  s: { portChecking: string; portInUse: (p: number) => string; portSuggest: (p: number) => string; portAvailable: string; portSelf: string };
149
149
  }) {
150
+ // Debounce auto-check on input change (500ms)
151
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
152
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
153
+ const v = parseInt(e.target.value, 10) || value;
154
+ onChange(v);
155
+ clearTimeout(timerRef.current);
156
+ if (v >= 1024 && v <= 65535) {
157
+ timerRef.current = setTimeout(() => onCheckPort(v), 500);
158
+ }
159
+ };
160
+ const handleBlur = () => {
161
+ // Cancel pending debounce — onBlur fires the check immediately
162
+ clearTimeout(timerRef.current);
163
+ onCheckPort(value);
164
+ };
165
+ useEffect(() => () => clearTimeout(timerRef.current), []);
166
+
150
167
  return (
151
168
  <Field label={label} hint={hint}>
152
169
  <div className="space-y-1.5">
153
170
  <Input
154
171
  type="number" min={1024} max={65535} value={value}
155
- onChange={e => onChange(parseInt(e.target.value, 10) || value)}
156
- onBlur={() => onCheckPort(value)}
172
+ onChange={handleChange}
173
+ onBlur={handleBlur}
157
174
  />
158
175
  {status.checking && (
159
176
  <p className="text-xs flex items-center gap-1" style={{ color: 'var(--muted-foreground)' }}>
@@ -217,6 +234,7 @@ function Step1({
217
234
  const [suggestions, setSuggestions] = useState<string[]>([]);
218
235
  const [showSuggestions, setShowSuggestions] = useState(false);
219
236
  const [activeSuggestion, setActiveSuggestion] = useState(-1);
237
+ const [showTemplatePickerAnyway, setShowTemplatePickerAnyway] = useState(false);
220
238
  const inputRef = useRef<HTMLInputElement>(null);
221
239
 
222
240
  // Debounced autocomplete
@@ -259,7 +277,12 @@ function Step1({
259
277
  body: JSON.stringify({ path: state.mindRoot }),
260
278
  })
261
279
  .then(r => r.json())
262
- .then(d => setPathInfo(d))
280
+ .then(d => {
281
+ setPathInfo(d);
282
+ setShowTemplatePickerAnyway(false);
283
+ // Non-empty directory: default to skip template (user can opt-in to merge)
284
+ if (d?.exists && !d.empty) update('template', '');
285
+ })
263
286
  .catch(() => setPathInfo(null));
264
287
  }, 600);
265
288
  return () => clearTimeout(timer);
@@ -338,12 +361,45 @@ function Step1({
338
361
  </div>
339
362
  )}
340
363
  </div>
341
- {pathInfo?.exists && !pathInfo.empty && (
342
- <p className="text-xs flex items-center gap-1 mt-1.5" style={{ color: 'var(--amber)' }}>
343
- <AlertTriangle size={11} /> {s.kbPathExists(pathInfo.count)}
344
- </p>
364
+ {/* Recommended default one-click accept */}
365
+ {state.mindRoot !== placeholder && placeholder !== s.kbPathDefault && (
366
+ <button type="button"
367
+ onClick={() => update('mindRoot', placeholder)}
368
+ className="mt-1.5 px-2.5 py-1 text-xs rounded-md border transition-colors hover:bg-muted/50"
369
+ style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
370
+ {s.kbPathUseDefault(placeholder)}
371
+ </button>
345
372
  )}
346
373
  </Field>
374
+ {/* Template selection — conditional on directory state */}
375
+ {pathInfo && pathInfo.exists && !pathInfo.empty && !showTemplatePickerAnyway ? (
376
+ <div>
377
+ <label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
378
+ <div className="rounded-lg border p-3 text-sm" style={{ borderColor: 'var(--amber)', background: 'rgba(245,158,11,0.06)' }}>
379
+ <p style={{ color: 'var(--amber)' }}>
380
+ {s.kbPathHasFiles(pathInfo.count)}
381
+ </p>
382
+ <div className="flex gap-2 mt-2">
383
+ <button type="button"
384
+ onClick={() => update('template', '')}
385
+ className="px-2.5 py-1 text-xs rounded-md border transition-colors"
386
+ style={{
387
+ borderColor: 'var(--amber)',
388
+ color: state.template === '' ? 'var(--background)' : 'var(--amber)',
389
+ background: state.template === '' ? 'var(--amber)' : 'transparent',
390
+ }}>
391
+ {state.template === '' ? <>{s.kbTemplateSkip} ✓</> : s.kbTemplateSkip}
392
+ </button>
393
+ <button type="button"
394
+ onClick={() => setShowTemplatePickerAnyway(true)}
395
+ className="px-2.5 py-1 text-xs rounded-md border transition-colors hover:bg-muted/50"
396
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}>
397
+ {s.kbTemplateMerge}
398
+ </button>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ ) : (
347
403
  <div>
348
404
  <label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
349
405
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
@@ -368,6 +424,7 @@ function Step1({
368
424
  ))}
369
425
  </div>
370
426
  </div>
427
+ )}
371
428
  </div>
372
429
  );
373
430
  }
@@ -507,6 +564,9 @@ function Step5({
507
564
  });
508
565
  };
509
566
 
567
+ const [showOtherAgents, setShowOtherAgents] = useState(false);
568
+ const [showAdvanced, setShowAdvanced] = useState(false);
569
+
510
570
  const getEffectiveTransport = (agent: AgentEntry) => {
511
571
  if (agentTransport === 'auto') return agent.preferredTransport;
512
572
  return agentTransport;
@@ -528,7 +588,7 @@ function Step5({
528
588
  );
529
589
  if (st.state === 'error') return (
530
590
  <span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
531
- style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
591
+ style={{ background: 'rgba(200,80,80,0.1)', color: 'var(--error)' }}>
532
592
  <XCircle size={10} /> {s.agentStatusError}
533
593
  {st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
534
594
  </span>
@@ -543,17 +603,45 @@ function Step5({
543
603
  if (agent.present) return (
544
604
  <span className="text-[11px] px-1.5 py-0.5 rounded"
545
605
  style={{ background: 'rgba(245,158,11,0.12)', color: '#f59e0b' }}>
546
- {s.agentDetected ?? 'detected'}
606
+ {s.agentDetected}
547
607
  </span>
548
608
  );
549
609
  return (
550
610
  <span className="text-[11px] px-1.5 py-0.5 rounded"
551
611
  style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
552
- {s.agentNotFound ?? s.agentNotInstalled}
612
+ {s.agentNotFound}
553
613
  </span>
554
614
  );
555
615
  };
556
616
 
617
+ const { detected, other } = useMemo(() => ({
618
+ detected: agents.filter(a => a.installed || a.present),
619
+ other: agents.filter(a => !a.installed && !a.present),
620
+ }), [agents]);
621
+
622
+ const renderAgentRow = (agent: AgentEntry, i: number) => (
623
+ <label key={agent.key}
624
+ className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
625
+ style={{
626
+ background: i % 2 === 0 ? 'var(--card)' : 'transparent',
627
+ borderTop: i > 0 ? '1px solid var(--border)' : undefined,
628
+ }}>
629
+ <input
630
+ type="checkbox"
631
+ checked={selectedAgents.has(agent.key)}
632
+ onChange={() => toggleAgent(agent.key)}
633
+ className="accent-amber-500"
634
+ disabled={agentStatuses[agent.key]?.state === 'installing'}
635
+ />
636
+ <span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
637
+ <span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
638
+ style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
639
+ {getEffectiveTransport(agent)}
640
+ </span>
641
+ {getStatusBadge(agent.key, agent)}
642
+ </label>
643
+ );
644
+
557
645
  return (
558
646
  <div className="space-y-5">
559
647
  <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
@@ -568,58 +656,107 @@ function Step5({
568
656
  </p>
569
657
  ) : (
570
658
  <>
571
- <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
572
- {agents.map((agent, i) => (
573
- <label key={agent.key}
574
- className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
575
- style={{
576
- background: i % 2 === 0 ? 'var(--card)' : 'transparent',
577
- borderTop: i > 0 ? '1px solid var(--border)' : undefined,
578
- }}>
579
- <input
580
- type="checkbox"
581
- checked={selectedAgents.has(agent.key)}
582
- onChange={() => toggleAgent(agent.key)}
583
- className="accent-amber-500"
584
- disabled={agentStatuses[agent.key]?.state === 'installing'}
585
- />
586
- <span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
587
- <span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
588
- style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
589
- {getEffectiveTransport(agent)}
590
- </span>
591
- {getStatusBadge(agent.key, agent)}
592
- </label>
593
- ))}
659
+ {/* Badge legend */}
660
+ <div className="flex items-center gap-4 text-[10px]" style={{ color: 'var(--muted-foreground)' }}>
661
+ <span className="flex items-center gap-1">
662
+ <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: '#22c55e' }} />
663
+ {s.badgeInstalled}
664
+ </span>
665
+ <span className="flex items-center gap-1">
666
+ <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: '#f59e0b' }} />
667
+ {s.badgeDetected}
668
+ </span>
669
+ <span className="flex items-center gap-1">
670
+ <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: 'var(--muted-foreground)' }} />
671
+ {s.badgeNotFound}
672
+ </span>
594
673
  </div>
595
- {/* Skill auto-install hint */}
596
- <div className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs"
597
- style={{ background: 'rgba(100,100,120,0.06)', color: 'var(--muted-foreground)' }}>
598
- <Brain size={13} className="shrink-0" />
599
- <span>{s.skillAutoHint(template === 'zh' ? 'mindos-zh' : 'mindos')}</span>
674
+
675
+ {/* Detected agents always visible */}
676
+ {detected.length > 0 ? (
677
+ <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
678
+ {detected.map((agent, i) => renderAgentRow(agent, i))}
679
+ </div>
680
+ ) : (
681
+ <p className="text-xs py-2" style={{ color: 'var(--muted-foreground)' }}>
682
+ {s.agentNoneDetected}
683
+ </p>
684
+ )}
685
+ {/* Other agents — collapsed by default */}
686
+ {other.length > 0 && (
687
+ <div>
688
+ <button
689
+ type="button"
690
+ onClick={() => setShowOtherAgents(!showOtherAgents)}
691
+ className="flex items-center gap-1.5 text-xs py-1.5 transition-colors"
692
+ style={{ color: 'var(--muted-foreground)' }}>
693
+ <ChevronDown size={12} className={`transition-transform ${showOtherAgents ? 'rotate-180' : ''}`} />
694
+ {s.agentShowMore(other.length)}
695
+ </button>
696
+ {showOtherAgents && (
697
+ <div className="rounded-xl border overflow-hidden mt-1" style={{ borderColor: 'var(--border)' }}>
698
+ {other.map((agent, i) => renderAgentRow(agent, i))}
699
+ </div>
700
+ )}
701
+ </div>
702
+ )}
703
+ {/* Skill context + auto-install hint */}
704
+ <div className="space-y-1.5">
705
+ <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
706
+ {s.skillWhat}
707
+ </p>
708
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs"
709
+ style={{ background: 'rgba(100,100,120,0.06)', color: 'var(--muted-foreground)' }}>
710
+ <Brain size={13} className="shrink-0" />
711
+ <span>{s.skillAutoHint(template === 'zh' ? 'mindos-zh' : 'mindos')}</span>
712
+ </div>
600
713
  </div>
601
- <div className="grid grid-cols-2 gap-4">
602
- <Field label={s.agentTransport}>
603
- <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'auto' | 'stdio' | 'http')}>
604
- <option value="auto">{s.agentTransportAuto}</option>
605
- <option value="stdio">{settingsMcp.transportStdio}</option>
606
- <option value="http">{settingsMcp.transportHttp}</option>
607
- </Select>
608
- </Field>
609
- <Field label={s.agentScope}>
610
- <Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
611
- <option value="global">{settingsMcp.global}</option>
612
- <option value="project">{settingsMcp.project}</option>
613
- </Select>
614
- </Field>
714
+ {/* Advanced options — collapsed by default */}
715
+ <div>
716
+ <button
717
+ type="button"
718
+ onClick={() => setShowAdvanced(!showAdvanced)}
719
+ className="flex items-center gap-1.5 text-xs py-1.5 transition-colors"
720
+ style={{ color: 'var(--muted-foreground)' }}>
721
+ <ChevronDown size={12} className={`transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
722
+ {s.agentAdvanced}
723
+ </button>
724
+ {showAdvanced && (
725
+ <div className="grid grid-cols-2 gap-4 mt-2">
726
+ <Field label={s.agentTransport}>
727
+ <Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'auto' | 'stdio' | 'http')}>
728
+ <option value="auto">{s.agentTransportAuto}</option>
729
+ <option value="stdio">{settingsMcp.transportStdio}</option>
730
+ <option value="http">{settingsMcp.transportHttp}</option>
731
+ </Select>
732
+ </Field>
733
+ <Field label={s.agentScope}>
734
+ <Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
735
+ <option value="global">{s.agentScopeGlobal}</option>
736
+ <option value="project">{s.agentScopeProject}</option>
737
+ </Select>
738
+ </Field>
739
+ </div>
740
+ )}
741
+ </div>
742
+ <div className="flex gap-2 mt-1">
743
+ <button
744
+ type="button"
745
+ onClick={() => setSelectedAgents(new Set(
746
+ agents.filter(a => a.installed || a.present).map(a => a.key)
747
+ ))}
748
+ className="text-xs px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
749
+ style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
750
+ {s.agentSelectDetected}
751
+ </button>
752
+ <button
753
+ type="button"
754
+ onClick={() => setSelectedAgents(new Set())}
755
+ className="text-xs px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
756
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}>
757
+ {s.agentSkipLater}
758
+ </button>
615
759
  </div>
616
- <button
617
- type="button"
618
- onClick={() => setSelectedAgents(new Set())}
619
- className="text-xs underline mt-1"
620
- style={{ color: 'var(--muted-foreground)' }}>
621
- {s.agentSkipLater}
622
- </button>
623
760
  </>
624
761
  )}
625
762
  </div>
@@ -687,8 +824,8 @@ function RestartBlock({ s, newPort }: { s: ReturnType<typeof useLocale>['t']['se
687
824
 
688
825
  // ─── Step 6: Review ───────────────────────────────────────────────────────────
689
826
  function Step6({
690
- state, selectedAgents, agentStatuses, onRetryAgent, error, needsRestart, maskKey, s,
691
- skillInstallResult,
827
+ state, selectedAgents, agentStatuses, onRetryAgent, error, needsRestart, s,
828
+ skillInstallResult, setupPhase,
692
829
  }: {
693
830
  state: SetupState;
694
831
  selectedAgents: Set<string>;
@@ -696,88 +833,90 @@ function Step6({
696
833
  onRetryAgent: (key: string) => void;
697
834
  error: string;
698
835
  needsRestart: boolean;
699
- maskKey: (key: string) => string;
700
836
  s: ReturnType<typeof useLocale>['t']['setup'];
701
837
  skillInstallResult: { ok?: boolean; skill?: string; error?: string } | null;
838
+ setupPhase: 'review' | 'saving' | 'agents' | 'skill' | 'done';
702
839
  }) {
703
- const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
704
- const rows: [string, string][] = [
840
+ const failedAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'error');
841
+
842
+ // Compact config summary (only key info)
843
+ const summaryRows: [string, string][] = [
705
844
  [s.kbPath, state.mindRoot],
706
- [s.template, state.template || '—'],
707
- [s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
708
- ...(state.provider !== 'skip' ? [
709
- [s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
710
- [s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
711
- ] : []),
712
- [s.webPort, String(state.webPort)],
713
- [s.mcpPort, String(state.mcpPort)],
714
- [s.authToken, state.authToken || '—'],
715
- [s.webPassword, state.webPassword ? '••••••••' : '(none)'],
716
- [s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
717
- [s.skillLabel, skillName],
845
+ [s.webPort, `${state.webPort} / ${state.mcpPort}`],
846
+ [s.agentToolsTitle, selectedAgents.size > 0 ? s.agentCountSummary(selectedAgents.size) : '—'],
718
847
  ];
719
848
 
720
- const failedAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'error');
721
- const successAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'ok');
849
+ // Progress stepper phases
850
+ type Phase = typeof setupPhase;
851
+ const phases: { key: Phase; label: string }[] = [
852
+ { key: 'saving', label: s.phaseSaving },
853
+ { key: 'agents', label: s.phaseAgents },
854
+ { key: 'skill', label: s.phaseSkill },
855
+ { key: 'done', label: s.phaseDone },
856
+ ];
857
+ const phaseOrder: Phase[] = ['saving', 'agents', 'skill', 'done'];
858
+ const currentIdx = phaseOrder.indexOf(setupPhase);
722
859
 
723
860
  return (
724
861
  <div className="space-y-5">
725
- <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
862
+ {/* Compact config summary */}
726
863
  <div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
727
- {rows.map(([label, value], i) => (
728
- <div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
864
+ {summaryRows.map(([label, value], i) => (
865
+ <div key={i} className="flex items-center justify-between px-4 py-2.5 text-sm"
729
866
  style={{
730
867
  background: i % 2 === 0 ? 'var(--card)' : 'transparent',
731
868
  borderTop: i > 0 ? '1px solid var(--border)' : undefined,
732
869
  }}>
733
870
  <span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
734
- <span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
871
+ <span className="font-mono text-xs truncate ml-4" style={{ color: 'var(--foreground)' }}>{value}</span>
735
872
  </div>
736
873
  ))}
737
874
  </div>
738
875
 
739
- {/* Agent verification results */}
740
- {successAgents.length > 0 && (
741
- <div className="space-y-1.5">
742
- <p className="text-xs font-medium" style={{ color: 'var(--muted-foreground)' }}>{s.reviewInstallResults}</p>
743
- {successAgents.map(([key, st]) => (
744
- <div key={key} className="flex items-center gap-2 text-xs px-3 py-1.5 rounded"
745
- style={{ background: 'rgba(34,197,94,0.06)' }}>
746
- <CheckCircle2 size={11} className="text-green-500 shrink-0" />
747
- <span style={{ color: 'var(--foreground)' }}>{key}</span>
748
- <span className="font-mono text-[10px] px-1 py-0.5 rounded"
749
- style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
750
- {st.transport || 'stdio'}
751
- </span>
752
- {st.transport === 'http' ? (
753
- st.verified ? (
754
- <span className="text-[10px] px-1.5 py-0.5 rounded"
755
- style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
756
- {s.agentVerified}
757
- </span>
758
- ) : (
759
- <span className="text-[10px] px-1.5 py-0.5 rounded"
760
- style={{ background: 'rgba(239,168,68,0.12)', color: '#f59e0b' }}
761
- title={st.verifyError}>
762
- {s.agentUnverified}
763
- </span>
764
- )
765
- ) : (
766
- <span className="text-[10px]" style={{ color: 'var(--muted-foreground)' }}>
767
- {s.agentVerifyNote}
876
+ {/* Before submit: review hint */}
877
+ {setupPhase === 'review' && (
878
+ <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
879
+ )}
880
+
881
+ {/* Progress stepper visible during/after setup */}
882
+ {setupPhase !== 'review' && (
883
+ <div className="space-y-2 py-2">
884
+ {phases.map(({ key, label }, i) => {
885
+ const idx = phaseOrder.indexOf(key);
886
+ const isDone = currentIdx > idx || (key === 'done' && setupPhase === 'done');
887
+ const isActive = setupPhase === key && key !== 'done';
888
+ const isPending = currentIdx < idx;
889
+ return (
890
+ <div key={key} className="flex items-center gap-3">
891
+ <div className="w-5 h-5 rounded-full flex items-center justify-center shrink-0 text-[10px]"
892
+ style={{
893
+ background: isDone ? 'rgba(34,197,94,0.15)' : isActive ? 'rgba(200,135,30,0.15)' : 'var(--muted)',
894
+ color: isDone ? '#22c55e' : isActive ? 'var(--amber)' : 'var(--muted-foreground)',
895
+ }}>
896
+ {isDone ? <CheckCircle2 size={12} /> : isActive ? <Loader2 size={12} className="animate-spin" /> : (i + 1)}
897
+ </div>
898
+ <span className="text-sm" style={{
899
+ color: isDone ? '#22c55e' : isActive ? 'var(--foreground)' : 'var(--muted-foreground)',
900
+ fontWeight: isActive ? 500 : 400,
901
+ opacity: isPending ? 0.5 : 1,
902
+ }}>
903
+ {label}
768
904
  </span>
769
- )}
770
- </div>
771
- ))}
905
+ </div>
906
+ );
907
+ })}
772
908
  </div>
773
909
  )}
774
910
 
775
- {failedAgents.length > 0 && (
776
- <div className="p-3 rounded-lg space-y-2" style={{ background: 'rgba(239,68,68,0.08)' }}>
777
- <p className="text-xs font-medium" style={{ color: '#ef4444' }}>{s.reviewInstallResults}</p>
911
+ {/* Agent failures expandable */}
912
+ {failedAgents.length > 0 && setupPhase === 'done' && (
913
+ <div className="p-3 rounded-lg space-y-2" style={{ background: 'rgba(200,80,80,0.08)' }}>
914
+ <p className="text-xs font-medium" style={{ color: 'var(--error)' }}>
915
+ {s.agentFailedCount(failedAgents.length)}
916
+ </p>
778
917
  {failedAgents.map(([key, st]) => (
779
918
  <div key={key} className="flex items-center justify-between gap-2">
780
- <span className="text-xs flex items-center gap-1" style={{ color: '#ef4444' }}>
919
+ <span className="text-xs flex items-center gap-1" style={{ color: 'var(--error)' }}>
781
920
  <XCircle size={11} /> {key}{st.message ? ` — ${st.message}` : ''}
782
921
  </span>
783
922
  <button
@@ -785,7 +924,7 @@ function Step6({
785
924
  onClick={() => onRetryAgent(key)}
786
925
  disabled={st.state === 'installing'}
787
926
  className="text-xs px-2 py-0.5 rounded border transition-colors disabled:opacity-40"
788
- style={{ borderColor: '#ef4444', color: '#ef4444' }}>
927
+ style={{ borderColor: 'var(--error)', color: 'var(--error)' }}>
789
928
  {st.state === 'installing' ? <Loader2 size={10} className="animate-spin inline" /> : s.retryAgent}
790
929
  </button>
791
930
  </div>
@@ -793,44 +932,45 @@ function Step6({
793
932
  <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentFailureNote}</p>
794
933
  </div>
795
934
  )}
796
- {/* Skill install result */}
797
- {skillInstallResult && (
798
- <div className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${
799
- skillInstallResult.ok ? '' : ''
800
- }`} style={{
801
- background: skillInstallResult.ok ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)',
935
+
936
+ {/* Skill result — compact */}
937
+ {skillInstallResult && setupPhase === 'done' && (
938
+ <div className="flex items-center gap-2 text-xs px-3 py-2 rounded-lg" style={{
939
+ background: skillInstallResult.ok ? 'rgba(34,197,94,0.06)' : 'rgba(200,80,80,0.06)',
802
940
  }}>
803
941
  {skillInstallResult.ok ? (
804
942
  <><CheckCircle2 size={11} className="text-green-500 shrink-0" />
805
943
  <span style={{ color: 'var(--foreground)' }}>{s.skillInstalled} — {skillInstallResult.skill}</span></>
806
944
  ) : (
807
- <><XCircle size={11} className="text-red-500 shrink-0" />
808
- <span style={{ color: '#ef4444' }}>{s.skillFailed}{skillInstallResult.error ? `: ${skillInstallResult.error}` : ''}</span></>
945
+ <><XCircle size={11} className="text-error shrink-0" />
946
+ <span style={{ color: 'var(--error)' }}>{s.skillFailed}{skillInstallResult.error ? `: ${skillInstallResult.error}` : ''}</span></>
809
947
  )}
810
948
  </div>
811
949
  )}
950
+
812
951
  {error && (
813
- <div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
952
+ <div className="p-3 rounded-lg text-sm text-error" style={{ background: 'rgba(200,80,80,0.1)' }}>
814
953
  {s.completeFailed}: {error}
815
954
  </div>
816
955
  )}
817
- {needsRestart && <RestartBlock s={s} newPort={state.webPort} />}
956
+ {needsRestart && setupPhase === 'done' && <RestartBlock s={s} newPort={state.webPort} />}
818
957
  </div>
819
958
  );
820
959
  }
821
960
 
822
961
  // ─── Step dots ────────────────────────────────────────────────────────────────
823
- function StepDots({ step, setStep, stepTitles }: {
962
+ function StepDots({ step, setStep, stepTitles, disabled }: {
824
963
  step: number;
825
964
  setStep: (s: number) => void;
826
965
  stepTitles: readonly string[];
966
+ disabled?: boolean;
827
967
  }) {
828
968
  return (
829
969
  <div className="flex items-center gap-2 mb-8">
830
970
  {stepTitles.map((title: string, i: number) => (
831
971
  <div key={i} className="flex items-center gap-2">
832
972
  {i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
833
- <button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
973
+ <button onClick={() => !disabled && i < step && setStep(i)} className="flex items-center gap-1.5 disabled:cursor-not-allowed disabled:opacity-60" disabled={disabled || i > step}>
834
974
  <div
835
975
  className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
836
976
  style={{
@@ -888,6 +1028,7 @@ export default function SetupWizard() {
888
1028
  const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
889
1029
  const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
890
1030
  const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
1031
+ const [setupPhase, setSetupPhase] = useState<'review' | 'saving' | 'agents' | 'skill' | 'done'>('review');
891
1032
 
892
1033
  // Load existing config as defaults on mount, generate token if none exists
893
1034
  useEffect(() => {
@@ -991,11 +1132,6 @@ export default function SetupWizard() {
991
1132
  }
992
1133
  }, []);
993
1134
 
994
- const maskKey = (key: string) => {
995
- if (!key) return '(not set)';
996
- if (key.length <= 8) return '•••';
997
- return key.slice(0, 6) + '•••' + key.slice(-3);
998
- };
999
1135
 
1000
1136
  const portConflict = state.webPort === state.mcpPort;
1001
1137
 
@@ -1016,6 +1152,7 @@ export default function SetupWizard() {
1016
1152
  const handleComplete = async () => {
1017
1153
  setSubmitting(true);
1018
1154
  setError('');
1155
+ setSetupPhase('saving');
1019
1156
  let restartNeeded = false;
1020
1157
 
1021
1158
  // 1. Save setup config
@@ -1046,11 +1183,13 @@ export default function SetupWizard() {
1046
1183
  if (restartNeeded) setNeedsRestart(true);
1047
1184
  } catch (e) {
1048
1185
  setError(e instanceof Error ? e.message : String(e));
1186
+ setSetupPhase('review');
1049
1187
  setSubmitting(false);
1050
1188
  return;
1051
1189
  }
1052
1190
 
1053
1191
  // 2. Install agents after config saved
1192
+ setSetupPhase('agents');
1054
1193
  if (selectedAgents.size > 0) {
1055
1194
  const initialStatuses: Record<string, AgentInstallStatus> = {};
1056
1195
  for (const key of selectedAgents) initialStatuses[key] = { state: 'installing' };
@@ -1096,6 +1235,7 @@ export default function SetupWizard() {
1096
1235
  }
1097
1236
 
1098
1237
  // 3. Install skill to agents
1238
+ setSetupPhase('skill');
1099
1239
  const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
1100
1240
  try {
1101
1241
  const skillRes = await fetch('/api/mcp/install-skill', {
@@ -1111,6 +1251,7 @@ export default function SetupWizard() {
1111
1251
 
1112
1252
  setSubmitting(false);
1113
1253
  setCompleted(true);
1254
+ setSetupPhase('done');
1114
1255
 
1115
1256
  if (restartNeeded) {
1116
1257
  // Config changed requiring restart — stay on page, show restart block
@@ -1169,7 +1310,7 @@ export default function SetupWizard() {
1169
1310
  </div>
1170
1311
 
1171
1312
  <div className="flex justify-center">
1172
- <StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} />
1313
+ <StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} disabled={submitting || completed} />
1173
1314
  </div>
1174
1315
 
1175
1316
  <h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
@@ -1209,8 +1350,9 @@ export default function SetupWizard() {
1209
1350
  state={state} selectedAgents={selectedAgents}
1210
1351
  agentStatuses={agentStatuses} onRetryAgent={retryAgent}
1211
1352
  error={error} needsRestart={needsRestart}
1212
- maskKey={maskKey} s={s}
1353
+ s={s}
1213
1354
  skillInstallResult={skillInstallResult}
1355
+ setupPhase={setupPhase}
1214
1356
  />
1215
1357
  )}
1216
1358
 
@@ -1218,7 +1360,7 @@ export default function SetupWizard() {
1218
1360
  <div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
1219
1361
  <button
1220
1362
  onClick={() => setStep(step - 1)}
1221
- disabled={step === 0}
1363
+ disabled={step === 0 || submitting || completed}
1222
1364
  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"
1223
1365
  style={{ color: 'var(--foreground)' }}>
1224
1366
  <ChevronLeft size={14} /> {s.back}