@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.
Files changed (104) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/changes/route.ts +7 -1
  5. package/app/app/api/file/route.ts +9 -0
  6. package/app/app/api/mcp/agents/route.ts +27 -1
  7. package/app/app/api/mcp/install-skill/route.ts +9 -24
  8. package/app/app/api/skills/route.ts +18 -2
  9. package/app/app/api/tree-version/route.ts +8 -0
  10. package/app/app/layout.tsx +1 -0
  11. package/app/app/page.tsx +1 -2
  12. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  13. package/app/components/ActivityBar.tsx +2 -2
  14. package/app/components/Backlinks.tsx +5 -5
  15. package/app/components/CreateSpaceModal.tsx +3 -2
  16. package/app/components/DirPicker.tsx +1 -1
  17. package/app/components/DirView.tsx +2 -3
  18. package/app/components/EditorWrapper.tsx +3 -3
  19. package/app/components/FileTree.tsx +25 -10
  20. package/app/components/GuideCard.tsx +4 -4
  21. package/app/components/HomeContent.tsx +44 -14
  22. package/app/components/MarkdownView.tsx +2 -2
  23. package/app/components/OnboardingView.tsx +1 -1
  24. package/app/components/Panel.tsx +1 -1
  25. package/app/components/RightAgentDetailPanel.tsx +2 -1
  26. package/app/components/RightAskPanel.tsx +1 -1
  27. package/app/components/SearchModal.tsx +10 -2
  28. package/app/components/SidebarLayout.tsx +36 -10
  29. package/app/components/ThemeToggle.tsx +1 -1
  30. package/app/components/agents/AgentDetailContent.tsx +454 -59
  31. package/app/components/agents/AgentsContentPage.tsx +89 -20
  32. package/app/components/agents/AgentsMcpSection.tsx +513 -85
  33. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  34. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  35. package/app/components/agents/AgentsSkillsSection.tsx +746 -105
  36. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  37. package/app/components/agents/agents-content-model.ts +308 -10
  38. package/app/components/ask/AskContent.tsx +34 -5
  39. package/app/components/ask/FileChip.tsx +1 -0
  40. package/app/components/ask/MentionPopover.tsx +13 -1
  41. package/app/components/ask/MessageList.tsx +5 -7
  42. package/app/components/ask/ToolCallBlock.tsx +4 -4
  43. package/app/components/changes/ChangesBanner.tsx +89 -13
  44. package/app/components/changes/ChangesContentPage.tsx +134 -51
  45. package/app/components/echo/EchoHero.tsx +10 -24
  46. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  47. package/app/components/echo/EchoPageSections.tsx +13 -9
  48. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  49. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  50. package/app/components/explore/ExploreContent.tsx +3 -7
  51. package/app/components/explore/UseCaseCard.tsx +4 -15
  52. package/app/components/panels/AgentsPanel.tsx +22 -128
  53. package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
  54. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
  55. package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
  56. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  57. package/app/components/panels/EchoPanel.tsx +8 -10
  58. package/app/components/panels/PanelNavRow.tsx +9 -2
  59. package/app/components/panels/PluginsPanel.tsx +5 -5
  60. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  61. package/app/components/renderers/agent-inspector/manifest.ts +5 -3
  62. package/app/components/renderers/config/manifest.ts +1 -0
  63. package/app/components/renderers/csv/manifest.ts +1 -0
  64. package/app/components/renderers/todo/manifest.ts +1 -0
  65. package/app/components/settings/AiTab.tsx +3 -3
  66. package/app/components/settings/AppearanceTab.tsx +2 -2
  67. package/app/components/settings/KnowledgeTab.tsx +3 -3
  68. package/app/components/settings/McpAgentInstall.tsx +3 -6
  69. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  70. package/app/components/settings/McpSkillRow.tsx +2 -3
  71. package/app/components/settings/McpSkillsSection.tsx +2 -2
  72. package/app/components/settings/McpTab.tsx +12 -13
  73. package/app/components/settings/MonitoringTab.tsx +13 -13
  74. package/app/components/settings/PluginsTab.tsx +6 -5
  75. package/app/components/settings/Primitives.tsx +3 -4
  76. package/app/components/settings/SettingsContent.tsx +3 -3
  77. package/app/components/settings/SyncTab.tsx +11 -17
  78. package/app/components/settings/UpdateTab.tsx +18 -21
  79. package/app/components/settings/types.ts +14 -0
  80. package/app/components/setup/StepKB.tsx +1 -1
  81. package/app/hooks/useMcpData.tsx +7 -4
  82. package/app/hooks/useMention.ts +25 -8
  83. package/app/lib/agent/log.ts +15 -18
  84. package/app/lib/agent/stream-consumer.ts +3 -0
  85. package/app/lib/agent/to-agent-messages.ts +6 -4
  86. package/app/lib/core/agent-audit-log.ts +280 -0
  87. package/app/lib/core/content-changes.ts +148 -8
  88. package/app/lib/core/index.ts +11 -0
  89. package/app/lib/fs.ts +16 -1
  90. package/app/lib/i18n-en.ts +317 -36
  91. package/app/lib/i18n-zh.ts +316 -35
  92. package/app/lib/mcp-agents.ts +273 -2
  93. package/app/lib/renderers/index.ts +1 -2
  94. package/app/lib/renderers/registry.ts +10 -0
  95. package/app/lib/types.ts +2 -0
  96. package/app/next-env.d.ts +1 -1
  97. package/bin/lib/mcp-agents.js +38 -13
  98. package/package.json +1 -1
  99. package/scripts/migrate-agent-audit-log.js +170 -0
  100. package/scripts/migrate-agent-diff.js +146 -0
  101. package/scripts/setup.js +12 -17
  102. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  103. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  104. 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, ShieldCheck, Activity, Compass } from 'lucide-react';
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 { resolveAgentStatus } from './agents-content-model';
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 enabledSkills = useMemo(() => mcp.skills.filter((s) => s.enabled), [mcp.skills]);
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
- <div>
37
- <Link href="/agents" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground mb-3">
38
- <ArrowLeft size={14} />
39
- {a.backToOverview}
40
- </Link>
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
- <section className="rounded-lg border border-border bg-card p-4">
46
- <h2 className="text-sm font-medium text-foreground mb-2">{a.detail.identity}</h2>
47
- <div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
48
- <DetailLine label={a.detail.agentKey} value={agent.key} />
49
- <DetailLine label={a.detail.status} value={status} />
50
- <DetailLine label={a.detail.transport} value={agent.transport ?? agent.preferredTransport} />
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
- <section className="rounded-lg border border-border bg-card p-4">
55
- <h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
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.connection}
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.endpoint} value={mcp.status?.endpoint ?? a.na} />
61
- <DetailLine label={a.detail.port} value={String(mcp.status?.port ?? a.na)} />
62
- <DetailLine label={a.detail.auth} value={mcp.status?.authConfigured ? a.detail.authConfigured : a.detail.authMissing} />
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
- <section className="rounded-lg border border-border bg-card p-4">
67
- <h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
68
- <ShieldCheck size={14} className="text-muted-foreground" />
69
- {a.detail.capabilities}
70
- </h2>
71
- <ul className="text-sm text-muted-foreground space-y-1">
72
- <li>{a.detail.projectScope}: {agent.hasProjectScope ? a.detail.yes : a.detail.no}</li>
73
- <li>{a.detail.globalScope}: {agent.hasGlobalScope ? a.detail.yes : a.detail.no}</li>
74
- <li>{a.detail.format}: {agent.format}</li>
75
- </ul>
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
- <section className="rounded-lg border border-border bg-card p-4">
79
- <h2 className="text-sm font-medium text-foreground mb-2">{a.detail.skillAssignments}</h2>
80
- {enabledSkills.length === 0 ? (
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
- <section className="rounded-lg border border-border bg-card p-4">
92
- <h2 className="text-sm font-medium text-foreground mb-2 flex items-center gap-1.5">
93
- <Activity size={14} className="text-muted-foreground" />
94
- {a.detail.recentActivity}
95
- </h2>
96
- <p className="text-sm text-muted-foreground">{a.detail.noActivity}</p>
97
- </section>
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
- <section className="rounded-lg border border-border bg-card p-4">
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
  }