@geminilight/mindos 0.5.28 → 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/app/api/update/route.ts +41 -0
- package/app/app/explore/page.tsx +12 -0
- package/app/components/ActivityBar.tsx +14 -7
- package/app/components/GuideCard.tsx +21 -7
- package/app/components/HomeContent.tsx +31 -97
- package/app/components/KeyboardShortcuts.tsx +102 -0
- package/app/components/Panel.tsx +12 -7
- package/app/components/SidebarLayout.tsx +21 -1
- package/app/components/UpdateBanner.tsx +19 -21
- package/app/components/explore/ExploreContent.tsx +100 -0
- package/app/components/explore/UseCaseCard.tsx +50 -0
- package/app/components/explore/use-cases.ts +30 -0
- package/app/components/panels/AgentsPanel.tsx +268 -131
- package/app/components/panels/PluginsPanel.tsx +87 -27
- package/app/components/settings/AiTab.tsx +5 -3
- package/app/components/settings/McpSkillsSection.tsx +12 -0
- package/app/components/settings/McpTab.tsx +28 -30
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UpdateTab.tsx +195 -0
- package/app/components/settings/types.ts +1 -1
- package/app/components/walkthrough/WalkthroughOverlay.tsx +224 -0
- package/app/components/walkthrough/WalkthroughProvider.tsx +133 -0
- package/app/components/walkthrough/WalkthroughTooltip.tsx +129 -0
- package/app/components/walkthrough/index.ts +3 -0
- package/app/components/walkthrough/steps.ts +21 -0
- package/app/hooks/useMcpData.tsx +166 -0
- package/app/lib/i18n-en.ts +182 -5
- package/app/lib/i18n-zh.ts +181 -4
- package/app/lib/mcp-snippets.ts +103 -0
- package/app/lib/settings.ts +4 -0
- package/app/next-env.d.ts +1 -1
- package/app/package.json +1 -0
- package/package.json +1 -1
- package/app/components/settings/McpServerStatus.tsx +0 -274
|
@@ -1,230 +1,367 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
4
|
-
import { Loader2, RefreshCw, ChevronDown, ChevronRight,
|
|
5
|
-
import {
|
|
6
|
-
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
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
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';
|
|
7
11
|
import PanelHeader from './PanelHeader';
|
|
8
12
|
|
|
9
13
|
interface AgentsPanelProps {
|
|
10
14
|
active: boolean;
|
|
11
15
|
maximized?: boolean;
|
|
12
16
|
onMaximize?: () => void;
|
|
13
|
-
/** Opens Settings Modal on a specific tab */
|
|
14
|
-
onOpenSettings?: (tab: 'mcp') => void;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export default function AgentsPanel({ active, maximized, onMaximize
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const [error, setError] = useState(false);
|
|
19
|
+
export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPanelProps) {
|
|
20
|
+
const { t } = useLocale();
|
|
21
|
+
const p = t.panels.agents;
|
|
22
|
+
const mcp = useMcpData();
|
|
22
23
|
const [refreshing, setRefreshing] = useState(false);
|
|
23
24
|
const [showNotDetected, setShowNotDetected] = useState(false);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
apiFetch<McpStatus>('/api/mcp/status'),
|
|
31
|
-
apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
|
|
32
|
-
]);
|
|
33
|
-
setMcpStatus(statusData);
|
|
34
|
-
setAgents(agentsData.agents);
|
|
35
|
-
setError(false);
|
|
36
|
-
} catch {
|
|
37
|
-
if (!silent) setError(true);
|
|
38
|
-
}
|
|
39
|
-
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();
|
|
40
31
|
setRefreshing(false);
|
|
41
|
-
}
|
|
32
|
+
};
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (active && !prevActive.current) {
|
|
47
|
-
fetchAll();
|
|
48
|
-
}
|
|
49
|
-
prevActive.current = active;
|
|
50
|
-
}, [active, fetchAll]);
|
|
34
|
+
const toggleAgent = (key: string) => {
|
|
35
|
+
setExpandedAgent(prev => prev === key ? null : key);
|
|
36
|
+
};
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
intervalRef.current = setInterval(() => fetchAll(true), 30_000);
|
|
58
|
-
return () => clearInterval(intervalRef.current);
|
|
59
|
-
}, [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);
|
|
60
41
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
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;
|
|
65
45
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const notFound = agents.filter(a => !a.present);
|
|
70
|
-
const connectedCount = connected.length;
|
|
46
|
+
const openAdvancedConfig = () => {
|
|
47
|
+
window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
|
|
48
|
+
};
|
|
71
49
|
|
|
72
50
|
return (
|
|
73
51
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
74
|
-
<PanelHeader title=
|
|
52
|
+
<PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
|
|
75
53
|
<div className="flex items-center gap-1.5">
|
|
76
|
-
{!loading && (
|
|
77
|
-
<span className="text-2xs text-muted-foreground">{
|
|
54
|
+
{!mcp.loading && (
|
|
55
|
+
<span className="text-2xs text-muted-foreground">{connected.length} {p.connected}</span>
|
|
78
56
|
)}
|
|
79
|
-
<button
|
|
80
|
-
onClick={handleRefresh}
|
|
81
|
-
disabled={refreshing}
|
|
57
|
+
<button onClick={handleRefresh} disabled={refreshing}
|
|
82
58
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors"
|
|
83
|
-
aria-label=
|
|
84
|
-
title="Refresh agent status"
|
|
85
|
-
>
|
|
59
|
+
aria-label={p.refresh} title={p.refresh}>
|
|
86
60
|
<RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
|
|
87
61
|
</button>
|
|
88
62
|
</div>
|
|
89
63
|
</PanelHeader>
|
|
90
64
|
|
|
91
65
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
92
|
-
{loading ? (
|
|
93
|
-
<div className="flex justify-center py-8">
|
|
94
|
-
|
|
95
|
-
</div>
|
|
96
|
-
) : error && agents.length === 0 ? (
|
|
66
|
+
{mcp.loading ? (
|
|
67
|
+
<div className="flex justify-center py-8"><Loader2 size={16} className="animate-spin text-muted-foreground" /></div>
|
|
68
|
+
) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
|
|
97
69
|
<div className="flex flex-col items-center gap-2 py-8 text-center px-4">
|
|
98
|
-
<p className="text-xs text-
|
|
99
|
-
<button
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
>
|
|
103
|
-
<RefreshCw size={11} /> Retry
|
|
70
|
+
<p className="text-xs text-muted-foreground">{p.noAgents}</p>
|
|
71
|
+
<button onClick={handleRefresh}
|
|
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">
|
|
73
|
+
<RefreshCw size={11} /> {p.retry}
|
|
104
74
|
</button>
|
|
105
75
|
</div>
|
|
106
76
|
) : (
|
|
107
77
|
<div className="px-3 py-3 space-y-4">
|
|
108
|
-
{/* MCP Server
|
|
78
|
+
{/* MCP Server status — single line */}
|
|
109
79
|
<div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
|
|
110
|
-
<span className="text-xs font-medium text-foreground">
|
|
111
|
-
{
|
|
80
|
+
<span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
|
|
81
|
+
{mcp.status?.running ? (
|
|
112
82
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
113
83
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
114
|
-
<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>
|
|
115
86
|
</span>
|
|
116
87
|
) : (
|
|
117
88
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
118
89
|
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
|
|
119
|
-
<span className="text-muted-foreground">
|
|
90
|
+
<span className="text-muted-foreground">{p.stopped}</span>
|
|
120
91
|
</span>
|
|
121
92
|
)}
|
|
122
93
|
</div>
|
|
123
94
|
|
|
124
|
-
{/* Connected */}
|
|
95
|
+
{/* Connected Agents */}
|
|
125
96
|
{connected.length > 0 && (
|
|
126
97
|
<section>
|
|
127
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
128
|
-
Connected ({connected.length})
|
|
129
|
-
</h3>
|
|
98
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
|
|
130
99
|
<div className="space-y-1.5">
|
|
131
100
|
{connected.map(agent => (
|
|
132
|
-
<AgentCard
|
|
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
|
+
/>
|
|
133
111
|
))}
|
|
134
112
|
</div>
|
|
135
113
|
</section>
|
|
136
114
|
)}
|
|
137
115
|
|
|
138
|
-
{/* Detected
|
|
116
|
+
{/* Detected Agents */}
|
|
139
117
|
{detected.length > 0 && (
|
|
140
118
|
<section>
|
|
141
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
142
|
-
Detected ({detected.length})
|
|
143
|
-
</h3>
|
|
119
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
|
|
144
120
|
<div className="space-y-1.5">
|
|
145
121
|
{detected.map(agent => (
|
|
146
122
|
<AgentCard
|
|
147
123
|
key={agent.key}
|
|
148
124
|
agent={agent}
|
|
149
|
-
|
|
150
|
-
|
|
125
|
+
agentStatus="detected"
|
|
126
|
+
mcpStatus={mcp.status}
|
|
127
|
+
expanded={expandedAgent === agent.key}
|
|
128
|
+
onToggle={() => toggleAgent(agent.key)}
|
|
129
|
+
onInstallAgent={mcp.installAgent}
|
|
130
|
+
t={p}
|
|
151
131
|
/>
|
|
152
132
|
))}
|
|
153
133
|
</div>
|
|
154
134
|
</section>
|
|
155
135
|
)}
|
|
156
136
|
|
|
157
|
-
{/* Not
|
|
137
|
+
{/* Not Found Agents (collapsed) */}
|
|
158
138
|
{notFound.length > 0 && (
|
|
159
139
|
<section>
|
|
160
|
-
<button
|
|
161
|
-
|
|
162
|
-
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors"
|
|
163
|
-
>
|
|
140
|
+
<button onClick={() => setShowNotDetected(!showNotDetected)}
|
|
141
|
+
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors">
|
|
164
142
|
{showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
165
|
-
|
|
143
|
+
{p.sectionNotDetected} ({notFound.length})
|
|
166
144
|
</button>
|
|
167
145
|
{showNotDetected && (
|
|
168
146
|
<div className="space-y-1.5">
|
|
169
147
|
{notFound.map(agent => (
|
|
170
|
-
<AgentCard
|
|
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
|
+
/>
|
|
171
158
|
))}
|
|
172
159
|
</div>
|
|
173
160
|
)}
|
|
174
161
|
</section>
|
|
175
162
|
)}
|
|
176
163
|
|
|
177
|
-
{/*
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
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>
|
|
182
208
|
)}
|
|
183
209
|
</div>
|
|
184
210
|
)}
|
|
185
211
|
</div>
|
|
186
212
|
|
|
187
|
-
{/* Footer */}
|
|
213
|
+
{/* Footer: Advanced Config link */}
|
|
188
214
|
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
189
|
-
<
|
|
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>
|
|
190
222
|
</div>
|
|
191
223
|
</div>
|
|
192
224
|
);
|
|
193
225
|
}
|
|
194
226
|
|
|
195
|
-
/* ──
|
|
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
|
+
|
|
242
|
+
/* ── Agent Card ── */
|
|
196
243
|
|
|
197
|
-
function AgentCard({
|
|
198
|
-
agent,
|
|
199
|
-
status,
|
|
200
|
-
onConnect,
|
|
201
|
-
}: {
|
|
244
|
+
function AgentCard({ agent, agentStatus, mcpStatus, expanded, onToggle, onInstallAgent, t }: {
|
|
202
245
|
agent: AgentInfo;
|
|
203
|
-
|
|
204
|
-
|
|
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>;
|
|
251
|
+
t: Record<string, any>;
|
|
205
252
|
}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
253
|
+
const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
|
|
254
|
+
const [copied, setCopied] = useState(false);
|
|
255
|
+
const [installing, setInstalling] = useState(false);
|
|
256
|
+
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
257
|
+
|
|
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]);
|
|
269
|
+
|
|
270
|
+
const handleInstall = async () => {
|
|
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
|
+
}
|
|
279
|
+
setInstalling(false);
|
|
280
|
+
};
|
|
210
281
|
|
|
211
282
|
return (
|
|
212
|
-
<div className="rounded-lg border border-border/60 bg-card/30
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
283
|
+
<div className="rounded-lg border border-border/60 bg-card/30 overflow-hidden">
|
|
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
|
+
>
|
|
289
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
290
|
+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
|
|
291
|
+
<span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
|
|
292
|
+
{agentStatus === 'connected' && agent.transport && (
|
|
293
|
+
<span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
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>
|
|
340
|
+
</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>
|
|
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>
|
|
364
|
+
</div>
|
|
228
365
|
)}
|
|
229
366
|
</div>
|
|
230
367
|
);
|