@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.
Files changed (104) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/changes/route.ts +7 -1
  5. package/app/app/api/file/route.ts +9 -0
  6. package/app/app/api/mcp/agents/route.ts +27 -1
  7. package/app/app/api/mcp/install-skill/route.ts +9 -24
  8. package/app/app/api/skills/route.ts +18 -2
  9. package/app/app/api/tree-version/route.ts +8 -0
  10. package/app/app/layout.tsx +1 -0
  11. package/app/app/page.tsx +1 -2
  12. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  13. package/app/components/ActivityBar.tsx +2 -2
  14. package/app/components/Backlinks.tsx +5 -5
  15. package/app/components/CreateSpaceModal.tsx +3 -2
  16. package/app/components/DirPicker.tsx +1 -1
  17. package/app/components/DirView.tsx +2 -3
  18. package/app/components/EditorWrapper.tsx +3 -3
  19. package/app/components/FileTree.tsx +25 -10
  20. package/app/components/GuideCard.tsx +4 -4
  21. package/app/components/HomeContent.tsx +44 -14
  22. package/app/components/MarkdownView.tsx +2 -2
  23. package/app/components/OnboardingView.tsx +1 -1
  24. package/app/components/Panel.tsx +1 -1
  25. package/app/components/RightAgentDetailPanel.tsx +2 -1
  26. package/app/components/RightAskPanel.tsx +1 -1
  27. package/app/components/SearchModal.tsx +10 -2
  28. package/app/components/SidebarLayout.tsx +36 -10
  29. package/app/components/ThemeToggle.tsx +1 -1
  30. package/app/components/agents/AgentDetailContent.tsx +454 -59
  31. package/app/components/agents/AgentsContentPage.tsx +89 -20
  32. package/app/components/agents/AgentsMcpSection.tsx +513 -85
  33. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  34. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  35. package/app/components/agents/AgentsSkillsSection.tsx +746 -105
  36. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  37. package/app/components/agents/agents-content-model.ts +308 -10
  38. package/app/components/ask/AskContent.tsx +34 -5
  39. package/app/components/ask/FileChip.tsx +1 -0
  40. package/app/components/ask/MentionPopover.tsx +13 -1
  41. package/app/components/ask/MessageList.tsx +5 -7
  42. package/app/components/ask/ToolCallBlock.tsx +4 -4
  43. package/app/components/changes/ChangesBanner.tsx +89 -13
  44. package/app/components/changes/ChangesContentPage.tsx +134 -51
  45. package/app/components/echo/EchoHero.tsx +10 -24
  46. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  47. package/app/components/echo/EchoPageSections.tsx +13 -9
  48. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  49. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  50. package/app/components/explore/ExploreContent.tsx +3 -7
  51. package/app/components/explore/UseCaseCard.tsx +4 -15
  52. package/app/components/panels/AgentsPanel.tsx +22 -128
  53. package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
  54. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
  55. package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
  56. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  57. package/app/components/panels/EchoPanel.tsx +8 -10
  58. package/app/components/panels/PanelNavRow.tsx +9 -2
  59. package/app/components/panels/PluginsPanel.tsx +5 -5
  60. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  61. package/app/components/renderers/agent-inspector/manifest.ts +5 -3
  62. package/app/components/renderers/config/manifest.ts +1 -0
  63. package/app/components/renderers/csv/manifest.ts +1 -0
  64. package/app/components/renderers/todo/manifest.ts +1 -0
  65. package/app/components/settings/AiTab.tsx +3 -3
  66. package/app/components/settings/AppearanceTab.tsx +2 -2
  67. package/app/components/settings/KnowledgeTab.tsx +3 -3
  68. package/app/components/settings/McpAgentInstall.tsx +3 -6
  69. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  70. package/app/components/settings/McpSkillRow.tsx +2 -3
  71. package/app/components/settings/McpSkillsSection.tsx +2 -2
  72. package/app/components/settings/McpTab.tsx +12 -13
  73. package/app/components/settings/MonitoringTab.tsx +13 -13
  74. package/app/components/settings/PluginsTab.tsx +6 -5
  75. package/app/components/settings/Primitives.tsx +3 -4
  76. package/app/components/settings/SettingsContent.tsx +3 -3
  77. package/app/components/settings/SyncTab.tsx +11 -17
  78. package/app/components/settings/UpdateTab.tsx +18 -21
  79. package/app/components/settings/types.ts +14 -0
  80. package/app/components/setup/StepKB.tsx +1 -1
  81. package/app/hooks/useMcpData.tsx +7 -4
  82. package/app/hooks/useMention.ts +25 -8
  83. package/app/lib/agent/log.ts +15 -18
  84. package/app/lib/agent/stream-consumer.ts +3 -0
  85. package/app/lib/agent/to-agent-messages.ts +6 -4
  86. package/app/lib/core/agent-audit-log.ts +280 -0
  87. package/app/lib/core/content-changes.ts +148 -8
  88. package/app/lib/core/index.ts +11 -0
  89. package/app/lib/fs.ts +16 -1
  90. package/app/lib/i18n-en.ts +317 -36
  91. package/app/lib/i18n-zh.ts +316 -35
  92. package/app/lib/mcp-agents.ts +273 -2
  93. package/app/lib/renderers/index.ts +1 -2
  94. package/app/lib/renderers/registry.ts +10 -0
  95. package/app/lib/types.ts +2 -0
  96. package/app/next-env.d.ts +1 -1
  97. package/bin/lib/mcp-agents.js +38 -13
  98. package/package.json +1 -1
  99. package/scripts/migrate-agent-audit-log.js +170 -0
  100. package/scripts/migrate-agent-diff.js +146 -0
  101. package/scripts/setup.js +12 -17
  102. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  103. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  104. 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
- connectionGraph: string;
19
- table: { agent: string; status: string; transport: string; actions: string };
20
- actions: { copySnippet: string; copied: string; testConnection: string; reconnect: string };
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 role="tabpanel" id="agents-panel-mcp" aria-labelledby="agents-tab-mcp" className="rounded-lg border border-border bg-card p-4 space-y-4">
30
- <div className="flex items-center justify-between">
31
- <h2 className="text-sm font-medium text-foreground flex items-center gap-2">
32
- <Server size={15} className="text-muted-foreground" />
33
- {copy.title}
34
- </h2>
35
- <button
36
- type="button"
37
- onClick={() => void mcp.refresh()}
38
- className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
39
- >
40
- <RefreshCw size={13} />
41
- {copy.refresh}
42
- </button>
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
- <div className="rounded-md border border-border bg-background p-3">
46
- <p className="text-xs font-medium text-muted-foreground mb-2">{copy.connectionGraph}</p>
47
- <div className="flex flex-wrap items-center gap-2 text-xs">
48
- <NodePill label={copy.status.connected} count={buckets.connected.length} tone="ok" />
49
- <span className="text-muted-foreground">→</span>
50
- <NodePill label={copy.status.detected} count={buckets.detected.length} tone="warn" />
51
- <span className="text-muted-foreground">→</span>
52
- <NodePill label={copy.status.notFound} count={buckets.notFound.length} tone="neutral" />
53
- <span className="mx-2 text-muted-foreground">|</span>
54
- <NodePill label={copy.title} count={mcp.status?.running ? 1 : 0} tone={mcp.status?.running ? 'ok' : 'neutral'} />
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
- <div className="overflow-x-auto">
59
- <table className="w-full text-sm">
60
- <thead>
61
- <tr className="text-left border-b border-border">
62
- <th className="py-2 font-medium text-muted-foreground">{copy.table.agent}</th>
63
- <th className="py-2 font-medium text-muted-foreground">{copy.table.status}</th>
64
- <th className="py-2 font-medium text-muted-foreground">{copy.table.transport}</th>
65
- <th className="py-2 font-medium text-muted-foreground">{copy.table.actions}</th>
66
- </tr>
67
- </thead>
68
- <tbody>
69
- {mcp.agents.map((agent) => (
70
- <tr key={agent.key} className="border-b border-border/60">
71
- <td className="py-2 text-foreground">
72
- <Link href={`/agents/${encodeURIComponent(agent.key)}`} className="hover:underline">{agent.name}</Link>
73
- </td>
74
- <td className="py-2 text-muted-foreground">{agent.present ? (agent.installed ? copy.status.connected : copy.status.detected) : copy.status.notFound}</td>
75
- <td className="py-2 text-muted-foreground">{agent.transport ?? agent.preferredTransport}</td>
76
- <td className="py-2">
77
- <div className="flex flex-wrap items-center gap-2">
78
- <button
79
- type="button"
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
- 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"
82
- >
83
- {copyState === agent.key ? copy.actions.copied : copy.actions.copySnippet}
84
- </button>
85
- <button type="button" 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">
86
- {copy.actions.testConnection}
87
- </button>
88
- <button type="button" 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">
89
- {copy.actions.reconnect}
90
- </button>
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
- </td>
93
- </tr>
94
- ))}
95
- </tbody>
96
- </table>
97
- </div>
98
- </section>
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
- function NodePill({
103
- label,
104
- count,
105
- tone,
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
- label: string;
108
- count: number;
109
- tone: 'ok' | 'warn' | 'neutral';
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 cls =
112
- tone === 'ok'
113
- ? 'bg-success/10 text-success'
114
- : tone === 'warn'
115
- ? 'bg-[var(--amber-dim)] text-[var(--amber)]'
116
- : 'bg-muted text-muted-foreground';
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
- <span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md ${cls}`}>
119
- <span>{label}</span>
120
- <span className="tabular-nums">{count}</span>
121
- </span>
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
  }