@geminilight/mindos 0.5.19 → 0.5.21
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/ask/route.ts +308 -172
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/SettingsModal.tsx +52 -58
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/AiTab.tsx +4 -25
- package/app/components/settings/AppearanceTab.tsx +31 -13
- package/app/components/settings/KnowledgeTab.tsx +13 -28
- package/app/components/settings/McpAgentInstall.tsx +227 -0
- package/app/components/settings/McpServerStatus.tsx +172 -0
- package/app/components/settings/McpSkillsSection.tsx +583 -0
- package/app/components/settings/McpTab.tsx +16 -728
- package/app/components/settings/PluginsTab.tsx +4 -27
- package/app/components/settings/Primitives.tsx +69 -0
- package/app/components/settings/ShortcutsTab.tsx +2 -4
- package/app/components/settings/SyncTab.tsx +8 -24
- package/app/components/settings/types.ts +116 -2
- package/app/lib/agent/context.ts +151 -87
- package/app/lib/agent/index.ts +4 -3
- package/app/lib/agent/model.ts +76 -10
- package/app/lib/agent/stream-consumer.ts +73 -77
- package/app/lib/agent/to-agent-messages.ts +106 -0
- package/app/lib/agent/tools.ts +260 -266
- package/app/lib/i18n-en.ts +480 -0
- package/app/lib/i18n-zh.ts +505 -0
- package/app/lib/i18n.ts +4 -947
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/app/package-lock.json +3258 -3093
- package/app/package.json +6 -3
- package/bin/cli.js +140 -5
- package/package.json +4 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
|
@@ -1,699 +1,13 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
Plug, CheckCircle2, AlertCircle, Loader2, Copy, Check,
|
|
6
|
-
ChevronDown, ChevronRight, Trash2, Plus, X,
|
|
7
|
-
} from 'lucide-react';
|
|
2
|
+
import { Loader2 } from 'lucide-react';
|
|
8
3
|
import { apiFetch } from '@/lib/api';
|
|
4
|
+
import type { McpStatus, AgentInfo, McpTabProps } from './types';
|
|
5
|
+
import ServerStatus from './McpServerStatus';
|
|
6
|
+
import AgentInstall from './McpAgentInstall';
|
|
7
|
+
import SkillsSection from './McpSkillsSection';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
interface McpStatus {
|
|
13
|
-
running: boolean;
|
|
14
|
-
transport: string;
|
|
15
|
-
endpoint: string;
|
|
16
|
-
port: number;
|
|
17
|
-
toolCount: number;
|
|
18
|
-
authConfigured: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface AgentInfo {
|
|
22
|
-
key: string;
|
|
23
|
-
name: string;
|
|
24
|
-
present: boolean;
|
|
25
|
-
installed: boolean;
|
|
26
|
-
scope?: string;
|
|
27
|
-
transport?: string;
|
|
28
|
-
configPath?: string;
|
|
29
|
-
hasProjectScope: boolean;
|
|
30
|
-
hasGlobalScope: boolean;
|
|
31
|
-
preferredTransport: 'stdio' | 'http';
|
|
32
|
-
// Snippet generation fields
|
|
33
|
-
format: 'json' | 'toml';
|
|
34
|
-
configKey: string;
|
|
35
|
-
globalNestedKey?: string;
|
|
36
|
-
globalPath: string;
|
|
37
|
-
projectPath?: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface SkillInfo {
|
|
41
|
-
name: string;
|
|
42
|
-
description: string;
|
|
43
|
-
path: string;
|
|
44
|
-
source: 'builtin' | 'user';
|
|
45
|
-
enabled: boolean;
|
|
46
|
-
editable: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface McpTabProps {
|
|
50
|
-
t: any;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
54
|
-
|
|
55
|
-
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
56
|
-
const [copied, setCopied] = useState(false);
|
|
57
|
-
const handleCopy = async () => {
|
|
58
|
-
try {
|
|
59
|
-
await navigator.clipboard.writeText(text);
|
|
60
|
-
setCopied(true);
|
|
61
|
-
setTimeout(() => setCopied(false), 2000);
|
|
62
|
-
} catch { /* clipboard unavailable */ }
|
|
63
|
-
};
|
|
64
|
-
return (
|
|
65
|
-
<button
|
|
66
|
-
onClick={handleCopy}
|
|
67
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
68
|
-
>
|
|
69
|
-
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
70
|
-
{copied ? 'Copied!' : label}
|
|
71
|
-
</button>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* ── Config Snippet Generator ─────────────────────────────────── */
|
|
76
|
-
|
|
77
|
-
function generateConfigSnippet(
|
|
78
|
-
agent: AgentInfo,
|
|
79
|
-
status: McpStatus,
|
|
80
|
-
token?: string,
|
|
81
|
-
): { snippet: string; path: string } {
|
|
82
|
-
const isRunning = status.running;
|
|
83
|
-
|
|
84
|
-
// Determine entry (stdio vs http)
|
|
85
|
-
const stdioEntry: Record<string, unknown> = { type: 'stdio', command: 'mindos', args: ['mcp'] };
|
|
86
|
-
const httpEntry: Record<string, unknown> = { url: status.endpoint };
|
|
87
|
-
if (token) httpEntry.headers = { Authorization: `Bearer ${token}` };
|
|
88
|
-
const entry = isRunning ? httpEntry : stdioEntry;
|
|
89
|
-
|
|
90
|
-
// TOML format (Codex)
|
|
91
|
-
if (agent.format === 'toml') {
|
|
92
|
-
const lines: string[] = [`[${agent.configKey}.mindos]`];
|
|
93
|
-
if (isRunning) {
|
|
94
|
-
lines.push(`type = "http"`);
|
|
95
|
-
lines.push(`url = "${status.endpoint}"`);
|
|
96
|
-
if (token) {
|
|
97
|
-
lines.push('');
|
|
98
|
-
lines.push(`[${agent.configKey}.mindos.headers]`);
|
|
99
|
-
lines.push(`Authorization = "Bearer ${token}"`);
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
lines.push(`command = "mindos"`);
|
|
103
|
-
lines.push(`args = ["mcp"]`);
|
|
104
|
-
lines.push('');
|
|
105
|
-
lines.push(`[${agent.configKey}.mindos.env]`);
|
|
106
|
-
lines.push(`MCP_TRANSPORT = "stdio"`);
|
|
107
|
-
}
|
|
108
|
-
return { snippet: lines.join('\n'), path: agent.globalPath };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// JSON with globalNestedKey (VS Code project-level uses flat key)
|
|
112
|
-
if (agent.globalNestedKey) {
|
|
113
|
-
// project-level: flat key structure
|
|
114
|
-
const projectSnippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
115
|
-
return { snippet: projectSnippet, path: agent.projectPath ?? agent.globalPath };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Standard JSON
|
|
119
|
-
const snippet = JSON.stringify({ [agent.configKey]: { mindos: entry } }, null, 2);
|
|
120
|
-
return { snippet, path: agent.globalPath };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/* ── MCP Server Status ─────────────────────────────────────────── */
|
|
124
|
-
|
|
125
|
-
function ServerStatus({ status, agents, t }: { status: McpStatus | null; agents: AgentInfo[]; t: any }) {
|
|
126
|
-
const m = t.settings?.mcp;
|
|
127
|
-
const [selectedAgent, setSelectedAgent] = useState<string>('');
|
|
128
|
-
|
|
129
|
-
// Auto-select first installed or first detected agent
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (agents.length > 0 && !selectedAgent) {
|
|
132
|
-
const first = agents.find(a => a.installed) ?? agents.find(a => a.present) ?? agents[0];
|
|
133
|
-
if (first) setSelectedAgent(first.key);
|
|
134
|
-
}
|
|
135
|
-
}, [agents, selectedAgent]);
|
|
136
|
-
|
|
137
|
-
if (!status) return null;
|
|
138
|
-
|
|
139
|
-
const currentAgent = agents.find(a => a.key === selectedAgent);
|
|
140
|
-
const snippetResult = currentAgent ? generateConfigSnippet(currentAgent, status) : null;
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<div className="space-y-3">
|
|
144
|
-
<div className="flex items-center gap-3">
|
|
145
|
-
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
|
146
|
-
<Plug size={16} className="text-muted-foreground" />
|
|
147
|
-
</div>
|
|
148
|
-
<div>
|
|
149
|
-
<h3 className="text-sm font-medium text-foreground">{m?.serverTitle ?? 'MindOS MCP Server'}</h3>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div className="space-y-1.5 text-sm pl-11">
|
|
154
|
-
<div className="flex items-center gap-2">
|
|
155
|
-
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.status ?? 'Status'}</span>
|
|
156
|
-
<span className={`text-xs flex items-center gap-1 ${status.running ? 'text-success' : 'text-muted-foreground'}`}>
|
|
157
|
-
<span className={`inline-block w-1.5 h-1.5 rounded-full ${status.running ? 'bg-success' : 'bg-muted-foreground'}`} />
|
|
158
|
-
{status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
|
|
159
|
-
</span>
|
|
160
|
-
</div>
|
|
161
|
-
<div className="flex items-center gap-2">
|
|
162
|
-
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.transport ?? 'Transport'}</span>
|
|
163
|
-
<span className="text-xs font-mono">{status.transport.toUpperCase()}</span>
|
|
164
|
-
</div>
|
|
165
|
-
<div className="flex items-center gap-2">
|
|
166
|
-
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.endpoint ?? 'Endpoint'}</span>
|
|
167
|
-
<span className="text-xs font-mono truncate">{status.endpoint}</span>
|
|
168
|
-
</div>
|
|
169
|
-
<div className="flex items-center gap-2">
|
|
170
|
-
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.tools ?? 'Tools'}</span>
|
|
171
|
-
<span className="text-xs">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} registered`}</span>
|
|
172
|
-
</div>
|
|
173
|
-
<div className="flex items-center gap-2">
|
|
174
|
-
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.auth ?? 'Auth'}</span>
|
|
175
|
-
<span className="text-xs">
|
|
176
|
-
{status.authConfigured
|
|
177
|
-
? <span className="text-success">{m?.authSet ?? 'Token set'}</span>
|
|
178
|
-
: <span className="text-muted-foreground">{m?.authNotSet ?? 'No token'}</span>}
|
|
179
|
-
</span>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<div className="flex items-center gap-2 pl-11">
|
|
184
|
-
<CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy Endpoint'} />
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
{/* Quick Setup — agent-specific config snippet */}
|
|
188
|
-
{agents.length > 0 && (
|
|
189
|
-
<div className="pl-11 pt-2 space-y-2.5">
|
|
190
|
-
<div className="flex items-center gap-2">
|
|
191
|
-
<span className="text-xs text-muted-foreground font-medium">
|
|
192
|
-
── {m?.quickSetup ?? 'Quick Setup'} ──
|
|
193
|
-
</span>
|
|
194
|
-
</div>
|
|
195
|
-
|
|
196
|
-
<div className="flex items-center gap-2">
|
|
197
|
-
<span className="text-xs text-muted-foreground shrink-0">{m?.configureFor ?? 'Configure for'}</span>
|
|
198
|
-
<select
|
|
199
|
-
value={selectedAgent}
|
|
200
|
-
onChange={e => setSelectedAgent(e.target.value)}
|
|
201
|
-
className="text-xs px-2 py-1 rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
202
|
-
>
|
|
203
|
-
{agents.map(a => (
|
|
204
|
-
<option key={a.key} value={a.key}>
|
|
205
|
-
{a.name}{a.installed ? ` ✓` : a.present ? ` ·` : ''}
|
|
206
|
-
</option>
|
|
207
|
-
))}
|
|
208
|
-
</select>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{snippetResult && (
|
|
212
|
-
<>
|
|
213
|
-
<div className="flex items-center gap-2">
|
|
214
|
-
<span className="text-xs text-muted-foreground shrink-0">{m?.configPath ?? 'Config path'}</span>
|
|
215
|
-
<span className="text-xs font-mono text-foreground">{snippetResult.path}</span>
|
|
216
|
-
</div>
|
|
217
|
-
|
|
218
|
-
<pre className="text-xs font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre">
|
|
219
|
-
{snippetResult.snippet}
|
|
220
|
-
</pre>
|
|
221
|
-
|
|
222
|
-
<CopyButton text={snippetResult.snippet} label={m?.copyConfig ?? 'Copy Config'} />
|
|
223
|
-
</>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
|
-
</div>
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/* ── Agent Install ─────────────────────────────────────────────── */
|
|
232
|
-
|
|
233
|
-
function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; onRefresh: () => void }) {
|
|
234
|
-
const m = t.settings?.mcp;
|
|
235
|
-
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
236
|
-
const [transport, setTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
237
|
-
const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
|
|
238
|
-
const [httpToken, setHttpToken] = useState('');
|
|
239
|
-
const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
|
|
240
|
-
const [installing, setInstalling] = useState(false);
|
|
241
|
-
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
242
|
-
|
|
243
|
-
const getEffectiveTransport = (agent: AgentInfo) => {
|
|
244
|
-
if (transport === 'auto') return agent.preferredTransport;
|
|
245
|
-
return transport;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const toggle = (key: string) => {
|
|
249
|
-
setSelected(prev => {
|
|
250
|
-
const next = new Set(prev);
|
|
251
|
-
if (next.has(key)) next.delete(key); else next.add(key);
|
|
252
|
-
return next;
|
|
253
|
-
});
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const handleInstall = async () => {
|
|
257
|
-
if (selected.size === 0) return;
|
|
258
|
-
setInstalling(true);
|
|
259
|
-
setMessage(null);
|
|
260
|
-
try {
|
|
261
|
-
const payload = {
|
|
262
|
-
agents: [...selected].map(key => {
|
|
263
|
-
const agent = agents.find(a => a.key === key);
|
|
264
|
-
const effectiveTransport = transport === 'auto'
|
|
265
|
-
? (agent?.preferredTransport || 'stdio')
|
|
266
|
-
: transport;
|
|
267
|
-
return {
|
|
268
|
-
key,
|
|
269
|
-
scope: scopes[key] || (agents.find(a => a.key === key)?.hasProjectScope ? 'project' : 'global'),
|
|
270
|
-
transport: effectiveTransport,
|
|
271
|
-
};
|
|
272
|
-
}),
|
|
273
|
-
transport,
|
|
274
|
-
...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
|
|
275
|
-
// For auto mode, pass http settings for agents that need it
|
|
276
|
-
...(transport === 'auto' ? { url: httpUrl, token: httpToken } : {}),
|
|
277
|
-
};
|
|
278
|
-
const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
|
|
279
|
-
method: 'POST',
|
|
280
|
-
headers: { 'Content-Type': 'application/json' },
|
|
281
|
-
body: JSON.stringify(payload),
|
|
282
|
-
});
|
|
283
|
-
const ok = res.results.filter(r => r.status === 'ok').length;
|
|
284
|
-
const fail = res.results.filter(r => r.status === 'error');
|
|
285
|
-
if (fail.length > 0) {
|
|
286
|
-
setMessage({ type: 'error', text: fail.map(f => `${f.agent}: ${f.message}`).join('; ') });
|
|
287
|
-
} else {
|
|
288
|
-
setMessage({ type: 'success', text: m?.installSuccess ? m.installSuccess(ok) : `${ok} agent(s) configured` });
|
|
289
|
-
}
|
|
290
|
-
setSelected(new Set());
|
|
291
|
-
onRefresh();
|
|
292
|
-
} catch {
|
|
293
|
-
setMessage({ type: 'error', text: m?.installFailed ?? 'Install failed' });
|
|
294
|
-
} finally {
|
|
295
|
-
setInstalling(false);
|
|
296
|
-
setTimeout(() => setMessage(null), 4000);
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Show http fields if transport is 'http', or 'auto' with any http-preferred agent selected
|
|
301
|
-
const showHttpFields = transport === 'http' || (transport === 'auto' && [...selected].some(key => {
|
|
302
|
-
const agent = agents.find(a => a.key === key);
|
|
303
|
-
return agent?.preferredTransport === 'http';
|
|
304
|
-
}));
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<div className="space-y-3 pt-2">
|
|
308
|
-
{/* Agent list */}
|
|
309
|
-
<div className="space-y-1">
|
|
310
|
-
{agents.map(agent => (
|
|
311
|
-
<div key={agent.key} className="flex items-center gap-3 py-1.5 text-sm">
|
|
312
|
-
<input
|
|
313
|
-
type="checkbox"
|
|
314
|
-
checked={selected.has(agent.key)}
|
|
315
|
-
onChange={() => toggle(agent.key)}
|
|
316
|
-
className="rounded border-border"
|
|
317
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
318
|
-
/>
|
|
319
|
-
<span className="w-28 shrink-0 text-xs">{agent.name}</span>
|
|
320
|
-
<span className="text-2xs px-1.5 py-0.5 rounded font-mono"
|
|
321
|
-
style={{ background: 'rgba(100,100,120,0.08)' }}>
|
|
322
|
-
{getEffectiveTransport(agent)}
|
|
323
|
-
</span>
|
|
324
|
-
{agent.installed ? (
|
|
325
|
-
<>
|
|
326
|
-
<span className="text-2xs px-1.5 py-0.5 rounded bg-success/15 text-success font-mono">
|
|
327
|
-
{agent.transport}
|
|
328
|
-
</span>
|
|
329
|
-
<span className="text-2xs text-muted-foreground">{agent.scope}</span>
|
|
330
|
-
</>
|
|
331
|
-
) : (
|
|
332
|
-
<span className="text-2xs text-muted-foreground">
|
|
333
|
-
{agent.present ? (m?.detected ?? 'Detected') : (m?.notFound ?? 'Not found')}
|
|
334
|
-
</span>
|
|
335
|
-
)}
|
|
336
|
-
{/* Scope selector */}
|
|
337
|
-
{selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
|
|
338
|
-
<select
|
|
339
|
-
value={scopes[agent.key] || 'project'}
|
|
340
|
-
onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
|
|
341
|
-
className="ml-auto text-2xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
|
|
342
|
-
>
|
|
343
|
-
<option value="project">{m?.project ?? 'Project'}</option>
|
|
344
|
-
<option value="global">{m?.global ?? 'Global'}</option>
|
|
345
|
-
</select>
|
|
346
|
-
)}
|
|
347
|
-
</div>
|
|
348
|
-
))}
|
|
349
|
-
</div>
|
|
350
|
-
|
|
351
|
-
{/* Select detected / Clear buttons */}
|
|
352
|
-
<div className="flex gap-2 text-xs pt-1">
|
|
353
|
-
<button type="button"
|
|
354
|
-
onClick={() => setSelected(new Set(
|
|
355
|
-
agents.filter(a => !a.installed && a.present).map(a => a.key)
|
|
356
|
-
))}
|
|
357
|
-
className="px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
|
|
358
|
-
style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
|
|
359
|
-
{m?.selectDetected ?? 'Select Detected'}
|
|
360
|
-
</button>
|
|
361
|
-
<button type="button"
|
|
362
|
-
onClick={() => setSelected(new Set())}
|
|
363
|
-
className="px-2.5 py-1 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground">
|
|
364
|
-
{m?.clearSelection ?? 'Clear'}
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
{/* Transport selector */}
|
|
369
|
-
<div className="flex items-center gap-4 text-xs pt-1">
|
|
370
|
-
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
371
|
-
<input
|
|
372
|
-
type="radio"
|
|
373
|
-
name="transport"
|
|
374
|
-
checked={transport === 'auto'}
|
|
375
|
-
onChange={() => setTransport('auto')}
|
|
376
|
-
className=""
|
|
377
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
378
|
-
/>
|
|
379
|
-
{m?.transportAuto ?? 'auto (recommended)'}
|
|
380
|
-
</label>
|
|
381
|
-
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
382
|
-
<input
|
|
383
|
-
type="radio"
|
|
384
|
-
name="transport"
|
|
385
|
-
checked={transport === 'stdio'}
|
|
386
|
-
onChange={() => setTransport('stdio')}
|
|
387
|
-
className=""
|
|
388
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
389
|
-
/>
|
|
390
|
-
{m?.transportStdio ?? 'stdio'}
|
|
391
|
-
</label>
|
|
392
|
-
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
393
|
-
<input
|
|
394
|
-
type="radio"
|
|
395
|
-
name="transport"
|
|
396
|
-
checked={transport === 'http'}
|
|
397
|
-
onChange={() => setTransport('http')}
|
|
398
|
-
className=""
|
|
399
|
-
style={{ accentColor: 'var(--amber)' }}
|
|
400
|
-
/>
|
|
401
|
-
{m?.transportHttp ?? 'http'}
|
|
402
|
-
</label>
|
|
403
|
-
</div>
|
|
404
|
-
|
|
405
|
-
{/* HTTP settings */}
|
|
406
|
-
{showHttpFields && (
|
|
407
|
-
<div className="space-y-2 pl-5 text-xs">
|
|
408
|
-
<div className="space-y-1">
|
|
409
|
-
<label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
|
|
410
|
-
<input
|
|
411
|
-
type="text"
|
|
412
|
-
value={httpUrl}
|
|
413
|
-
onChange={e => setHttpUrl(e.target.value)}
|
|
414
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
415
|
-
/>
|
|
416
|
-
</div>
|
|
417
|
-
<div className="space-y-1">
|
|
418
|
-
<label className="text-muted-foreground">{m?.httpToken ?? 'Auth Token'}</label>
|
|
419
|
-
<input
|
|
420
|
-
type="password"
|
|
421
|
-
value={httpToken}
|
|
422
|
-
onChange={e => setHttpToken(e.target.value)}
|
|
423
|
-
placeholder="Bearer token"
|
|
424
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
425
|
-
/>
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
)}
|
|
429
|
-
|
|
430
|
-
{/* Install button */}
|
|
431
|
-
<button
|
|
432
|
-
onClick={handleInstall}
|
|
433
|
-
disabled={selected.size === 0 || installing}
|
|
434
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
435
|
-
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
436
|
-
>
|
|
437
|
-
{installing && <Loader2 size={12} className="animate-spin" />}
|
|
438
|
-
{installing ? (m?.installing ?? 'Installing...') : (m?.installSelected ?? 'Install Selected')}
|
|
439
|
-
</button>
|
|
440
|
-
|
|
441
|
-
{/* Message */}
|
|
442
|
-
{message && (
|
|
443
|
-
<div className="flex items-center gap-1.5 text-xs" role="status">
|
|
444
|
-
{message.type === 'success' ? (
|
|
445
|
-
<><CheckCircle2 size={12} className="text-success" /><span className="text-success">{message.text}</span></>
|
|
446
|
-
) : (
|
|
447
|
-
<><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
|
|
448
|
-
)}
|
|
449
|
-
</div>
|
|
450
|
-
)}
|
|
451
|
-
</div>
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/* ── Skills Section ────────────────────────────────────────────── */
|
|
456
|
-
|
|
457
|
-
function SkillsSection({ t }: { t: any }) {
|
|
458
|
-
const m = t.settings?.mcp;
|
|
459
|
-
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
460
|
-
const [loading, setLoading] = useState(true);
|
|
461
|
-
const [expanded, setExpanded] = useState<string | null>(null);
|
|
462
|
-
const [adding, setAdding] = useState(false);
|
|
463
|
-
const [newName, setNewName] = useState('');
|
|
464
|
-
const [newDesc, setNewDesc] = useState('');
|
|
465
|
-
const [newContent, setNewContent] = useState('');
|
|
466
|
-
const [saving, setSaving] = useState(false);
|
|
467
|
-
const [error, setError] = useState('');
|
|
468
|
-
|
|
469
|
-
const fetchSkills = useCallback(async () => {
|
|
470
|
-
try {
|
|
471
|
-
const data = await apiFetch<{ skills: SkillInfo[] }>('/api/skills');
|
|
472
|
-
setSkills(data.skills);
|
|
473
|
-
} catch { /* ignore */ }
|
|
474
|
-
setLoading(false);
|
|
475
|
-
}, []);
|
|
476
|
-
|
|
477
|
-
useEffect(() => { fetchSkills(); }, [fetchSkills]);
|
|
478
|
-
|
|
479
|
-
const handleToggle = async (name: string, enabled: boolean) => {
|
|
480
|
-
try {
|
|
481
|
-
await apiFetch('/api/skills', {
|
|
482
|
-
method: 'POST',
|
|
483
|
-
headers: { 'Content-Type': 'application/json' },
|
|
484
|
-
body: JSON.stringify({ action: 'toggle', name, enabled }),
|
|
485
|
-
});
|
|
486
|
-
setSkills(prev => prev.map(s => s.name === name ? { ...s, enabled } : s));
|
|
487
|
-
} catch { /* ignore */ }
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const handleDelete = async (name: string) => {
|
|
491
|
-
const confirmMsg = m?.skillDeleteConfirm ? m.skillDeleteConfirm(name) : `Delete skill "${name}"?`;
|
|
492
|
-
if (!confirm(confirmMsg)) return;
|
|
493
|
-
try {
|
|
494
|
-
await apiFetch('/api/skills', {
|
|
495
|
-
method: 'POST',
|
|
496
|
-
headers: { 'Content-Type': 'application/json' },
|
|
497
|
-
body: JSON.stringify({ action: 'delete', name }),
|
|
498
|
-
});
|
|
499
|
-
fetchSkills();
|
|
500
|
-
} catch { /* ignore */ }
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const handleCreate = async () => {
|
|
504
|
-
if (!newName.trim()) return;
|
|
505
|
-
setSaving(true);
|
|
506
|
-
setError('');
|
|
507
|
-
try {
|
|
508
|
-
await apiFetch('/api/skills', {
|
|
509
|
-
method: 'POST',
|
|
510
|
-
headers: { 'Content-Type': 'application/json' },
|
|
511
|
-
body: JSON.stringify({ action: 'create', name: newName.trim(), description: newDesc.trim(), content: newContent }),
|
|
512
|
-
});
|
|
513
|
-
setAdding(false);
|
|
514
|
-
setNewName('');
|
|
515
|
-
setNewDesc('');
|
|
516
|
-
setNewContent('');
|
|
517
|
-
fetchSkills();
|
|
518
|
-
} catch (err: unknown) {
|
|
519
|
-
setError(err instanceof Error ? err.message : 'Failed to create skill');
|
|
520
|
-
} finally {
|
|
521
|
-
setSaving(false);
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
if (loading) {
|
|
526
|
-
return (
|
|
527
|
-
<div className="flex justify-center py-4">
|
|
528
|
-
<Loader2 size={16} className="animate-spin text-muted-foreground" />
|
|
529
|
-
</div>
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return (
|
|
534
|
-
<div className="space-y-3 pt-2">
|
|
535
|
-
{/* Skill language switcher */}
|
|
536
|
-
{(() => {
|
|
537
|
-
const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
|
|
538
|
-
const currentLang = mindosEnabled ? 'en' : 'zh';
|
|
539
|
-
const handleLangSwitch = async (lang: 'en' | 'zh') => {
|
|
540
|
-
if (lang === currentLang) return;
|
|
541
|
-
if (lang === 'en') {
|
|
542
|
-
await handleToggle('mindos', true);
|
|
543
|
-
await handleToggle('mindos-zh', false);
|
|
544
|
-
} else {
|
|
545
|
-
await handleToggle('mindos-zh', true);
|
|
546
|
-
await handleToggle('mindos', false);
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
return (
|
|
550
|
-
<div className="flex items-center gap-2 text-xs">
|
|
551
|
-
<span className="text-muted-foreground">{m?.skillLanguage ?? 'Skill Language'}</span>
|
|
552
|
-
<div className="flex rounded-md border border-border overflow-hidden">
|
|
553
|
-
<button
|
|
554
|
-
onClick={() => handleLangSwitch('en')}
|
|
555
|
-
className={`px-2.5 py-1 text-xs transition-colors ${
|
|
556
|
-
currentLang === 'en'
|
|
557
|
-
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
558
|
-
: 'text-muted-foreground hover:bg-muted'
|
|
559
|
-
}`}
|
|
560
|
-
>
|
|
561
|
-
{m?.skillLangEn ?? 'English'}
|
|
562
|
-
</button>
|
|
563
|
-
<button
|
|
564
|
-
onClick={() => handleLangSwitch('zh')}
|
|
565
|
-
className={`px-2.5 py-1 text-xs transition-colors border-l border-border ${
|
|
566
|
-
currentLang === 'zh'
|
|
567
|
-
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
568
|
-
: 'text-muted-foreground hover:bg-muted'
|
|
569
|
-
}`}
|
|
570
|
-
>
|
|
571
|
-
{m?.skillLangZh ?? '中文'}
|
|
572
|
-
</button>
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
);
|
|
576
|
-
})()}
|
|
577
|
-
|
|
578
|
-
{skills.map(skill => (
|
|
579
|
-
<div key={skill.name} className="border border-border rounded-lg overflow-hidden">
|
|
580
|
-
<div
|
|
581
|
-
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
582
|
-
onClick={() => setExpanded(expanded === skill.name ? null : skill.name)}
|
|
583
|
-
>
|
|
584
|
-
{expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
585
|
-
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
586
|
-
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
587
|
-
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
588
|
-
}`}>
|
|
589
|
-
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
590
|
-
</span>
|
|
591
|
-
{/* Toggle */}
|
|
592
|
-
<button
|
|
593
|
-
onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }}
|
|
594
|
-
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
|
|
595
|
-
skill.enabled ? 'bg-success' : 'bg-muted-foreground/30'
|
|
596
|
-
}`}
|
|
597
|
-
>
|
|
598
|
-
<span className={`inline-block h-3 w-3 rounded-full bg-white transition-transform ${
|
|
599
|
-
skill.enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
|
600
|
-
}`} />
|
|
601
|
-
</button>
|
|
602
|
-
</div>
|
|
603
|
-
|
|
604
|
-
{expanded === skill.name && (
|
|
605
|
-
<div className="px-3 py-2 border-t border-border text-xs space-y-1.5 bg-muted/20">
|
|
606
|
-
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
607
|
-
<p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
|
|
608
|
-
{skill.editable && (
|
|
609
|
-
<button
|
|
610
|
-
onClick={() => handleDelete(skill.name)}
|
|
611
|
-
className="flex items-center gap-1 text-2xs text-destructive hover:underline"
|
|
612
|
-
>
|
|
613
|
-
<Trash2 size={10} />
|
|
614
|
-
{m?.deleteSkill ?? 'Delete'}
|
|
615
|
-
</button>
|
|
616
|
-
)}
|
|
617
|
-
</div>
|
|
618
|
-
)}
|
|
619
|
-
</div>
|
|
620
|
-
))}
|
|
621
|
-
|
|
622
|
-
{/* Add skill form */}
|
|
623
|
-
{adding ? (
|
|
624
|
-
<div className="border border-border rounded-lg p-3 space-y-2">
|
|
625
|
-
<div className="flex items-center justify-between">
|
|
626
|
-
<span className="text-xs font-medium">{m?.addSkill ?? '+ Add Skill'}</span>
|
|
627
|
-
<button onClick={() => setAdding(false)} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
|
|
628
|
-
<X size={12} />
|
|
629
|
-
</button>
|
|
630
|
-
</div>
|
|
631
|
-
<div className="space-y-1">
|
|
632
|
-
<label className="text-2xs text-muted-foreground">{m?.skillName ?? 'Name'}</label>
|
|
633
|
-
<input
|
|
634
|
-
type="text"
|
|
635
|
-
value={newName}
|
|
636
|
-
onChange={e => setNewName(e.target.value.replace(/[^a-z0-9-]/g, ''))}
|
|
637
|
-
placeholder="my-skill"
|
|
638
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
639
|
-
/>
|
|
640
|
-
</div>
|
|
641
|
-
<div className="space-y-1">
|
|
642
|
-
<label className="text-2xs text-muted-foreground">{m?.skillDesc ?? 'Description'}</label>
|
|
643
|
-
<input
|
|
644
|
-
type="text"
|
|
645
|
-
value={newDesc}
|
|
646
|
-
onChange={e => setNewDesc(e.target.value)}
|
|
647
|
-
placeholder="What does this skill do?"
|
|
648
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
649
|
-
/>
|
|
650
|
-
</div>
|
|
651
|
-
<div className="space-y-1">
|
|
652
|
-
<label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
|
|
653
|
-
<textarea
|
|
654
|
-
value={newContent}
|
|
655
|
-
onChange={e => setNewContent(e.target.value)}
|
|
656
|
-
rows={6}
|
|
657
|
-
placeholder="Skill instructions (markdown)..."
|
|
658
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring resize-y font-mono"
|
|
659
|
-
/>
|
|
660
|
-
</div>
|
|
661
|
-
{error && (
|
|
662
|
-
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
663
|
-
<AlertCircle size={10} />
|
|
664
|
-
{error}
|
|
665
|
-
</p>
|
|
666
|
-
)}
|
|
667
|
-
<div className="flex items-center gap-2">
|
|
668
|
-
<button
|
|
669
|
-
onClick={handleCreate}
|
|
670
|
-
disabled={!newName.trim() || saving}
|
|
671
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
672
|
-
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
673
|
-
>
|
|
674
|
-
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
675
|
-
{m?.saveSkill ?? 'Save'}
|
|
676
|
-
</button>
|
|
677
|
-
<button
|
|
678
|
-
onClick={() => setAdding(false)}
|
|
679
|
-
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
680
|
-
>
|
|
681
|
-
{m?.cancelSkill ?? 'Cancel'}
|
|
682
|
-
</button>
|
|
683
|
-
</div>
|
|
684
|
-
</div>
|
|
685
|
-
) : (
|
|
686
|
-
<button
|
|
687
|
-
onClick={() => setAdding(true)}
|
|
688
|
-
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
689
|
-
>
|
|
690
|
-
<Plus size={12} />
|
|
691
|
-
{m?.addSkill ?? '+ Add Skill'}
|
|
692
|
-
</button>
|
|
693
|
-
)}
|
|
694
|
-
</div>
|
|
695
|
-
);
|
|
696
|
-
}
|
|
9
|
+
// Re-export types for backward compatibility
|
|
10
|
+
export type { McpStatus, AgentInfo, SkillInfo, McpTabProps } from './types';
|
|
697
11
|
|
|
698
12
|
/* ── Main McpTab ───────────────────────────────────────────────── */
|
|
699
13
|
|
|
@@ -701,8 +15,6 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
701
15
|
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
702
16
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
703
17
|
const [loading, setLoading] = useState(true);
|
|
704
|
-
const [showAgents, setShowAgents] = useState(false);
|
|
705
|
-
const [showSkills, setShowSkills] = useState(false);
|
|
706
18
|
|
|
707
19
|
const fetchAll = useCallback(async () => {
|
|
708
20
|
try {
|
|
@@ -730,45 +42,21 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
730
42
|
|
|
731
43
|
return (
|
|
732
44
|
<div className="space-y-6">
|
|
733
|
-
{/* MCP Server Status —
|
|
45
|
+
{/* MCP Server Status — compact card */}
|
|
734
46
|
<div className="rounded-xl border p-4" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
|
735
47
|
<ServerStatus status={mcpStatus} agents={agents} t={t} />
|
|
736
48
|
</div>
|
|
737
49
|
|
|
738
|
-
{/* Agent
|
|
739
|
-
<div
|
|
740
|
-
<
|
|
741
|
-
|
|
742
|
-
onClick={() => setShowAgents(!showAgents)}
|
|
743
|
-
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
|
|
744
|
-
style={{ color: 'var(--foreground)' }}
|
|
745
|
-
>
|
|
746
|
-
<span>{m?.agentsTitle ?? 'Agent Configuration'}</span>
|
|
747
|
-
<ChevronDown size={14} className={`transition-transform text-muted-foreground ${showAgents ? 'rotate-180' : ''}`} />
|
|
748
|
-
</button>
|
|
749
|
-
{showAgents && (
|
|
750
|
-
<div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
|
|
751
|
-
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
752
|
-
</div>
|
|
753
|
-
)}
|
|
50
|
+
{/* Agent Configuration */}
|
|
51
|
+
<div>
|
|
52
|
+
<h3 className="text-sm font-medium text-foreground mb-3">{m?.agentsTitle ?? 'Agent Configuration'}</h3>
|
|
53
|
+
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
754
54
|
</div>
|
|
755
55
|
|
|
756
|
-
{/* Skills
|
|
757
|
-
<div
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
onClick={() => setShowSkills(!showSkills)}
|
|
761
|
-
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
|
|
762
|
-
style={{ color: 'var(--foreground)' }}
|
|
763
|
-
>
|
|
764
|
-
<span>{m?.skillsTitle ?? 'Skills'}</span>
|
|
765
|
-
<ChevronDown size={14} className={`transition-transform text-muted-foreground ${showSkills ? 'rotate-180' : ''}`} />
|
|
766
|
-
</button>
|
|
767
|
-
{showSkills && (
|
|
768
|
-
<div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
|
|
769
|
-
<SkillsSection t={t} />
|
|
770
|
-
</div>
|
|
771
|
-
)}
|
|
56
|
+
{/* Skills */}
|
|
57
|
+
<div>
|
|
58
|
+
<h3 className="text-sm font-medium text-foreground mb-3">{m?.skillsTitle ?? 'Skills'}</h3>
|
|
59
|
+
<SkillsSection t={t} />
|
|
772
60
|
</div>
|
|
773
61
|
</div>
|
|
774
62
|
);
|