@geminilight/mindos 0.5.29 → 0.5.30
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/components/HomeContent.tsx +20 -97
- package/app/components/SidebarLayout.tsx +3 -0
- package/app/components/panels/AgentsPanel.tsx +238 -92
- package/app/components/panels/PluginsPanel.tsx +79 -22
- package/app/components/settings/McpSkillsSection.tsx +12 -0
- package/app/components/settings/McpTab.tsx +28 -30
- package/app/hooks/useMcpData.tsx +166 -0
- package/app/lib/i18n-en.ts +18 -0
- package/app/lib/i18n-zh.ts +18 -0
- package/app/lib/mcp-snippets.ts +103 -0
- package/package.json +1 -1
- package/app/components/settings/McpServerStatus.tsx +0 -274
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { FileText, Table, Clock, Sparkles,
|
|
5
|
-
import { useState, useEffect
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass } from 'lucide-react';
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
8
|
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
@@ -27,8 +27,6 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
27
27
|
const { t } = useLocale();
|
|
28
28
|
const [showAll, setShowAll] = useState(false);
|
|
29
29
|
const [suggestionIdx, setSuggestionIdx] = useState(0);
|
|
30
|
-
const [hintId, setHintId] = useState<string | null>(null);
|
|
31
|
-
const hintTimer = useRef<ReturnType<typeof setTimeout>>(null);
|
|
32
30
|
|
|
33
31
|
const suggestions = t.ask?.suggestions ?? [
|
|
34
32
|
'Summarize this document',
|
|
@@ -44,15 +42,6 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
44
42
|
return () => clearInterval(interval);
|
|
45
43
|
}, [suggestions.length]);
|
|
46
44
|
|
|
47
|
-
// Cleanup hint timer on unmount
|
|
48
|
-
useEffect(() => () => { if (hintTimer.current) clearTimeout(hintTimer.current); }, []);
|
|
49
|
-
|
|
50
|
-
function showHint(id: string) {
|
|
51
|
-
if (hintTimer.current) clearTimeout(hintTimer.current);
|
|
52
|
-
setHintId(id);
|
|
53
|
-
hintTimer.current = setTimeout(() => setHintId(null), 3000);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
45
|
const existingSet = new Set(existingFiles ?? []);
|
|
57
46
|
|
|
58
47
|
// Empty knowledge base → show onboarding
|
|
@@ -62,9 +51,9 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
62
51
|
|
|
63
52
|
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
64
53
|
|
|
65
|
-
// Only show renderers
|
|
66
|
-
// Opt-in renderers (like Graph) have no entryPath and are toggled from the view toolbar.
|
|
54
|
+
// Only show renderers that are available (have entryPath + file exists) as quick-access chips
|
|
67
55
|
const renderers = getAllRenderers().filter(r => r.entryPath);
|
|
56
|
+
const availablePlugins = renderers.filter(r => r.entryPath && existingSet.has(r.entryPath));
|
|
68
57
|
|
|
69
58
|
const lastFile = recent[0];
|
|
70
59
|
|
|
@@ -166,89 +155,23 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
166
155
|
</Link>
|
|
167
156
|
</div>
|
|
168
157
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 items-start">
|
|
185
|
-
{renderers.map((r) => {
|
|
186
|
-
const entryPath = r.entryPath ?? null;
|
|
187
|
-
const available = !entryPath || existingSet.has(entryPath);
|
|
188
|
-
|
|
189
|
-
if (!available) {
|
|
190
|
-
return (
|
|
191
|
-
<button
|
|
192
|
-
key={r.id}
|
|
193
|
-
onClick={() => showHint(r.id)}
|
|
194
|
-
className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all opacity-60 cursor-pointer hover:opacity-80 text-left"
|
|
195
|
-
style={{ borderColor: 'var(--border)' }}
|
|
196
|
-
>
|
|
197
|
-
<div className="flex items-center gap-2.5">
|
|
198
|
-
<span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
199
|
-
<span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
|
|
200
|
-
{r.name}
|
|
201
|
-
</span>
|
|
202
|
-
</div>
|
|
203
|
-
<p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
|
|
204
|
-
{r.description}
|
|
205
|
-
</p>
|
|
206
|
-
{hintId === r.id ? (
|
|
207
|
-
<p className="text-2xs animate-in" style={{ color: 'var(--amber)' }} role="status">
|
|
208
|
-
{(t.home.createToActivate ?? 'Create {file} to activate').replace('{file}', entryPath ?? '')}
|
|
209
|
-
</p>
|
|
210
|
-
) : (
|
|
211
|
-
<div className="flex flex-wrap gap-1">
|
|
212
|
-
{r.tags.slice(0, 3).map(tag => (
|
|
213
|
-
<span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
214
|
-
{tag}
|
|
215
|
-
</span>
|
|
216
|
-
))}
|
|
217
|
-
</div>
|
|
218
|
-
)}
|
|
219
|
-
</button>
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return (
|
|
224
|
-
<Link
|
|
225
|
-
key={r.id}
|
|
226
|
-
href={entryPath ? `/view/${encodePath(entryPath)}` : '#'}
|
|
227
|
-
className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
|
|
228
|
-
style={{ borderColor: 'var(--border)' }}
|
|
229
|
-
>
|
|
230
|
-
<div className="flex items-center gap-2.5">
|
|
231
|
-
<span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
232
|
-
<span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
|
|
233
|
-
{r.name}
|
|
234
|
-
</span>
|
|
235
|
-
</div>
|
|
236
|
-
<p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
|
|
237
|
-
{r.description}
|
|
238
|
-
</p>
|
|
239
|
-
<div className="flex flex-wrap gap-1">
|
|
240
|
-
{r.tags.slice(0, 3).map(tag => (
|
|
241
|
-
<span key={tag} className="text-2xs px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
242
|
-
{tag}
|
|
243
|
-
</span>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
</Link>
|
|
247
|
-
);
|
|
248
|
-
})}
|
|
158
|
+
{/* Plugin quick-access chips — only show available plugins */}
|
|
159
|
+
{availablePlugins.length > 0 && (
|
|
160
|
+
<div className="flex flex-wrap gap-1.5 mt-3" style={{ paddingLeft: '1rem' }}>
|
|
161
|
+
{availablePlugins.map(r => (
|
|
162
|
+
<Link
|
|
163
|
+
key={r.id}
|
|
164
|
+
href={`/view/${encodePath(r.entryPath!)}`}
|
|
165
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 hover:bg-muted/60"
|
|
166
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
167
|
+
>
|
|
168
|
+
<span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
|
|
169
|
+
<span>{r.name}</span>
|
|
170
|
+
</Link>
|
|
171
|
+
))}
|
|
249
172
|
</div>
|
|
250
|
-
|
|
251
|
-
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
252
175
|
|
|
253
176
|
{/* Recently modified — timeline feed */}
|
|
254
177
|
{recent.length > 0 && (() => {
|
|
@@ -23,6 +23,7 @@ import { useAskModal } from '@/hooks/useAskModal';
|
|
|
23
23
|
import { FileNode } from '@/lib/types';
|
|
24
24
|
import { useLocale } from '@/lib/LocaleContext';
|
|
25
25
|
import { WalkthroughProvider } from './walkthrough';
|
|
26
|
+
import McpProvider from '@/hooks/useMcpData';
|
|
26
27
|
import type { Tab } from './settings/types';
|
|
27
28
|
|
|
28
29
|
interface SidebarLayoutProps {
|
|
@@ -304,6 +305,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
304
305
|
|
|
305
306
|
return (
|
|
306
307
|
<WalkthroughProvider>
|
|
308
|
+
<McpProvider>
|
|
307
309
|
<>
|
|
308
310
|
{/* Skip link */}
|
|
309
311
|
<a
|
|
@@ -471,6 +473,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
471
473
|
}
|
|
472
474
|
`}</style>
|
|
473
475
|
</>
|
|
476
|
+
</McpProvider>
|
|
474
477
|
</WalkthroughProvider>
|
|
475
478
|
);
|
|
476
479
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
4
|
-
import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
5
|
-
import {
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
+
import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe, Settings } from 'lucide-react';
|
|
5
|
+
import { useMcpData } from '@/hooks/useMcpData';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
-
import
|
|
7
|
+
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
8
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
9
|
+
import { Toggle } from '../settings/Primitives';
|
|
10
|
+
import type { AgentInfo, McpStatus, SkillInfo } from '../settings/types';
|
|
8
11
|
import PanelHeader from './PanelHeader';
|
|
9
12
|
|
|
10
13
|
interface AgentsPanelProps {
|
|
@@ -16,54 +19,39 @@ interface AgentsPanelProps {
|
|
|
16
19
|
export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPanelProps) {
|
|
17
20
|
const { t } = useLocale();
|
|
18
21
|
const p = t.panels.agents;
|
|
19
|
-
const
|
|
20
|
-
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
21
|
-
const [loading, setLoading] = useState(true);
|
|
22
|
-
const [error, setError] = useState(false);
|
|
22
|
+
const mcp = useMcpData();
|
|
23
23
|
const [refreshing, setRefreshing] = useState(false);
|
|
24
24
|
const [showNotDetected, setShowNotDetected] = useState(false);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
apiFetch<McpStatus>('/api/mcp/status'),
|
|
32
|
-
apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
|
|
33
|
-
]);
|
|
34
|
-
setMcpStatus(statusData);
|
|
35
|
-
setAgents(agentsData.agents);
|
|
36
|
-
setError(false);
|
|
37
|
-
} catch {
|
|
38
|
-
if (!silent) setError(true);
|
|
39
|
-
}
|
|
40
|
-
setLoading(false);
|
|
25
|
+
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
|
26
|
+
const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
|
|
27
|
+
|
|
28
|
+
const handleRefresh = async () => {
|
|
29
|
+
setRefreshing(true);
|
|
30
|
+
await mcp.refresh();
|
|
41
31
|
setRefreshing(false);
|
|
42
|
-
}
|
|
32
|
+
};
|
|
43
33
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
prevActive.current = active;
|
|
48
|
-
}, [active, fetchAll]);
|
|
34
|
+
const toggleAgent = (key: string) => {
|
|
35
|
+
setExpandedAgent(prev => prev === key ? null : key);
|
|
36
|
+
};
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return () => clearInterval(intervalRef.current);
|
|
54
|
-
}, [active, fetchAll]);
|
|
38
|
+
const connected = mcp.agents.filter(a => a.present && a.installed);
|
|
39
|
+
const detected = mcp.agents.filter(a => a.present && !a.installed);
|
|
40
|
+
const notFound = mcp.agents.filter(a => !a.present);
|
|
55
41
|
|
|
56
|
-
const
|
|
42
|
+
const customSkills = mcp.skills.filter(s => s.source === 'user');
|
|
43
|
+
const builtinSkills = mcp.skills.filter(s => s.source === 'builtin');
|
|
44
|
+
const activeSkillCount = mcp.skills.filter(s => s.enabled).length;
|
|
57
45
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
const openAdvancedConfig = () => {
|
|
47
|
+
window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
|
|
48
|
+
};
|
|
61
49
|
|
|
62
50
|
return (
|
|
63
51
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
64
52
|
<PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
|
|
65
53
|
<div className="flex items-center gap-1.5">
|
|
66
|
-
{!loading && (
|
|
54
|
+
{!mcp.loading && (
|
|
67
55
|
<span className="text-2xs text-muted-foreground">{connected.length} {p.connected}</span>
|
|
68
56
|
)}
|
|
69
57
|
<button onClick={handleRefresh} disabled={refreshing}
|
|
@@ -75,11 +63,11 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
75
63
|
</PanelHeader>
|
|
76
64
|
|
|
77
65
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
78
|
-
{loading ? (
|
|
66
|
+
{mcp.loading ? (
|
|
79
67
|
<div className="flex justify-center py-8"><Loader2 size={16} className="animate-spin text-muted-foreground" /></div>
|
|
80
|
-
) :
|
|
68
|
+
) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
|
|
81
69
|
<div className="flex flex-col items-center gap-2 py-8 text-center px-4">
|
|
82
|
-
<p className="text-xs text-
|
|
70
|
+
<p className="text-xs text-muted-foreground">{p.noAgents}</p>
|
|
83
71
|
<button onClick={handleRefresh}
|
|
84
72
|
className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
85
73
|
<RefreshCw size={11} /> {p.retry}
|
|
@@ -87,12 +75,14 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
87
75
|
</div>
|
|
88
76
|
) : (
|
|
89
77
|
<div className="px-3 py-3 space-y-4">
|
|
78
|
+
{/* MCP Server status — single line */}
|
|
90
79
|
<div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
|
|
91
80
|
<span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
|
|
92
|
-
{
|
|
81
|
+
{mcp.status?.running ? (
|
|
93
82
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
94
83
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
95
|
-
<span className="text-emerald-600 dark:text-emerald-400">:{
|
|
84
|
+
<span className="text-emerald-600 dark:text-emerald-400">:{mcp.status.port}</span>
|
|
85
|
+
<span className="text-muted-foreground">· {mcp.status.toolCount} tools</span>
|
|
96
86
|
</span>
|
|
97
87
|
) : (
|
|
98
88
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
@@ -102,26 +92,49 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
102
92
|
)}
|
|
103
93
|
</div>
|
|
104
94
|
|
|
95
|
+
{/* Connected Agents */}
|
|
105
96
|
{connected.length > 0 && (
|
|
106
97
|
<section>
|
|
107
98
|
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
|
|
108
99
|
<div className="space-y-1.5">
|
|
109
|
-
{connected.map(agent => (
|
|
100
|
+
{connected.map(agent => (
|
|
101
|
+
<AgentCard
|
|
102
|
+
key={agent.key}
|
|
103
|
+
agent={agent}
|
|
104
|
+
agentStatus="connected"
|
|
105
|
+
mcpStatus={mcp.status}
|
|
106
|
+
expanded={expandedAgent === agent.key}
|
|
107
|
+
onToggle={() => toggleAgent(agent.key)}
|
|
108
|
+
onInstallAgent={mcp.installAgent}
|
|
109
|
+
t={p}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
110
112
|
</div>
|
|
111
113
|
</section>
|
|
112
114
|
)}
|
|
113
115
|
|
|
116
|
+
{/* Detected Agents */}
|
|
114
117
|
{detected.length > 0 && (
|
|
115
118
|
<section>
|
|
116
119
|
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
|
|
117
120
|
<div className="space-y-1.5">
|
|
118
121
|
{detected.map(agent => (
|
|
119
|
-
<AgentCard
|
|
122
|
+
<AgentCard
|
|
123
|
+
key={agent.key}
|
|
124
|
+
agent={agent}
|
|
125
|
+
agentStatus="detected"
|
|
126
|
+
mcpStatus={mcp.status}
|
|
127
|
+
expanded={expandedAgent === agent.key}
|
|
128
|
+
onToggle={() => toggleAgent(agent.key)}
|
|
129
|
+
onInstallAgent={mcp.installAgent}
|
|
130
|
+
t={p}
|
|
131
|
+
/>
|
|
120
132
|
))}
|
|
121
133
|
</div>
|
|
122
134
|
</section>
|
|
123
135
|
)}
|
|
124
136
|
|
|
137
|
+
{/* Not Found Agents (collapsed) */}
|
|
125
138
|
{notFound.length > 0 && (
|
|
126
139
|
<section>
|
|
127
140
|
<button onClick={() => setShowNotDetected(!showNotDetected)}
|
|
@@ -131,90 +144,223 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
131
144
|
</button>
|
|
132
145
|
{showNotDetected && (
|
|
133
146
|
<div className="space-y-1.5">
|
|
134
|
-
{notFound.map(agent => (
|
|
147
|
+
{notFound.map(agent => (
|
|
148
|
+
<AgentCard
|
|
149
|
+
key={agent.key}
|
|
150
|
+
agent={agent}
|
|
151
|
+
agentStatus="notFound"
|
|
152
|
+
mcpStatus={mcp.status}
|
|
153
|
+
expanded={expandedAgent === agent.key}
|
|
154
|
+
onToggle={() => toggleAgent(agent.key)}
|
|
155
|
+
onInstallAgent={mcp.installAgent}
|
|
156
|
+
t={p}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
135
159
|
</div>
|
|
136
160
|
)}
|
|
137
161
|
</section>
|
|
138
162
|
)}
|
|
139
163
|
|
|
140
|
-
{
|
|
141
|
-
|
|
164
|
+
{/* ── Skills Section ── */}
|
|
165
|
+
{mcp.skills.length > 0 && (
|
|
166
|
+
<section>
|
|
167
|
+
<div className="flex items-center justify-between mb-2">
|
|
168
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
169
|
+
{p.skillsTitle} <span className="normal-case font-normal">{activeSkillCount} {p.skillsActive}</span>
|
|
170
|
+
</h3>
|
|
171
|
+
<button
|
|
172
|
+
onClick={openAdvancedConfig}
|
|
173
|
+
className="text-2xs text-muted-foreground hover:text-foreground transition-colors"
|
|
174
|
+
>
|
|
175
|
+
{p.newSkill}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Custom skills */}
|
|
180
|
+
{customSkills.length > 0 && (
|
|
181
|
+
<div className="space-y-0.5 mb-2">
|
|
182
|
+
{customSkills.map(skill => (
|
|
183
|
+
<SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Built-in skills (collapsed) */}
|
|
189
|
+
{builtinSkills.length > 0 && (
|
|
190
|
+
<>
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => setShowBuiltinSkills(!showBuiltinSkills)}
|
|
193
|
+
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors mb-1"
|
|
194
|
+
>
|
|
195
|
+
{showBuiltinSkills ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
196
|
+
{p.builtinSkills} ({builtinSkills.length})
|
|
197
|
+
</button>
|
|
198
|
+
{showBuiltinSkills && (
|
|
199
|
+
<div className="space-y-0.5">
|
|
200
|
+
{builtinSkills.map(skill => (
|
|
201
|
+
<SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
</section>
|
|
142
208
|
)}
|
|
143
209
|
</div>
|
|
144
210
|
)}
|
|
145
211
|
</div>
|
|
146
212
|
|
|
213
|
+
{/* Footer: Advanced Config link */}
|
|
147
214
|
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
148
|
-
<
|
|
215
|
+
<button
|
|
216
|
+
onClick={openAdvancedConfig}
|
|
217
|
+
className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
218
|
+
>
|
|
219
|
+
<Settings size={11} />
|
|
220
|
+
{p.advancedConfig}
|
|
221
|
+
</button>
|
|
149
222
|
</div>
|
|
150
223
|
</div>
|
|
151
224
|
);
|
|
152
225
|
}
|
|
153
226
|
|
|
227
|
+
/* ── Skill Row ── */
|
|
228
|
+
|
|
229
|
+
function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: string, enabled: boolean) => void }) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md hover:bg-muted/30 transition-colors">
|
|
232
|
+
<span className="text-xs text-foreground truncate">{skill.name}</span>
|
|
233
|
+
<Toggle
|
|
234
|
+
size="sm"
|
|
235
|
+
checked={skill.enabled}
|
|
236
|
+
onChange={(v) => onToggle(skill.name, v)}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
154
242
|
/* ── Agent Card ── */
|
|
155
243
|
|
|
156
|
-
function AgentCard({ agent,
|
|
244
|
+
function AgentCard({ agent, agentStatus, mcpStatus, expanded, onToggle, onInstallAgent, t }: {
|
|
157
245
|
agent: AgentInfo;
|
|
158
|
-
|
|
159
|
-
|
|
246
|
+
agentStatus: 'connected' | 'detected' | 'notFound';
|
|
247
|
+
mcpStatus: McpStatus | null;
|
|
248
|
+
expanded: boolean;
|
|
249
|
+
onToggle: () => void;
|
|
250
|
+
onInstallAgent: (key: string, opts?: { scope?: string; transport?: string }) => Promise<boolean>;
|
|
160
251
|
t: Record<string, any>;
|
|
161
252
|
}) {
|
|
162
|
-
const [
|
|
253
|
+
const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
|
|
254
|
+
const [copied, setCopied] = useState(false);
|
|
163
255
|
const [installing, setInstalling] = useState(false);
|
|
164
256
|
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
165
257
|
|
|
166
|
-
const dot =
|
|
258
|
+
const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
|
|
259
|
+
|
|
260
|
+
const snippet = useMemo(() => generateSnippet(agent, mcpStatus, transport), [agent, mcpStatus, transport]);
|
|
261
|
+
|
|
262
|
+
const handleCopy = useCallback(async () => {
|
|
263
|
+
const ok = await copyToClipboard(snippet.snippet);
|
|
264
|
+
if (ok) {
|
|
265
|
+
setCopied(true);
|
|
266
|
+
setTimeout(() => setCopied(false), 2000);
|
|
267
|
+
}
|
|
268
|
+
}, [snippet.snippet]);
|
|
167
269
|
|
|
168
270
|
const handleInstall = async () => {
|
|
169
|
-
setInstalling(true);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
else { setResult({ type: 'error', text: r?.error ?? 'Install failed' }); }
|
|
178
|
-
} catch { setResult({ type: 'error', text: 'Network error' }); }
|
|
271
|
+
setInstalling(true);
|
|
272
|
+
setResult(null);
|
|
273
|
+
const ok = await onInstallAgent(agent.key);
|
|
274
|
+
if (ok) {
|
|
275
|
+
setResult({ type: 'success', text: `${agent.name} ${t.connected}` });
|
|
276
|
+
} else {
|
|
277
|
+
setResult({ type: 'error', text: 'Install failed' });
|
|
278
|
+
}
|
|
179
279
|
setInstalling(false);
|
|
180
280
|
};
|
|
181
281
|
|
|
182
282
|
return (
|
|
183
283
|
<div className="rounded-lg border border-border/60 bg-card/30 overflow-hidden">
|
|
184
|
-
|
|
284
|
+
{/* Header row — always clickable to expand */}
|
|
285
|
+
<button
|
|
286
|
+
onClick={onToggle}
|
|
287
|
+
className="w-full px-3 py-2 flex items-center justify-between gap-2 hover:bg-muted/30 transition-colors text-left"
|
|
288
|
+
>
|
|
185
289
|
<div className="flex items-center gap-2 min-w-0">
|
|
186
290
|
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
|
|
187
291
|
<span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
|
|
188
|
-
{
|
|
292
|
+
{agentStatus === 'connected' && agent.transport && (
|
|
189
293
|
<span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
|
|
190
294
|
)}
|
|
191
295
|
</div>
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
296
|
+
{expanded ? <ChevronDown size={10} className="text-muted-foreground shrink-0" /> : <ChevronRight size={10} className="text-muted-foreground shrink-0" />}
|
|
297
|
+
</button>
|
|
298
|
+
|
|
299
|
+
{/* Expanded: snippet + actions */}
|
|
300
|
+
{expanded && (
|
|
301
|
+
<div className="px-3 pb-3 pt-1 border-t border-border/40 space-y-2.5">
|
|
302
|
+
{/* Detected: Connect button */}
|
|
303
|
+
{agentStatus === 'detected' && (
|
|
304
|
+
<>
|
|
305
|
+
<button onClick={handleInstall} disabled={installing}
|
|
306
|
+
className="w-full flex items-center justify-center gap-1.5 px-2.5 py-1.5 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors"
|
|
307
|
+
style={{ background: 'var(--amber)' }}>
|
|
308
|
+
{installing ? <Loader2 size={11} className="animate-spin" /> : null}
|
|
309
|
+
{installing ? t.installing : t.install(agent.name)}
|
|
310
|
+
</button>
|
|
311
|
+
{result && (
|
|
312
|
+
<div className={`flex items-center gap-1.5 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
|
|
313
|
+
{result.type === 'success' ? <CheckCircle2 size={11} /> : <AlertCircle size={11} />}
|
|
314
|
+
{result.text}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{/* Transport toggle */}
|
|
321
|
+
<div className="flex items-center rounded-md border border-border overflow-hidden w-fit">
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => setTransport('stdio')}
|
|
324
|
+
className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
|
|
325
|
+
transport === 'stdio' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
326
|
+
}`}
|
|
327
|
+
>
|
|
328
|
+
<Monitor size={10} />
|
|
329
|
+
{t.transportLocal}
|
|
330
|
+
</button>
|
|
331
|
+
<button
|
|
332
|
+
onClick={() => setTransport('http')}
|
|
333
|
+
className={`flex items-center gap-1 px-2 py-1 text-2xs transition-colors ${
|
|
334
|
+
transport === 'http' ? 'bg-muted text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
|
335
|
+
}`}
|
|
336
|
+
>
|
|
337
|
+
<Globe size={10} />
|
|
338
|
+
{t.transportRemote}
|
|
339
|
+
</button>
|
|
205
340
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{installing ? t.installing : t.install(agent.name)}
|
|
211
|
-
</button>
|
|
212
|
-
{result && (
|
|
213
|
-
<div className={`flex items-center gap-1.5 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
|
|
214
|
-
{result.type === 'success' ? <CheckCircle2 size={11} /> : <AlertCircle size={11} />}
|
|
215
|
-
{result.text}
|
|
216
|
-
</div>
|
|
341
|
+
|
|
342
|
+
{/* No auth warning for HTTP */}
|
|
343
|
+
{transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
|
|
344
|
+
<p className="text-2xs" style={{ color: 'var(--amber)' }}>{t.noAuthWarning}</p>
|
|
217
345
|
)}
|
|
346
|
+
|
|
347
|
+
{/* Config snippet */}
|
|
348
|
+
<pre className="text-[10px] font-mono bg-muted/50 border border-border rounded-lg p-2.5 overflow-x-auto whitespace-pre select-all max-h-[200px] overflow-y-auto">
|
|
349
|
+
{snippet.displaySnippet}
|
|
350
|
+
</pre>
|
|
351
|
+
|
|
352
|
+
{/* Copy + path */}
|
|
353
|
+
<div className="flex items-center gap-2 text-2xs">
|
|
354
|
+
<button
|
|
355
|
+
onClick={handleCopy}
|
|
356
|
+
className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
357
|
+
>
|
|
358
|
+
{copied ? <Check size={10} /> : <Copy size={10} />}
|
|
359
|
+
{copied ? t.copied : t.copyConfig}
|
|
360
|
+
</button>
|
|
361
|
+
<span className="text-muted-foreground">→</span>
|
|
362
|
+
<span className="font-mono text-muted-foreground truncate">{snippet.path}</span>
|
|
363
|
+
</div>
|
|
218
364
|
</div>
|
|
219
365
|
)}
|
|
220
366
|
</div>
|