@geminilight/mindos 0.5.3 → 0.5.6
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 +21 -8
- package/README_zh.md +20 -7
- package/app/app/api/mcp/agents/route.ts +1 -0
- package/app/app/api/mcp/install/route.ts +62 -7
- package/app/app/api/mcp/install-skill/route.ts +127 -0
- package/app/app/api/setup/route.ts +5 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +41 -27
- package/app/components/HomeContent.tsx +3 -1
- package/app/components/SetupWizard.tsx +135 -14
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/CsvRenderer.tsx +6 -12
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/renderers/graph/manifest.ts +3 -2
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/McpTab.tsx +85 -7
- package/app/components/settings/PluginsTab.tsx +31 -16
- package/app/lib/i18n.ts +36 -6
- package/app/lib/mcp-agents.ts +10 -9
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/renderers/registry.ts +7 -0
- package/app/lib/renderers/useRendererState.ts +114 -0
- package/app/package-lock.json +311 -2
- package/bin/cli.js +11 -1
- package/bin/lib/mcp-agents.js +9 -9
- package/bin/lib/stop.js +72 -32
- package/mcp/src/index.ts +5 -0
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +9 -2
- package/scripts/setup.js +131 -19
|
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef } 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,
|
|
7
|
+
XCircle, Zap, Brain, SkipForward, Info,
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
10
|
import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
|
|
@@ -39,12 +39,16 @@ interface AgentEntry {
|
|
|
39
39
|
installed: boolean;
|
|
40
40
|
hasProjectScope: boolean;
|
|
41
41
|
hasGlobalScope: boolean;
|
|
42
|
+
preferredTransport: 'stdio' | 'http';
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
45
46
|
interface AgentInstallStatus {
|
|
46
47
|
state: AgentInstallState;
|
|
47
48
|
message?: string;
|
|
49
|
+
transport?: string;
|
|
50
|
+
verified?: boolean;
|
|
51
|
+
verifyError?: string;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
const TEMPLATES: Array<{ id: Template; icon: React.ReactNode; dirs: string[] }> = [
|
|
@@ -469,7 +473,7 @@ function Step3({
|
|
|
469
473
|
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
|
|
470
474
|
)}
|
|
471
475
|
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
472
|
-
<
|
|
476
|
+
<Info size={12} /> {s.portRestartWarning}
|
|
473
477
|
</p>
|
|
474
478
|
</div>
|
|
475
479
|
);
|
|
@@ -479,19 +483,20 @@ function Step3({
|
|
|
479
483
|
function Step5({
|
|
480
484
|
agents, agentsLoading, selectedAgents, setSelectedAgents,
|
|
481
485
|
agentTransport, setAgentTransport, agentScope, setAgentScope,
|
|
482
|
-
agentStatuses, s, settingsMcp,
|
|
486
|
+
agentStatuses, s, settingsMcp, template,
|
|
483
487
|
}: {
|
|
484
488
|
agents: AgentEntry[];
|
|
485
489
|
agentsLoading: boolean;
|
|
486
490
|
selectedAgents: Set<string>;
|
|
487
491
|
setSelectedAgents: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
488
|
-
agentTransport: 'stdio' | 'http';
|
|
489
|
-
setAgentTransport: (v: 'stdio' | 'http') => void;
|
|
492
|
+
agentTransport: 'auto' | 'stdio' | 'http';
|
|
493
|
+
setAgentTransport: (v: 'auto' | 'stdio' | 'http') => void;
|
|
490
494
|
agentScope: 'global' | 'project';
|
|
491
495
|
setAgentScope: (v: 'global' | 'project') => void;
|
|
492
496
|
agentStatuses: Record<string, AgentInstallStatus>;
|
|
493
497
|
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
494
498
|
settingsMcp: ReturnType<typeof useLocale>['t']['settings']['mcp'];
|
|
499
|
+
template: Template;
|
|
495
500
|
}) {
|
|
496
501
|
const toggleAgent = (key: string) => {
|
|
497
502
|
setSelectedAgents(prev => {
|
|
@@ -501,6 +506,11 @@ function Step5({
|
|
|
501
506
|
});
|
|
502
507
|
};
|
|
503
508
|
|
|
509
|
+
const getEffectiveTransport = (agent: AgentEntry) => {
|
|
510
|
+
if (agentTransport === 'auto') return agent.preferredTransport;
|
|
511
|
+
return agentTransport;
|
|
512
|
+
};
|
|
513
|
+
|
|
504
514
|
const getStatusBadge = (key: string, installed: boolean) => {
|
|
505
515
|
const st = agentStatuses[key];
|
|
506
516
|
if (st) {
|
|
@@ -567,13 +577,24 @@ function Step5({
|
|
|
567
577
|
disabled={agentStatuses[agent.key]?.state === 'installing'}
|
|
568
578
|
/>
|
|
569
579
|
<span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
|
|
580
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
|
|
581
|
+
style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
|
|
582
|
+
{getEffectiveTransport(agent)}
|
|
583
|
+
</span>
|
|
570
584
|
{getStatusBadge(agent.key, agent.installed)}
|
|
571
585
|
</label>
|
|
572
586
|
))}
|
|
573
587
|
</div>
|
|
588
|
+
{/* Skill auto-install hint */}
|
|
589
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs"
|
|
590
|
+
style={{ background: 'rgba(100,100,120,0.06)', color: 'var(--muted-foreground)' }}>
|
|
591
|
+
<Brain size={13} className="shrink-0" />
|
|
592
|
+
<span>{s.skillAutoHint(template === 'zh' ? 'mindos-zh' : 'mindos')}</span>
|
|
593
|
+
</div>
|
|
574
594
|
<div className="grid grid-cols-2 gap-4">
|
|
575
595
|
<Field label={s.agentTransport}>
|
|
576
|
-
<Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
|
|
596
|
+
<Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'auto' | 'stdio' | 'http')}>
|
|
597
|
+
<option value="auto">{s.agentTransportAuto}</option>
|
|
577
598
|
<option value="stdio">{settingsMcp.transportStdio}</option>
|
|
578
599
|
<option value="http">{settingsMcp.transportHttp}</option>
|
|
579
600
|
</Select>
|
|
@@ -660,6 +681,7 @@ function RestartBlock({ s, newPort }: { s: ReturnType<typeof useLocale>['t']['se
|
|
|
660
681
|
// ─── Step 6: Review ───────────────────────────────────────────────────────────
|
|
661
682
|
function Step6({
|
|
662
683
|
state, selectedAgents, agentStatuses, onRetryAgent, error, needsRestart, maskKey, s,
|
|
684
|
+
skillInstallResult,
|
|
663
685
|
}: {
|
|
664
686
|
state: SetupState;
|
|
665
687
|
selectedAgents: Set<string>;
|
|
@@ -669,7 +691,9 @@ function Step6({
|
|
|
669
691
|
needsRestart: boolean;
|
|
670
692
|
maskKey: (key: string) => string;
|
|
671
693
|
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
694
|
+
skillInstallResult: { ok?: boolean; skill?: string; error?: string } | null;
|
|
672
695
|
}) {
|
|
696
|
+
const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
673
697
|
const rows: [string, string][] = [
|
|
674
698
|
[s.kbPath, state.mindRoot],
|
|
675
699
|
[s.template, state.template || '—'],
|
|
@@ -683,9 +707,11 @@ function Step6({
|
|
|
683
707
|
[s.authToken, state.authToken || '—'],
|
|
684
708
|
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
685
709
|
[s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
|
|
710
|
+
[s.skillLabel, skillName],
|
|
686
711
|
];
|
|
687
712
|
|
|
688
713
|
const failedAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'error');
|
|
714
|
+
const successAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'ok');
|
|
689
715
|
|
|
690
716
|
return (
|
|
691
717
|
<div className="space-y-5">
|
|
@@ -702,6 +728,43 @@ function Step6({
|
|
|
702
728
|
</div>
|
|
703
729
|
))}
|
|
704
730
|
</div>
|
|
731
|
+
|
|
732
|
+
{/* Agent verification results */}
|
|
733
|
+
{successAgents.length > 0 && (
|
|
734
|
+
<div className="space-y-1.5">
|
|
735
|
+
<p className="text-xs font-medium" style={{ color: 'var(--muted-foreground)' }}>{s.reviewInstallResults}</p>
|
|
736
|
+
{successAgents.map(([key, st]) => (
|
|
737
|
+
<div key={key} className="flex items-center gap-2 text-xs px-3 py-1.5 rounded"
|
|
738
|
+
style={{ background: 'rgba(34,197,94,0.06)' }}>
|
|
739
|
+
<CheckCircle2 size={11} className="text-green-500 shrink-0" />
|
|
740
|
+
<span style={{ color: 'var(--foreground)' }}>{key}</span>
|
|
741
|
+
<span className="font-mono text-[10px] px-1 py-0.5 rounded"
|
|
742
|
+
style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
|
|
743
|
+
{st.transport || 'stdio'}
|
|
744
|
+
</span>
|
|
745
|
+
{st.transport === 'http' ? (
|
|
746
|
+
st.verified ? (
|
|
747
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
|
748
|
+
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
749
|
+
{s.agentVerified}
|
|
750
|
+
</span>
|
|
751
|
+
) : (
|
|
752
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
|
753
|
+
style={{ background: 'rgba(239,168,68,0.12)', color: '#f59e0b' }}
|
|
754
|
+
title={st.verifyError}>
|
|
755
|
+
{s.agentUnverified}
|
|
756
|
+
</span>
|
|
757
|
+
)
|
|
758
|
+
) : (
|
|
759
|
+
<span className="text-[10px]" style={{ color: 'var(--muted-foreground)' }}>
|
|
760
|
+
{s.agentVerifyNote}
|
|
761
|
+
</span>
|
|
762
|
+
)}
|
|
763
|
+
</div>
|
|
764
|
+
))}
|
|
765
|
+
</div>
|
|
766
|
+
)}
|
|
767
|
+
|
|
705
768
|
{failedAgents.length > 0 && (
|
|
706
769
|
<div className="p-3 rounded-lg space-y-2" style={{ background: 'rgba(239,68,68,0.08)' }}>
|
|
707
770
|
<p className="text-xs font-medium" style={{ color: '#ef4444' }}>{s.reviewInstallResults}</p>
|
|
@@ -723,6 +786,22 @@ function Step6({
|
|
|
723
786
|
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentFailureNote}</p>
|
|
724
787
|
</div>
|
|
725
788
|
)}
|
|
789
|
+
{/* Skill install result */}
|
|
790
|
+
{skillInstallResult && (
|
|
791
|
+
<div className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${
|
|
792
|
+
skillInstallResult.ok ? '' : ''
|
|
793
|
+
}`} style={{
|
|
794
|
+
background: skillInstallResult.ok ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)',
|
|
795
|
+
}}>
|
|
796
|
+
{skillInstallResult.ok ? (
|
|
797
|
+
<><CheckCircle2 size={11} className="text-green-500 shrink-0" />
|
|
798
|
+
<span style={{ color: 'var(--foreground)' }}>{s.skillInstalled} — {skillInstallResult.skill}</span></>
|
|
799
|
+
) : (
|
|
800
|
+
<><XCircle size={11} className="text-red-500 shrink-0" />
|
|
801
|
+
<span style={{ color: '#ef4444' }}>{s.skillFailed}{skillInstallResult.error ? `: ${skillInstallResult.error}` : ''}</span></>
|
|
802
|
+
)}
|
|
803
|
+
</div>
|
|
804
|
+
)}
|
|
726
805
|
{error && (
|
|
727
806
|
<div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
|
|
728
807
|
{s.completeFailed}: {error}
|
|
@@ -798,9 +877,10 @@ export default function SetupWizard() {
|
|
|
798
877
|
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
799
878
|
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
800
879
|
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
801
|
-
const [agentTransport, setAgentTransport] = useState<'stdio' | 'http'>('
|
|
880
|
+
const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
802
881
|
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
803
882
|
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
883
|
+
const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
|
|
804
884
|
|
|
805
885
|
// Load existing config as defaults on mount, generate token if none exists
|
|
806
886
|
useEffect(() => {
|
|
@@ -970,7 +1050,13 @@ export default function SetupWizard() {
|
|
|
970
1050
|
setAgentStatuses(initialStatuses);
|
|
971
1051
|
|
|
972
1052
|
try {
|
|
973
|
-
const agentsPayload = Array.from(selectedAgents).map(key =>
|
|
1053
|
+
const agentsPayload = Array.from(selectedAgents).map(key => {
|
|
1054
|
+
const agent = agents.find(a => a.key === key);
|
|
1055
|
+
const effectiveTransport = agentTransport === 'auto'
|
|
1056
|
+
? (agent?.preferredTransport || 'stdio')
|
|
1057
|
+
: agentTransport;
|
|
1058
|
+
return { key, scope: agentScope, transport: effectiveTransport };
|
|
1059
|
+
});
|
|
974
1060
|
const res = await fetch('/api/mcp/install', {
|
|
975
1061
|
method: 'POST',
|
|
976
1062
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -984,8 +1070,14 @@ export default function SetupWizard() {
|
|
|
984
1070
|
const data = await res.json();
|
|
985
1071
|
if (data.results) {
|
|
986
1072
|
const updated: Record<string, AgentInstallStatus> = {};
|
|
987
|
-
for (const r of data.results as Array<{ agent: string; status: string; message?: string }>) {
|
|
988
|
-
updated[r.agent] = {
|
|
1073
|
+
for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
|
|
1074
|
+
updated[r.agent] = {
|
|
1075
|
+
state: r.status === 'ok' ? 'ok' : 'error',
|
|
1076
|
+
message: r.message,
|
|
1077
|
+
transport: r.transport,
|
|
1078
|
+
verified: r.verified,
|
|
1079
|
+
verifyError: r.verifyError,
|
|
1080
|
+
};
|
|
989
1081
|
}
|
|
990
1082
|
setAgentStatuses(updated);
|
|
991
1083
|
}
|
|
@@ -996,6 +1088,20 @@ export default function SetupWizard() {
|
|
|
996
1088
|
}
|
|
997
1089
|
}
|
|
998
1090
|
|
|
1091
|
+
// 3. Install skill to agents
|
|
1092
|
+
const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
1093
|
+
try {
|
|
1094
|
+
const skillRes = await fetch('/api/mcp/install-skill', {
|
|
1095
|
+
method: 'POST',
|
|
1096
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1097
|
+
body: JSON.stringify({ skill: skillName, agents: Array.from(selectedAgents) }),
|
|
1098
|
+
});
|
|
1099
|
+
const skillData = await skillRes.json();
|
|
1100
|
+
setSkillInstallResult(skillData);
|
|
1101
|
+
} catch {
|
|
1102
|
+
setSkillInstallResult({ error: 'Failed to install skill' });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
999
1105
|
setSubmitting(false);
|
|
1000
1106
|
setCompleted(true);
|
|
1001
1107
|
|
|
@@ -1009,11 +1115,15 @@ export default function SetupWizard() {
|
|
|
1009
1115
|
const retryAgent = useCallback(async (key: string) => {
|
|
1010
1116
|
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
|
|
1011
1117
|
try {
|
|
1118
|
+
const agent = agents.find(a => a.key === key);
|
|
1119
|
+
const effectiveTransport = agentTransport === 'auto'
|
|
1120
|
+
? (agent?.preferredTransport || 'stdio')
|
|
1121
|
+
: agentTransport;
|
|
1012
1122
|
const res = await fetch('/api/mcp/install', {
|
|
1013
1123
|
method: 'POST',
|
|
1014
1124
|
headers: { 'Content-Type': 'application/json' },
|
|
1015
1125
|
body: JSON.stringify({
|
|
1016
|
-
agents: [{ key, scope: agentScope }],
|
|
1126
|
+
agents: [{ key, scope: agentScope, transport: effectiveTransport }],
|
|
1017
1127
|
transport: agentTransport,
|
|
1018
1128
|
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
1019
1129
|
token: state.authToken || undefined,
|
|
@@ -1021,13 +1131,22 @@ export default function SetupWizard() {
|
|
|
1021
1131
|
});
|
|
1022
1132
|
const data = await res.json();
|
|
1023
1133
|
if (data.results?.[0]) {
|
|
1024
|
-
const r = data.results[0] as { agent: string; status: string; message?: string };
|
|
1025
|
-
setAgentStatuses(prev => ({
|
|
1134
|
+
const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
|
|
1135
|
+
setAgentStatuses(prev => ({
|
|
1136
|
+
...prev,
|
|
1137
|
+
[key]: {
|
|
1138
|
+
state: r.status === 'ok' ? 'ok' : 'error',
|
|
1139
|
+
message: r.message,
|
|
1140
|
+
transport: r.transport,
|
|
1141
|
+
verified: r.verified,
|
|
1142
|
+
verifyError: r.verifyError,
|
|
1143
|
+
},
|
|
1144
|
+
}));
|
|
1026
1145
|
}
|
|
1027
1146
|
} catch {
|
|
1028
1147
|
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
|
|
1029
1148
|
}
|
|
1030
|
-
}, [agentScope, agentTransport, state.mcpPort, state.authToken]);
|
|
1149
|
+
}, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
|
|
1031
1150
|
|
|
1032
1151
|
return (
|
|
1033
1152
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
@@ -1075,6 +1194,7 @@ export default function SetupWizard() {
|
|
|
1075
1194
|
agentTransport={agentTransport} setAgentTransport={setAgentTransport}
|
|
1076
1195
|
agentScope={agentScope} setAgentScope={setAgentScope}
|
|
1077
1196
|
agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
|
|
1197
|
+
template={state.template}
|
|
1078
1198
|
/>
|
|
1079
1199
|
)}
|
|
1080
1200
|
{step === 5 && (
|
|
@@ -1083,6 +1203,7 @@ export default function SetupWizard() {
|
|
|
1083
1203
|
agentStatuses={agentStatuses} onRetryAgent={retryAgent}
|
|
1084
1204
|
error={error} needsRestart={needsRestart}
|
|
1085
1205
|
maskKey={maskKey} s={s}
|
|
1206
|
+
skillInstallResult={skillInstallResult}
|
|
1086
1207
|
/>
|
|
1087
1208
|
)}
|
|
1088
1209
|
|
|
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
|
|
|
8
8
|
icon: '🧩',
|
|
9
9
|
tags: ['config', 'json', 'settings', 'schema'],
|
|
10
10
|
builtin: true,
|
|
11
|
+
core: true,
|
|
11
12
|
entryPath: 'CONFIG.json',
|
|
12
13
|
match: ({ filePath, extension }) => extension === 'json' && /(^|\/)CONFIG\.json$/i.test(filePath),
|
|
13
14
|
load: () => import('./ConfigRenderer').then(m => ({ default: m.ConfigRenderer })),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useCallback, useState } from 'react';
|
|
4
4
|
import { LayoutGrid, Columns, Table2, Settings2 } from 'lucide-react';
|
|
5
5
|
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
6
|
import type { ViewType, CsvConfig } from './types';
|
|
7
|
-
import { defaultConfig,
|
|
7
|
+
import { defaultConfig, parseCSV } from './types';
|
|
8
|
+
import { useRendererState } from '@/lib/renderers/useRendererState';
|
|
8
9
|
import { TableView } from './TableView';
|
|
9
10
|
import { GalleryView } from './GalleryView';
|
|
10
11
|
import { BoardView } from './BoardView';
|
|
@@ -18,21 +19,14 @@ const VIEW_TABS: { id: ViewType; icon: React.ReactNode; label: string }[] = [
|
|
|
18
19
|
|
|
19
20
|
export function CsvRenderer({ filePath, content, saveAction }: RendererContext) {
|
|
20
21
|
const { headers, rows } = useMemo(() => parseCSV(content), [content]);
|
|
21
|
-
const
|
|
22
|
-
const [
|
|
22
|
+
const def = useMemo(() => defaultConfig(headers), [headers]);
|
|
23
|
+
const [cfg, setCfg] = useRendererState<CsvConfig>('csv', filePath, def);
|
|
23
24
|
const [showConfig, setShowConfig] = useState(false);
|
|
24
25
|
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
setCfg(loadConfig(filePath, headers));
|
|
27
|
-
setConfigLoaded(true);
|
|
28
|
-
}, [filePath]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
29
|
-
|
|
30
26
|
const updateConfig = useCallback((next: CsvConfig) => {
|
|
31
27
|
setCfg(next);
|
|
32
|
-
|
|
33
|
-
}, [filePath]);
|
|
28
|
+
}, [setCfg]);
|
|
34
29
|
|
|
35
|
-
if (!configLoaded) return null;
|
|
36
30
|
const view = cfg.activeView;
|
|
37
31
|
|
|
38
32
|
return (
|
|
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
|
|
|
8
8
|
icon: '📊',
|
|
9
9
|
tags: ['csv', 'table', 'gallery', 'board', 'data'],
|
|
10
10
|
builtin: true,
|
|
11
|
+
core: true,
|
|
11
12
|
entryPath: 'Resources/Products.csv',
|
|
12
13
|
match: ({ extension, filePath }) => extension === 'csv' && !/\bTODO\b/i.test(filePath),
|
|
13
14
|
load: () => import('./CsvRenderer').then(m => ({ default: m.CsvRenderer })),
|
|
@@ -8,7 +8,8 @@ export const manifest: RendererDefinition = {
|
|
|
8
8
|
icon: '🕸️',
|
|
9
9
|
tags: ['graph', 'wiki', 'links', 'visualization'],
|
|
10
10
|
builtin: true,
|
|
11
|
-
entryPath
|
|
12
|
-
|
|
11
|
+
// No entryPath — Graph is a global toggle, not bound to a specific file.
|
|
12
|
+
// Graph is opt-in via a global toggle; never auto-match in the registry.
|
|
13
|
+
match: () => false,
|
|
13
14
|
load: () => import('./GraphRenderer').then(m => ({ default: m.GraphRenderer })),
|
|
14
15
|
};
|
|
@@ -8,6 +8,7 @@ export const manifest: RendererDefinition = {
|
|
|
8
8
|
icon: '✅',
|
|
9
9
|
tags: ['productivity', 'tasks', 'markdown'],
|
|
10
10
|
builtin: true,
|
|
11
|
+
core: true,
|
|
11
12
|
entryPath: 'TODO.md',
|
|
12
13
|
match: ({ filePath }) => /\bTODO\b.*\.(md|csv)$/i.test(filePath),
|
|
13
14
|
load: () => import('./TodoRenderer').then(m => ({ default: m.TodoRenderer })),
|
|
@@ -28,6 +28,7 @@ interface AgentInfo {
|
|
|
28
28
|
configPath?: string;
|
|
29
29
|
hasProjectScope: boolean;
|
|
30
30
|
hasGlobalScope: boolean;
|
|
31
|
+
preferredTransport: 'stdio' | 'http';
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface SkillInfo {
|
|
@@ -133,13 +134,18 @@ function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
|
|
|
133
134
|
function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; onRefresh: () => void }) {
|
|
134
135
|
const m = t.settings?.mcp;
|
|
135
136
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
136
|
-
const [transport, setTransport] = useState<'stdio' | 'http'>('
|
|
137
|
+
const [transport, setTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
137
138
|
const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
|
|
138
139
|
const [httpToken, setHttpToken] = useState('');
|
|
139
140
|
const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
|
|
140
141
|
const [installing, setInstalling] = useState(false);
|
|
141
142
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
142
143
|
|
|
144
|
+
const getEffectiveTransport = (agent: AgentInfo) => {
|
|
145
|
+
if (transport === 'auto') return agent.preferredTransport;
|
|
146
|
+
return transport;
|
|
147
|
+
};
|
|
148
|
+
|
|
143
149
|
const toggle = (key: string) => {
|
|
144
150
|
setSelected(prev => {
|
|
145
151
|
const next = new Set(prev);
|
|
@@ -154,12 +160,21 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
154
160
|
setMessage(null);
|
|
155
161
|
try {
|
|
156
162
|
const payload = {
|
|
157
|
-
agents: [...selected].map(key =>
|
|
158
|
-
key
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
agents: [...selected].map(key => {
|
|
164
|
+
const agent = agents.find(a => a.key === key);
|
|
165
|
+
const effectiveTransport = transport === 'auto'
|
|
166
|
+
? (agent?.preferredTransport || 'stdio')
|
|
167
|
+
: transport;
|
|
168
|
+
return {
|
|
169
|
+
key,
|
|
170
|
+
scope: scopes[key] || (agents.find(a => a.key === key)?.hasProjectScope ? 'project' : 'global'),
|
|
171
|
+
transport: effectiveTransport,
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
161
174
|
transport,
|
|
162
175
|
...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
|
|
176
|
+
// For auto mode, pass http settings for agents that need it
|
|
177
|
+
...(transport === 'auto' ? { url: httpUrl, token: httpToken } : {}),
|
|
163
178
|
};
|
|
164
179
|
const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
|
|
165
180
|
method: 'POST',
|
|
@@ -183,6 +198,12 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
183
198
|
}
|
|
184
199
|
};
|
|
185
200
|
|
|
201
|
+
// Show http fields if transport is 'http', or 'auto' with any http-preferred agent selected
|
|
202
|
+
const showHttpFields = transport === 'http' || (transport === 'auto' && [...selected].some(key => {
|
|
203
|
+
const agent = agents.find(a => a.key === key);
|
|
204
|
+
return agent?.preferredTransport === 'http';
|
|
205
|
+
}));
|
|
206
|
+
|
|
186
207
|
return (
|
|
187
208
|
<div className="space-y-3">
|
|
188
209
|
<SectionLabel>{m?.agentsTitle ?? 'Agent Configuration'}</SectionLabel>
|
|
@@ -198,6 +219,10 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
198
219
|
className="rounded border-border accent-amber-500"
|
|
199
220
|
/>
|
|
200
221
|
<span className="w-28 shrink-0 text-xs">{agent.name}</span>
|
|
222
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
|
|
223
|
+
style={{ background: 'rgba(100,100,120,0.08)' }}>
|
|
224
|
+
{getEffectiveTransport(agent)}
|
|
225
|
+
</span>
|
|
201
226
|
{agent.installed ? (
|
|
202
227
|
<>
|
|
203
228
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-green-500/15 text-green-500 font-mono">
|
|
@@ -225,6 +250,16 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
225
250
|
|
|
226
251
|
{/* Transport selector */}
|
|
227
252
|
<div className="flex items-center gap-4 text-xs pt-1">
|
|
253
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
254
|
+
<input
|
|
255
|
+
type="radio"
|
|
256
|
+
name="transport"
|
|
257
|
+
checked={transport === 'auto'}
|
|
258
|
+
onChange={() => setTransport('auto')}
|
|
259
|
+
className="accent-amber-500"
|
|
260
|
+
/>
|
|
261
|
+
{m?.transportAuto ?? 'auto (recommended)'}
|
|
262
|
+
</label>
|
|
228
263
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
229
264
|
<input
|
|
230
265
|
type="radio"
|
|
@@ -233,7 +268,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
233
268
|
onChange={() => setTransport('stdio')}
|
|
234
269
|
className="accent-amber-500"
|
|
235
270
|
/>
|
|
236
|
-
{m?.transportStdio ?? 'stdio
|
|
271
|
+
{m?.transportStdio ?? 'stdio'}
|
|
237
272
|
</label>
|
|
238
273
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
239
274
|
<input
|
|
@@ -248,7 +283,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
248
283
|
</div>
|
|
249
284
|
|
|
250
285
|
{/* HTTP settings */}
|
|
251
|
-
{
|
|
286
|
+
{showHttpFields && (
|
|
252
287
|
<div className="space-y-2 pl-5 text-xs">
|
|
253
288
|
<div className="space-y-1">
|
|
254
289
|
<label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
|
|
@@ -379,6 +414,49 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
379
414
|
<div className="space-y-3">
|
|
380
415
|
<SectionLabel>{m?.skillsTitle ?? 'Skills'}</SectionLabel>
|
|
381
416
|
|
|
417
|
+
{/* Skill language switcher */}
|
|
418
|
+
{(() => {
|
|
419
|
+
const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
|
|
420
|
+
const currentLang = mindosEnabled ? 'en' : 'zh';
|
|
421
|
+
const handleLangSwitch = async (lang: 'en' | 'zh') => {
|
|
422
|
+
if (lang === currentLang) return;
|
|
423
|
+
if (lang === 'en') {
|
|
424
|
+
await handleToggle('mindos', true);
|
|
425
|
+
await handleToggle('mindos-zh', false);
|
|
426
|
+
} else {
|
|
427
|
+
await handleToggle('mindos-zh', true);
|
|
428
|
+
await handleToggle('mindos', false);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
return (
|
|
432
|
+
<div className="flex items-center gap-2 text-xs">
|
|
433
|
+
<span className="text-muted-foreground">{m?.skillLanguage ?? 'Skill Language'}</span>
|
|
434
|
+
<div className="flex rounded-md border border-border overflow-hidden">
|
|
435
|
+
<button
|
|
436
|
+
onClick={() => handleLangSwitch('en')}
|
|
437
|
+
className={`px-2.5 py-1 text-xs transition-colors ${
|
|
438
|
+
currentLang === 'en'
|
|
439
|
+
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
440
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
441
|
+
}`}
|
|
442
|
+
>
|
|
443
|
+
{m?.skillLangEn ?? 'English'}
|
|
444
|
+
</button>
|
|
445
|
+
<button
|
|
446
|
+
onClick={() => handleLangSwitch('zh')}
|
|
447
|
+
className={`px-2.5 py-1 text-xs transition-colors border-l border-border ${
|
|
448
|
+
currentLang === 'zh'
|
|
449
|
+
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
450
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
451
|
+
}`}
|
|
452
|
+
>
|
|
453
|
+
{m?.skillLangZh ?? '中文'}
|
|
454
|
+
</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
})()}
|
|
459
|
+
|
|
382
460
|
{skills.map(skill => (
|
|
383
461
|
<div key={skill.name} className="border border-border rounded-lg overflow-hidden">
|
|
384
462
|
<div
|
|
@@ -19,7 +19,8 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
|
|
|
19
19
|
) : (
|
|
20
20
|
<div className="flex flex-col gap-3">
|
|
21
21
|
{getAllRenderers().map(renderer => {
|
|
22
|
-
const
|
|
22
|
+
const isCore = !!renderer.core;
|
|
23
|
+
const enabled = isCore ? true : (pluginStates[renderer.id] ?? true);
|
|
23
24
|
return (
|
|
24
25
|
<div
|
|
25
26
|
key={renderer.id}
|
|
@@ -31,7 +32,12 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
|
|
|
31
32
|
<div className="min-w-0">
|
|
32
33
|
<div className="flex items-center gap-2 flex-wrap">
|
|
33
34
|
<span className="text-sm font-medium text-foreground">{renderer.name}</span>
|
|
34
|
-
{
|
|
35
|
+
{isCore && (
|
|
36
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-600/15 text-amber-600 font-mono">
|
|
37
|
+
core
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
{renderer.builtin && !isCore && (
|
|
35
41
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono">
|
|
36
42
|
{t.settings.plugins.builtinBadge}
|
|
37
43
|
</span>
|
|
@@ -51,21 +57,30 @@ export function PluginsTab({ pluginStates, setPluginStates, t }: PluginsTabProps
|
|
|
51
57
|
</div>
|
|
52
58
|
</div>
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
onClick={() => {
|
|
56
|
-
const next = !enabled;
|
|
57
|
-
setRendererEnabled(renderer.id, next);
|
|
58
|
-
setPluginStates(s => ({ ...s, [renderer.id]: next }));
|
|
59
|
-
}}
|
|
60
|
-
role="switch"
|
|
61
|
-
aria-checked={enabled}
|
|
62
|
-
className={`shrink-0 w-9 h-5 rounded-full transition-colors relative ${enabled ? 'bg-amber-600' : 'bg-muted border border-border'}`}
|
|
63
|
-
title={enabled ? t.settings.plugins.enabled : t.settings.plugins.disabled}
|
|
64
|
-
>
|
|
60
|
+
{isCore ? (
|
|
65
61
|
<span
|
|
66
|
-
className=
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
className="shrink-0 w-9 h-5 rounded-full bg-amber-600 relative cursor-not-allowed opacity-60"
|
|
63
|
+
title={t.settings.plugins.coreHint ?? 'Core renderer — always enabled'}
|
|
64
|
+
>
|
|
65
|
+
<span className="absolute top-[3px] left-[18px] w-3.5 h-3.5 rounded-full shadow-sm bg-white" />
|
|
66
|
+
</span>
|
|
67
|
+
) : (
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => {
|
|
70
|
+
const next = !enabled;
|
|
71
|
+
setRendererEnabled(renderer.id, next);
|
|
72
|
+
setPluginStates(s => ({ ...s, [renderer.id]: next }));
|
|
73
|
+
}}
|
|
74
|
+
role="switch"
|
|
75
|
+
aria-checked={enabled}
|
|
76
|
+
className={`shrink-0 w-9 h-5 rounded-full transition-colors relative ${enabled ? 'bg-amber-600' : 'bg-muted border border-border'}`}
|
|
77
|
+
title={enabled ? t.settings.plugins.enabled : t.settings.plugins.disabled}
|
|
78
|
+
>
|
|
79
|
+
<span
|
|
80
|
+
className={`absolute top-[3px] w-3.5 h-3.5 rounded-full shadow-sm transition-all ${enabled ? 'left-[18px] bg-white' : 'left-[3px] bg-muted-foreground/50'}`}
|
|
81
|
+
/>
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
69
84
|
</div>
|
|
70
85
|
</div>
|
|
71
86
|
);
|