@geminilight/mindos 0.5.64 → 0.5.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +6 -11
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +1 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +35 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +70 -5
- package/app/components/agents/AgentsMcpSection.tsx +474 -159
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +739 -121
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +292 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +1 -2
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +12 -104
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
- package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +3 -3
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +2 -2
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +4 -2
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/prompt.ts +17 -29
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +9 -0
- package/app/lib/i18n-en.ts +259 -33
- package/app/lib/i18n-zh.ts +258 -32
- package/app/lib/mcp-agents.ts +231 -2
- package/app/lib/types.ts +2 -0
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { RefreshCw, Search, Server } from 'lucide-react';
|
|
6
6
|
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
7
|
-
import type { AgentBuckets } from './agents-content-model';
|
|
8
|
-
import
|
|
9
|
-
|
|
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';
|
|
10
30
|
|
|
11
31
|
export default function AgentsMcpSection({
|
|
12
32
|
copy,
|
|
@@ -18,21 +38,34 @@ export default function AgentsMcpSection({
|
|
|
18
38
|
copy: {
|
|
19
39
|
title: string;
|
|
20
40
|
refresh: string;
|
|
21
|
-
|
|
22
|
-
tabs: {
|
|
23
|
-
manage: string;
|
|
24
|
-
topology: string;
|
|
25
|
-
};
|
|
41
|
+
tabs: { byAgent: string; byServer: string; [k: string]: string };
|
|
26
42
|
searchPlaceholder: string;
|
|
27
43
|
emptyState: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 };
|
|
36
69
|
status: { connected: string; detected: string; notFound: string };
|
|
37
70
|
};
|
|
38
71
|
mcp: McpContextValue;
|
|
@@ -40,197 +73,479 @@ export default function AgentsMcpSection({
|
|
|
40
73
|
copyState: string | null;
|
|
41
74
|
onCopySnippet: (agentKey: string) => Promise<void>;
|
|
42
75
|
}) {
|
|
76
|
+
const [view, setView] = useState<McpView>('byServer');
|
|
43
77
|
const [query, setQuery] = useState('');
|
|
44
78
|
const [statusFilter, setStatusFilter] = useState<AgentStatusFilter>('all');
|
|
79
|
+
const [transportFilter, setTransportFilter] = useState<AgentTransportFilter>('all');
|
|
45
80
|
const [busyAction, setBusyAction] = useState<string | null>(null);
|
|
46
|
-
const [
|
|
81
|
+
const [bulkMessage, setBulkMessage] = useState<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const sortedAgents = useMemo(() => sortAgentsByStatus(mcp.agents), [mcp.agents]);
|
|
47
84
|
const filteredAgents = useMemo(
|
|
48
|
-
() =>
|
|
49
|
-
[mcp.agents, query, statusFilter],
|
|
85
|
+
() => sortAgentsByStatus(filterAgentsForMcpWorkspace(mcp.agents, { query, status: statusFilter, transport: transportFilter })),
|
|
86
|
+
[mcp.agents, query, statusFilter, transportFilter],
|
|
50
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]);
|
|
51
94
|
|
|
52
|
-
async function
|
|
53
|
-
setBusyAction(`
|
|
95
|
+
async function handleReconnect(agent: (typeof mcp.agents)[number]) {
|
|
96
|
+
setBusyAction(`reconnect:${agent.key}`);
|
|
54
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 });
|
|
55
101
|
await mcp.refresh();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('[mcp] reconnect failed', err);
|
|
56
104
|
} finally {
|
|
57
105
|
setBusyAction(null);
|
|
58
106
|
}
|
|
59
107
|
}
|
|
60
108
|
|
|
61
|
-
async function
|
|
62
|
-
setBusyAction(`
|
|
109
|
+
async function handleInstallMindos(agentKey: string) {
|
|
110
|
+
setBusyAction(`install:${agentKey}`);
|
|
63
111
|
try {
|
|
64
|
-
|
|
65
|
-
const transport = agent.transport === 'http' ? 'http' : 'stdio';
|
|
66
|
-
await mcp.installAgent(agent.key, { scope, transport });
|
|
112
|
+
await mcp.installAgent(agentKey, { scope: 'global', transport: 'stdio' });
|
|
67
113
|
await mcp.refresh();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('[mcp] install mindos failed', err);
|
|
68
116
|
} finally {
|
|
69
117
|
setBusyAction(null);
|
|
70
118
|
}
|
|
71
119
|
}
|
|
72
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
|
+
|
|
73
140
|
return (
|
|
74
|
-
<section className="
|
|
75
|
-
|
|
76
|
-
|
|
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">
|
|
77
145
|
<h2 className="text-sm font-medium text-foreground flex items-center gap-2">
|
|
78
|
-
<Server size={15} className="text-muted-foreground" />
|
|
146
|
+
<Server size={15} className="text-muted-foreground" aria-hidden="true" />
|
|
79
147
|
{copy.title}
|
|
80
148
|
</h2>
|
|
81
149
|
<button
|
|
82
150
|
type="button"
|
|
83
|
-
onClick={() => void mcp.refresh()}
|
|
84
|
-
|
|
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"
|
|
85
155
|
>
|
|
86
|
-
<RefreshCw size={13} />
|
|
156
|
+
<RefreshCw size={13} className={isRefreshing ? 'animate-spin' : ''} />
|
|
87
157
|
{copy.refresh}
|
|
88
158
|
</button>
|
|
89
159
|
</div>
|
|
90
|
-
<div className="flex items-center gap-1 rounded-md border border-border p-
|
|
91
|
-
<
|
|
92
|
-
<
|
|
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')} />
|
|
93
163
|
</div>
|
|
94
164
|
</div>
|
|
95
165
|
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<NodePill label={copy.title} count={mcp.status?.running ? 1 : 0} tone={mcp.status?.running ? 'ok' : 'neutral'} />
|
|
107
|
-
</div>
|
|
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} />
|
|
108
176
|
</div>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
value={query}
|
|
116
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
117
|
-
placeholder={copy.searchPlaceholder}
|
|
118
|
-
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"
|
|
119
|
-
/>
|
|
120
|
-
</label>
|
|
121
|
-
<div role="group" aria-label={copy.table.status} className="flex items-center gap-1 rounded-md border border-border p-1 bg-background">
|
|
122
|
-
<StatusFilterButton active={statusFilter === 'all'} label={copy.filters.all} onClick={() => setStatusFilter('all')} />
|
|
123
|
-
<StatusFilterButton active={statusFilter === 'connected'} label={copy.filters.connected} onClick={() => setStatusFilter('connected')} />
|
|
124
|
-
<StatusFilterButton active={statusFilter === 'detected'} label={copy.filters.detected} onClick={() => setStatusFilter('detected')} />
|
|
125
|
-
<StatusFilterButton active={statusFilter === 'notFound'} label={copy.filters.notFound} onClick={() => setStatusFilter('notFound')} />
|
|
126
|
-
</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>
|
|
127
183
|
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
128
186
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</button>
|
|
164
|
-
<button
|
|
165
|
-
type="button"
|
|
166
|
-
onClick={() => void handleReconnect(agent)}
|
|
167
|
-
disabled={busyAction !== null}
|
|
168
|
-
className="text-xs px-2 py-1 rounded border border-border hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
|
169
|
-
>
|
|
170
|
-
{copy.actions.reconnect}
|
|
171
|
-
</button>
|
|
172
|
-
</div>
|
|
173
|
-
</td>
|
|
174
|
-
</tr>
|
|
175
|
-
))}
|
|
176
|
-
{filteredAgents.length === 0 && (
|
|
177
|
-
<tr>
|
|
178
|
-
<td colSpan={4} className="py-4 text-sm text-muted-foreground text-center">
|
|
179
|
-
{copy.emptyState}
|
|
180
|
-
</td>
|
|
181
|
-
</tr>
|
|
182
|
-
)}
|
|
183
|
-
</tbody>
|
|
184
|
-
</table>
|
|
185
|
-
</div>
|
|
186
|
-
</>
|
|
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
|
+
/>
|
|
187
221
|
)}
|
|
188
222
|
</section>
|
|
189
223
|
);
|
|
190
224
|
}
|
|
191
225
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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,
|
|
196
242
|
}: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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>;
|
|
200
256
|
}) {
|
|
201
257
|
return (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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')} />
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
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
|
|
319
|
+
onClick={() => void onCopySnippet(agent.key)}
|
|
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
|
+
)}
|
|
340
|
+
</div>
|
|
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
|
+
</>
|
|
212
360
|
);
|
|
213
361
|
}
|
|
214
362
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
363
|
+
/* ────────── By Server View (Avatar Grid) ────────── */
|
|
364
|
+
|
|
365
|
+
function ByServerView({
|
|
366
|
+
copy,
|
|
367
|
+
servers,
|
|
368
|
+
allAgents,
|
|
369
|
+
busyAction,
|
|
370
|
+
onInstallMindos,
|
|
371
|
+
onReconnect,
|
|
219
372
|
}: {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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>;
|
|
223
379
|
}) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
|
|
230
429
|
return (
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
</>
|
|
235
550
|
);
|
|
236
551
|
}
|