@geminilight/mindos 0.5.64 → 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 (85) 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/file/route.ts +9 -0
  5. package/app/app/api/mcp/agents/route.ts +27 -1
  6. package/app/app/api/skills/route.ts +18 -2
  7. package/app/app/api/tree-version/route.ts +8 -0
  8. package/app/components/ActivityBar.tsx +2 -2
  9. package/app/components/Backlinks.tsx +5 -5
  10. package/app/components/CreateSpaceModal.tsx +3 -2
  11. package/app/components/DirPicker.tsx +1 -1
  12. package/app/components/DirView.tsx +2 -3
  13. package/app/components/EditorWrapper.tsx +3 -3
  14. package/app/components/FileTree.tsx +25 -10
  15. package/app/components/GuideCard.tsx +4 -4
  16. package/app/components/HomeContent.tsx +6 -11
  17. package/app/components/MarkdownView.tsx +2 -2
  18. package/app/components/OnboardingView.tsx +1 -1
  19. package/app/components/Panel.tsx +1 -1
  20. package/app/components/RightAgentDetailPanel.tsx +1 -1
  21. package/app/components/RightAskPanel.tsx +1 -1
  22. package/app/components/SearchModal.tsx +10 -2
  23. package/app/components/SidebarLayout.tsx +35 -10
  24. package/app/components/ThemeToggle.tsx +1 -1
  25. package/app/components/agents/AgentDetailContent.tsx +454 -59
  26. package/app/components/agents/AgentsContentPage.tsx +70 -5
  27. package/app/components/agents/AgentsMcpSection.tsx +474 -159
  28. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  29. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  30. package/app/components/agents/AgentsSkillsSection.tsx +739 -121
  31. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  32. package/app/components/agents/agents-content-model.ts +292 -10
  33. package/app/components/ask/AskContent.tsx +34 -5
  34. package/app/components/ask/FileChip.tsx +1 -0
  35. package/app/components/ask/MentionPopover.tsx +13 -1
  36. package/app/components/ask/MessageList.tsx +5 -7
  37. package/app/components/ask/ToolCallBlock.tsx +4 -4
  38. package/app/components/changes/ChangesBanner.tsx +1 -2
  39. package/app/components/echo/EchoHero.tsx +10 -24
  40. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  41. package/app/components/echo/EchoPageSections.tsx +13 -9
  42. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  43. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  44. package/app/components/explore/ExploreContent.tsx +3 -7
  45. package/app/components/explore/UseCaseCard.tsx +4 -15
  46. package/app/components/panels/AgentsPanel.tsx +12 -104
  47. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  48. package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
  49. package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
  50. package/app/components/panels/EchoPanel.tsx +8 -10
  51. package/app/components/panels/PanelNavRow.tsx +9 -2
  52. package/app/components/panels/PluginsPanel.tsx +2 -2
  53. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  54. package/app/components/renderers/agent-inspector/manifest.ts +3 -3
  55. package/app/components/renderers/todo/manifest.ts +1 -0
  56. package/app/components/settings/AiTab.tsx +3 -3
  57. package/app/components/settings/AppearanceTab.tsx +2 -2
  58. package/app/components/settings/KnowledgeTab.tsx +3 -3
  59. package/app/components/settings/McpAgentInstall.tsx +3 -6
  60. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  61. package/app/components/settings/McpSkillRow.tsx +2 -3
  62. package/app/components/settings/McpSkillsSection.tsx +2 -2
  63. package/app/components/settings/McpTab.tsx +12 -13
  64. package/app/components/settings/MonitoringTab.tsx +13 -13
  65. package/app/components/settings/PluginsTab.tsx +2 -2
  66. package/app/components/settings/Primitives.tsx +3 -4
  67. package/app/components/settings/SettingsContent.tsx +3 -3
  68. package/app/components/settings/SyncTab.tsx +11 -17
  69. package/app/components/settings/UpdateTab.tsx +18 -21
  70. package/app/components/settings/types.ts +14 -0
  71. package/app/components/setup/StepKB.tsx +1 -1
  72. package/app/hooks/useMcpData.tsx +4 -2
  73. package/app/hooks/useMention.ts +25 -8
  74. package/app/lib/agent/log.ts +15 -18
  75. package/app/lib/agent/stream-consumer.ts +3 -0
  76. package/app/lib/agent/to-agent-messages.ts +6 -4
  77. package/app/lib/core/agent-audit-log.ts +280 -0
  78. package/app/lib/core/index.ts +11 -0
  79. package/app/lib/fs.ts +9 -0
  80. package/app/lib/i18n-en.ts +259 -33
  81. package/app/lib/i18n-zh.ts +258 -32
  82. package/app/lib/mcp-agents.ts +231 -2
  83. package/app/lib/types.ts +2 -0
  84. package/package.json +1 -1
  85. package/scripts/migrate-agent-audit-log.js +170 -0
@@ -137,18 +137,43 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
137
137
 
138
138
  const agentDockOpen = agentDetailKey !== null && lp.activePanel === 'agents';
139
139
 
140
- // Refresh file tree periodically
140
+ // Refresh file tree when server-side tree version changes.
141
+ // Polls a lightweight version counter every 3s — only calls router.refresh()
142
+ // (which rebuilds the full tree) when the version actually changes.
141
143
  useEffect(() => {
144
+ let lastVersion = -1;
145
+ let stopped = false;
146
+
147
+ const checkVersion = async () => {
148
+ if (stopped || document.visibilityState === 'hidden') return;
149
+ try {
150
+ const res = await fetch('/api/tree-version');
151
+ if (!res.ok) return;
152
+ const { v } = (await res.json()) as { v: number };
153
+ if (lastVersion === -1) {
154
+ lastVersion = v;
155
+ return;
156
+ }
157
+ if (v !== lastVersion) {
158
+ lastVersion = v;
159
+ router.refresh();
160
+ window.dispatchEvent(new Event('mindos:files-changed'));
161
+ }
162
+ } catch (err) { console.debug('[tree-version] poll failed', err); }
163
+ };
164
+
142
165
  const onVisible = () => {
143
- if (document.visibilityState === 'visible') router.refresh();
166
+ if (document.visibilityState === 'visible') void checkVersion();
144
167
  };
168
+
169
+ void checkVersion();
170
+ const interval = setInterval(() => void checkVersion(), 3_000);
145
171
  document.addEventListener('visibilitychange', onVisible);
146
- const interval = setInterval(() => {
147
- if (document.visibilityState === 'visible') router.refresh();
148
- }, 30_000);
172
+
149
173
  return () => {
150
- document.removeEventListener('visibilitychange', onVisible);
174
+ stopped = true;
151
175
  clearInterval(interval);
176
+ document.removeEventListener('visibilitychange', onVisible);
152
177
  };
153
178
  }, [router]);
154
179
 
@@ -224,8 +249,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
224
249
  {/* Skip link */}
225
250
  <a
226
251
  href="#main-content"
227
- className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[60] focus:px-4 focus:py-2 focus:rounded-lg focus:text-sm focus:font-medium focus:font-display"
228
- style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
252
+ className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[60] focus:px-4 focus:py-2 focus:rounded-lg focus:text-sm focus:font-medium focus:font-display bg-[var(--amber)] text-[var(--amber-foreground)]"
229
253
  >
230
254
  Skip to main content
231
255
  </a>
@@ -235,7 +259,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
235
259
  activePanel={railActivePanel}
236
260
  onPanelChange={lp.setActivePanel}
237
261
  onAgentsClick={() => {
238
- lp.setActivePanel((current) => (current === 'agents' ? null : 'agents'));
262
+ const wasActive = lp.activePanel === 'agents';
263
+ lp.setActivePanel(wasActive ? null : 'agents');
264
+ if (!wasActive) router.push('/agents');
239
265
  setAgentDetailKey(null);
240
266
  }}
241
267
  syncStatus={syncStatus}
@@ -273,7 +299,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
273
299
  maximized={lp.panelMaximized}
274
300
  onMaximize={lp.handlePanelMaximize}
275
301
  selectedAgentKey={agentDockOpen ? agentDetailKey : null}
276
- onOpenAgentDetail={setAgentDetailKey}
277
302
  />
278
303
  </div>
279
304
  <div className={`flex flex-col h-full ${lp.activePanel === 'discover' ? '' : 'hidden'}`}>
@@ -27,7 +27,7 @@ export default function ThemeToggle() {
27
27
  return (
28
28
  <button
29
29
  onClick={toggle}
30
- className="p-1.5 rounded-lg hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
30
+ className="p-1.5 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
31
31
  title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
32
32
  >
33
33
  {dark ? <Sun size={15} /> : <Moon size={15} />}
@@ -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
  }