@geminilight/mindos 0.5.7 → 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.
- package/README.md +18 -17
- package/README_zh.md +15 -14
- package/app/app/api/mcp/agents/route.ts +7 -0
- package/app/app/api/mcp/install-skill/route.ts +7 -1
- package/app/app/api/setup/check-port/route.ts +27 -3
- package/app/app/api/setup/route.ts +2 -9
- package/app/app/globals.css +18 -2
- package/app/app/login/page.tsx +1 -1
- package/app/app/view/[...path]/ViewPageClient.tsx +9 -9
- package/app/components/AskModal.tsx +1 -1
- package/app/components/FileTree.tsx +5 -5
- package/app/components/HomeContent.tsx +1 -1
- package/app/components/SetupWizard.tsx +283 -141
- package/app/components/SyncStatusBar.tsx +3 -3
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/ask/SessionHistory.tsx +1 -1
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
- package/app/components/renderers/config/ConfigRenderer.tsx +3 -3
- package/app/components/renderers/csv/types.ts +1 -1
- package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
- package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
- package/app/components/settings/McpTab.tsx +66 -24
- package/app/components/settings/Primitives.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +5 -5
- package/app/lib/i18n.ts +48 -4
- package/app/lib/mcp-agents.ts +81 -10
- package/bin/lib/build.js +25 -0
- package/bin/lib/gateway.js +44 -4
- package/bin/lib/mcp-agents.js +81 -0
- package/bin/lib/mcp-install.js +34 -4
- package/package.json +13 -1
- package/scripts/setup.js +43 -6
- package/skills/project-wiki/SKILL.md +223 -0
- package/skills/project-wiki/assets/api-reference.tmpl.md +49 -0
- package/skills/project-wiki/assets/backlog.tmpl.md +15 -0
- package/skills/project-wiki/assets/changelog.tmpl.md +16 -0
- package/skills/project-wiki/assets/conventions.tmpl.md +29 -0
- package/skills/project-wiki/assets/design-exploration.tmpl.md +26 -0
- package/skills/project-wiki/assets/design-principle.tmpl.md +48 -0
- package/skills/project-wiki/assets/development-guide.tmpl.md +38 -0
- package/skills/project-wiki/assets/glossary.tmpl.md +9 -0
- package/skills/project-wiki/assets/known-pitfalls.tmpl.md +21 -0
- package/skills/project-wiki/assets/postmortem.tmpl.md +38 -0
- package/skills/project-wiki/assets/product-proposal.tmpl.md +41 -0
- package/skills/project-wiki/assets/project-roadmap.tmpl.md +23 -0
- package/skills/project-wiki/assets/stage-x.tmpl.md +78 -0
- package/skills/project-wiki/assets/system-architecture.tmpl.md +62 -0
- package/skills/project-wiki/references/file-reference.md +254 -0
- package/skills/project-wiki/references/writing-guide.md +28 -0
- package/app/data/pages/home-dark.png +0 -0
- package/app/data/pages/home-mobile-crop.png +0 -0
- package/app/data/pages/home-mobile.png +0 -0
- package/app/data/pages/home.png +0 -0
- package/app/data/pages/view-dir.png +0 -0
- package/app/data/pages/view-file-bot.png +0 -0
- package/app/data/pages/view-file-dark-crop.png +0 -0
- package/app/data/pages/view-file-dark.png +0 -0
- package/app/data/pages/view-file-mobile.png +0 -0
- package/app/data/pages/view-file-sm.png +0 -0
- package/app/data/pages/view-file-top.png +0 -0
- package/app/data/pages/view-file.png +0 -0
- package/app/eslint.config.mjs +0 -18
- package/app/public/landing/index.html +0 -353
- package/app/public/landing/style.css +0 -216
- package/app/vitest.config.ts +0 -14
- package/assets/demo-flow-zh.html +0 -622
|
@@ -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={
|
|
156
|
-
onBlur={
|
|
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 =>
|
|
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
|
-
{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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,
|
|
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
|
|
704
|
-
|
|
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.
|
|
707
|
-
[s.
|
|
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
|
-
|
|
721
|
-
|
|
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
|
-
|
|
862
|
+
{/* Compact config summary */}
|
|
726
863
|
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
727
|
-
{
|
|
728
|
-
<div key={i} className="flex items-center justify-between px-4 py-
|
|
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
|
-
{/*
|
|
740
|
-
{
|
|
741
|
-
<
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
<
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
771
|
-
)
|
|
905
|
+
</div>
|
|
906
|
+
);
|
|
907
|
+
})}
|
|
772
908
|
</div>
|
|
773
909
|
)}
|
|
774
910
|
|
|
775
|
-
{
|
|
776
|
-
|
|
777
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
797
|
-
{
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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-
|
|
808
|
-
<span style={{ color: '
|
|
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-
|
|
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
|
-
|
|
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}
|