@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.
- 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/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
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from 'react';
|
|
4
|
-
import
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ChevronDown, ChevronRight, Search, Trash2, Zap } from 'lucide-react';
|
|
5
6
|
import { Toggle } from '@/components/settings/Primitives';
|
|
7
|
+
import { apiFetch } from '@/lib/api';
|
|
6
8
|
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
+
import type {
|
|
10
|
+
AgentBuckets,
|
|
11
|
+
SkillCapabilityFilter,
|
|
12
|
+
SkillWorkspaceStatusFilter,
|
|
13
|
+
UnifiedSourceFilter,
|
|
14
|
+
} from './agents-content-model';
|
|
15
|
+
import {
|
|
16
|
+
ActionButton,
|
|
17
|
+
AddAvatarButton,
|
|
18
|
+
AgentAvatar,
|
|
19
|
+
AgentPickerPopover,
|
|
20
|
+
BulkMessage,
|
|
21
|
+
ConfirmDialog,
|
|
22
|
+
EmptyState,
|
|
23
|
+
PillButton,
|
|
24
|
+
SearchInput,
|
|
25
|
+
} from './AgentsPrimitives';
|
|
26
|
+
import SkillDetailPopover from './SkillDetailPopover';
|
|
27
|
+
import {
|
|
28
|
+
aggregateCrossAgentSkills,
|
|
29
|
+
buildSkillAttentionSet,
|
|
30
|
+
buildUnifiedSkillList,
|
|
31
|
+
capabilityForSkill,
|
|
32
|
+
createBulkUnifiedTogglePlan,
|
|
33
|
+
filterUnifiedSkills,
|
|
34
|
+
groupUnifiedSkills,
|
|
35
|
+
resolveAgentStatus,
|
|
36
|
+
sortAgentsByStatus,
|
|
37
|
+
summarizeBulkSkillToggleResults,
|
|
38
|
+
type UnifiedSkillItem,
|
|
39
|
+
} from './agents-content-model';
|
|
40
|
+
|
|
41
|
+
type SkillView = 'bySkill' | 'byAgent';
|
|
9
42
|
|
|
10
43
|
export default function AgentsSkillsSection({
|
|
11
44
|
copy,
|
|
@@ -14,156 +47,741 @@ export default function AgentsSkillsSection({
|
|
|
14
47
|
}: {
|
|
15
48
|
copy: {
|
|
16
49
|
title: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
50
|
+
summaryEnabled: (n: number) => string;
|
|
51
|
+
summaryDisabled: (n: number) => string;
|
|
52
|
+
summaryAttention: (n: number) => string;
|
|
53
|
+
summaryNative: (n: number) => string;
|
|
54
|
+
tabs: { bySkill: string; byAgent: string; [k: string]: string };
|
|
22
55
|
searchPlaceholder: string;
|
|
23
56
|
sourceAll: string;
|
|
24
57
|
sourceBuiltin: string;
|
|
25
58
|
sourceUser: string;
|
|
26
|
-
|
|
59
|
+
sourceNative: string;
|
|
60
|
+
statusAll: string;
|
|
61
|
+
noSkillsMatchFilter: string;
|
|
62
|
+
statusEnabled: string;
|
|
63
|
+
statusDisabled: string;
|
|
64
|
+
statusAttention: string;
|
|
65
|
+
capabilityAll: string;
|
|
66
|
+
bulkEnableFiltered: string;
|
|
67
|
+
bulkDisableFiltered: string;
|
|
68
|
+
bulkRunning: string;
|
|
69
|
+
bulkNoChanges: string;
|
|
70
|
+
bulkAllSucceeded: (n: number) => string;
|
|
71
|
+
bulkPartialFailed: (ok: number, failed: number) => string;
|
|
72
|
+
resultCount: (n: number) => string;
|
|
73
|
+
agentNativeSkills: string;
|
|
74
|
+
agentMindosSkills: string;
|
|
75
|
+
noAgentsYet: string;
|
|
76
|
+
moreSkills: (n: number) => string;
|
|
77
|
+
addAgentToSkill: string;
|
|
78
|
+
removeAgentFromSkill: string;
|
|
79
|
+
confirmRemoveAgentTitle: string;
|
|
80
|
+
confirmRemoveAgentMessage: (agent: string, skill: string) => string;
|
|
81
|
+
cancelSkillAction: string;
|
|
82
|
+
noAvailableAgentsForSkill: string;
|
|
83
|
+
manualSkillHint: string;
|
|
84
|
+
skillDescription: string;
|
|
85
|
+
skillNoDescription: string;
|
|
86
|
+
skillAgentCount: (n: number) => string;
|
|
87
|
+
skillDeleteAction: string;
|
|
88
|
+
confirmDeleteSkillTitle: string;
|
|
89
|
+
confirmDeleteSkillMessage: (name: string) => string;
|
|
90
|
+
skillDeleted: string;
|
|
91
|
+
skillDeleteFailed: string;
|
|
92
|
+
copyInstallCmd: string;
|
|
93
|
+
installCmdCopied: string;
|
|
94
|
+
quickStatsMcp: (n: number) => string;
|
|
95
|
+
quickStatsSkills: (n: number) => string;
|
|
96
|
+
showAllNative: (n: number) => string;
|
|
97
|
+
collapseNative: string;
|
|
27
98
|
groupLabels: Record<'research' | 'coding' | 'docs' | 'ops' | 'memory', string>;
|
|
99
|
+
skillPopover: {
|
|
100
|
+
close: string;
|
|
101
|
+
source: string;
|
|
102
|
+
sourceBuiltin: string;
|
|
103
|
+
sourceUser: string;
|
|
104
|
+
sourceNative: string;
|
|
105
|
+
capability: string;
|
|
106
|
+
path: string;
|
|
107
|
+
enabled: string;
|
|
108
|
+
disabled: string;
|
|
109
|
+
agents: string;
|
|
110
|
+
noAgents: string;
|
|
111
|
+
content: string;
|
|
112
|
+
loading: string;
|
|
113
|
+
loadFailed: string;
|
|
114
|
+
retry: string;
|
|
115
|
+
copyContent: string;
|
|
116
|
+
copied: string;
|
|
117
|
+
noDescription: string;
|
|
118
|
+
deleteSkill: string;
|
|
119
|
+
confirmDeleteTitle: string;
|
|
120
|
+
confirmDeleteMessage: (name: string) => string;
|
|
121
|
+
confirmDeleteAction: string;
|
|
122
|
+
cancelAction: string;
|
|
123
|
+
deleted: string;
|
|
124
|
+
deleteFailed: string;
|
|
125
|
+
};
|
|
126
|
+
[k: string]: unknown;
|
|
28
127
|
};
|
|
29
128
|
mcp: McpContextValue;
|
|
30
129
|
buckets: AgentBuckets;
|
|
31
130
|
}) {
|
|
131
|
+
const [view, setView] = useState<SkillView>('bySkill');
|
|
32
132
|
const [query, setQuery] = useState('');
|
|
33
|
-
const [source, setSource] = useState<
|
|
34
|
-
const [
|
|
133
|
+
const [source, setSource] = useState<UnifiedSourceFilter>('all');
|
|
134
|
+
const [status, setStatus] = useState<SkillWorkspaceStatusFilter>('all');
|
|
135
|
+
const [capability, setCapability] = useState<SkillCapabilityFilter>('all');
|
|
136
|
+
const [bulkRunning, setBulkRunning] = useState(false);
|
|
137
|
+
const [bulkMessage, setBulkMessage] = useState<string | null>(null);
|
|
138
|
+
const [detailSkill, setDetailSkill] = useState<string | null>(null);
|
|
139
|
+
|
|
140
|
+
const crossAgentSkills = useMemo(() => aggregateCrossAgentSkills(mcp.agents), [mcp.agents]);
|
|
141
|
+
const sortedAgents = useMemo(() => sortAgentsByStatus(mcp.agents), [mcp.agents]);
|
|
142
|
+
|
|
143
|
+
const unified = useMemo(
|
|
144
|
+
() => buildUnifiedSkillList(mcp.skills, crossAgentSkills),
|
|
145
|
+
[mcp.skills, crossAgentSkills],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const enabledCount = useMemo(() => mcp.skills.filter((s) => s.enabled).length, [mcp.skills]);
|
|
149
|
+
const disabledCount = useMemo(() => mcp.skills.filter((s) => !s.enabled).length, [mcp.skills]);
|
|
150
|
+
const attentionCount = useMemo(() => buildSkillAttentionSet(mcp.skills).size, [mcp.skills]);
|
|
151
|
+
const nativeCount = useMemo(() => unified.filter((s) => s.kind === 'native').length, [unified]);
|
|
152
|
+
|
|
153
|
+
const filtered = useMemo(
|
|
154
|
+
() => filterUnifiedSkills(unified, { query, source, status, capability }),
|
|
155
|
+
[unified, query, source, status, capability],
|
|
156
|
+
);
|
|
157
|
+
const grouped = useMemo(() => groupUnifiedSkills(filtered), [filtered]);
|
|
158
|
+
|
|
159
|
+
const capabilityOptions = useMemo(
|
|
160
|
+
() => (['research', 'coding', 'docs', 'ops', 'memory'] as const).map((key) => ({ key, label: copy.groupLabels[key] })),
|
|
161
|
+
[copy.groupLabels],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const detailUnified = useMemo(
|
|
165
|
+
() => (detailSkill ? unified.find((s) => s.name === detailSkill) ?? null : null),
|
|
166
|
+
[detailSkill, unified],
|
|
167
|
+
);
|
|
168
|
+
const detailSkillInfo = useMemo(
|
|
169
|
+
() => (detailSkill ? mcp.skills.find((s) => s.name === detailSkill) ?? null : null),
|
|
170
|
+
[detailSkill, mcp.skills],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const handleDeleteFromPopover = useCallback(async (name: string) => {
|
|
174
|
+
await apiFetch('/api/skills', {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ action: 'delete', name }),
|
|
178
|
+
});
|
|
179
|
+
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
180
|
+
await mcp.refresh();
|
|
181
|
+
}, [mcp]);
|
|
35
182
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
183
|
+
const runBulkToggle = async (targetEnabled: boolean) => {
|
|
184
|
+
if (bulkRunning) return;
|
|
185
|
+
const plan = createBulkUnifiedTogglePlan(filtered, targetEnabled);
|
|
186
|
+
if (plan.length === 0) {
|
|
187
|
+
setBulkMessage(copy.bulkNoChanges);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
setBulkRunning(true);
|
|
191
|
+
setBulkMessage(copy.bulkRunning);
|
|
192
|
+
const results: Array<{ skillName: string; ok: boolean }> = [];
|
|
193
|
+
for (const skillName of plan) {
|
|
194
|
+
const ok = await mcp.toggleSkill(skillName, targetEnabled);
|
|
195
|
+
results.push({ skillName, ok });
|
|
196
|
+
}
|
|
197
|
+
const summary = summarizeBulkSkillToggleResults(results);
|
|
198
|
+
setBulkMessage(
|
|
199
|
+
summary.failed === 0
|
|
200
|
+
? copy.bulkAllSucceeded(summary.succeeded)
|
|
201
|
+
: copy.bulkPartialFailed(summary.succeeded, summary.failed),
|
|
202
|
+
);
|
|
203
|
+
setBulkRunning(false);
|
|
204
|
+
};
|
|
39
205
|
|
|
40
206
|
return (
|
|
41
|
-
<section className="
|
|
207
|
+
<section className="space-y-4 overflow-hidden" aria-label={copy.title}>
|
|
208
|
+
{/* Header */}
|
|
42
209
|
<div className="flex items-center justify-between gap-3">
|
|
43
|
-
<h2 className="text-sm font-medium text-foreground
|
|
44
|
-
<div className="flex items-center gap-1 rounded-md border border-border p-
|
|
45
|
-
<
|
|
46
|
-
<
|
|
210
|
+
<h2 className="text-sm font-medium text-foreground">{copy.title}</h2>
|
|
211
|
+
<div className="flex items-center gap-1 rounded-md border border-border p-0.5 bg-background" role="tablist" aria-label={copy.title}>
|
|
212
|
+
<PillButton active={view === 'bySkill'} label={copy.tabs.bySkill} onClick={() => setView('bySkill')} />
|
|
213
|
+
<PillButton active={view === 'byAgent'} label={copy.tabs.byAgent} onClick={() => setView('byAgent')} />
|
|
47
214
|
</div>
|
|
48
215
|
</div>
|
|
49
|
-
<p className="text-xs text-muted-foreground">{copy.capabilityGroups}</p>
|
|
50
|
-
|
|
51
|
-
{view === 'manage' ? (
|
|
52
|
-
<>
|
|
53
|
-
<div className="flex flex-col md:flex-row gap-2">
|
|
54
|
-
<label className="relative flex-1">
|
|
55
|
-
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
56
|
-
<input
|
|
57
|
-
value={query}
|
|
58
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
59
|
-
placeholder={copy.searchPlaceholder}
|
|
60
|
-
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"
|
|
61
|
-
/>
|
|
62
|
-
</label>
|
|
63
|
-
<div className="flex items-center gap-1 rounded-md border border-border p-1 bg-background">
|
|
64
|
-
<SourceFilterButton active={source === 'all'} label={copy.sourceAll} onClick={() => setSource('all')} />
|
|
65
|
-
<SourceFilterButton active={source === 'builtin'} label={copy.sourceBuiltin} onClick={() => setSource('builtin')} />
|
|
66
|
-
<SourceFilterButton active={source === 'user'} label={copy.sourceUser} onClick={() => setSource('user')} />
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
216
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
217
|
+
{/* Compact status strip */}
|
|
218
|
+
<div className="rounded-lg border border-border bg-card p-3">
|
|
219
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
|
|
220
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
221
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--success)]" aria-hidden="true" />
|
|
222
|
+
{copy.summaryEnabled(enabledCount)}
|
|
223
|
+
</span>
|
|
224
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
225
|
+
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground" aria-hidden="true" />
|
|
226
|
+
{copy.summaryDisabled(disabledCount)}
|
|
227
|
+
</span>
|
|
228
|
+
{attentionCount > 0 && (
|
|
229
|
+
<span className="inline-flex items-center gap-1.5 text-[var(--amber)]">
|
|
230
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]" aria-hidden="true" />
|
|
231
|
+
{copy.summaryAttention(attentionCount)}
|
|
232
|
+
</span>
|
|
233
|
+
)}
|
|
234
|
+
{nativeCount > 0 && (
|
|
235
|
+
<>
|
|
236
|
+
<span className="text-muted-foreground/40" aria-hidden="true">|</span>
|
|
237
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
238
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]" aria-hidden="true" />
|
|
239
|
+
{copy.summaryNative(nativeCount)}
|
|
240
|
+
</span>
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Search */}
|
|
247
|
+
<SearchInput
|
|
248
|
+
value={query}
|
|
249
|
+
onChange={setQuery}
|
|
250
|
+
placeholder={copy.searchPlaceholder}
|
|
251
|
+
ariaLabel={copy.searchPlaceholder}
|
|
252
|
+
icon={Search}
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
{view === 'bySkill' ? (
|
|
256
|
+
<BySkillView
|
|
257
|
+
copy={copy}
|
|
258
|
+
filtered={filtered}
|
|
259
|
+
grouped={grouped}
|
|
260
|
+
allAgents={sortedAgents}
|
|
261
|
+
source={source}
|
|
262
|
+
status={status}
|
|
263
|
+
capability={capability}
|
|
264
|
+
capabilityOptions={capabilityOptions}
|
|
265
|
+
bulkRunning={bulkRunning}
|
|
266
|
+
bulkMessage={bulkMessage}
|
|
267
|
+
onSourceChange={setSource}
|
|
268
|
+
onStatusChange={setStatus}
|
|
269
|
+
onCapabilityChange={setCapability}
|
|
270
|
+
onBulkToggle={runBulkToggle}
|
|
271
|
+
onToggleSkill={mcp.toggleSkill}
|
|
272
|
+
onRefresh={mcp.refresh}
|
|
273
|
+
onOpenDetail={setDetailSkill}
|
|
274
|
+
/>
|
|
275
|
+
) : (
|
|
276
|
+
<ByAgentView
|
|
277
|
+
copy={copy}
|
|
278
|
+
agents={sortedAgents}
|
|
279
|
+
skills={mcp.skills}
|
|
280
|
+
crossAgentSkills={crossAgentSkills}
|
|
281
|
+
query={query}
|
|
282
|
+
onToggleSkill={mcp.toggleSkill}
|
|
283
|
+
onOpenDetail={setDetailSkill}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Skill detail popover */}
|
|
288
|
+
<SkillDetailPopover
|
|
289
|
+
open={detailSkill !== null}
|
|
290
|
+
skillName={detailSkill}
|
|
291
|
+
skill={detailSkillInfo}
|
|
292
|
+
agentNames={detailUnified?.agents ?? []}
|
|
293
|
+
isNative={detailUnified?.kind === 'native'}
|
|
294
|
+
nativeSourcePath={
|
|
295
|
+
detailUnified?.kind === 'native' && detailUnified.agents.length > 0
|
|
296
|
+
? mcp.agents.find((a) => a.name === detailUnified.agents[0])?.installedSkillSourcePath
|
|
297
|
+
: undefined
|
|
298
|
+
}
|
|
299
|
+
copy={copy.skillPopover}
|
|
300
|
+
onClose={() => setDetailSkill(null)}
|
|
301
|
+
onToggle={mcp.toggleSkill}
|
|
302
|
+
onDelete={handleDeleteFromPopover}
|
|
303
|
+
onRefresh={mcp.refresh}
|
|
304
|
+
/>
|
|
305
|
+
</section>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ────────── By Skill View (Unified: MindOS + Native) ────────── */
|
|
310
|
+
|
|
311
|
+
function BySkillView({
|
|
312
|
+
copy,
|
|
313
|
+
filtered,
|
|
314
|
+
grouped,
|
|
315
|
+
allAgents,
|
|
316
|
+
source,
|
|
317
|
+
status,
|
|
318
|
+
capability,
|
|
319
|
+
capabilityOptions,
|
|
320
|
+
bulkRunning,
|
|
321
|
+
bulkMessage,
|
|
322
|
+
onSourceChange,
|
|
323
|
+
onStatusChange,
|
|
324
|
+
onCapabilityChange,
|
|
325
|
+
onBulkToggle,
|
|
326
|
+
onToggleSkill,
|
|
327
|
+
onRefresh,
|
|
328
|
+
onOpenDetail,
|
|
329
|
+
}: {
|
|
330
|
+
copy: Parameters<typeof AgentsSkillsSection>[0]['copy'];
|
|
331
|
+
filtered: UnifiedSkillItem[];
|
|
332
|
+
grouped: ReturnType<typeof groupUnifiedSkills>;
|
|
333
|
+
allAgents: ReturnType<typeof sortAgentsByStatus>;
|
|
334
|
+
source: UnifiedSourceFilter;
|
|
335
|
+
status: SkillWorkspaceStatusFilter;
|
|
336
|
+
capability: SkillCapabilityFilter;
|
|
337
|
+
capabilityOptions: Array<{ key: string; label: string }>;
|
|
338
|
+
bulkRunning: boolean;
|
|
339
|
+
bulkMessage: string | null;
|
|
340
|
+
onSourceChange: (s: UnifiedSourceFilter) => void;
|
|
341
|
+
onStatusChange: (s: SkillWorkspaceStatusFilter) => void;
|
|
342
|
+
onCapabilityChange: (s: SkillCapabilityFilter) => void;
|
|
343
|
+
onBulkToggle: (enabled: boolean) => Promise<void>;
|
|
344
|
+
onToggleSkill: (name: string, enabled: boolean) => Promise<boolean>;
|
|
345
|
+
onRefresh: () => Promise<void>;
|
|
346
|
+
onOpenDetail: (name: string) => void;
|
|
347
|
+
}) {
|
|
348
|
+
const [confirmAgentRemove, setConfirmAgentRemove] = useState<{ agentName: string; skillName: string } | null>(null);
|
|
349
|
+
const [confirmSkillDelete, setConfirmSkillDelete] = useState<string | null>(null);
|
|
350
|
+
const [pickerSkill, setPickerSkill] = useState<string | null>(null);
|
|
351
|
+
const [hintMessage, setHintMessage] = useState<string | null>(null);
|
|
352
|
+
const [deleteBusy, setDeleteBusy] = useState<string | null>(null);
|
|
353
|
+
|
|
354
|
+
const handleConfirmAgentRemove = useCallback(() => {
|
|
355
|
+
setConfirmAgentRemove(null);
|
|
356
|
+
setHintMessage(copy.manualSkillHint);
|
|
357
|
+
setTimeout(() => setHintMessage(null), 4000);
|
|
358
|
+
}, [copy.manualSkillHint]);
|
|
359
|
+
|
|
360
|
+
const handleDeleteSkill = useCallback(async (name: string) => {
|
|
361
|
+
setConfirmSkillDelete(null);
|
|
362
|
+
setDeleteBusy(name);
|
|
363
|
+
try {
|
|
364
|
+
await apiFetch('/api/skills', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: { 'Content-Type': 'application/json' },
|
|
367
|
+
body: JSON.stringify({ action: 'delete', name }),
|
|
368
|
+
});
|
|
369
|
+
setHintMessage(copy.skillDeleted);
|
|
370
|
+
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
371
|
+
await onRefresh();
|
|
372
|
+
} catch {
|
|
373
|
+
setHintMessage(copy.skillDeleteFailed);
|
|
374
|
+
} finally {
|
|
375
|
+
setDeleteBusy(null);
|
|
376
|
+
setTimeout(() => setHintMessage(null), 3000);
|
|
377
|
+
}
|
|
378
|
+
}, [copy.skillDeleted, copy.skillDeleteFailed, onRefresh]);
|
|
379
|
+
|
|
380
|
+
const sortedGrouped = useMemo(() => {
|
|
381
|
+
const entries: Array<[string, UnifiedSkillItem[]]> = [];
|
|
382
|
+
for (const [key, skills] of Object.entries(grouped)) {
|
|
383
|
+
if (skills.length === 0) continue;
|
|
384
|
+
const sorted = [...skills].sort((a, b) => {
|
|
385
|
+
if (a.kind !== b.kind) return a.kind === 'mindos' ? -1 : 1;
|
|
386
|
+
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
387
|
+
return a.name.localeCompare(b.name);
|
|
388
|
+
});
|
|
389
|
+
entries.push([key, sorted]);
|
|
390
|
+
}
|
|
391
|
+
return entries;
|
|
392
|
+
}, [grouped]);
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<>
|
|
396
|
+
{/* Filters */}
|
|
397
|
+
<div className="flex flex-wrap gap-2">
|
|
398
|
+
<div role="group" aria-label="Source" className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
399
|
+
<PillButton active={source === 'all'} label={copy.sourceAll} onClick={() => onSourceChange('all')} />
|
|
400
|
+
<PillButton active={source === 'builtin'} label={copy.sourceBuiltin} onClick={() => onSourceChange('builtin')} />
|
|
401
|
+
<PillButton active={source === 'user'} label={copy.sourceUser} onClick={() => onSourceChange('user')} />
|
|
402
|
+
<PillButton active={source === 'native'} label={copy.sourceNative} onClick={() => onSourceChange('native')} />
|
|
403
|
+
</div>
|
|
404
|
+
<div role="group" aria-label="Status" className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
405
|
+
<PillButton active={status === 'all'} label={copy.statusAll} onClick={() => onStatusChange('all')} />
|
|
406
|
+
<PillButton active={status === 'enabled'} label={copy.statusEnabled} onClick={() => onStatusChange('enabled')} />
|
|
407
|
+
<PillButton active={status === 'disabled'} label={copy.statusDisabled} onClick={() => onStatusChange('disabled')} />
|
|
408
|
+
<PillButton active={status === 'attention'} label={copy.statusAttention} onClick={() => onStatusChange('attention')} />
|
|
409
|
+
</div>
|
|
410
|
+
<div role="group" aria-label="Capability" className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
411
|
+
<PillButton active={capability === 'all'} label={copy.capabilityAll} onClick={() => onCapabilityChange('all')} />
|
|
412
|
+
{capabilityOptions.map((opt) => (
|
|
413
|
+
<PillButton key={opt.key} active={capability === opt.key} label={opt.label} onClick={() => onCapabilityChange(opt.key as SkillCapabilityFilter)} />
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
{/* Bulk actions (only affect MindOS skills) */}
|
|
419
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
420
|
+
<ActionButton
|
|
421
|
+
onClick={() => void onBulkToggle(true)}
|
|
422
|
+
disabled={bulkRunning}
|
|
423
|
+
busy={bulkRunning}
|
|
424
|
+
label={copy.bulkEnableFiltered}
|
|
425
|
+
/>
|
|
426
|
+
<ActionButton
|
|
427
|
+
onClick={() => void onBulkToggle(false)}
|
|
428
|
+
disabled={bulkRunning}
|
|
429
|
+
busy={false}
|
|
430
|
+
label={copy.bulkDisableFiltered}
|
|
431
|
+
/>
|
|
432
|
+
<span className="text-2xs text-muted-foreground tabular-nums">{copy.resultCount(filtered.length)}</span>
|
|
433
|
+
<BulkMessage message={bulkMessage} />
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
{hintMessage && (
|
|
437
|
+
<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">
|
|
438
|
+
{hintMessage}
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{/* Grouped unified skill list */}
|
|
443
|
+
{sortedGrouped.length === 0 ? (
|
|
444
|
+
<EmptyState message={copy.noSkillsMatchFilter} />
|
|
445
|
+
) : (
|
|
446
|
+
<div className="space-y-3">
|
|
447
|
+
{sortedGrouped.map(([groupKey, sortedSkills]) => (
|
|
448
|
+
<div key={groupKey}>
|
|
449
|
+
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
450
|
+
{copy.groupLabels[groupKey as keyof typeof copy.groupLabels]} <span className="tabular-nums">({sortedSkills.length})</span>
|
|
451
|
+
</div>
|
|
452
|
+
<div className="space-y-3">
|
|
453
|
+
{sortedSkills.map((skill) => {
|
|
454
|
+
const availableAgents = allAgents
|
|
455
|
+
.filter((a) => !skill.agents.includes(a.name))
|
|
456
|
+
.map((a) => ({ key: a.key, name: a.name }));
|
|
457
|
+
const isUserSkill = skill.kind === 'mindos' && skill.source === 'user';
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<div key={skill.name} className="rounded-lg border border-border bg-card p-4 hover:border-border/60 hover:shadow-sm transition-all duration-150">
|
|
461
|
+
{/* Skill header */}
|
|
462
|
+
<div className="flex items-center justify-between gap-2 mb-2">
|
|
463
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
464
|
+
<Zap size={14} className={`shrink-0 ${skill.enabled ? 'text-[var(--amber)]' : 'text-muted-foreground/50'}`} aria-hidden="true" />
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
onClick={() => onOpenDetail(skill.name)}
|
|
468
|
+
className="text-sm font-medium text-foreground 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"
|
|
469
|
+
>
|
|
470
|
+
{skill.name}
|
|
471
|
+
</button>
|
|
472
|
+
<span className={`text-2xs shrink-0 px-1.5 py-0.5 rounded ${
|
|
473
|
+
skill.kind === 'native'
|
|
474
|
+
? 'bg-muted text-muted-foreground'
|
|
475
|
+
: skill.source === 'builtin'
|
|
476
|
+
? 'bg-muted text-muted-foreground'
|
|
477
|
+
: 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
478
|
+
}`}>
|
|
479
|
+
{skill.kind === 'native' ? copy.sourceNative : skill.source === 'builtin' ? copy.sourceBuiltin : copy.sourceUser}
|
|
480
|
+
</span>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
483
|
+
{isUserSkill && (
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
onClick={(e) => { e.stopPropagation(); setConfirmSkillDelete(skill.name); }}
|
|
487
|
+
disabled={deleteBusy === skill.name}
|
|
488
|
+
className="text-muted-foreground hover:text-destructive cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded p-1 disabled:opacity-50 transition-colors duration-150"
|
|
489
|
+
aria-label={`${copy.skillDeleteAction} ${skill.name}`}
|
|
490
|
+
>
|
|
491
|
+
<Trash2 size={14} />
|
|
492
|
+
</button>
|
|
493
|
+
)}
|
|
494
|
+
{skill.kind === 'mindos' ? (
|
|
495
|
+
<Toggle size="sm" checked={skill.enabled} onChange={(v) => void onToggleSkill(skill.name, v)} />
|
|
496
|
+
) : (
|
|
497
|
+
<span className="text-2xs text-muted-foreground/60 select-none" aria-label="read-only">—</span>
|
|
498
|
+
)}
|
|
499
|
+
<div className="relative">
|
|
500
|
+
<AddAvatarButton
|
|
501
|
+
onClick={() => setPickerSkill(pickerSkill === skill.name ? null : skill.name)}
|
|
502
|
+
label={copy.addAgentToSkill}
|
|
503
|
+
size="sm"
|
|
504
|
+
/>
|
|
505
|
+
<AgentPickerPopover
|
|
506
|
+
open={pickerSkill === skill.name}
|
|
507
|
+
agents={availableAgents}
|
|
508
|
+
emptyLabel={copy.noAvailableAgentsForSkill}
|
|
509
|
+
onSelect={() => {
|
|
510
|
+
setPickerSkill(null);
|
|
511
|
+
setHintMessage(copy.manualSkillHint);
|
|
512
|
+
setTimeout(() => setHintMessage(null), 4000);
|
|
513
|
+
}}
|
|
514
|
+
onClose={() => setPickerSkill(null)}
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
85
517
|
</div>
|
|
86
|
-
<Toggle size="sm" checked={skill.enabled} onChange={(v) => void mcp.toggleSkill(skill.name, v)} />
|
|
87
518
|
</div>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
519
|
+
|
|
520
|
+
{/* Agent count */}
|
|
521
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-muted-foreground mb-3">
|
|
522
|
+
<span className="tabular-nums">{copy.skillAgentCount(skill.agents.length)}</span>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{/* Agent avatar grid */}
|
|
526
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
527
|
+
{skill.agents.map((name) => (
|
|
528
|
+
<AgentAvatar
|
|
529
|
+
key={name}
|
|
530
|
+
name={name}
|
|
531
|
+
onRemove={() => setConfirmAgentRemove({ agentName: name, skillName: skill.name })}
|
|
532
|
+
/>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
})}
|
|
91
538
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</>
|
|
95
|
-
) : (
|
|
96
|
-
<div className="rounded-md border border-border bg-background p-3">
|
|
97
|
-
<div className="overflow-x-auto">
|
|
98
|
-
<table className="w-full text-xs">
|
|
99
|
-
<thead>
|
|
100
|
-
<tr className="border-b border-border">
|
|
101
|
-
<th className="text-left py-2 text-muted-foreground font-medium">Skill</th>
|
|
102
|
-
{knownAgents.map((agent) => (
|
|
103
|
-
<th key={agent.key} className="text-left py-2 text-muted-foreground font-medium pr-3">{agent.name}</th>
|
|
104
|
-
))}
|
|
105
|
-
</tr>
|
|
106
|
-
</thead>
|
|
107
|
-
<tbody>
|
|
108
|
-
{filtered.map((skill) => (
|
|
109
|
-
<tr key={skill.name} className="border-b border-border/40">
|
|
110
|
-
<td className="py-2 text-foreground pr-3">{skill.name}</td>
|
|
111
|
-
{knownAgents.map((agent) => (
|
|
112
|
-
<td key={`${skill.name}:${agent.key}`} className="py-2 pr-3 text-muted-foreground">
|
|
113
|
-
{agent.present ? (skill.enabled ? 'Enabled' : 'Disabled') : 'Unsupported'}
|
|
114
|
-
</td>
|
|
115
|
-
))}
|
|
116
|
-
</tr>
|
|
117
|
-
))}
|
|
118
|
-
</tbody>
|
|
119
|
-
</table>
|
|
120
|
-
</div>
|
|
539
|
+
</div>
|
|
540
|
+
))}
|
|
121
541
|
</div>
|
|
122
542
|
)}
|
|
123
|
-
|
|
543
|
+
|
|
544
|
+
{/* Confirm: remove agent from skill */}
|
|
545
|
+
<ConfirmDialog
|
|
546
|
+
open={confirmAgentRemove !== null}
|
|
547
|
+
title={copy.confirmRemoveAgentTitle}
|
|
548
|
+
message={confirmAgentRemove ? copy.confirmRemoveAgentMessage(confirmAgentRemove.agentName, confirmAgentRemove.skillName) : ''}
|
|
549
|
+
confirmLabel={copy.removeAgentFromSkill}
|
|
550
|
+
cancelLabel={copy.cancelSkillAction}
|
|
551
|
+
onConfirm={handleConfirmAgentRemove}
|
|
552
|
+
onCancel={() => setConfirmAgentRemove(null)}
|
|
553
|
+
variant="destructive"
|
|
554
|
+
/>
|
|
555
|
+
|
|
556
|
+
{/* Confirm: delete skill */}
|
|
557
|
+
<ConfirmDialog
|
|
558
|
+
open={confirmSkillDelete !== null}
|
|
559
|
+
title={copy.confirmDeleteSkillTitle}
|
|
560
|
+
message={confirmSkillDelete ? copy.confirmDeleteSkillMessage(confirmSkillDelete) : ''}
|
|
561
|
+
confirmLabel={copy.skillDeleteAction}
|
|
562
|
+
cancelLabel={copy.cancelSkillAction}
|
|
563
|
+
onConfirm={() => confirmSkillDelete && void handleDeleteSkill(confirmSkillDelete)}
|
|
564
|
+
onCancel={() => setConfirmSkillDelete(null)}
|
|
565
|
+
variant="destructive"
|
|
566
|
+
/>
|
|
567
|
+
</>
|
|
124
568
|
);
|
|
125
569
|
}
|
|
126
570
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
571
|
+
/* ────────── By Agent View ────────── */
|
|
572
|
+
|
|
573
|
+
function ByAgentView({
|
|
574
|
+
copy,
|
|
575
|
+
agents,
|
|
576
|
+
skills,
|
|
577
|
+
crossAgentSkills,
|
|
578
|
+
query,
|
|
579
|
+
onToggleSkill,
|
|
580
|
+
onOpenDetail,
|
|
131
581
|
}: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
582
|
+
copy: Parameters<typeof AgentsSkillsSection>[0]['copy'];
|
|
583
|
+
agents: ReturnType<typeof sortAgentsByStatus>;
|
|
584
|
+
skills: McpContextValue['skills'];
|
|
585
|
+
crossAgentSkills: ReturnType<typeof aggregateCrossAgentSkills>;
|
|
586
|
+
query: string;
|
|
587
|
+
onToggleSkill: (name: string, enabled: boolean) => Promise<boolean>;
|
|
588
|
+
onOpenDetail: (name: string) => void;
|
|
135
589
|
}) {
|
|
590
|
+
const q = query.trim().toLowerCase();
|
|
591
|
+
|
|
592
|
+
const filteredAgents = useMemo(() => {
|
|
593
|
+
if (!q) return agents;
|
|
594
|
+
return agents.filter((a) => {
|
|
595
|
+
const haystack = `${a.name} ${a.key}`.toLowerCase();
|
|
596
|
+
if (haystack.includes(q)) return true;
|
|
597
|
+
const nativeSkills = a.installedSkillNames ?? [];
|
|
598
|
+
return nativeSkills.some((s) => s.toLowerCase().includes(q));
|
|
599
|
+
});
|
|
600
|
+
}, [agents, q]);
|
|
601
|
+
|
|
602
|
+
const filteredSkills = useMemo(() => {
|
|
603
|
+
if (!q) return skills;
|
|
604
|
+
return skills.filter((s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
|
|
605
|
+
}, [skills, q]);
|
|
606
|
+
|
|
607
|
+
if (filteredAgents.length === 0) {
|
|
608
|
+
return <EmptyState message={copy.noAgentsYet} />;
|
|
609
|
+
}
|
|
610
|
+
|
|
136
611
|
return (
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
612
|
+
<div className="space-y-3">
|
|
613
|
+
{filteredAgents.map((agent) => {
|
|
614
|
+
const agentStatus = resolveAgentStatus(agent);
|
|
615
|
+
const nativeSkills = (agent.installedSkillNames ?? []).sort();
|
|
616
|
+
const mcpServers = agent.configuredMcpServers ?? [];
|
|
617
|
+
const agentMindosSkills = (agent.present ? filteredSkills : [])
|
|
618
|
+
.slice()
|
|
619
|
+
.sort((a, b) => {
|
|
620
|
+
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
621
|
+
return a.name.localeCompare(b.name);
|
|
622
|
+
});
|
|
623
|
+
const totalSkills = nativeSkills.length + agentMindosSkills.length;
|
|
624
|
+
|
|
625
|
+
return (
|
|
626
|
+
<AgentCard
|
|
627
|
+
key={agent.key}
|
|
628
|
+
agentKey={agent.key}
|
|
629
|
+
name={agent.name}
|
|
630
|
+
status={agentStatus}
|
|
631
|
+
skillMode={agent.skillMode}
|
|
632
|
+
mcpCount={mcpServers.length}
|
|
633
|
+
totalSkills={totalSkills}
|
|
634
|
+
nativeSkills={nativeSkills}
|
|
635
|
+
mindosSkills={agentMindosSkills}
|
|
636
|
+
copy={copy}
|
|
637
|
+
onToggleSkill={onToggleSkill}
|
|
638
|
+
onOpenDetail={onOpenDetail}
|
|
639
|
+
/>
|
|
640
|
+
);
|
|
641
|
+
})}
|
|
642
|
+
</div>
|
|
146
643
|
);
|
|
147
644
|
}
|
|
148
645
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
646
|
+
/* ────────── Agent Card (Skills ByAgent) ────────── */
|
|
647
|
+
|
|
648
|
+
function AgentCard({
|
|
649
|
+
agentKey,
|
|
650
|
+
name,
|
|
651
|
+
status,
|
|
652
|
+
skillMode,
|
|
653
|
+
mcpCount,
|
|
654
|
+
totalSkills,
|
|
655
|
+
nativeSkills,
|
|
656
|
+
mindosSkills,
|
|
657
|
+
copy,
|
|
658
|
+
onToggleSkill,
|
|
659
|
+
onOpenDetail,
|
|
153
660
|
}: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
661
|
+
agentKey: string;
|
|
662
|
+
name: string;
|
|
663
|
+
status: 'connected' | 'detected' | 'notFound';
|
|
664
|
+
skillMode: string | undefined;
|
|
665
|
+
mcpCount: number;
|
|
666
|
+
totalSkills: number;
|
|
667
|
+
nativeSkills: string[];
|
|
668
|
+
mindosSkills: McpContextValue['skills'];
|
|
669
|
+
copy: Parameters<typeof AgentsSkillsSection>[0]['copy'];
|
|
670
|
+
onToggleSkill: (name: string, enabled: boolean) => Promise<boolean>;
|
|
671
|
+
onOpenDetail: (name: string) => void;
|
|
157
672
|
}) {
|
|
673
|
+
const [nativeExpanded, setNativeExpanded] = useState(false);
|
|
674
|
+
const NATIVE_COLLAPSE_THRESHOLD = 6;
|
|
675
|
+
const showNativeToggle = nativeSkills.length > NATIVE_COLLAPSE_THRESHOLD;
|
|
676
|
+
const visibleNative = nativeExpanded ? nativeSkills : nativeSkills.slice(0, NATIVE_COLLAPSE_THRESHOLD);
|
|
677
|
+
|
|
158
678
|
return (
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
679
|
+
<div className="rounded-lg border border-border bg-card hover:border-border/60 hover:shadow-sm transition-all duration-150 overflow-hidden">
|
|
680
|
+
{/* Card header with avatar */}
|
|
681
|
+
<div className="flex items-center gap-3 p-4 pb-0">
|
|
682
|
+
<AgentAvatar name={name} status={status} />
|
|
683
|
+
<div className="min-w-0 flex-1">
|
|
684
|
+
<div className="flex items-center gap-2">
|
|
685
|
+
<Link href={`/agents/${encodeURIComponent(agentKey)}`} className="text-sm font-medium text-foreground hover:underline cursor-pointer truncate">
|
|
686
|
+
{name}
|
|
687
|
+
</Link>
|
|
688
|
+
{skillMode && (
|
|
689
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${
|
|
690
|
+
skillMode === 'universal' ? 'bg-success/10 text-success'
|
|
691
|
+
: skillMode === 'additional' ? 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
692
|
+
: 'bg-muted text-muted-foreground'
|
|
693
|
+
}`}>
|
|
694
|
+
{skillMode}
|
|
695
|
+
</span>
|
|
696
|
+
)}
|
|
697
|
+
</div>
|
|
698
|
+
<div className="flex items-center gap-2 mt-0.5 text-2xs text-muted-foreground">
|
|
699
|
+
<span className="tabular-nums">{copy.quickStatsMcp(mcpCount)}</span>
|
|
700
|
+
<span aria-hidden="true">·</span>
|
|
701
|
+
<span className="tabular-nums">{copy.quickStatsSkills(totalSkills)}</span>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
{/* Skill sections */}
|
|
707
|
+
<div className="p-4 pt-3 space-y-3">
|
|
708
|
+
{/* Native skills */}
|
|
709
|
+
<div>
|
|
710
|
+
<div className="flex items-center justify-between mb-2">
|
|
711
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
712
|
+
{copy.agentNativeSkills} <span className="tabular-nums">({nativeSkills.length})</span>
|
|
713
|
+
</p>
|
|
714
|
+
{showNativeToggle && (
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
onClick={() => setNativeExpanded(!nativeExpanded)}
|
|
718
|
+
className="text-2xs text-muted-foreground hover:text-foreground cursor-pointer flex items-center gap-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded transition-colors duration-150"
|
|
719
|
+
>
|
|
720
|
+
{nativeExpanded ? (
|
|
721
|
+
<><ChevronDown size={12} />{copy.collapseNative}</>
|
|
722
|
+
) : (
|
|
723
|
+
<><ChevronRight size={12} />{copy.showAllNative(nativeSkills.length)}</>
|
|
724
|
+
)}
|
|
725
|
+
</button>
|
|
726
|
+
)}
|
|
727
|
+
</div>
|
|
728
|
+
{nativeSkills.length === 0 ? (
|
|
729
|
+
<p className="text-2xs text-muted-foreground/60">—</p>
|
|
730
|
+
) : (
|
|
731
|
+
<div className="space-y-0.5 max-h-[200px] overflow-y-auto">
|
|
732
|
+
{visibleNative.map((n) => (
|
|
733
|
+
<button
|
|
734
|
+
key={n}
|
|
735
|
+
type="button"
|
|
736
|
+
onClick={() => onOpenDetail(n)}
|
|
737
|
+
className="w-full flex items-center gap-1.5 py-1 min-h-[28px] hover:bg-muted/30 -mx-1.5 px-1.5 rounded transition-colors duration-100 cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
738
|
+
>
|
|
739
|
+
<Zap size={12} className="shrink-0 text-muted-foreground/50" aria-hidden="true" />
|
|
740
|
+
<span className="text-xs text-foreground truncate hover:text-[var(--amber)] transition-colors duration-150">{n}</span>
|
|
741
|
+
</button>
|
|
742
|
+
))}
|
|
743
|
+
{!nativeExpanded && nativeSkills.length > NATIVE_COLLAPSE_THRESHOLD && (
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
onClick={() => setNativeExpanded(true)}
|
|
747
|
+
className="w-full text-left text-2xs text-muted-foreground hover:text-foreground py-1 px-1.5 -mx-1.5 cursor-pointer transition-colors duration-150"
|
|
748
|
+
>
|
|
749
|
+
+{nativeSkills.length - NATIVE_COLLAPSE_THRESHOLD}
|
|
750
|
+
</button>
|
|
751
|
+
)}
|
|
752
|
+
</div>
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
{/* MindOS skills */}
|
|
757
|
+
{mindosSkills.length > 0 && (
|
|
758
|
+
<div>
|
|
759
|
+
<p className="text-2xs font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
760
|
+
{copy.agentMindosSkills} <span className="tabular-nums">({mindosSkills.filter((s) => s.enabled).length}/{mindosSkills.length})</span>
|
|
761
|
+
</p>
|
|
762
|
+
<div className="space-y-0.5 max-h-[240px] overflow-y-auto">
|
|
763
|
+
{mindosSkills.map((skill) => (
|
|
764
|
+
<div key={skill.name} className="flex items-center justify-between gap-2 py-1 min-h-[32px] hover:bg-muted/30 -mx-1.5 px-1.5 rounded transition-colors duration-100">
|
|
765
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
766
|
+
<Zap size={12} className={`shrink-0 ${skill.enabled ? 'text-[var(--amber)]' : 'text-muted-foreground/50'}`} aria-hidden="true" />
|
|
767
|
+
<button
|
|
768
|
+
type="button"
|
|
769
|
+
onClick={() => onOpenDetail(skill.name)}
|
|
770
|
+
className="text-xs text-foreground 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"
|
|
771
|
+
>
|
|
772
|
+
{skill.name}
|
|
773
|
+
</button>
|
|
774
|
+
<span className="text-2xs text-muted-foreground shrink-0">
|
|
775
|
+
{copy.groupLabels[capabilityForSkill(skill) as keyof typeof copy.groupLabels]}
|
|
776
|
+
</span>
|
|
777
|
+
</div>
|
|
778
|
+
<Toggle size="sm" checked={skill.enabled} onChange={(v) => void onToggleSkill(skill.name, v)} />
|
|
779
|
+
</div>
|
|
780
|
+
))}
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
)}
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
168
786
|
);
|
|
169
787
|
}
|