@geminilight/mindos 0.5.3 → 0.5.5
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/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 +96 -0
- package/app/app/api/setup/route.ts +5 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +4 -20
- 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/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/registry.ts +7 -0
- package/app/lib/renderers/useRendererState.ts +114 -0
- package/bin/lib/mcp-agents.js +9 -9
- package/package.json +1 -1
- package/scripts/setup.js +121 -18
|
@@ -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
|
);
|
package/app/lib/i18n.ts
CHANGED
|
@@ -171,6 +171,7 @@ export const messages = {
|
|
|
171
171
|
enabled: 'Enabled',
|
|
172
172
|
disabled: 'Disabled',
|
|
173
173
|
matchHint: 'Auto-activates on',
|
|
174
|
+
coreHint: 'Core renderer — always enabled',
|
|
174
175
|
noPlugins: 'No renderers installed.',
|
|
175
176
|
comingSoon: 'Plugin marketplace coming soon.',
|
|
176
177
|
},
|
|
@@ -198,6 +199,7 @@ export const messages = {
|
|
|
198
199
|
notInstalled: 'Not installed',
|
|
199
200
|
transportStdio: 'stdio (recommended)',
|
|
200
201
|
transportHttp: 'http',
|
|
202
|
+
transportAuto: 'auto (recommended)',
|
|
201
203
|
httpUrl: 'MCP URL',
|
|
202
204
|
httpToken: 'Auth Token',
|
|
203
205
|
installSelected: 'Install Selected',
|
|
@@ -221,8 +223,10 @@ export const messages = {
|
|
|
221
223
|
skillContent: 'Content',
|
|
222
224
|
skillNameConflict: 'A skill with this name already exists',
|
|
223
225
|
skillDeleteConfirm: (name: string) => `Delete skill "${name}"? This cannot be undone.`,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
skillLanguage: 'Skill Language',
|
|
227
|
+
skillLangEn: 'English',
|
|
228
|
+
skillLangZh: '中文',
|
|
229
|
+
}, save: 'Save',
|
|
226
230
|
saved: 'Saved',
|
|
227
231
|
saveFailed: 'Save failed',
|
|
228
232
|
reconfigure: 'Reconfigure',
|
|
@@ -292,7 +296,7 @@ export const messages = {
|
|
|
292
296
|
webPort: 'Web UI port',
|
|
293
297
|
mcpPort: 'MCP server port',
|
|
294
298
|
portHint: 'Valid range: 1024–65535',
|
|
295
|
-
portRestartWarning: '
|
|
299
|
+
portRestartWarning: 'The service will start on these ports after setup completes.',
|
|
296
300
|
portInUse: (p: number) => `Port ${p} is already in use.`,
|
|
297
301
|
portSuggest: (p: number) => `Use ${p}`,
|
|
298
302
|
portChecking: 'Checking…',
|
|
@@ -324,6 +328,17 @@ export const messages = {
|
|
|
324
328
|
agentStatusOk: 'configured',
|
|
325
329
|
agentStatusError: 'failed',
|
|
326
330
|
agentInstalling: 'Configuring…',
|
|
331
|
+
agentTransportAuto: 'Auto (recommended)',
|
|
332
|
+
agentTransportLabel: 'Transport',
|
|
333
|
+
agentVerified: 'verified',
|
|
334
|
+
agentUnverified: 'unverified',
|
|
335
|
+
agentVerifyNote: 'stdio agents are verified after restart',
|
|
336
|
+
// Skill auto-install
|
|
337
|
+
skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
|
|
338
|
+
skillLabel: 'Skill',
|
|
339
|
+
skillInstalling: 'Installing skill…',
|
|
340
|
+
skillInstalled: 'Skill installed',
|
|
341
|
+
skillFailed: 'Skill install failed',
|
|
327
342
|
// Step 2 — AI skip card
|
|
328
343
|
aiSkipTitle: 'Skip for now',
|
|
329
344
|
aiSkipDesc: 'You can add an API key later in Settings → AI.',
|
|
@@ -525,6 +540,7 @@ export const messages = {
|
|
|
525
540
|
enabled: '已启用',
|
|
526
541
|
disabled: '已禁用',
|
|
527
542
|
matchHint: '自动匹配',
|
|
543
|
+
coreHint: '核心渲染器 — 始终启用',
|
|
528
544
|
noPlugins: '暂无渲染器。',
|
|
529
545
|
comingSoon: '插件市场即将上线。',
|
|
530
546
|
},
|
|
@@ -552,6 +568,7 @@ export const messages = {
|
|
|
552
568
|
notInstalled: '未安装',
|
|
553
569
|
transportStdio: 'stdio(推荐)',
|
|
554
570
|
transportHttp: 'http',
|
|
571
|
+
transportAuto: '自动(推荐)',
|
|
555
572
|
httpUrl: 'MCP URL',
|
|
556
573
|
httpToken: '认证 Token',
|
|
557
574
|
installSelected: '安装选中',
|
|
@@ -575,8 +592,10 @@ export const messages = {
|
|
|
575
592
|
skillContent: '内容',
|
|
576
593
|
skillNameConflict: '同名 skill 已存在',
|
|
577
594
|
skillDeleteConfirm: (name: string) => `确定删除「${name}」?此操作不可撤销。`,
|
|
578
|
-
|
|
579
|
-
|
|
595
|
+
skillLanguage: 'Skill 语言',
|
|
596
|
+
skillLangEn: 'English',
|
|
597
|
+
skillLangZh: '中文',
|
|
598
|
+
}, save: '保存',
|
|
580
599
|
saved: '已保存',
|
|
581
600
|
saveFailed: '保存失败',
|
|
582
601
|
reconfigure: '重新配置',
|
|
@@ -646,7 +665,7 @@ export const messages = {
|
|
|
646
665
|
webPort: 'Web UI 端口',
|
|
647
666
|
mcpPort: 'MCP 服务端口',
|
|
648
667
|
portHint: '有效范围:1024–65535',
|
|
649
|
-
portRestartWarning: '
|
|
668
|
+
portRestartWarning: '完成配置后,服务将以这些端口启动。',
|
|
650
669
|
portInUse: (p: number) => `端口 ${p} 已被占用。`,
|
|
651
670
|
portSuggest: (p: number) => `使用 ${p}`,
|
|
652
671
|
portChecking: '检测中…',
|
|
@@ -678,6 +697,17 @@ export const messages = {
|
|
|
678
697
|
agentStatusOk: '已配置',
|
|
679
698
|
agentStatusError: '失败',
|
|
680
699
|
agentInstalling: '配置中…',
|
|
700
|
+
agentTransportAuto: '自动(推荐)',
|
|
701
|
+
agentTransportLabel: '传输方式',
|
|
702
|
+
agentVerified: '已验证',
|
|
703
|
+
agentUnverified: '未验证',
|
|
704
|
+
agentVerifyNote: 'stdio agent 需重启后验证',
|
|
705
|
+
// Skill auto-install
|
|
706
|
+
skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
|
|
707
|
+
skillLabel: 'Skill',
|
|
708
|
+
skillInstalling: '正在安装 Skill…',
|
|
709
|
+
skillInstalled: 'Skill 已安装',
|
|
710
|
+
skillFailed: 'Skill 安装失败',
|
|
681
711
|
// Step 2 — AI skip card
|
|
682
712
|
aiSkipTitle: '暂时跳过',
|
|
683
713
|
aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
|
package/app/lib/mcp-agents.ts
CHANGED
|
@@ -11,18 +11,19 @@ export interface AgentDef {
|
|
|
11
11
|
project: string | null;
|
|
12
12
|
global: string;
|
|
13
13
|
key: string;
|
|
14
|
+
preferredTransport: 'stdio' | 'http';
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const MCP_AGENTS: Record<string, AgentDef> = {
|
|
17
|
-
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
|
|
18
|
-
'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
|
|
19
|
-
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
|
|
20
|
-
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
|
|
21
|
-
'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
|
|
22
|
-
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
|
|
23
|
-
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
|
|
24
|
-
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
|
|
25
|
-
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
|
|
18
|
+
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
19
|
+
'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers', preferredTransport: 'http' },
|
|
20
|
+
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
21
|
+
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
22
|
+
'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
23
|
+
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
24
|
+
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
25
|
+
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
26
|
+
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
|
|
@@ -15,6 +15,7 @@ export interface RendererDefinition {
|
|
|
15
15
|
icon: string; // emoji or short string
|
|
16
16
|
tags: string[];
|
|
17
17
|
builtin: boolean; // true = ships with MindOS; false = user-installed (future)
|
|
18
|
+
core?: boolean; // true = default renderer for a file type, cannot be disabled by user
|
|
18
19
|
entryPath?: string; // canonical entry file shown on home page (e.g. 'TODO.md')
|
|
19
20
|
match: (ctx: Pick<RendererContext, 'filePath' | 'extension'>) => boolean;
|
|
20
21
|
// Provide either `component` (eager) or `load` (lazy). Prefer `load` for code-splitting.
|
|
@@ -38,6 +39,9 @@ export function loadDisabledState() {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function setRendererEnabled(id: string, enabled: boolean) {
|
|
42
|
+
// Core renderers cannot be disabled
|
|
43
|
+
const def = registry.find(r => r.id === id);
|
|
44
|
+
if (def?.core) return;
|
|
41
45
|
if (enabled) {
|
|
42
46
|
_disabledIds.delete(id);
|
|
43
47
|
} else {
|
|
@@ -49,6 +53,9 @@ export function setRendererEnabled(id: string, enabled: boolean) {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export function isRendererEnabled(id: string): boolean {
|
|
56
|
+
// Core renderers cannot be disabled
|
|
57
|
+
const def = registry.find(r => r.id === id);
|
|
58
|
+
if (def?.core) return true;
|
|
52
59
|
return !_disabledIds.has(id);
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore, useCallback, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unified per-file state hook for renderers.
|
|
7
|
+
*
|
|
8
|
+
* Each renderer stores its state under a namespaced localStorage key:
|
|
9
|
+
* `mindos-renderer:{rendererId}:{filePath}`
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const [cfg, setCfg] = useRendererState<CsvConfig>('csv', filePath, defaultCfg);
|
|
13
|
+
*
|
|
14
|
+
* The state is reactive — changes from other tabs/windows or from other
|
|
15
|
+
* components calling the setter will trigger a re-render via
|
|
16
|
+
* `useSyncExternalStore`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CHANGE_EVENT = 'mindos-renderer-state-change';
|
|
20
|
+
|
|
21
|
+
function storageKey(rendererId: string, filePath: string): string {
|
|
22
|
+
return `mindos-renderer:${rendererId}:${filePath}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useRendererState<T>(
|
|
26
|
+
rendererId: string,
|
|
27
|
+
filePath: string,
|
|
28
|
+
defaultValue: T,
|
|
29
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
30
|
+
const key = storageKey(rendererId, filePath);
|
|
31
|
+
|
|
32
|
+
// Cache parsed value to maintain referential stability for useSyncExternalStore.
|
|
33
|
+
// Without this, JSON.parse returns a new object on every getSnapshot call,
|
|
34
|
+
// causing Object.is to fail → infinite re-renders for non-primitive types.
|
|
35
|
+
const cacheRef = useRef<{ key: string; raw: string | null; parsed: T }>({ key, raw: null, parsed: defaultValue });
|
|
36
|
+
|
|
37
|
+
// Reset cache when key changes (different file or different renderer)
|
|
38
|
+
if (cacheRef.current.key !== key) {
|
|
39
|
+
cacheRef.current = { key, raw: null, parsed: defaultValue };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const state = useSyncExternalStore(
|
|
43
|
+
(onStoreChange) => {
|
|
44
|
+
const listener = () => onStoreChange();
|
|
45
|
+
window.addEventListener('storage', listener);
|
|
46
|
+
window.addEventListener(CHANGE_EVENT, listener);
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener('storage', listener);
|
|
49
|
+
window.removeEventListener(CHANGE_EVENT, listener);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
() => {
|
|
53
|
+
try {
|
|
54
|
+
const raw = localStorage.getItem(key);
|
|
55
|
+
if (raw === cacheRef.current.raw) return cacheRef.current.parsed;
|
|
56
|
+
if (raw === null) {
|
|
57
|
+
cacheRef.current = { key, raw: null, parsed: defaultValue };
|
|
58
|
+
return defaultValue;
|
|
59
|
+
}
|
|
60
|
+
const parsed = JSON.parse(raw) as T;
|
|
61
|
+
cacheRef.current = { key, raw, parsed };
|
|
62
|
+
return parsed;
|
|
63
|
+
} catch {
|
|
64
|
+
return defaultValue;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => defaultValue,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const setState = useCallback(
|
|
71
|
+
(value: T | ((prev: T) => T)) => {
|
|
72
|
+
try {
|
|
73
|
+
const current = (() => {
|
|
74
|
+
try {
|
|
75
|
+
const raw = localStorage.getItem(key);
|
|
76
|
+
return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
|
|
77
|
+
} catch {
|
|
78
|
+
return defaultValue;
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;
|
|
82
|
+
const serialized = JSON.stringify(next);
|
|
83
|
+
localStorage.setItem(key, serialized);
|
|
84
|
+
// Update cache eagerly so the next getSnapshot returns stable ref
|
|
85
|
+
cacheRef.current = { key, raw: serialized, parsed: next };
|
|
86
|
+
} catch { /* ignore quota errors */ }
|
|
87
|
+
window.dispatchEvent(new Event(CHANGE_EVENT));
|
|
88
|
+
},
|
|
89
|
+
[key, defaultValue],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return [state, setState];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Non-hook helpers for reading/writing renderer state outside React components.
|
|
97
|
+
*/
|
|
98
|
+
export function getRendererState<T>(rendererId: string, filePath: string, defaultValue: T): T {
|
|
99
|
+
if (typeof window === 'undefined') return defaultValue;
|
|
100
|
+
try {
|
|
101
|
+
const raw = localStorage.getItem(storageKey(rendererId, filePath));
|
|
102
|
+
return raw !== null ? (JSON.parse(raw) as T) : defaultValue;
|
|
103
|
+
} catch {
|
|
104
|
+
return defaultValue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setRendererState<T>(rendererId: string, filePath: string, value: T): void {
|
|
109
|
+
if (typeof window === 'undefined') return;
|
|
110
|
+
try {
|
|
111
|
+
localStorage.setItem(storageKey(rendererId, filePath), JSON.stringify(value));
|
|
112
|
+
} catch { /* ignore */ }
|
|
113
|
+
window.dispatchEvent(new Event(CHANGE_EVENT));
|
|
114
|
+
}
|
package/bin/lib/mcp-agents.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const MCP_AGENTS = {
|
|
7
|
-
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers' },
|
|
8
|
-
'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers' },
|
|
9
|
-
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers' },
|
|
10
|
-
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers' },
|
|
11
|
-
'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers' },
|
|
12
|
-
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers' },
|
|
13
|
-
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers' },
|
|
14
|
-
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers' },
|
|
15
|
-
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers' },
|
|
7
|
+
'claude-code': { name: 'Claude Code', project: '.mcp.json', global: '~/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
8
|
+
'claude-desktop': { name: 'Claude Desktop', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '~/.config/Claude/claude_desktop_config.json', key: 'mcpServers', preferredTransport: 'http' },
|
|
9
|
+
'cursor': { name: 'Cursor', project: '.cursor/mcp.json', global: '~/.cursor/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
10
|
+
'windsurf': { name: 'Windsurf', project: null, global: '~/.codeium/windsurf/mcp_config.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
11
|
+
'cline': { name: 'Cline', project: null, global: process.platform === 'darwin' ? '~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json' : '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
12
|
+
'trae': { name: 'Trae', project: '.trae/mcp.json', global: '~/.trae/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
13
|
+
'gemini-cli': { name: 'Gemini CLI', project: '.gemini/settings.json', global: '~/.gemini/settings.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
14
|
+
'openclaw': { name: 'OpenClaw', project: null, global: '~/.openclaw/mcp.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
15
|
+
'codebuddy': { name: 'CodeBuddy', project: null, global: '~/.claude-internal/.claude.json', key: 'mcpServers', preferredTransport: 'stdio' },
|
|
16
16
|
};
|
package/package.json
CHANGED