@geminilight/mindos 0.5.63 → 0.5.65
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 +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/changes/route.ts +7 -1
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/mcp/install-skill/route.ts +9 -24
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/app/layout.tsx +1 -0
- package/app/app/page.tsx +1 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +44 -14
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +2 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +36 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +89 -20
- package/app/components/agents/AgentsMcpSection.tsx +513 -85
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +746 -105
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +308 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +89 -13
- package/app/components/changes/ChangesContentPage.tsx +134 -51
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +22 -128
- package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
- package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
- package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +5 -5
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +5 -3
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +6 -5
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +7 -4
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/content-changes.ts +148 -8
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +16 -1
- package/app/lib/i18n-en.ts +317 -36
- package/app/lib/i18n-zh.ts +316 -35
- package/app/lib/mcp-agents.ts +273 -2
- package/app/lib/renderers/index.ts +1 -2
- package/app/lib/renderers/registry.ts +10 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-agents.js +38 -13
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
- package/scripts/migrate-agent-diff.js +146 -0
- package/scripts/setup.js +12 -17
- package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
- package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
- package/app/components/renderers/diff/manifest.ts +0 -14
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
|
-
import { ArrowLeft, Server,
|
|
4
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import { ArrowLeft, Server, Search, Trash2, Zap } from 'lucide-react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { useMcpData } from '@/hooks/useMcpData';
|
|
8
|
-
import {
|
|
8
|
+
import { apiFetch } from '@/lib/api';
|
|
9
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
10
|
+
import { generateSnippet } from '@/lib/mcp-snippets';
|
|
11
|
+
import {
|
|
12
|
+
aggregateCrossAgentMcpServers,
|
|
13
|
+
aggregateCrossAgentSkills,
|
|
14
|
+
filterSkillsForAgentDetail,
|
|
15
|
+
resolveAgentStatus,
|
|
16
|
+
type AgentDetailSkillSourceFilter,
|
|
17
|
+
} from './agents-content-model';
|
|
18
|
+
import { AgentAvatar, ActionButton, ConfirmDialog, PillButton } from './AgentsPrimitives';
|
|
19
|
+
import SkillDetailPopover from './SkillDetailPopover';
|
|
9
20
|
|
|
10
21
|
export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
11
22
|
const { t } = useLocale();
|
|
@@ -13,7 +24,171 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
13
24
|
const mcp = useMcpData();
|
|
14
25
|
|
|
15
26
|
const agent = useMemo(() => mcp.agents.find((item) => item.key === agentKey), [mcp.agents, agentKey]);
|
|
16
|
-
const
|
|
27
|
+
const [skillQuery, setSkillQuery] = useState('');
|
|
28
|
+
const [skillSource, setSkillSource] = useState<AgentDetailSkillSourceFilter>('all');
|
|
29
|
+
const [skillBusy, setSkillBusy] = useState<string | null>(null);
|
|
30
|
+
const [editingSkill, setEditingSkill] = useState<string | null>(null);
|
|
31
|
+
const [editContent, setEditContent] = useState('');
|
|
32
|
+
const [editError, setEditError] = useState<string | null>(null);
|
|
33
|
+
const [saveBusy, setSaveBusy] = useState(false);
|
|
34
|
+
const [snippetCopied, setSnippetCopied] = useState(false);
|
|
35
|
+
const [mcpBusy, setMcpBusy] = useState(false);
|
|
36
|
+
const [mcpMessage, setMcpMessage] = useState<string | null>(null);
|
|
37
|
+
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
38
|
+
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
39
|
+
const [confirmMcpRemove, setConfirmMcpRemove] = useState<string | null>(null);
|
|
40
|
+
const [mcpHint, setMcpHint] = useState<string | null>(null);
|
|
41
|
+
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const filteredSkills = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
filterSkillsForAgentDetail(mcp.skills, {
|
|
46
|
+
query: skillQuery,
|
|
47
|
+
source: skillSource,
|
|
48
|
+
}),
|
|
49
|
+
[mcp.skills, skillQuery, skillSource],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const skillSummary = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
total: mcp.skills.length,
|
|
55
|
+
enabled: mcp.skills.filter((s) => s.enabled).length,
|
|
56
|
+
builtin: mcp.skills.filter((s) => s.source === 'builtin').length,
|
|
57
|
+
user: mcp.skills.filter((s) => s.source === 'user').length,
|
|
58
|
+
}),
|
|
59
|
+
[mcp.skills],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const crossAgentMcpMap = useMemo(() => {
|
|
63
|
+
const all = aggregateCrossAgentMcpServers(mcp.agents);
|
|
64
|
+
const map = new Map<string, string[]>();
|
|
65
|
+
for (const srv of all) map.set(srv.serverName, srv.agents);
|
|
66
|
+
return map;
|
|
67
|
+
}, [mcp.agents]);
|
|
68
|
+
|
|
69
|
+
const crossAgentSkillMap = useMemo(() => {
|
|
70
|
+
const all = aggregateCrossAgentSkills(mcp.agents);
|
|
71
|
+
const map = new Map<string, string[]>();
|
|
72
|
+
for (const sk of all) map.set(sk.skillName, sk.agents);
|
|
73
|
+
return map;
|
|
74
|
+
}, [mcp.agents]);
|
|
75
|
+
|
|
76
|
+
const status = agent ? resolveAgentStatus(agent) : 'notFound';
|
|
77
|
+
const currentScope = agent?.scope === 'project' ? 'project' : 'global';
|
|
78
|
+
const currentTransport: 'stdio' | 'http' = agent?.transport === 'http' ? 'http' : 'stdio';
|
|
79
|
+
const snippet = useMemo(
|
|
80
|
+
() => agent ? generateSnippet(agent, mcp.status, currentTransport) : { snippet: '', path: '' },
|
|
81
|
+
[agent, mcp.status, currentTransport],
|
|
82
|
+
);
|
|
83
|
+
const nativeInstalledSkills = agent?.installedSkillNames ?? [];
|
|
84
|
+
const configuredMcpServers = agent?.configuredMcpServers ?? [];
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
const handleSkillToggle = useCallback(async (name: string, enabled: boolean) => {
|
|
88
|
+
setSkillBusy(name);
|
|
89
|
+
setEditError(null);
|
|
90
|
+
try {
|
|
91
|
+
await mcp.toggleSkill(name, enabled);
|
|
92
|
+
await mcp.refresh();
|
|
93
|
+
} finally {
|
|
94
|
+
setSkillBusy(null);
|
|
95
|
+
}
|
|
96
|
+
}, [mcp]);
|
|
97
|
+
|
|
98
|
+
const handleStartEditSkill = useCallback(async (name: string) => {
|
|
99
|
+
setEditError(null);
|
|
100
|
+
setSkillBusy(name);
|
|
101
|
+
try {
|
|
102
|
+
const res = await apiFetch<{ content: string }>('/api/skills', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({ action: 'read', name }),
|
|
106
|
+
});
|
|
107
|
+
setEditingSkill(name);
|
|
108
|
+
setEditContent(res.content);
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
setEditError(err instanceof Error ? err.message : a.detail.skillReadFailed);
|
|
111
|
+
} finally {
|
|
112
|
+
setSkillBusy(null);
|
|
113
|
+
}
|
|
114
|
+
}, [a.detail.skillReadFailed]);
|
|
115
|
+
|
|
116
|
+
const handleSaveSkill = useCallback(async () => {
|
|
117
|
+
if (!editingSkill) return;
|
|
118
|
+
setSaveBusy(true);
|
|
119
|
+
setEditError(null);
|
|
120
|
+
try {
|
|
121
|
+
await apiFetch('/api/skills', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ action: 'update', name: editingSkill, content: editContent }),
|
|
125
|
+
});
|
|
126
|
+
setEditingSkill(null);
|
|
127
|
+
setEditContent('');
|
|
128
|
+
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
129
|
+
await mcp.refresh();
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
setEditError(err instanceof Error ? err.message : a.detail.skillSaveFailed);
|
|
132
|
+
} finally {
|
|
133
|
+
setSaveBusy(false);
|
|
134
|
+
}
|
|
135
|
+
}, [editingSkill, editContent, a.detail.skillSaveFailed, mcp]);
|
|
136
|
+
|
|
137
|
+
const handleDeleteSkill = useCallback(async (name: string) => {
|
|
138
|
+
setConfirmDelete(null);
|
|
139
|
+
setSkillBusy(name);
|
|
140
|
+
try {
|
|
141
|
+
await apiFetch('/api/skills', {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'Content-Type': 'application/json' },
|
|
144
|
+
body: JSON.stringify({ action: 'delete', name }),
|
|
145
|
+
});
|
|
146
|
+
setDeleteMsg(a.detail.skillDeleteSuccess);
|
|
147
|
+
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
148
|
+
await mcp.refresh();
|
|
149
|
+
} catch {
|
|
150
|
+
setDeleteMsg(a.detail.skillDeleteFailed);
|
|
151
|
+
} finally {
|
|
152
|
+
setSkillBusy(null);
|
|
153
|
+
setTimeout(() => setDeleteMsg(null), 3000);
|
|
154
|
+
}
|
|
155
|
+
}, [a.detail.skillDeleteSuccess, a.detail.skillDeleteFailed, mcp]);
|
|
156
|
+
|
|
157
|
+
const handleCopySnippet = useCallback(async () => {
|
|
158
|
+
const ok = await copyToClipboard(snippet.snippet);
|
|
159
|
+
if (!ok) return;
|
|
160
|
+
setSnippetCopied(true);
|
|
161
|
+
setTimeout(() => setSnippetCopied(false), 1200);
|
|
162
|
+
}, [snippet.snippet]);
|
|
163
|
+
|
|
164
|
+
const handleApplyMcpConfig = useCallback(async (scope: 'project' | 'global', transport: 'stdio' | 'http') => {
|
|
165
|
+
if (!agent) return;
|
|
166
|
+
setMcpBusy(true);
|
|
167
|
+
setMcpMessage(a.detail.mcpApplying);
|
|
168
|
+
try {
|
|
169
|
+
const ok = await mcp.installAgent(agent.key, { scope, transport });
|
|
170
|
+
await mcp.refresh();
|
|
171
|
+
setMcpMessage(ok ? a.detail.mcpApplySuccess : a.detail.mcpApplyFailed);
|
|
172
|
+
} finally {
|
|
173
|
+
setMcpBusy(false);
|
|
174
|
+
}
|
|
175
|
+
}, [a.detail.mcpApplying, a.detail.mcpApplySuccess, a.detail.mcpApplyFailed, mcp, agent]);
|
|
176
|
+
|
|
177
|
+
const handleDeleteSkillFromPopover = useCallback(async (name: string) => {
|
|
178
|
+
await apiFetch('/api/skills', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({ action: 'delete', name }),
|
|
182
|
+
});
|
|
183
|
+
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
184
|
+
await mcp.refresh();
|
|
185
|
+
}, [mcp]);
|
|
186
|
+
|
|
187
|
+
const handleMcpRemoveConfirm = useCallback(() => {
|
|
188
|
+
setConfirmMcpRemove(null);
|
|
189
|
+
setMcpHint(a.detail.mcpServerHint);
|
|
190
|
+
setTimeout(() => setMcpHint(null), 4000);
|
|
191
|
+
}, [a.detail.mcpServerHint]);
|
|
17
192
|
|
|
18
193
|
if (!agent) {
|
|
19
194
|
return (
|
|
@@ -29,80 +204,300 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
|
|
|
29
204
|
);
|
|
30
205
|
}
|
|
31
206
|
|
|
32
|
-
const status = resolveAgentStatus(agent);
|
|
33
207
|
|
|
34
208
|
return (
|
|
35
209
|
<div className="content-width px-4 md:px-6 py-8 md:py-10 space-y-4">
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">{agent.name}</h1>
|
|
42
|
-
<p className="mt-1 text-sm text-muted-foreground">{a.detailSubtitle}</p>
|
|
43
|
-
</div>
|
|
210
|
+
{/* Back link */}
|
|
211
|
+
<Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground">
|
|
212
|
+
<ArrowLeft size={14} />
|
|
213
|
+
{a.backToOverview}
|
|
214
|
+
</Link>
|
|
44
215
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<div className="
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
216
|
+
{/* ═══════════ AGENT PROFILE (consolidated header) ═══════════ */}
|
|
217
|
+
<section className="rounded-lg border border-border bg-card p-4 space-y-3">
|
|
218
|
+
<div className="flex items-center gap-3">
|
|
219
|
+
<AgentAvatar name={agent.name} status={status} size="md" />
|
|
220
|
+
<div className="min-w-0">
|
|
221
|
+
<h1 className="text-xl font-semibold tracking-tight font-display text-foreground">{agent.name}</h1>
|
|
222
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 mt-0.5">
|
|
223
|
+
<span className={`text-2xs font-medium px-1.5 py-0.5 rounded ${
|
|
224
|
+
status === 'connected' ? 'bg-success/10 text-success'
|
|
225
|
+
: status === 'detected' ? 'bg-[var(--amber-subtle)] text-[var(--amber)]'
|
|
226
|
+
: 'bg-muted text-muted-foreground'
|
|
227
|
+
}`}>{status}</span>
|
|
228
|
+
<span className="text-2xs text-muted-foreground font-mono">{agent.transport ?? agent.preferredTransport}</span>
|
|
229
|
+
<span className="text-2xs text-muted-foreground">·</span>
|
|
230
|
+
<span className="text-2xs text-muted-foreground">{agent.skillMode ?? a.na}</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground pt-2 border-t border-border">
|
|
235
|
+
<span>{a.detail.format}: <span className="text-foreground">{agent.format}</span></span>
|
|
236
|
+
<span>{a.detail.lastActivityAt}: <span className="text-foreground tabular-nums">{agent.runtimeLastActivityAt ?? a.na}</span></span>
|
|
237
|
+
<span>{configuredMcpServers.length} MCP · {nativeInstalledSkills.length} skills</span>
|
|
51
238
|
</div>
|
|
52
239
|
</section>
|
|
53
240
|
|
|
54
|
-
|
|
55
|
-
|
|
241
|
+
{/* ═══════════ MCP MANAGEMENT ═══════════ */}
|
|
242
|
+
<section className="rounded-lg border border-border bg-card p-4 space-y-3">
|
|
243
|
+
<h2 className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
|
56
244
|
<Server size={14} className="text-muted-foreground" />
|
|
57
|
-
{a.detail.
|
|
245
|
+
{a.detail.mcpManagement}
|
|
58
246
|
</h2>
|
|
247
|
+
|
|
248
|
+
{/* MCP status row */}
|
|
59
249
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
|
60
|
-
<DetailLine label={a.detail.
|
|
61
|
-
<DetailLine label={a.detail.
|
|
62
|
-
<DetailLine label={a.detail.
|
|
250
|
+
<DetailLine label={a.detail.mcpInstalled} value={agent.installed ? a.detail.yes : a.detail.no} />
|
|
251
|
+
<DetailLine label={a.detail.mcpScope} value={agent.scope ?? a.na} />
|
|
252
|
+
<DetailLine label={a.detail.mcpConfigPath} value={agent.configPath ?? a.na} />
|
|
63
253
|
</div>
|
|
64
|
-
</section>
|
|
65
254
|
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
255
|
+
{/* Configured MCP servers with management */}
|
|
256
|
+
<div className="rounded-lg border border-border bg-background p-4 space-y-2">
|
|
257
|
+
<div className="flex items-center justify-between">
|
|
258
|
+
<p className="text-xs font-semibold text-foreground">{a.detail.configuredMcpServers}</p>
|
|
259
|
+
<span className="text-2xs text-muted-foreground tabular-nums">{a.detail.configuredMcpServersCount(configuredMcpServers.length)}</span>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{mcpHint && (
|
|
263
|
+
<div role="status" aria-live="polite" className="rounded-md border border-border bg-muted/50 px-3 py-2 text-xs text-muted-foreground animate-in fade-in duration-200">
|
|
264
|
+
{mcpHint}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{configuredMcpServers.length === 0 ? (
|
|
269
|
+
<p className="text-xs text-muted-foreground">{a.detail.configuredMcpServersEmpty}</p>
|
|
270
|
+
) : (
|
|
271
|
+
<div className="space-y-1.5 max-h-[280px] overflow-y-auto">
|
|
272
|
+
{configuredMcpServers.map((name) => {
|
|
273
|
+
const sharedWith = (crossAgentMcpMap.get(name) ?? []).filter((n) => n !== agent.name);
|
|
274
|
+
return (
|
|
275
|
+
<div key={name} className="flex items-center gap-2 rounded-md border border-border/60 px-2.5 py-2 group/mcp hover:border-border hover:bg-muted/20 transition-all duration-100">
|
|
276
|
+
<Server size={11} className="text-[var(--amber)] shrink-0" />
|
|
277
|
+
<span className="text-xs font-medium text-foreground flex-1 min-w-0 truncate">{name}</span>
|
|
278
|
+
{sharedWith.length > 0 && (
|
|
279
|
+
<div className="flex items-center gap-1">
|
|
280
|
+
{sharedWith.slice(0, 3).map((n) => (
|
|
281
|
+
<AgentAvatar key={n} name={n} size="sm" />
|
|
282
|
+
))}
|
|
283
|
+
{sharedWith.length > 3 && <span className="text-2xs text-muted-foreground">+{sharedWith.length - 3}</span>}
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
onClick={() => setConfirmMcpRemove(name)}
|
|
289
|
+
className="text-2xs text-muted-foreground hover:text-destructive cursor-pointer opacity-0 group-hover/mcp:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1 py-0.5 transition-all duration-150"
|
|
290
|
+
aria-label={`${a.detail.mcpServerRemove} ${name}`}
|
|
291
|
+
>
|
|
292
|
+
<Trash2 size={12} />
|
|
293
|
+
</button>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
})}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* MCP actions */}
|
|
302
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
303
|
+
<ActionButton
|
|
304
|
+
onClick={() => void handleCopySnippet()}
|
|
305
|
+
disabled={false}
|
|
306
|
+
busy={false}
|
|
307
|
+
label={snippetCopied ? a.detail.mcpCopied : a.detail.mcpCopySnippet}
|
|
308
|
+
/>
|
|
309
|
+
<ActionButton
|
|
310
|
+
onClick={() => void mcp.refresh()}
|
|
311
|
+
disabled={false}
|
|
312
|
+
busy={false}
|
|
313
|
+
label={a.detail.mcpRefresh}
|
|
314
|
+
/>
|
|
315
|
+
<ActionButton
|
|
316
|
+
onClick={() => void handleApplyMcpConfig(currentScope, currentTransport)}
|
|
317
|
+
disabled={mcpBusy}
|
|
318
|
+
busy={mcpBusy}
|
|
319
|
+
label={a.detail.mcpReconnect}
|
|
320
|
+
variant="primary"
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
<p className="text-2xs text-muted-foreground truncate">{snippet.path}</p>
|
|
324
|
+
{mcpMessage && <p className="text-xs text-muted-foreground animate-in fade-in duration-200">{mcpMessage}</p>}
|
|
76
325
|
</section>
|
|
77
326
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
327
|
+
{/* ═══════════ SKILL ASSIGNMENTS ═══════════ */}
|
|
328
|
+
<section className="rounded-lg border border-border bg-card p-4 space-y-3">
|
|
329
|
+
<div className="flex items-center justify-between">
|
|
330
|
+
<h2 className="text-sm font-medium text-foreground">{a.detail.skillAssignments}</h2>
|
|
331
|
+
<div className="flex items-center gap-3 text-2xs text-muted-foreground tabular-nums">
|
|
332
|
+
<span>MindOS {skillSummary.total}</span>
|
|
333
|
+
<span>{a.detail.skillsEnabled.split(' ')[0]} {skillSummary.enabled}</span>
|
|
334
|
+
<span>{a.detail.nativeInstalledSkills} {nativeInstalledSkills.length}</span>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Search + filters */}
|
|
339
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
340
|
+
<label className="relative flex-1">
|
|
341
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
|
342
|
+
<input
|
|
343
|
+
value={skillQuery}
|
|
344
|
+
onChange={(e) => setSkillQuery(e.target.value)}
|
|
345
|
+
placeholder={a.detail.skillsSearchPlaceholder}
|
|
346
|
+
className="w-full h-9 rounded-md border border-border bg-background pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors duration-150"
|
|
347
|
+
/>
|
|
348
|
+
</label>
|
|
349
|
+
<div className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
350
|
+
<PillButton active={skillSource === 'all'} label={a.detail.skillsFilterAll} onClick={() => setSkillSource('all')} />
|
|
351
|
+
<PillButton active={skillSource === 'builtin'} label={a.detail.skillsFilterBuiltin} onClick={() => setSkillSource('builtin')} />
|
|
352
|
+
<PillButton active={skillSource === 'user'} label={a.detail.skillsFilterUser} onClick={() => setSkillSource('user')} />
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
{deleteMsg && (
|
|
357
|
+
<div role="status" aria-live="polite" className="rounded-md border border-border bg-muted/50 px-3 py-2 text-xs text-muted-foreground animate-in fade-in duration-200">
|
|
358
|
+
{deleteMsg}
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{/* MindOS Skills */}
|
|
363
|
+
{filteredSkills.length > 0 && (
|
|
364
|
+
<div>
|
|
365
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
366
|
+
MindOS Skills <span className="tabular-nums">({filteredSkills.filter((s) => s.enabled).length}/{filteredSkills.length})</span>
|
|
367
|
+
</p>
|
|
368
|
+
<ul className="space-y-0.5">
|
|
369
|
+
{filteredSkills.map((skill) => {
|
|
370
|
+
const isEditing = editingSkill === skill.name;
|
|
371
|
+
return (
|
|
372
|
+
<li key={skill.name} className="rounded-md hover:bg-muted/30 transition-colors duration-100">
|
|
373
|
+
<div className="flex items-center gap-2 py-1.5 px-1.5 group/skill">
|
|
374
|
+
<Zap size={13} className={`shrink-0 ${skill.enabled ? 'text-[var(--amber)]' : 'text-muted-foreground/50'}`} aria-hidden="true" />
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={() => setDetailSkillName(skill.name)}
|
|
378
|
+
className="text-xs text-foreground flex-1 min-w-0 truncate hover:text-[var(--amber)] cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded text-left"
|
|
379
|
+
>
|
|
380
|
+
{skill.name}
|
|
381
|
+
</button>
|
|
382
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-dim)] text-[var(--amber)]'}`}>
|
|
383
|
+
{skill.source === 'builtin' ? a.detail.skillsSourceBuiltin : a.detail.skillsSourceUser}
|
|
384
|
+
</span>
|
|
385
|
+
|
|
386
|
+
<div className="flex items-center gap-1 shrink-0 md:opacity-0 md:group-hover/skill:opacity-100 md:focus-within:opacity-100 transition-opacity duration-150">
|
|
387
|
+
<ActionButton
|
|
388
|
+
onClick={() => void handleSkillToggle(skill.name, !skill.enabled)}
|
|
389
|
+
disabled={skillBusy === skill.name}
|
|
390
|
+
busy={skillBusy === skill.name}
|
|
391
|
+
label={skill.enabled ? a.detail.skillDisable : a.detail.skillEnable}
|
|
392
|
+
/>
|
|
393
|
+
{skill.editable && (
|
|
394
|
+
<>
|
|
395
|
+
<ActionButton
|
|
396
|
+
onClick={() => void handleStartEditSkill(skill.name)}
|
|
397
|
+
disabled={skillBusy === skill.name || saveBusy}
|
|
398
|
+
busy={false}
|
|
399
|
+
label={a.detail.skillEdit}
|
|
400
|
+
/>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
onClick={() => setConfirmDelete(skill.name)}
|
|
404
|
+
disabled={skillBusy === skill.name}
|
|
405
|
+
className="inline-flex items-center justify-center min-h-[28px] px-1.5 rounded-md text-muted-foreground hover:text-destructive cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150"
|
|
406
|
+
aria-label={`${a.detail.skillDelete} ${skill.name}`}
|
|
407
|
+
>
|
|
408
|
+
<Trash2 size={13} />
|
|
409
|
+
</button>
|
|
410
|
+
</>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{isEditing && (
|
|
416
|
+
<div className="px-3 pb-3 pt-0 space-y-2">
|
|
417
|
+
<textarea
|
|
418
|
+
value={editContent}
|
|
419
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
420
|
+
className="mt-2 w-full h-40 rounded-md border border-border bg-background px-3 py-2 text-xs font-mono text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-y"
|
|
421
|
+
/>
|
|
422
|
+
<div className="flex items-center gap-2">
|
|
423
|
+
<ActionButton onClick={() => void handleSaveSkill()} disabled={saveBusy} busy={saveBusy} label={a.detail.skillSave} variant="primary" />
|
|
424
|
+
<ActionButton onClick={() => setEditingSkill(null)} disabled={false} busy={false} label={a.detail.skillCancel} />
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</li>
|
|
429
|
+
);
|
|
430
|
+
})}
|
|
431
|
+
</ul>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{filteredSkills.length === 0 && nativeInstalledSkills.length === 0 && (
|
|
81
436
|
<p className="text-sm text-muted-foreground">{a.detail.noSkills}</p>
|
|
82
|
-
) : (
|
|
83
|
-
<ul className="text-sm text-muted-foreground space-y-1">
|
|
84
|
-
{enabledSkills.map((skill) => (
|
|
85
|
-
<li key={skill.name}>- {skill.name}</li>
|
|
86
|
-
))}
|
|
87
|
-
</ul>
|
|
88
437
|
)}
|
|
89
|
-
</section>
|
|
90
438
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
439
|
+
{/* Native installed skills — same row style */}
|
|
440
|
+
{nativeInstalledSkills.length > 0 && (
|
|
441
|
+
<div>
|
|
442
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
443
|
+
{a.detail.nativeInstalledSkills} <span className="tabular-nums">({nativeInstalledSkills.length})</span>
|
|
444
|
+
</p>
|
|
445
|
+
<div className="space-y-0.5 max-h-[280px] overflow-y-auto">
|
|
446
|
+
{nativeInstalledSkills.map((name) => (
|
|
447
|
+
<button
|
|
448
|
+
key={name}
|
|
449
|
+
type="button"
|
|
450
|
+
onClick={() => setDetailSkillName(name)}
|
|
451
|
+
className="w-full flex items-center gap-2 py-1.5 px-1.5 rounded-md hover:bg-muted/30 transition-colors duration-100 cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
452
|
+
>
|
|
453
|
+
<Zap size={13} className="shrink-0 text-muted-foreground/50" aria-hidden="true" />
|
|
454
|
+
<span className="text-xs text-foreground flex-1 min-w-0 truncate hover:text-[var(--amber)] transition-colors duration-150">{name}</span>
|
|
455
|
+
</button>
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
98
460
|
|
|
99
|
-
|
|
100
|
-
<h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
|
|
101
|
-
<Compass size={14} className="text-muted-foreground" />
|
|
102
|
-
{a.detail.spaceReach}
|
|
103
|
-
</h2>
|
|
104
|
-
<p className="text-sm text-muted-foreground">{a.detail.noSpaceReach}</p>
|
|
461
|
+
{editError && <p className="text-xs text-error">{editError}</p>}
|
|
105
462
|
</section>
|
|
463
|
+
|
|
464
|
+
{/* ═══════════ Confirm Dialogs ═══════════ */}
|
|
465
|
+
<ConfirmDialog
|
|
466
|
+
open={confirmDelete !== null}
|
|
467
|
+
title={a.detail.skillDelete}
|
|
468
|
+
message={confirmDelete ? a.detail.skillDeleteConfirm(confirmDelete) : ''}
|
|
469
|
+
confirmLabel={a.detail.skillDelete}
|
|
470
|
+
cancelLabel={a.detail.skillCancel}
|
|
471
|
+
onConfirm={() => confirmDelete && void handleDeleteSkill(confirmDelete)}
|
|
472
|
+
onCancel={() => setConfirmDelete(null)}
|
|
473
|
+
variant="destructive"
|
|
474
|
+
/>
|
|
475
|
+
|
|
476
|
+
<ConfirmDialog
|
|
477
|
+
open={confirmMcpRemove !== null}
|
|
478
|
+
title={a.detail.mcpServerRemove}
|
|
479
|
+
message={confirmMcpRemove ? a.detail.mcpServerRemoveConfirm(confirmMcpRemove) : ''}
|
|
480
|
+
confirmLabel={a.detail.mcpServerRemove}
|
|
481
|
+
cancelLabel={a.detail.skillCancel}
|
|
482
|
+
onConfirm={handleMcpRemoveConfirm}
|
|
483
|
+
onCancel={() => setConfirmMcpRemove(null)}
|
|
484
|
+
variant="destructive"
|
|
485
|
+
/>
|
|
486
|
+
|
|
487
|
+
{/* Skill detail popover */}
|
|
488
|
+
<SkillDetailPopover
|
|
489
|
+
open={detailSkillName !== null}
|
|
490
|
+
skillName={detailSkillName}
|
|
491
|
+
skill={detailSkillName ? mcp.skills.find((s) => s.name === detailSkillName) ?? null : null}
|
|
492
|
+
agentNames={detailSkillName ? (crossAgentSkillMap.get(detailSkillName) ?? []) : []}
|
|
493
|
+
isNative={detailSkillName ? !mcp.skills.some((s) => s.name === detailSkillName) : false}
|
|
494
|
+
nativeSourcePath={agent?.installedSkillSourcePath}
|
|
495
|
+
copy={a.skills.skillPopover}
|
|
496
|
+
onClose={() => setDetailSkillName(null)}
|
|
497
|
+
onToggle={mcp.toggleSkill}
|
|
498
|
+
onDelete={handleDeleteSkillFromPopover}
|
|
499
|
+
onRefresh={mcp.refresh}
|
|
500
|
+
/>
|
|
106
501
|
</div>
|
|
107
502
|
);
|
|
108
503
|
}
|