@geminilight/mindos 0.3.0 → 0.4.0
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 +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +17 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +4 -4
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +178 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +24 -0
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
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';
|
|
8
|
+
import { SectionLabel } from './Primitives';
|
|
9
|
+
import { apiFetch } from '@/lib/api';
|
|
10
|
+
|
|
11
|
+
/* ── Types ─────────────────────────────────────────────────────── */
|
|
12
|
+
|
|
13
|
+
interface McpStatus {
|
|
14
|
+
running: boolean;
|
|
15
|
+
transport: string;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
port: number;
|
|
18
|
+
toolCount: number;
|
|
19
|
+
authConfigured: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AgentInfo {
|
|
23
|
+
key: string;
|
|
24
|
+
name: string;
|
|
25
|
+
installed: boolean;
|
|
26
|
+
scope?: string;
|
|
27
|
+
transport?: string;
|
|
28
|
+
configPath?: string;
|
|
29
|
+
hasProjectScope: boolean;
|
|
30
|
+
hasGlobalScope: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SkillInfo {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
path: string;
|
|
37
|
+
source: 'builtin' | 'user';
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
editable: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface McpTabProps {
|
|
43
|
+
t: any;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
49
|
+
const [copied, setCopied] = useState(false);
|
|
50
|
+
const handleCopy = async () => {
|
|
51
|
+
try {
|
|
52
|
+
await navigator.clipboard.writeText(text);
|
|
53
|
+
setCopied(true);
|
|
54
|
+
setTimeout(() => setCopied(false), 2000);
|
|
55
|
+
} catch { /* clipboard unavailable */ }
|
|
56
|
+
};
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
onClick={handleCopy}
|
|
60
|
+
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"
|
|
61
|
+
>
|
|
62
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
63
|
+
{copied ? 'Copied!' : label}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── MCP Server Status ─────────────────────────────────────────── */
|
|
69
|
+
|
|
70
|
+
function ServerStatus({ status, t }: { status: McpStatus | null; t: any }) {
|
|
71
|
+
const m = t.settings?.mcp;
|
|
72
|
+
if (!status) return null;
|
|
73
|
+
|
|
74
|
+
const configSnippet = JSON.stringify({
|
|
75
|
+
mcpServers: {
|
|
76
|
+
mindos: status.running
|
|
77
|
+
? { url: status.endpoint }
|
|
78
|
+
: { type: 'stdio', command: 'mindos', args: ['mcp'] },
|
|
79
|
+
},
|
|
80
|
+
}, null, 2);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="space-y-3">
|
|
84
|
+
<div className="flex items-center gap-3">
|
|
85
|
+
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
|
86
|
+
<Plug size={16} className="text-muted-foreground" />
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<h3 className="text-sm font-medium text-foreground">{m?.serverTitle ?? 'MindOS MCP Server'}</h3>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="space-y-1.5 text-sm pl-11">
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.status ?? 'Status'}</span>
|
|
96
|
+
<span className={`text-xs flex items-center gap-1 ${status.running ? 'text-green-500' : 'text-muted-foreground'}`}>
|
|
97
|
+
<span className={`inline-block w-1.5 h-1.5 rounded-full ${status.running ? 'bg-green-500' : 'bg-muted-foreground'}`} />
|
|
98
|
+
{status.running ? (m?.running ?? 'Running') : (m?.stopped ?? 'Stopped')}
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.transport ?? 'Transport'}</span>
|
|
103
|
+
<span className="text-xs font-mono">{status.transport.toUpperCase()}</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex items-center gap-2">
|
|
106
|
+
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.endpoint ?? 'Endpoint'}</span>
|
|
107
|
+
<span className="text-xs font-mono truncate">{status.endpoint}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.tools ?? 'Tools'}</span>
|
|
111
|
+
<span className="text-xs">{m?.toolsRegistered ? m.toolsRegistered(status.toolCount) : `${status.toolCount} registered`}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<span className="text-muted-foreground w-20 shrink-0 text-xs">{m?.auth ?? 'Auth'}</span>
|
|
115
|
+
<span className="text-xs">
|
|
116
|
+
{status.authConfigured
|
|
117
|
+
? <span className="text-green-500">{m?.authSet ?? 'Token set'}</span>
|
|
118
|
+
: <span className="text-muted-foreground">{m?.authNotSet ?? 'No token'}</span>}
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="flex items-center gap-2 pl-11">
|
|
124
|
+
<CopyButton text={status.endpoint} label={m?.copyEndpoint ?? 'Copy Endpoint'} />
|
|
125
|
+
<CopyButton text={configSnippet} label={m?.copyConfig ?? 'Copy Config'} />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Agent Install ─────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; onRefresh: () => void }) {
|
|
134
|
+
const m = t.settings?.mcp;
|
|
135
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
136
|
+
const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
|
|
137
|
+
const [httpUrl, setHttpUrl] = useState('http://localhost:8787/mcp');
|
|
138
|
+
const [httpToken, setHttpToken] = useState('');
|
|
139
|
+
const [scopes, setScopes] = useState<Record<string, 'project' | 'global'>>({});
|
|
140
|
+
const [installing, setInstalling] = useState(false);
|
|
141
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
142
|
+
|
|
143
|
+
const toggle = (key: string) => {
|
|
144
|
+
setSelected(prev => {
|
|
145
|
+
const next = new Set(prev);
|
|
146
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
147
|
+
return next;
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleInstall = async () => {
|
|
152
|
+
if (selected.size === 0) return;
|
|
153
|
+
setInstalling(true);
|
|
154
|
+
setMessage(null);
|
|
155
|
+
try {
|
|
156
|
+
const payload = {
|
|
157
|
+
agents: [...selected].map(key => ({
|
|
158
|
+
key,
|
|
159
|
+
scope: scopes[key] || (agents.find(a => a.key === key)?.hasProjectScope ? 'project' : 'global'),
|
|
160
|
+
})),
|
|
161
|
+
transport,
|
|
162
|
+
...(transport === 'http' ? { url: httpUrl, token: httpToken } : {}),
|
|
163
|
+
};
|
|
164
|
+
const res = await apiFetch<{ results: Array<{ agent: string; status: string; message?: string }> }>('/api/mcp/install', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify(payload),
|
|
168
|
+
});
|
|
169
|
+
const ok = res.results.filter(r => r.status === 'ok').length;
|
|
170
|
+
const fail = res.results.filter(r => r.status === 'error');
|
|
171
|
+
if (fail.length > 0) {
|
|
172
|
+
setMessage({ type: 'error', text: fail.map(f => `${f.agent}: ${f.message}`).join('; ') });
|
|
173
|
+
} else {
|
|
174
|
+
setMessage({ type: 'success', text: m?.installSuccess ? m.installSuccess(ok) : `${ok} agent(s) configured` });
|
|
175
|
+
}
|
|
176
|
+
setSelected(new Set());
|
|
177
|
+
onRefresh();
|
|
178
|
+
} catch {
|
|
179
|
+
setMessage({ type: 'error', text: m?.installFailed ?? 'Install failed' });
|
|
180
|
+
} finally {
|
|
181
|
+
setInstalling(false);
|
|
182
|
+
setTimeout(() => setMessage(null), 4000);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="space-y-3">
|
|
188
|
+
<SectionLabel>{m?.agentsTitle ?? 'Agent Configuration'}</SectionLabel>
|
|
189
|
+
|
|
190
|
+
{/* Agent list */}
|
|
191
|
+
<div className="space-y-1">
|
|
192
|
+
{agents.map(agent => (
|
|
193
|
+
<div key={agent.key} className="flex items-center gap-3 py-1.5 text-sm">
|
|
194
|
+
<input
|
|
195
|
+
type="checkbox"
|
|
196
|
+
checked={selected.has(agent.key)}
|
|
197
|
+
onChange={() => toggle(agent.key)}
|
|
198
|
+
className="rounded border-border accent-amber-500"
|
|
199
|
+
/>
|
|
200
|
+
<span className="w-28 shrink-0 text-xs">{agent.name}</span>
|
|
201
|
+
{agent.installed ? (
|
|
202
|
+
<>
|
|
203
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-green-500/15 text-green-500 font-mono">
|
|
204
|
+
{agent.transport}
|
|
205
|
+
</span>
|
|
206
|
+
<span className="text-[10px] text-muted-foreground">{agent.scope}</span>
|
|
207
|
+
</>
|
|
208
|
+
) : (
|
|
209
|
+
<span className="text-[10px] text-muted-foreground">{m?.notInstalled ?? 'Not installed'}</span>
|
|
210
|
+
)}
|
|
211
|
+
{/* Scope selector */}
|
|
212
|
+
{selected.has(agent.key) && agent.hasProjectScope && agent.hasGlobalScope && (
|
|
213
|
+
<select
|
|
214
|
+
value={scopes[agent.key] || 'project'}
|
|
215
|
+
onChange={e => setScopes({ ...scopes, [agent.key]: e.target.value as 'project' | 'global' })}
|
|
216
|
+
className="ml-auto text-[10px] px-1.5 py-0.5 rounded border border-border bg-background text-foreground"
|
|
217
|
+
>
|
|
218
|
+
<option value="project">{m?.project ?? 'Project'}</option>
|
|
219
|
+
<option value="global">{m?.global ?? 'Global'}</option>
|
|
220
|
+
</select>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Transport selector */}
|
|
227
|
+
<div className="flex items-center gap-4 text-xs pt-1">
|
|
228
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
229
|
+
<input
|
|
230
|
+
type="radio"
|
|
231
|
+
name="transport"
|
|
232
|
+
checked={transport === 'stdio'}
|
|
233
|
+
onChange={() => setTransport('stdio')}
|
|
234
|
+
className="accent-amber-500"
|
|
235
|
+
/>
|
|
236
|
+
{m?.transportStdio ?? 'stdio (recommended)'}
|
|
237
|
+
</label>
|
|
238
|
+
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
239
|
+
<input
|
|
240
|
+
type="radio"
|
|
241
|
+
name="transport"
|
|
242
|
+
checked={transport === 'http'}
|
|
243
|
+
onChange={() => setTransport('http')}
|
|
244
|
+
className="accent-amber-500"
|
|
245
|
+
/>
|
|
246
|
+
{m?.transportHttp ?? 'http'}
|
|
247
|
+
</label>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* HTTP settings */}
|
|
251
|
+
{transport === 'http' && (
|
|
252
|
+
<div className="space-y-2 pl-5 text-xs">
|
|
253
|
+
<div className="space-y-1">
|
|
254
|
+
<label className="text-muted-foreground">{m?.httpUrl ?? 'MCP URL'}</label>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
value={httpUrl}
|
|
258
|
+
onChange={e => setHttpUrl(e.target.value)}
|
|
259
|
+
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:ring-1 focus:ring-ring"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="space-y-1">
|
|
263
|
+
<label className="text-muted-foreground">{m?.httpToken ?? 'Auth Token'}</label>
|
|
264
|
+
<input
|
|
265
|
+
type="password"
|
|
266
|
+
value={httpToken}
|
|
267
|
+
onChange={e => setHttpToken(e.target.value)}
|
|
268
|
+
placeholder="Bearer token"
|
|
269
|
+
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:ring-1 focus:ring-ring"
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Install button */}
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleInstall}
|
|
278
|
+
disabled={selected.size === 0 || installing}
|
|
279
|
+
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"
|
|
280
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
281
|
+
>
|
|
282
|
+
{installing && <Loader2 size={12} className="animate-spin" />}
|
|
283
|
+
{installing ? (m?.installing ?? 'Installing...') : (m?.installSelected ?? 'Install Selected')}
|
|
284
|
+
</button>
|
|
285
|
+
|
|
286
|
+
{/* Message */}
|
|
287
|
+
{message && (
|
|
288
|
+
<div className="flex items-center gap-1.5 text-xs" role="status">
|
|
289
|
+
{message.type === 'success' ? (
|
|
290
|
+
<><CheckCircle2 size={12} className="text-green-500" /><span className="text-green-500">{message.text}</span></>
|
|
291
|
+
) : (
|
|
292
|
+
<><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ── Skills Section ────────────────────────────────────────────── */
|
|
301
|
+
|
|
302
|
+
function SkillsSection({ t }: { t: any }) {
|
|
303
|
+
const m = t.settings?.mcp;
|
|
304
|
+
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
305
|
+
const [loading, setLoading] = useState(true);
|
|
306
|
+
const [expanded, setExpanded] = useState<string | null>(null);
|
|
307
|
+
const [adding, setAdding] = useState(false);
|
|
308
|
+
const [newName, setNewName] = useState('');
|
|
309
|
+
const [newDesc, setNewDesc] = useState('');
|
|
310
|
+
const [newContent, setNewContent] = useState('');
|
|
311
|
+
const [saving, setSaving] = useState(false);
|
|
312
|
+
const [error, setError] = useState('');
|
|
313
|
+
|
|
314
|
+
const fetchSkills = useCallback(async () => {
|
|
315
|
+
try {
|
|
316
|
+
const data = await apiFetch<{ skills: SkillInfo[] }>('/api/skills');
|
|
317
|
+
setSkills(data.skills);
|
|
318
|
+
} catch { /* ignore */ }
|
|
319
|
+
setLoading(false);
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
useEffect(() => { fetchSkills(); }, [fetchSkills]);
|
|
323
|
+
|
|
324
|
+
const handleToggle = async (name: string, enabled: boolean) => {
|
|
325
|
+
try {
|
|
326
|
+
await apiFetch('/api/skills', {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
body: JSON.stringify({ action: 'toggle', name, enabled }),
|
|
330
|
+
});
|
|
331
|
+
setSkills(prev => prev.map(s => s.name === name ? { ...s, enabled } : s));
|
|
332
|
+
} catch { /* ignore */ }
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const handleDelete = async (name: string) => {
|
|
336
|
+
const confirmMsg = m?.skillDeleteConfirm ? m.skillDeleteConfirm(name) : `Delete skill "${name}"?`;
|
|
337
|
+
if (!confirm(confirmMsg)) return;
|
|
338
|
+
try {
|
|
339
|
+
await apiFetch('/api/skills', {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
headers: { 'Content-Type': 'application/json' },
|
|
342
|
+
body: JSON.stringify({ action: 'delete', name }),
|
|
343
|
+
});
|
|
344
|
+
fetchSkills();
|
|
345
|
+
} catch { /* ignore */ }
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const handleCreate = async () => {
|
|
349
|
+
if (!newName.trim()) return;
|
|
350
|
+
setSaving(true);
|
|
351
|
+
setError('');
|
|
352
|
+
try {
|
|
353
|
+
await apiFetch('/api/skills', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({ action: 'create', name: newName.trim(), description: newDesc.trim(), content: newContent }),
|
|
357
|
+
});
|
|
358
|
+
setAdding(false);
|
|
359
|
+
setNewName('');
|
|
360
|
+
setNewDesc('');
|
|
361
|
+
setNewContent('');
|
|
362
|
+
fetchSkills();
|
|
363
|
+
} catch (err: unknown) {
|
|
364
|
+
setError(err instanceof Error ? err.message : 'Failed to create skill');
|
|
365
|
+
} finally {
|
|
366
|
+
setSaving(false);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (loading) {
|
|
371
|
+
return (
|
|
372
|
+
<div className="flex justify-center py-4">
|
|
373
|
+
<Loader2 size={16} className="animate-spin text-muted-foreground" />
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="space-y-3">
|
|
380
|
+
<SectionLabel>{m?.skillsTitle ?? 'Skills'}</SectionLabel>
|
|
381
|
+
|
|
382
|
+
{skills.map(skill => (
|
|
383
|
+
<div key={skill.name} className="border border-border rounded-lg overflow-hidden">
|
|
384
|
+
<div
|
|
385
|
+
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
386
|
+
onClick={() => setExpanded(expanded === skill.name ? null : skill.name)}
|
|
387
|
+
>
|
|
388
|
+
{expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
389
|
+
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
390
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
|
|
391
|
+
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
392
|
+
}`}>
|
|
393
|
+
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
394
|
+
</span>
|
|
395
|
+
{/* Toggle */}
|
|
396
|
+
<button
|
|
397
|
+
onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }}
|
|
398
|
+
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
|
|
399
|
+
skill.enabled ? 'bg-green-500' : 'bg-muted-foreground/30'
|
|
400
|
+
}`}
|
|
401
|
+
>
|
|
402
|
+
<span className={`inline-block h-3 w-3 rounded-full bg-white transition-transform ${
|
|
403
|
+
skill.enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
|
404
|
+
}`} />
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{expanded === skill.name && (
|
|
409
|
+
<div className="px-3 py-2 border-t border-border text-xs space-y-1.5 bg-muted/20">
|
|
410
|
+
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
411
|
+
<p className="text-muted-foreground font-mono text-[10px]">{skill.path}</p>
|
|
412
|
+
{skill.editable && (
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => handleDelete(skill.name)}
|
|
415
|
+
className="flex items-center gap-1 text-[10px] text-destructive hover:underline"
|
|
416
|
+
>
|
|
417
|
+
<Trash2 size={10} />
|
|
418
|
+
{m?.deleteSkill ?? 'Delete'}
|
|
419
|
+
</button>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
))}
|
|
425
|
+
|
|
426
|
+
{/* Add skill form */}
|
|
427
|
+
{adding ? (
|
|
428
|
+
<div className="border border-border rounded-lg p-3 space-y-2">
|
|
429
|
+
<div className="flex items-center justify-between">
|
|
430
|
+
<span className="text-xs font-medium">{m?.addSkill ?? '+ Add Skill'}</span>
|
|
431
|
+
<button onClick={() => setAdding(false)} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
|
|
432
|
+
<X size={12} />
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
<div className="space-y-1">
|
|
436
|
+
<label className="text-[10px] text-muted-foreground">{m?.skillName ?? 'Name'}</label>
|
|
437
|
+
<input
|
|
438
|
+
type="text"
|
|
439
|
+
value={newName}
|
|
440
|
+
onChange={e => setNewName(e.target.value.replace(/[^a-z0-9-]/g, ''))}
|
|
441
|
+
placeholder="my-skill"
|
|
442
|
+
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:ring-1 focus:ring-ring"
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
445
|
+
<div className="space-y-1">
|
|
446
|
+
<label className="text-[10px] text-muted-foreground">{m?.skillDesc ?? 'Description'}</label>
|
|
447
|
+
<input
|
|
448
|
+
type="text"
|
|
449
|
+
value={newDesc}
|
|
450
|
+
onChange={e => setNewDesc(e.target.value)}
|
|
451
|
+
placeholder="What does this skill do?"
|
|
452
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring"
|
|
453
|
+
/>
|
|
454
|
+
</div>
|
|
455
|
+
<div className="space-y-1">
|
|
456
|
+
<label className="text-[10px] text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
|
|
457
|
+
<textarea
|
|
458
|
+
value={newContent}
|
|
459
|
+
onChange={e => setNewContent(e.target.value)}
|
|
460
|
+
rows={6}
|
|
461
|
+
placeholder="Skill instructions (markdown)..."
|
|
462
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring resize-y font-mono"
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
{error && (
|
|
466
|
+
<p className="text-[10px] text-destructive flex items-center gap-1">
|
|
467
|
+
<AlertCircle size={10} />
|
|
468
|
+
{error}
|
|
469
|
+
</p>
|
|
470
|
+
)}
|
|
471
|
+
<div className="flex items-center gap-2">
|
|
472
|
+
<button
|
|
473
|
+
onClick={handleCreate}
|
|
474
|
+
disabled={!newName.trim() || saving}
|
|
475
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
476
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
477
|
+
>
|
|
478
|
+
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
479
|
+
{m?.saveSkill ?? 'Save'}
|
|
480
|
+
</button>
|
|
481
|
+
<button
|
|
482
|
+
onClick={() => setAdding(false)}
|
|
483
|
+
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
484
|
+
>
|
|
485
|
+
{m?.cancelSkill ?? 'Cancel'}
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
) : (
|
|
490
|
+
<button
|
|
491
|
+
onClick={() => setAdding(true)}
|
|
492
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
493
|
+
>
|
|
494
|
+
<Plus size={12} />
|
|
495
|
+
{m?.addSkill ?? '+ Add Skill'}
|
|
496
|
+
</button>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* ── Main McpTab ───────────────────────────────────────────────── */
|
|
503
|
+
|
|
504
|
+
export function McpTab({ t }: McpTabProps) {
|
|
505
|
+
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
506
|
+
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
507
|
+
const [loading, setLoading] = useState(true);
|
|
508
|
+
|
|
509
|
+
const fetchAll = useCallback(async () => {
|
|
510
|
+
try {
|
|
511
|
+
const [statusData, agentsData] = await Promise.all([
|
|
512
|
+
apiFetch<McpStatus>('/api/mcp/status'),
|
|
513
|
+
apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
|
|
514
|
+
]);
|
|
515
|
+
setMcpStatus(statusData);
|
|
516
|
+
setAgents(agentsData.agents);
|
|
517
|
+
} catch { /* ignore */ }
|
|
518
|
+
setLoading(false);
|
|
519
|
+
}, []);
|
|
520
|
+
|
|
521
|
+
useEffect(() => { fetchAll(); }, [fetchAll]);
|
|
522
|
+
|
|
523
|
+
if (loading) {
|
|
524
|
+
return (
|
|
525
|
+
<div className="flex justify-center py-8">
|
|
526
|
+
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
527
|
+
</div>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div className="space-y-6">
|
|
533
|
+
{/* MCP Server Status */}
|
|
534
|
+
<ServerStatus status={mcpStatus} t={t} />
|
|
535
|
+
|
|
536
|
+
{/* Divider */}
|
|
537
|
+
<div className="border-t border-border" />
|
|
538
|
+
|
|
539
|
+
{/* Agent Install */}
|
|
540
|
+
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
541
|
+
|
|
542
|
+
{/* Divider */}
|
|
543
|
+
<div className="border-t border-border" />
|
|
544
|
+
|
|
545
|
+
{/* Skills */}
|
|
546
|
+
<SkillsSection t={t} />
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
}
|