@geminilight/mindos 0.5.63 → 0.5.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/changes/route.ts +7 -1
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/mcp/install-skill/route.ts +9 -24
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/app/layout.tsx +1 -0
- package/app/app/page.tsx +1 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +44 -14
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +2 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +36 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +89 -20
- package/app/components/agents/AgentsMcpSection.tsx +513 -85
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +746 -105
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +308 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +89 -13
- package/app/components/changes/ChangesContentPage.tsx +134 -51
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +22 -128
- package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
- package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
- package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +5 -5
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +5 -3
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +6 -5
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +7 -4
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/content-changes.ts +148 -8
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +16 -1
- package/app/lib/i18n-en.ts +317 -36
- package/app/lib/i18n-zh.ts +316 -35
- package/app/lib/mcp-agents.ts +273 -2
- package/app/lib/renderers/index.ts +1 -2
- package/app/lib/renderers/registry.ts +10 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-agents.js +38 -13
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
- package/scripts/migrate-agent-diff.js +146 -0
- package/scripts/setup.js +12 -17
- package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
- package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
- package/app/components/renderers/diff/manifest.ts +0 -14
|
@@ -1,11 +1,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,133 +47,741 @@ export default function AgentsSkillsSection({
|
|
|
14
47
|
}: {
|
|
15
48
|
copy: {
|
|
16
49
|
title: string;
|
|
17
|
-
|
|
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 };
|
|
18
55
|
searchPlaceholder: string;
|
|
19
56
|
sourceAll: string;
|
|
20
57
|
sourceBuiltin: string;
|
|
21
58
|
sourceUser: string;
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
|
24
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;
|
|
25
127
|
};
|
|
26
128
|
mcp: McpContextValue;
|
|
27
129
|
buckets: AgentBuckets;
|
|
28
130
|
}) {
|
|
131
|
+
const [view, setView] = useState<SkillView>('bySkill');
|
|
29
132
|
const [query, setQuery] = useState('');
|
|
30
|
-
const [source, setSource] = useState<
|
|
31
|
-
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
|
+
);
|
|
32
172
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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]);
|
|
182
|
+
|
|
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
|
+
};
|
|
36
205
|
|
|
37
206
|
return (
|
|
38
|
-
<section
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
207
|
+
<section className="space-y-4 overflow-hidden" aria-label={copy.title}>
|
|
208
|
+
{/* Header */}
|
|
209
|
+
<div className="flex items-center justify-between gap-3">
|
|
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')} />
|
|
214
|
+
</div>
|
|
42
215
|
</div>
|
|
43
216
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
)}
|
|
58
243
|
</div>
|
|
59
244
|
</div>
|
|
60
245
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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>
|
|
84
416
|
</div>
|
|
85
417
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
onClick={() =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<th key={agent.key} className="text-left py-2 text-muted-foreground font-medium pr-3">{agent.name}</th>
|
|
103
|
-
))}
|
|
104
|
-
</tr>
|
|
105
|
-
</thead>
|
|
106
|
-
<tbody>
|
|
107
|
-
{filtered.map((skill) => (
|
|
108
|
-
<tr key={skill.name} className="border-b border-border/40">
|
|
109
|
-
<td className="py-2 text-foreground pr-3">{skill.name}</td>
|
|
110
|
-
{knownAgents.map((agent) => (
|
|
111
|
-
<td key={`${skill.name}:${agent.key}`} className="py-2 pr-3 text-muted-foreground">
|
|
112
|
-
{agent.present ? (skill.enabled ? 'Enabled' : 'Disabled') : 'Unsupported'}
|
|
113
|
-
</td>
|
|
114
|
-
))}
|
|
115
|
-
</tr>
|
|
116
|
-
))}
|
|
117
|
-
</tbody>
|
|
118
|
-
</table>
|
|
119
|
-
</div>
|
|
120
|
-
)}
|
|
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} />
|
|
121
434
|
</div>
|
|
122
|
-
|
|
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>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
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
|
+
})}
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
))}
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
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
|
+
</>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* ────────── By Agent View ────────── */
|
|
572
|
+
|
|
573
|
+
function ByAgentView({
|
|
574
|
+
copy,
|
|
575
|
+
agents,
|
|
576
|
+
skills,
|
|
577
|
+
crossAgentSkills,
|
|
578
|
+
query,
|
|
579
|
+
onToggleSkill,
|
|
580
|
+
onOpenDetail,
|
|
581
|
+
}: {
|
|
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;
|
|
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
|
+
|
|
611
|
+
return (
|
|
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>
|
|
123
643
|
);
|
|
124
644
|
}
|
|
125
645
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
130
660
|
}: {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
134
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
|
+
|
|
135
678
|
return (
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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>
|
|
145
786
|
);
|
|
146
787
|
}
|