@geminilight/mindos 0.5.51 → 0.5.54
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/README.md +7 -7
- package/README_zh.md +5 -5
- package/app/app/layout.tsx +2 -0
- package/app/components/ActivityBar.tsx +3 -2
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAgentDetailPanel.tsx +121 -0
- package/app/components/SidebarLayout.tsx +62 -5
- package/app/components/UpdateOverlay.tsx +124 -0
- package/app/components/help/HelpContent.tsx +10 -7
- package/app/components/panels/AgentsPanel.tsx +156 -178
- package/app/components/panels/AgentsPanelAgentDetail.tsx +193 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +116 -0
- package/app/components/panels/AgentsPanelAgentListRow.tsx +101 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +48 -0
- package/app/components/panels/DiscoverPanel.tsx +6 -46
- package/app/components/panels/EchoPanel.tsx +82 -0
- package/app/components/panels/PanelNavRow.tsx +51 -0
- package/app/components/panels/agents-panel-resolve-status.ts +13 -0
- package/app/components/settings/McpSkillsSection.tsx +88 -2
- package/app/components/settings/McpTab.tsx +26 -0
- package/app/components/settings/UpdateTab.tsx +65 -27
- package/app/lib/i18n-en.ts +31 -2
- package/app/lib/i18n-zh.ts +30 -2
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { Loader2, RefreshCw, ChevronDown, ChevronRight,
|
|
3
|
+
import { useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { Loader2, RefreshCw, ChevronDown, ChevronRight, Settings } from 'lucide-react';
|
|
5
5
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { Toggle } from '../settings/Primitives';
|
|
8
|
-
import type {
|
|
8
|
+
import type { SkillInfo } from '../settings/types';
|
|
9
9
|
import PanelHeader from './PanelHeader';
|
|
10
|
+
import { AgentsPanelHubNav } from './AgentsPanelHubNav';
|
|
11
|
+
import { AgentsPanelAgentGroups } from './AgentsPanelAgentGroups';
|
|
10
12
|
|
|
11
13
|
interface AgentsPanelProps {
|
|
12
14
|
active: boolean;
|
|
13
15
|
maximized?: boolean;
|
|
14
16
|
onMaximize?: () => void;
|
|
17
|
+
/** Highlights the row for the agent whose detail is open in the right dock. */
|
|
18
|
+
selectedAgentKey?: string | null;
|
|
19
|
+
onOpenAgentDetail?: (key: string) => void;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
export default function AgentsPanel({
|
|
22
|
+
export default function AgentsPanel({
|
|
23
|
+
active,
|
|
24
|
+
maximized,
|
|
25
|
+
onMaximize,
|
|
26
|
+
selectedAgentKey = null,
|
|
27
|
+
onOpenAgentDetail,
|
|
28
|
+
}: AgentsPanelProps) {
|
|
18
29
|
const { t } = useLocale();
|
|
19
30
|
const p = t.panels.agents;
|
|
20
31
|
const mcp = useMcpData();
|
|
@@ -22,12 +33,23 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
22
33
|
const [showNotDetected, setShowNotDetected] = useState(false);
|
|
23
34
|
const [showBuiltinSkills, setShowBuiltinSkills] = useState(false);
|
|
24
35
|
|
|
36
|
+
const overviewRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const skillsRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
|
|
25
39
|
const handleRefresh = async () => {
|
|
26
40
|
setRefreshing(true);
|
|
27
41
|
await mcp.refresh();
|
|
28
42
|
setRefreshing(false);
|
|
29
43
|
};
|
|
30
44
|
|
|
45
|
+
const scrollTo = useCallback((el: HTMLElement | null) => {
|
|
46
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const openAdvancedConfig = () => {
|
|
50
|
+
window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'mcp' } }));
|
|
51
|
+
};
|
|
52
|
+
|
|
31
53
|
const connected = mcp.agents.filter(a => a.present && a.installed);
|
|
32
54
|
const detected = mcp.agents.filter(a => a.present && !a.installed);
|
|
33
55
|
const notFound = mcp.agents.filter(a => !a.present);
|
|
@@ -36,20 +58,45 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
36
58
|
const builtinSkills = mcp.skills.filter(s => s.source === 'builtin');
|
|
37
59
|
const activeSkillCount = mcp.skills.filter(s => s.enabled).length;
|
|
38
60
|
|
|
39
|
-
const
|
|
40
|
-
|
|
61
|
+
const listCopy = {
|
|
62
|
+
installing: p.installing,
|
|
63
|
+
install: p.install,
|
|
41
64
|
};
|
|
42
65
|
|
|
66
|
+
const hubCopy = {
|
|
67
|
+
navOverview: p.navOverview,
|
|
68
|
+
navMcp: p.navMcp,
|
|
69
|
+
navSkills: p.navSkills,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const hub = (
|
|
73
|
+
<AgentsPanelHubNav
|
|
74
|
+
copy={hubCopy}
|
|
75
|
+
connectedCount={connected.length}
|
|
76
|
+
overviewRef={overviewRef}
|
|
77
|
+
skillsRef={skillsRef}
|
|
78
|
+
scrollTo={scrollTo}
|
|
79
|
+
openAdvancedConfig={openAdvancedConfig}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
43
83
|
return (
|
|
44
84
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
45
85
|
<PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
|
|
46
86
|
<div className="flex items-center gap-1.5">
|
|
47
87
|
{!mcp.loading && (
|
|
48
|
-
<span className="text-2xs text-muted-foreground">
|
|
88
|
+
<span className="text-2xs text-muted-foreground">
|
|
89
|
+
{connected.length} {p.connected}
|
|
90
|
+
</span>
|
|
49
91
|
)}
|
|
50
|
-
<button
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
<button
|
|
93
|
+
onClick={handleRefresh}
|
|
94
|
+
disabled={refreshing}
|
|
95
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
96
|
+
aria-label={p.refresh}
|
|
97
|
+
title={p.refresh}
|
|
98
|
+
type="button"
|
|
99
|
+
>
|
|
53
100
|
<RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
|
|
54
101
|
</button>
|
|
55
102
|
</div>
|
|
@@ -57,19 +104,14 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
57
104
|
|
|
58
105
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
59
106
|
{mcp.loading ? (
|
|
60
|
-
<div className="flex justify-center py-8"
|
|
61
|
-
|
|
62
|
-
<div className="flex flex-col items-center gap-2 py-8 text-center px-4">
|
|
63
|
-
<p className="text-xs text-muted-foreground">{p.noAgents}</p>
|
|
64
|
-
<button onClick={handleRefresh}
|
|
65
|
-
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">
|
|
66
|
-
<RefreshCw size={11} /> {p.retry}
|
|
67
|
-
</button>
|
|
107
|
+
<div className="flex justify-center py-8">
|
|
108
|
+
<Loader2 size={16} className="animate-spin text-muted-foreground" />
|
|
68
109
|
</div>
|
|
69
|
-
) : (
|
|
70
|
-
<div className="
|
|
71
|
-
{
|
|
72
|
-
<div className="
|
|
110
|
+
) : mcp.agents.length === 0 && mcp.skills.length === 0 ? (
|
|
111
|
+
<div className="flex flex-col gap-2 py-4 px-0">
|
|
112
|
+
{hub}
|
|
113
|
+
<div className="mx-4 border-t border-border" />
|
|
114
|
+
<div ref={overviewRef} className="mx-3 rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
|
|
73
115
|
<span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
|
|
74
116
|
{mcp.status?.running ? (
|
|
75
117
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
@@ -84,121 +126,117 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
84
126
|
</span>
|
|
85
127
|
)}
|
|
86
128
|
</div>
|
|
129
|
+
<div ref={skillsRef} className="mx-3 scroll-mt-2 rounded-lg border border-dashed border-border px-3 py-3 text-center">
|
|
130
|
+
<p className="text-xs text-muted-foreground mb-2">{p.noAgents}</p>
|
|
131
|
+
<p className="text-2xs text-muted-foreground mb-3">{p.skillsEmptyHint}</p>
|
|
132
|
+
<button
|
|
133
|
+
onClick={handleRefresh}
|
|
134
|
+
type="button"
|
|
135
|
+
className="inline-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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
136
|
+
>
|
|
137
|
+
<RefreshCw size={11} /> {p.retry}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="pb-3">
|
|
143
|
+
{hub}
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
{connected.length > 0 && (
|
|
90
|
-
<section>
|
|
91
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
|
|
92
|
-
<div className="space-y-1.5">
|
|
93
|
-
{connected.map(agent => (
|
|
94
|
-
<AgentCard
|
|
95
|
-
key={agent.key}
|
|
96
|
-
agent={agent}
|
|
97
|
-
agentStatus="connected"
|
|
98
|
-
onInstallAgent={mcp.installAgent}
|
|
99
|
-
t={p}
|
|
100
|
-
/>
|
|
101
|
-
))}
|
|
102
|
-
</div>
|
|
103
|
-
</section>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{/* Detected Agents */}
|
|
107
|
-
{detected.length > 0 && (
|
|
108
|
-
<section>
|
|
109
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
|
|
110
|
-
<div className="space-y-1.5">
|
|
111
|
-
{detected.map(agent => (
|
|
112
|
-
<AgentCard
|
|
113
|
-
key={agent.key}
|
|
114
|
-
agent={agent}
|
|
115
|
-
agentStatus="detected"
|
|
116
|
-
onInstallAgent={mcp.installAgent}
|
|
117
|
-
t={p}
|
|
118
|
-
/>
|
|
119
|
-
))}
|
|
120
|
-
</div>
|
|
121
|
-
</section>
|
|
122
|
-
)}
|
|
145
|
+
<div className="mx-4 border-t border-border" />
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
className="flex items-center gap-1 text-[11px]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
agentStatus="notFound"
|
|
139
|
-
onInstallAgent={mcp.installAgent}
|
|
140
|
-
t={p}
|
|
141
|
-
/>
|
|
142
|
-
))}
|
|
143
|
-
</div>
|
|
147
|
+
<div className="px-3 py-3 space-y-4">
|
|
148
|
+
<div ref={overviewRef} className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between scroll-mt-2">
|
|
149
|
+
<span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
|
|
150
|
+
{mcp.status?.running ? (
|
|
151
|
+
<span className="flex items-center gap-1.5 text-[11px]">
|
|
152
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
153
|
+
<span className="text-emerald-600 dark:text-emerald-400">:{mcp.status.port}</span>
|
|
154
|
+
<span className="text-muted-foreground">· {mcp.status.toolCount} tools</span>
|
|
155
|
+
</span>
|
|
156
|
+
) : (
|
|
157
|
+
<span className="flex items-center gap-1.5 text-[11px]">
|
|
158
|
+
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
|
|
159
|
+
<span className="text-muted-foreground">{p.stopped}</span>
|
|
160
|
+
</span>
|
|
144
161
|
)}
|
|
145
|
-
</
|
|
146
|
-
)}
|
|
162
|
+
</div>
|
|
147
163
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
<AgentsPanelAgentGroups
|
|
165
|
+
connected={connected}
|
|
166
|
+
detected={detected}
|
|
167
|
+
notFound={notFound}
|
|
168
|
+
onOpenDetail={onOpenAgentDetail}
|
|
169
|
+
selectedAgentKey={selectedAgentKey}
|
|
170
|
+
mcp={mcp}
|
|
171
|
+
listCopy={listCopy}
|
|
172
|
+
showNotDetected={showNotDetected}
|
|
173
|
+
setShowNotDetected={setShowNotDetected}
|
|
174
|
+
p={{
|
|
175
|
+
rosterLabel: p.rosterLabel,
|
|
176
|
+
sectionConnected: p.sectionConnected,
|
|
177
|
+
sectionDetected: p.sectionDetected,
|
|
178
|
+
sectionNotDetected: p.sectionNotDetected,
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
162
181
|
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
<div className="space-y-0.5 mb-2">
|
|
166
|
-
{customSkills.map(skill => (
|
|
167
|
-
<SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{/* Built-in skills (collapsed) */}
|
|
173
|
-
{builtinSkills.length > 0 && (
|
|
182
|
+
<section ref={skillsRef} className="scroll-mt-2">
|
|
183
|
+
{mcp.skills.length > 0 ? (
|
|
174
184
|
<>
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
{
|
|
185
|
+
<div className="flex items-center justify-between mb-2">
|
|
186
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
187
|
+
{p.skillsTitle} <span className="normal-case font-normal">{activeSkillCount} {p.skillsActive}</span>
|
|
188
|
+
</h3>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={openAdvancedConfig}
|
|
192
|
+
className="text-2xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
193
|
+
>
|
|
194
|
+
{p.newSkill}
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{customSkills.length > 0 && (
|
|
199
|
+
<div className="space-y-0.5 mb-2">
|
|
200
|
+
{customSkills.map(skill => (
|
|
185
201
|
<SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
|
|
186
202
|
))}
|
|
187
203
|
</div>
|
|
188
204
|
)}
|
|
205
|
+
|
|
206
|
+
{builtinSkills.length > 0 && (
|
|
207
|
+
<>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={() => setShowBuiltinSkills(!showBuiltinSkills)}
|
|
211
|
+
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors mb-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
212
|
+
>
|
|
213
|
+
{showBuiltinSkills ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
214
|
+
{p.builtinSkills} ({builtinSkills.length})
|
|
215
|
+
</button>
|
|
216
|
+
{showBuiltinSkills && (
|
|
217
|
+
<div className="space-y-0.5">
|
|
218
|
+
{builtinSkills.map(skill => (
|
|
219
|
+
<SkillRow key={skill.name} skill={skill} onToggle={mcp.toggleSkill} />
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
189
225
|
</>
|
|
226
|
+
) : (
|
|
227
|
+
<p className="text-2xs text-muted-foreground py-1">{p.skillsEmptyHint}</p>
|
|
190
228
|
)}
|
|
191
229
|
</section>
|
|
192
|
-
|
|
230
|
+
</div>
|
|
193
231
|
</div>
|
|
194
232
|
)}
|
|
195
233
|
</div>
|
|
196
234
|
|
|
197
|
-
{/* Footer: Advanced Config link */}
|
|
198
235
|
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
199
236
|
<button
|
|
237
|
+
type="button"
|
|
200
238
|
onClick={openAdvancedConfig}
|
|
201
|
-
className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
239
|
+
className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
202
240
|
>
|
|
203
241
|
<Settings size={11} />
|
|
204
242
|
{p.advancedConfig}
|
|
@@ -208,71 +246,11 @@ export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPan
|
|
|
208
246
|
);
|
|
209
247
|
}
|
|
210
248
|
|
|
211
|
-
/* ── Skill Row ── */
|
|
212
|
-
|
|
213
249
|
function SkillRow({ skill, onToggle }: { skill: SkillInfo; onToggle: (name: string, enabled: boolean) => void }) {
|
|
214
250
|
return (
|
|
215
251
|
<div className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md hover:bg-muted/30 transition-colors">
|
|
216
252
|
<span className="text-xs text-foreground truncate">{skill.name}</span>
|
|
217
|
-
<Toggle
|
|
218
|
-
size="sm"
|
|
219
|
-
checked={skill.enabled}
|
|
220
|
-
onChange={(v) => onToggle(skill.name, v)}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/* ── Agent Card (compact — no snippet, config viewing is in Settings) ── */
|
|
227
|
-
|
|
228
|
-
function AgentCard({ agent, agentStatus, onInstallAgent, t }: {
|
|
229
|
-
agent: AgentInfo;
|
|
230
|
-
agentStatus: 'connected' | 'detected' | 'notFound';
|
|
231
|
-
onInstallAgent: (key: string) => Promise<boolean>;
|
|
232
|
-
t: Record<string, any>;
|
|
233
|
-
}) {
|
|
234
|
-
const [installing, setInstalling] = useState(false);
|
|
235
|
-
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
236
|
-
|
|
237
|
-
const dot = agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
|
|
238
|
-
|
|
239
|
-
const handleInstall = async () => {
|
|
240
|
-
setInstalling(true);
|
|
241
|
-
setResult(null);
|
|
242
|
-
const ok = await onInstallAgent(agent.key);
|
|
243
|
-
setResult(ok
|
|
244
|
-
? { type: 'success', text: `${agent.name} ${t.connected}` }
|
|
245
|
-
: { type: 'error', text: 'Install failed' });
|
|
246
|
-
setInstalling(false);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<div className="rounded-lg border border-border/60 bg-card/30 px-3 py-2 flex items-center justify-between gap-2">
|
|
251
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
252
|
-
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
|
|
253
|
-
<span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
|
|
254
|
-
{agentStatus === 'connected' && agent.transport && (
|
|
255
|
-
<span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
|
|
256
|
-
)}
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
{/* Detected: Install button */}
|
|
260
|
-
{agentStatus === 'detected' && (
|
|
261
|
-
<button onClick={handleInstall} disabled={installing}
|
|
262
|
-
className="flex items-center gap-1 px-2 py-1 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors shrink-0"
|
|
263
|
-
style={{ background: 'var(--amber)' }}>
|
|
264
|
-
{installing ? <Loader2 size={10} className="animate-spin" /> : null}
|
|
265
|
-
{installing ? t.installing : t.install(agent.name)}
|
|
266
|
-
</button>
|
|
267
|
-
)}
|
|
268
|
-
|
|
269
|
-
{/* Install result */}
|
|
270
|
-
{result && (
|
|
271
|
-
<span className={`flex items-center gap-1 text-2xs shrink-0 ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
|
|
272
|
-
{result.type === 'success' ? <CheckCircle2 size={10} /> : <AlertCircle size={10} />}
|
|
273
|
-
{result.text}
|
|
274
|
-
</span>
|
|
275
|
-
)}
|
|
253
|
+
<Toggle size="sm" checked={skill.enabled} onChange={v => onToggle(skill.name, v)} />
|
|
276
254
|
</div>
|
|
277
255
|
);
|
|
278
256
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import { ChevronLeft, X, Loader2, CheckCircle2, AlertCircle, Copy, Check, Monitor, Globe } from 'lucide-react';
|
|
5
|
+
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
6
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
7
|
+
import type { AgentInfo, McpStatus } from '../settings/types';
|
|
8
|
+
import type { AgentsPanelAgentDetailStatus } from './agents-panel-resolve-status';
|
|
9
|
+
|
|
10
|
+
export type { AgentsPanelAgentDetailStatus };
|
|
11
|
+
|
|
12
|
+
export interface AgentsPanelAgentDetailCopy {
|
|
13
|
+
connected: string;
|
|
14
|
+
installing: string;
|
|
15
|
+
install: (name: string) => string;
|
|
16
|
+
copyConfig: string;
|
|
17
|
+
copied: string;
|
|
18
|
+
transportLocal: string;
|
|
19
|
+
transportRemote: string;
|
|
20
|
+
configPath: string;
|
|
21
|
+
notFoundDetail: string;
|
|
22
|
+
backToList: string;
|
|
23
|
+
/** Close button (dock header) — aria-label */
|
|
24
|
+
closeDetail?: string;
|
|
25
|
+
agentDetailTransport: string;
|
|
26
|
+
agentDetailSnippet: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function AgentsPanelAgentDetail({
|
|
30
|
+
agent,
|
|
31
|
+
agentStatus,
|
|
32
|
+
mcpStatus,
|
|
33
|
+
onBack,
|
|
34
|
+
onInstallAgent,
|
|
35
|
+
copy,
|
|
36
|
+
headerVariant = 'inline',
|
|
37
|
+
}: {
|
|
38
|
+
agent: AgentInfo;
|
|
39
|
+
agentStatus: AgentsPanelAgentDetailStatus;
|
|
40
|
+
mcpStatus: McpStatus | null;
|
|
41
|
+
onBack: () => void;
|
|
42
|
+
onInstallAgent: (key: string) => Promise<boolean>;
|
|
43
|
+
copy: AgentsPanelAgentDetailCopy;
|
|
44
|
+
/** `dock`: right-side sheet title + X. `inline`: back chevron (legacy sidebar drill). */
|
|
45
|
+
headerVariant?: 'inline' | 'dock';
|
|
46
|
+
}) {
|
|
47
|
+
const [installing, setInstalling] = useState(false);
|
|
48
|
+
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
49
|
+
const [transport, setTransport] = useState<'stdio' | 'http'>(() => agent.preferredTransport);
|
|
50
|
+
const [copied, setCopied] = useState(false);
|
|
51
|
+
|
|
52
|
+
const snippet = useMemo(() => {
|
|
53
|
+
if (agentStatus === 'notFound') return null;
|
|
54
|
+
return generateSnippet(agent, mcpStatus, transport);
|
|
55
|
+
}, [agent, mcpStatus, transport, agentStatus]);
|
|
56
|
+
|
|
57
|
+
const handleInstall = async () => {
|
|
58
|
+
setInstalling(true);
|
|
59
|
+
setResult(null);
|
|
60
|
+
const ok = await onInstallAgent(agent.key);
|
|
61
|
+
setResult(
|
|
62
|
+
ok
|
|
63
|
+
? { type: 'success', text: `${agent.name} ${copy.connected}` }
|
|
64
|
+
: { type: 'error', text: 'Install failed' },
|
|
65
|
+
);
|
|
66
|
+
setInstalling(false);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleCopy = async () => {
|
|
70
|
+
if (!snippet) return;
|
|
71
|
+
const ok = await copyToClipboard(snippet.snippet);
|
|
72
|
+
if (ok) {
|
|
73
|
+
setCopied(true);
|
|
74
|
+
setTimeout(() => setCopied(false), 2000);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const dot =
|
|
79
|
+
agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col h-full min-h-0">
|
|
83
|
+
{headerVariant === 'dock' ? (
|
|
84
|
+
<header className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-4 py-3 bg-card">
|
|
85
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
86
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
|
|
87
|
+
<h2 className="text-sm font-semibold text-foreground truncate font-display">{agent.name}</h2>
|
|
88
|
+
</div>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={onBack}
|
|
92
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
|
|
93
|
+
aria-label={copy.closeDetail ?? 'Close'}
|
|
94
|
+
>
|
|
95
|
+
<X size={16} />
|
|
96
|
+
</button>
|
|
97
|
+
</header>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="sticky top-0 z-10 flex items-center gap-2 border-b border-border bg-background/95 px-3 py-2.5 backdrop-blur-sm shrink-0">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={onBack}
|
|
103
|
+
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground rounded-md px-1 py-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
|
|
104
|
+
>
|
|
105
|
+
<ChevronLeft size={16} />
|
|
106
|
+
{copy.backToList}
|
|
107
|
+
</button>
|
|
108
|
+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
|
|
109
|
+
<span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<div className="px-4 py-4 space-y-4 flex-1 min-h-0 overflow-y-auto">
|
|
114
|
+
{agentStatus === 'detected' && (
|
|
115
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={handleInstall}
|
|
119
|
+
disabled={installing}
|
|
120
|
+
className="inline-flex items-center gap-1.5 px-3 py-2 text-xs rounded-lg font-medium text-[var(--amber-foreground)] disabled:opacity-50 bg-[var(--amber)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
121
|
+
>
|
|
122
|
+
{installing ? <Loader2 size={14} className="animate-spin" /> : null}
|
|
123
|
+
{installing ? copy.installing : copy.install(agent.name)}
|
|
124
|
+
</button>
|
|
125
|
+
{result && (
|
|
126
|
+
<span
|
|
127
|
+
className={`flex items-center gap-1 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}
|
|
128
|
+
>
|
|
129
|
+
{result.type === 'success' ? <CheckCircle2 size={12} /> : <AlertCircle size={12} />}
|
|
130
|
+
{result.text}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{agentStatus === 'notFound' && (
|
|
137
|
+
<p className="text-sm text-muted-foreground leading-relaxed">{copy.notFoundDetail}</p>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{agentStatus !== 'notFound' && snippet && (
|
|
141
|
+
<>
|
|
142
|
+
<div>
|
|
143
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1">{copy.configPath}</p>
|
|
144
|
+
<p className="text-xs font-mono text-foreground break-all">{snippet.path}</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div>
|
|
148
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{copy.agentDetailTransport}</p>
|
|
149
|
+
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-muted/40 w-fit">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => setTransport('stdio')}
|
|
153
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
154
|
+
transport === 'stdio' ? 'bg-card shadow-sm text-foreground' : 'text-muted-foreground'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
<Monitor size={13} />
|
|
158
|
+
{copy.transportLocal}
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => setTransport('http')}
|
|
163
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
164
|
+
transport === 'http' ? 'bg-card shadow-sm text-foreground' : 'text-muted-foreground'
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<Globe size={13} />
|
|
168
|
+
{copy.transportRemote}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div>
|
|
174
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{copy.agentDetailSnippet}</p>
|
|
175
|
+
<pre className="text-[11px] font-mono bg-muted/50 border border-border rounded-lg p-3 overflow-x-auto whitespace-pre max-h-[min(320px,50vh)] overflow-y-auto select-all">
|
|
176
|
+
{snippet.displaySnippet}
|
|
177
|
+
</pre>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={handleCopy}
|
|
183
|
+
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
184
|
+
>
|
|
185
|
+
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
186
|
+
{copied ? copy.copied : copy.copyConfig}
|
|
187
|
+
</button>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|