@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,9 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
4
|
import Link from 'next/link';
|
|
4
|
-
import { RefreshCw, Server } from 'lucide-react';
|
|
5
|
+
import { RefreshCw, Search, Server } from 'lucide-react';
|
|
5
6
|
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
6
|
-
import type { AgentBuckets } from './agents-content-model';
|
|
7
|
+
import type { AgentBuckets, AgentStatusFilter, AgentTransportFilter } from './agents-content-model';
|
|
8
|
+
import {
|
|
9
|
+
ActionButton,
|
|
10
|
+
AddAvatarButton,
|
|
11
|
+
AgentAvatar,
|
|
12
|
+
AgentPickerPopover,
|
|
13
|
+
BulkMessage,
|
|
14
|
+
ConfirmDialog,
|
|
15
|
+
EmptyState,
|
|
16
|
+
PillButton,
|
|
17
|
+
SearchInput,
|
|
18
|
+
StatusDot,
|
|
19
|
+
} from './AgentsPrimitives';
|
|
20
|
+
import {
|
|
21
|
+
aggregateCrossAgentMcpServers,
|
|
22
|
+
|
|
23
|
+
filterAgentsForMcpWorkspace,
|
|
24
|
+
resolveAgentStatus,
|
|
25
|
+
sortAgentsByStatus,
|
|
26
|
+
summarizeMcpBulkReconnectResults,
|
|
27
|
+
} from './agents-content-model';
|
|
28
|
+
|
|
29
|
+
type McpView = 'byAgent' | 'byServer';
|
|
7
30
|
|
|
8
31
|
export default function AgentsMcpSection({
|
|
9
32
|
copy,
|
|
@@ -15,9 +38,34 @@ export default function AgentsMcpSection({
|
|
|
15
38
|
copy: {
|
|
16
39
|
title: string;
|
|
17
40
|
refresh: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
tabs: { byAgent: string; byServer: string; [k: string]: string };
|
|
42
|
+
searchPlaceholder: string;
|
|
43
|
+
emptyState: string;
|
|
44
|
+
resultCount: (n: number) => string;
|
|
45
|
+
crossAgentServersEmpty: string;
|
|
46
|
+
riskMcpStopped: string;
|
|
47
|
+
bulkReconnectFiltered: string;
|
|
48
|
+
bulkRunning: string;
|
|
49
|
+
bulkSummary: (ok: number, failed: number) => string;
|
|
50
|
+
installMindos: string;
|
|
51
|
+
mcpServerLabel: string;
|
|
52
|
+
searchServersPlaceholder: string;
|
|
53
|
+
serverAgentCount: (n: number) => string;
|
|
54
|
+
addAgent: string;
|
|
55
|
+
removeFromServer: string;
|
|
56
|
+
confirmRemoveTitle: string;
|
|
57
|
+
confirmRemoveMessage: (agent: string, server: string) => string;
|
|
58
|
+
cancel: string;
|
|
59
|
+
noAvailableAgents: string;
|
|
60
|
+
manualRemoveHint: string;
|
|
61
|
+
reconnectAllInServer: string;
|
|
62
|
+
reconnectAllRunning: string;
|
|
63
|
+
reconnectAllDone: (ok: number, failed: number) => string;
|
|
64
|
+
serverTransport: (t: string) => string;
|
|
65
|
+
transportFilters: { all: string; stdio: string; http: string; other: string };
|
|
66
|
+
filters: { all: string; connected: string; detected: string; notFound: string };
|
|
67
|
+
table: { status: string; transport: string; [k: string]: string };
|
|
68
|
+
actions: { copySnippet: string; copied: string; reconnect: string; [k: string]: string };
|
|
21
69
|
status: { connected: string; detected: string; notFound: string };
|
|
22
70
|
};
|
|
23
71
|
mcp: McpContextValue;
|
|
@@ -25,99 +73,479 @@ export default function AgentsMcpSection({
|
|
|
25
73
|
copyState: string | null;
|
|
26
74
|
onCopySnippet: (agentKey: string) => Promise<void>;
|
|
27
75
|
}) {
|
|
76
|
+
const [view, setView] = useState<McpView>('byServer');
|
|
77
|
+
const [query, setQuery] = useState('');
|
|
78
|
+
const [statusFilter, setStatusFilter] = useState<AgentStatusFilter>('all');
|
|
79
|
+
const [transportFilter, setTransportFilter] = useState<AgentTransportFilter>('all');
|
|
80
|
+
const [busyAction, setBusyAction] = useState<string | null>(null);
|
|
81
|
+
const [bulkMessage, setBulkMessage] = useState<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const sortedAgents = useMemo(() => sortAgentsByStatus(mcp.agents), [mcp.agents]);
|
|
84
|
+
const filteredAgents = useMemo(
|
|
85
|
+
() => sortAgentsByStatus(filterAgentsForMcpWorkspace(mcp.agents, { query, status: statusFilter, transport: transportFilter })),
|
|
86
|
+
[mcp.agents, query, statusFilter, transportFilter],
|
|
87
|
+
);
|
|
88
|
+
const crossAgentServers = useMemo(() => aggregateCrossAgentMcpServers(mcp.agents), [mcp.agents]);
|
|
89
|
+
const filteredServers = useMemo(() => {
|
|
90
|
+
const q = query.trim().toLowerCase();
|
|
91
|
+
if (!q) return crossAgentServers;
|
|
92
|
+
return crossAgentServers.filter((srv) => srv.serverName.toLowerCase().includes(q));
|
|
93
|
+
}, [crossAgentServers, query]);
|
|
94
|
+
|
|
95
|
+
async function handleReconnect(agent: (typeof mcp.agents)[number]) {
|
|
96
|
+
setBusyAction(`reconnect:${agent.key}`);
|
|
97
|
+
try {
|
|
98
|
+
const scope = agent.scope === 'project' ? 'project' : 'global';
|
|
99
|
+
const transport = agent.transport === 'http' ? 'http' : 'stdio';
|
|
100
|
+
await mcp.installAgent(agent.key, { scope, transport });
|
|
101
|
+
await mcp.refresh();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('[mcp] reconnect failed', err);
|
|
104
|
+
} finally {
|
|
105
|
+
setBusyAction(null);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleInstallMindos(agentKey: string) {
|
|
110
|
+
setBusyAction(`install:${agentKey}`);
|
|
111
|
+
try {
|
|
112
|
+
await mcp.installAgent(agentKey, { scope: 'global', transport: 'stdio' });
|
|
113
|
+
await mcp.refresh();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('[mcp] install mindos failed', err);
|
|
116
|
+
} finally {
|
|
117
|
+
setBusyAction(null);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function handleBulkReconnect() {
|
|
122
|
+
if (busyAction !== null || filteredAgents.length === 0) return;
|
|
123
|
+
setBusyAction('bulk');
|
|
124
|
+
setBulkMessage(copy.bulkRunning);
|
|
125
|
+
const results: Array<{ agentKey: string; ok: boolean }> = [];
|
|
126
|
+
for (const agent of filteredAgents) {
|
|
127
|
+
const scope = agent.scope === 'project' ? 'project' : 'global';
|
|
128
|
+
const transport = agent.transport === 'http' ? 'http' : 'stdio';
|
|
129
|
+
const ok = await mcp.installAgent(agent.key, { scope, transport });
|
|
130
|
+
results.push({ agentKey: agent.key, ok });
|
|
131
|
+
}
|
|
132
|
+
await mcp.refresh();
|
|
133
|
+
const summary = summarizeMcpBulkReconnectResults(results);
|
|
134
|
+
setBulkMessage(copy.bulkSummary(summary.succeeded, summary.failed));
|
|
135
|
+
setBusyAction(null);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isRefreshing = busyAction === 'refresh';
|
|
139
|
+
|
|
28
140
|
return (
|
|
29
|
-
<section
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
141
|
+
<section className="space-y-4 overflow-hidden" aria-label={copy.title}>
|
|
142
|
+
{/* Header */}
|
|
143
|
+
<div className="flex items-center justify-between gap-2">
|
|
144
|
+
<div className="flex items-center gap-2.5">
|
|
145
|
+
<h2 className="text-sm font-medium text-foreground flex items-center gap-2">
|
|
146
|
+
<Server size={15} className="text-muted-foreground" aria-hidden="true" />
|
|
147
|
+
{copy.title}
|
|
148
|
+
</h2>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => { setBusyAction('refresh'); void mcp.refresh().finally(() => setBusyAction(null)); }}
|
|
152
|
+
disabled={isRefreshing}
|
|
153
|
+
aria-label={copy.refresh}
|
|
154
|
+
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-1.5 py-1 hover:bg-muted disabled:opacity-50 transition-colors duration-150"
|
|
155
|
+
>
|
|
156
|
+
<RefreshCw size={13} className={isRefreshing ? 'animate-spin' : ''} />
|
|
157
|
+
{copy.refresh}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex items-center gap-1 rounded-md border border-border p-0.5 bg-background" role="tablist" aria-label={copy.title}>
|
|
161
|
+
<PillButton active={view === 'byServer'} label={copy.tabs.byServer} onClick={() => setView('byServer')} />
|
|
162
|
+
<PillButton active={view === 'byAgent'} label={copy.tabs.byAgent} onClick={() => setView('byAgent')} />
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Compact status strip + risk alerts */}
|
|
167
|
+
<div className="rounded-lg border border-border bg-card p-3">
|
|
168
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
|
|
169
|
+
<StatusDot tone="ok" label={copy.filters.connected} count={buckets.connected.length} />
|
|
170
|
+
<StatusDot tone="warn" label={copy.filters.detected} count={buckets.detected.length} />
|
|
171
|
+
{buckets.notFound.length > 0 && (
|
|
172
|
+
<StatusDot tone="neutral" label={copy.filters.notFound} count={buckets.notFound.length} />
|
|
173
|
+
)}
|
|
174
|
+
<span className="text-muted-foreground/40" aria-hidden="true">|</span>
|
|
175
|
+
<StatusDot tone={mcp.status?.running ? 'ok' : 'neutral'} label={copy.mcpServerLabel} count={mcp.status?.running ? 1 : 0} />
|
|
176
|
+
</div>
|
|
177
|
+
{!mcp.status?.running && (
|
|
178
|
+
<div className="mt-2 pt-2 border-t border-border/60" role="alert">
|
|
179
|
+
<p className="text-xs text-destructive flex items-center gap-1.5">
|
|
180
|
+
<span className="w-1.5 h-1.5 rounded-full bg-destructive shrink-0" aria-hidden="true" />
|
|
181
|
+
{copy.riskMcpStopped}
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
43
185
|
</div>
|
|
44
186
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
187
|
+
{/* Search */}
|
|
188
|
+
<SearchInput
|
|
189
|
+
value={query}
|
|
190
|
+
onChange={setQuery}
|
|
191
|
+
placeholder={view === 'byAgent' ? copy.searchPlaceholder : copy.searchServersPlaceholder}
|
|
192
|
+
ariaLabel={copy.searchPlaceholder}
|
|
193
|
+
icon={Search}
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
{view === 'byAgent' ? (
|
|
197
|
+
<ByAgentView
|
|
198
|
+
copy={copy}
|
|
199
|
+
agents={filteredAgents}
|
|
200
|
+
busyAction={busyAction}
|
|
201
|
+
copyState={copyState}
|
|
202
|
+
bulkMessage={bulkMessage}
|
|
203
|
+
statusFilter={statusFilter}
|
|
204
|
+
transportFilter={transportFilter}
|
|
205
|
+
onStatusFilter={setStatusFilter}
|
|
206
|
+
onTransportFilter={setTransportFilter}
|
|
207
|
+
onCopySnippet={onCopySnippet}
|
|
208
|
+
onReconnect={handleReconnect}
|
|
209
|
+
onInstallMindos={handleInstallMindos}
|
|
210
|
+
onBulkReconnect={handleBulkReconnect}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
<ByServerView
|
|
214
|
+
copy={copy}
|
|
215
|
+
servers={filteredServers}
|
|
216
|
+
allAgents={sortedAgents}
|
|
217
|
+
busyAction={busyAction}
|
|
218
|
+
onInstallMindos={handleInstallMindos}
|
|
219
|
+
onReconnect={handleReconnect}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ────────── By Agent View ────────── */
|
|
227
|
+
|
|
228
|
+
function ByAgentView({
|
|
229
|
+
copy,
|
|
230
|
+
agents,
|
|
231
|
+
busyAction,
|
|
232
|
+
copyState,
|
|
233
|
+
bulkMessage,
|
|
234
|
+
statusFilter,
|
|
235
|
+
transportFilter,
|
|
236
|
+
onStatusFilter,
|
|
237
|
+
onTransportFilter,
|
|
238
|
+
onCopySnippet,
|
|
239
|
+
onReconnect,
|
|
240
|
+
onInstallMindos,
|
|
241
|
+
onBulkReconnect,
|
|
242
|
+
}: {
|
|
243
|
+
copy: Parameters<typeof AgentsMcpSection>[0]['copy'];
|
|
244
|
+
agents: ReturnType<typeof sortAgentsByStatus>;
|
|
245
|
+
busyAction: string | null;
|
|
246
|
+
copyState: string | null;
|
|
247
|
+
bulkMessage: string | null;
|
|
248
|
+
statusFilter: AgentStatusFilter;
|
|
249
|
+
transportFilter: AgentTransportFilter;
|
|
250
|
+
onStatusFilter: (f: AgentStatusFilter) => void;
|
|
251
|
+
onTransportFilter: (f: AgentTransportFilter) => void;
|
|
252
|
+
onCopySnippet: (agentKey: string) => Promise<void>;
|
|
253
|
+
onReconnect: (agent: ReturnType<typeof sortAgentsByStatus>[number]) => Promise<void>;
|
|
254
|
+
onInstallMindos: (agentKey: string) => Promise<void>;
|
|
255
|
+
onBulkReconnect: () => Promise<void>;
|
|
256
|
+
}) {
|
|
257
|
+
return (
|
|
258
|
+
<>
|
|
259
|
+
{/* Filters */}
|
|
260
|
+
<div className="flex flex-wrap gap-2">
|
|
261
|
+
<div role="group" aria-label={copy.table.status} className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
262
|
+
<PillButton active={statusFilter === 'all'} label={copy.filters.all} onClick={() => onStatusFilter('all')} />
|
|
263
|
+
<PillButton active={statusFilter === 'connected'} label={copy.filters.connected} onClick={() => onStatusFilter('connected')} />
|
|
264
|
+
<PillButton active={statusFilter === 'detected'} label={copy.filters.detected} onClick={() => onStatusFilter('detected')} />
|
|
265
|
+
<PillButton active={statusFilter === 'notFound'} label={copy.filters.notFound} onClick={() => onStatusFilter('notFound')} />
|
|
266
|
+
</div>
|
|
267
|
+
<div role="group" aria-label={copy.table.transport} className="flex items-center gap-0.5 rounded-md border border-border p-0.5 bg-background">
|
|
268
|
+
<PillButton active={transportFilter === 'all'} label={copy.transportFilters.all} onClick={() => onTransportFilter('all')} />
|
|
269
|
+
<PillButton active={transportFilter === 'stdio'} label={copy.transportFilters.stdio} onClick={() => onTransportFilter('stdio')} />
|
|
270
|
+
<PillButton active={transportFilter === 'http'} label={copy.transportFilters.http} onClick={() => onTransportFilter('http')} />
|
|
271
|
+
<PillButton active={transportFilter === 'other'} label={copy.transportFilters.other} onClick={() => onTransportFilter('other')} />
|
|
55
272
|
</div>
|
|
56
273
|
</div>
|
|
57
274
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
275
|
+
{/* Bulk actions + result count */}
|
|
276
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
277
|
+
<ActionButton
|
|
278
|
+
onClick={() => void onBulkReconnect()}
|
|
279
|
+
disabled={busyAction !== null || agents.length === 0}
|
|
280
|
+
busy={busyAction === 'bulk'}
|
|
281
|
+
label={copy.bulkReconnectFiltered}
|
|
282
|
+
/>
|
|
283
|
+
<span className="text-2xs text-muted-foreground tabular-nums">{copy.resultCount(agents.length)}</span>
|
|
284
|
+
<BulkMessage message={bulkMessage} />
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Agent cards */}
|
|
288
|
+
{agents.length === 0 ? (
|
|
289
|
+
<EmptyState message={copy.emptyState} />
|
|
290
|
+
) : (
|
|
291
|
+
<div className="space-y-2">
|
|
292
|
+
{agents.map((agent) => {
|
|
293
|
+
const status = resolveAgentStatus(agent);
|
|
294
|
+
const mcpServers = agent.configuredMcpServers ?? [];
|
|
295
|
+
const nativeSkillCount = (agent.installedSkillNames ?? []).length;
|
|
296
|
+
return (
|
|
297
|
+
<div key={agent.key} className="rounded-lg border border-border bg-card group hover:border-border/60 hover:shadow-sm transition-all duration-150">
|
|
298
|
+
{/* Card header with avatar */}
|
|
299
|
+
<div className="flex items-center gap-3 p-3">
|
|
300
|
+
<AgentAvatar name={agent.name} status={status} />
|
|
301
|
+
<div className="min-w-0 flex-1">
|
|
302
|
+
<div className="flex items-center gap-2">
|
|
303
|
+
<Link href={`/agents/${encodeURIComponent(agent.key)}`} className="text-sm font-medium text-foreground hover:underline cursor-pointer truncate">
|
|
304
|
+
{agent.name}
|
|
305
|
+
</Link>
|
|
306
|
+
<span className="text-2xs text-muted-foreground font-mono shrink-0">{agent.transport ?? agent.preferredTransport}</span>
|
|
307
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${status === 'connected' ? 'bg-success/10 text-success' : status === 'detected' ? 'bg-[var(--amber-dim)] text-[var(--amber)]' : 'bg-muted text-muted-foreground'}`}>
|
|
308
|
+
{copy.status[status]}
|
|
309
|
+
</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div className="flex items-center gap-2 mt-0.5 text-2xs text-muted-foreground">
|
|
312
|
+
<span className="tabular-nums">{mcpServers.length} MCP</span>
|
|
313
|
+
<span aria-hidden="true">·</span>
|
|
314
|
+
<span className="tabular-nums">{nativeSkillCount} skills</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
<div className="flex items-center gap-1.5 shrink-0 md:opacity-0 md:group-hover:opacity-100 md:focus-within:opacity-100 transition-opacity duration-150">
|
|
318
|
+
<ActionButton
|
|
80
319
|
onClick={() => void onCopySnippet(agent.key)}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{copyState === agent.key ? copy.actions.copied : copy.actions.copySnippet}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
320
|
+
disabled={false}
|
|
321
|
+
busy={false}
|
|
322
|
+
label={copyState === agent.key ? copy.actions.copied : copy.actions.copySnippet}
|
|
323
|
+
/>
|
|
324
|
+
{agent.installed ? (
|
|
325
|
+
<ActionButton
|
|
326
|
+
onClick={() => void onReconnect(agent)}
|
|
327
|
+
disabled={busyAction !== null}
|
|
328
|
+
busy={busyAction === `reconnect:${agent.key}`}
|
|
329
|
+
label={copy.actions.reconnect}
|
|
330
|
+
/>
|
|
331
|
+
) : (
|
|
332
|
+
<ActionButton
|
|
333
|
+
onClick={() => void onInstallMindos(agent.key)}
|
|
334
|
+
disabled={busyAction !== null}
|
|
335
|
+
busy={busyAction === `install:${agent.key}`}
|
|
336
|
+
label={copy.installMindos}
|
|
337
|
+
variant="primary"
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
91
340
|
</div>
|
|
92
|
-
</
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* MCP server chips */}
|
|
344
|
+
{mcpServers.length > 0 && (
|
|
345
|
+
<div className="flex flex-wrap gap-1 px-3 pb-3 ml-12">
|
|
346
|
+
{mcpServers.map((name) => (
|
|
347
|
+
<span key={name} className="inline-flex items-center gap-1 rounded-full bg-muted/60 px-2 py-0.5 text-2xs text-muted-foreground">
|
|
348
|
+
<Server size={9} className="text-[var(--amber)] shrink-0" aria-hidden="true" />
|
|
349
|
+
{name}
|
|
350
|
+
</span>
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</>
|
|
99
360
|
);
|
|
100
361
|
}
|
|
101
362
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
363
|
+
/* ────────── By Server View (Avatar Grid) ────────── */
|
|
364
|
+
|
|
365
|
+
function ByServerView({
|
|
366
|
+
copy,
|
|
367
|
+
servers,
|
|
368
|
+
allAgents,
|
|
369
|
+
busyAction,
|
|
370
|
+
onInstallMindos,
|
|
371
|
+
onReconnect,
|
|
106
372
|
}: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
373
|
+
copy: Parameters<typeof AgentsMcpSection>[0]['copy'];
|
|
374
|
+
servers: ReturnType<typeof aggregateCrossAgentMcpServers>;
|
|
375
|
+
allAgents: ReturnType<typeof sortAgentsByStatus>;
|
|
376
|
+
busyAction: string | null;
|
|
377
|
+
onInstallMindos: (agentKey: string) => Promise<void>;
|
|
378
|
+
onReconnect: (agent: ReturnType<typeof sortAgentsByStatus>[number]) => Promise<void>;
|
|
110
379
|
}) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
380
|
+
const [pickerServer, setPickerServer] = useState<string | null>(null);
|
|
381
|
+
const [confirmState, setConfirmState] = useState<{ agentName: string; serverName: string } | null>(null);
|
|
382
|
+
const [hintMessage, setHintMessage] = useState<string | null>(null);
|
|
383
|
+
const [reconnectingServer, setReconnectingServer] = useState<string | null>(null);
|
|
384
|
+
const [reconnectMsg, setReconnectMsg] = useState<Record<string, string>>({});
|
|
385
|
+
|
|
386
|
+
const isMindosServer = (name: string) => name.toLowerCase().includes('mindos');
|
|
387
|
+
|
|
388
|
+
const handleAddAgent = useCallback(
|
|
389
|
+
async (agentKey: string, serverName: string) => {
|
|
390
|
+
setPickerServer(null);
|
|
391
|
+
if (isMindosServer(serverName)) {
|
|
392
|
+
await onInstallMindos(agentKey);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
[onInstallMindos],
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const handleConfirmRemove = useCallback(() => {
|
|
399
|
+
setConfirmState(null);
|
|
400
|
+
setHintMessage(copy.manualRemoveHint);
|
|
401
|
+
setTimeout(() => setHintMessage(null), 4000);
|
|
402
|
+
}, [copy.manualRemoveHint]);
|
|
403
|
+
|
|
404
|
+
const handleReconnectAllInServer = useCallback(
|
|
405
|
+
async (serverName: string, agents: typeof allAgents) => {
|
|
406
|
+
setReconnectingServer(serverName);
|
|
407
|
+
setReconnectMsg((prev) => ({ ...prev, [serverName]: copy.reconnectAllRunning }));
|
|
408
|
+
let ok = 0;
|
|
409
|
+
let failed = 0;
|
|
410
|
+
for (const agent of agents) {
|
|
411
|
+
try {
|
|
412
|
+
await onReconnect(agent);
|
|
413
|
+
ok++;
|
|
414
|
+
} catch {
|
|
415
|
+
failed++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
setReconnectMsg((prev) => ({ ...prev, [serverName]: copy.reconnectAllDone(ok, failed) }));
|
|
419
|
+
setReconnectingServer(null);
|
|
420
|
+
setTimeout(() => setReconnectMsg((prev) => { const next = { ...prev }; delete next[serverName]; return next; }), 4000);
|
|
421
|
+
},
|
|
422
|
+
[copy, onReconnect],
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
if (servers.length === 0) {
|
|
426
|
+
return <EmptyState message={copy.crossAgentServersEmpty} />;
|
|
427
|
+
}
|
|
428
|
+
|
|
117
429
|
return (
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
430
|
+
<>
|
|
431
|
+
<div className="space-y-3">
|
|
432
|
+
<p className="text-2xs text-muted-foreground tabular-nums">{copy.resultCount(servers.length)}</p>
|
|
433
|
+
{hintMessage && (
|
|
434
|
+
<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">
|
|
435
|
+
{hintMessage}
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
{servers.map((srv) => {
|
|
439
|
+
const agentDetails = srv.agents
|
|
440
|
+
.map((name) => allAgents.find((a) => a.name === name))
|
|
441
|
+
.filter(Boolean) as typeof allAgents;
|
|
442
|
+
const orphanNames = srv.agents.filter((name) => !agentDetails.some((a) => a.name === name));
|
|
443
|
+
const canManage = isMindosServer(srv.serverName);
|
|
444
|
+
const availableToAdd = canManage
|
|
445
|
+
? allAgents.filter((a) => !srv.agents.includes(a.name))
|
|
446
|
+
: [];
|
|
447
|
+
|
|
448
|
+
const connectedCount = agentDetails.filter((a) => resolveAgentStatus(a) === 'connected').length;
|
|
449
|
+
const detectedCount = agentDetails.filter((a) => resolveAgentStatus(a) === 'detected').length;
|
|
450
|
+
const notFoundCount = agentDetails.length - connectedCount - detectedCount + orphanNames.length;
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<div key={srv.serverName} className="rounded-lg border border-border bg-card p-4 hover:border-border/60 hover:shadow-sm transition-all duration-150">
|
|
454
|
+
{/* Server header */}
|
|
455
|
+
<div className="flex items-center justify-between gap-2 mb-2">
|
|
456
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
457
|
+
<Server size={14} className="text-[var(--amber)] shrink-0" aria-hidden="true" />
|
|
458
|
+
<span className="text-sm font-medium text-foreground truncate">{srv.serverName}</span>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
461
|
+
{canManage && agentDetails.length > 0 && (
|
|
462
|
+
<ActionButton
|
|
463
|
+
onClick={() => void handleReconnectAllInServer(srv.serverName, agentDetails)}
|
|
464
|
+
disabled={reconnectingServer !== null || busyAction !== null}
|
|
465
|
+
busy={reconnectingServer === srv.serverName}
|
|
466
|
+
label={copy.reconnectAllInServer}
|
|
467
|
+
busyLabel={copy.reconnectAllRunning}
|
|
468
|
+
/>
|
|
469
|
+
)}
|
|
470
|
+
{canManage && (
|
|
471
|
+
<div className="relative">
|
|
472
|
+
<AddAvatarButton
|
|
473
|
+
onClick={() => setPickerServer(pickerServer === srv.serverName ? null : srv.serverName)}
|
|
474
|
+
label={copy.addAgent}
|
|
475
|
+
size="sm"
|
|
476
|
+
/>
|
|
477
|
+
<AgentPickerPopover
|
|
478
|
+
open={pickerServer === srv.serverName}
|
|
479
|
+
agents={availableToAdd.map((a) => ({ key: a.key, name: a.name }))}
|
|
480
|
+
emptyLabel={copy.noAvailableAgents}
|
|
481
|
+
onSelect={(key) => void handleAddAgent(key, srv.serverName)}
|
|
482
|
+
onClose={() => setPickerServer(null)}
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
{/* Agent status breakdown */}
|
|
490
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-muted-foreground mb-3">
|
|
491
|
+
<span className="tabular-nums">{copy.serverAgentCount(srv.agents.length)}</span>
|
|
492
|
+
{connectedCount > 0 && (
|
|
493
|
+
<span className="inline-flex items-center gap-1">
|
|
494
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--success)]" aria-hidden="true" />
|
|
495
|
+
{connectedCount}
|
|
496
|
+
</span>
|
|
497
|
+
)}
|
|
498
|
+
{detectedCount > 0 && (
|
|
499
|
+
<span className="inline-flex items-center gap-1">
|
|
500
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)]" aria-hidden="true" />
|
|
501
|
+
{detectedCount}
|
|
502
|
+
</span>
|
|
503
|
+
)}
|
|
504
|
+
{notFoundCount > 0 && (
|
|
505
|
+
<span className="inline-flex items-center gap-1">
|
|
506
|
+
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground" aria-hidden="true" />
|
|
507
|
+
{notFoundCount}
|
|
508
|
+
</span>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
{reconnectMsg[srv.serverName] && (
|
|
513
|
+
<p role="status" className="text-2xs text-muted-foreground mb-2 animate-in fade-in duration-200">{reconnectMsg[srv.serverName]}</p>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{/* Agent avatar grid */}
|
|
517
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
518
|
+
{agentDetails.map((agent) => {
|
|
519
|
+
const agentStatus = resolveAgentStatus(agent);
|
|
520
|
+
return (
|
|
521
|
+
<Link key={agent.key} href={`/agents/${encodeURIComponent(agent.key)}`} className="cursor-pointer">
|
|
522
|
+
<AgentAvatar
|
|
523
|
+
name={agent.name}
|
|
524
|
+
status={agentStatus}
|
|
525
|
+
onRemove={canManage ? () => setConfirmState({ agentName: agent.name, serverName: srv.serverName }) : undefined}
|
|
526
|
+
/>
|
|
527
|
+
</Link>
|
|
528
|
+
);
|
|
529
|
+
})}
|
|
530
|
+
{orphanNames.map((name) => (
|
|
531
|
+
<AgentAvatar key={name} name={name} status="notFound" />
|
|
532
|
+
))}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
})}
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<ConfirmDialog
|
|
540
|
+
open={confirmState !== null}
|
|
541
|
+
title={copy.confirmRemoveTitle}
|
|
542
|
+
message={confirmState ? copy.confirmRemoveMessage(confirmState.agentName, confirmState.serverName) : ''}
|
|
543
|
+
confirmLabel={copy.removeFromServer}
|
|
544
|
+
cancelLabel={copy.cancel}
|
|
545
|
+
onConfirm={handleConfirmRemove}
|
|
546
|
+
onCancel={() => setConfirmState(null)}
|
|
547
|
+
variant="destructive"
|
|
548
|
+
/>
|
|
549
|
+
</>
|
|
122
550
|
);
|
|
123
551
|
}
|