@geminilight/mindos 0.5.64 → 0.5.66
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/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- 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 +6 -11
- 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 +1 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +35 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +70 -5
- package/app/components/agents/AgentsMcpSection.tsx +474 -159
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +739 -121
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +292 -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 +1 -2
- 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 +12 -104
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
- package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +3 -3
- 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 +2 -2
- 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 +4 -2
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/prompt.ts +17 -29
- 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/index.ts +11 -0
- package/app/lib/fs.ts +9 -0
- package/app/lib/i18n-en.ts +259 -33
- package/app/lib/i18n-zh.ts +258 -32
- package/app/lib/mcp-agents.ts +231 -2
- package/app/lib/types.ts +2 -0
- package/package.json +1 -1
- 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
|
|
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')
|
|
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
|
-
|
|
147
|
-
if (document.visibilityState === 'visible') router.refresh();
|
|
148
|
-
}, 30_000);
|
|
172
|
+
|
|
149
173
|
return () => {
|
|
150
|
-
|
|
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.
|
|
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-
|
|
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,
|
|
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
|
}
|