@yancyyu/openhermit 1.6.36 → 1.6.38

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 (91) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-B_QAoeaA.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-PB9luAZU.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-Dfzcp_Ry.js → _basePickBy-CIhniz70.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-B5u2Yiq2.js → _baseUniq-cKAW4Q8I.js} +1 -1
  5. package/dist-renderer/assets/{arc-DElOI7qz.js → arc-YmNsoDXW.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Cf6f4tCu.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-FJUdo9Ry.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-BvJQS9lb.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
  9. package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-n-SGLbin.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-Dwle9tlA.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-Dic8YxQz.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-3n5jC1jk.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-BqizUB3O.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-JRDmD8o9.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-BxFpQw92.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-ByqPwtW9.js → chunk-TZMSLE5B-B0xGXInl.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +1 -0
  20. package/dist-renderer/assets/clone-BTNuUva-.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CVztr86T.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-CIui920O.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-CyL8-bgb.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-CM_67YoY.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DtrtPSGg.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-bOICzF9d.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CJV1g9Hr.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CXbhDo09.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vbXopTpS.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
  30. package/dist-renderer/assets/{graph-CY2T-j4q.js → graph-DOe5j8dH.js} +1 -1
  31. package/dist-renderer/assets/{index-Dv7q-OB0.js → index-B2Dy7M2G.js} +1 -1
  32. package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
  33. package/dist-renderer/assets/{index-CpyChjme.js → index-BySQS7AB.js} +1 -1
  34. package/dist-renderer/assets/{index-S-i9egm8.js → index-C_okzZXP.js} +1 -1
  35. package/dist-renderer/assets/{index-Mrh4pTHw.js → index-CzWxVCRL.js} +1 -1
  36. package/dist-renderer/assets/{index-Dn-BpzSm.js → index-V7dAKPqd.js} +571 -607
  37. package/dist-renderer/assets/{index-C9ONRXVI.js → index-VJ-MM9xa.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-BNg14AdU.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-2_PkPCiu.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DjTx7qoU.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
  41. package/dist-renderer/assets/{layout-DZlHGGN0.js → layout-B-z3y17c.js} +1 -1
  42. package/dist-renderer/assets/{linear-DnlOm48z.js → linear-D-RTX5UW.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-B-nrgt7V.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BToJFvWR.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C0qoxXvH.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DCKGwsGN.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CS6JCcu7.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-C9pAWoSR.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BTTX_v1m.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BHk97lQJ.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CSWCEzCQ.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CmiIc68g.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-DhwSTphI.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +2 -2
  56. package/src/main/ipc/extensions.ts +29 -50
  57. package/src/main/server.ts +17 -26
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -51
  59. package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
  61. package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -7
  62. package/src/renderer/api/httpClient.ts +108 -22
  63. package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -96
  64. package/src/renderer/components/extensions/plugins/PluginCard.tsx +8 -0
  65. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +13 -8
  66. package/src/renderer/components/team/TeamDetailView.tsx +15 -0
  67. package/src/renderer/components/team/tools/AddMcpInline.tsx +47 -0
  68. package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
  69. package/src/renderer/components/team/tools/McpChip.tsx +42 -0
  70. package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
  71. package/src/renderer/components/team/tools/ToolsSection.tsx +208 -0
  72. package/src/shared/types/extensions/api.ts +9 -0
  73. package/src/shared/types/extensions/index.ts +4 -0
  74. package/src/shared/types/extensions/mcp.ts +41 -0
  75. package/src/shared/utils/extensionNormalizers.ts +22 -0
  76. package/dist-renderer/assets/channel-DnbgZg0A.js +0 -1
  77. package/dist-renderer/assets/classDiagram-2ON5EDUG-BAD4p014.js +0 -1
  78. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BAD4p014.js +0 -1
  79. package/dist-renderer/assets/clone-CRX5ZTPd.js +0 -1
  80. package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
  81. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
  82. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
  83. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
  84. package/src/main/services/extensions/install/McpInstallService.ts +0 -407
  85. package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
  86. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
  87. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
  88. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
  89. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
  90. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
  91. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
@@ -1,593 +0,0 @@
1
- /**
2
- * McpServersPanel — search and browse the MCP server catalog.
3
- */
4
-
5
- import { useEffect, useMemo, useState } from 'react';
6
-
7
- import { Badge } from '@renderer/components/ui/badge';
8
- import { Button } from '@renderer/components/ui/button';
9
- import {
10
- Select,
11
- SelectContent,
12
- SelectItem,
13
- SelectTrigger,
14
- SelectValue,
15
- } from '@renderer/components/ui/select';
16
- import { useStore } from '@renderer/store';
17
- import { formatRelativeTime } from '@renderer/utils/formatters';
18
- import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
19
- import { CLI_NOT_FOUND_MARKER } from '@shared/constants/cli';
20
- import {
21
- getMcpDiagnosticKey,
22
- getMcpOperationKey,
23
- getMcpProjectStateKey,
24
- getPreferredMcpInstallationEntry,
25
- sanitizeMcpServerName,
26
- } from '@shared/utils/extensionNormalizers';
27
- import { AlertTriangle, RefreshCw, Search, Server, Trash2 } from 'lucide-react';
28
- import { useShallow } from 'zustand/react/shallow';
29
-
30
- import { SearchInput } from '../common/SearchInput';
31
-
32
- import { McpServerCard } from './McpServerCard';
33
- import { McpServerDetailDialog } from './McpServerDetailDialog';
34
-
35
- import type { CliInstallationStatus } from '@shared/types';
36
- import type {
37
- InstalledMcpEntry,
38
- InstallScope,
39
- McpCatalogItem,
40
- McpServerDiagnostic,
41
- } from '@shared/types/extensions';
42
-
43
- type McpSortValue = 'name-asc' | 'name-desc' | 'tools-desc';
44
-
45
- const MCP_SORT_OPTIONS: { value: McpSortValue; label: string }[] = [
46
- { value: 'name-asc', label: 'Name A→Z' },
47
- { value: 'name-desc', label: 'Name Z→A' },
48
- { value: 'tools-desc', label: 'Most tools' },
49
- ];
50
-
51
- function sortMcpServers(servers: McpCatalogItem[], sort: McpSortValue): McpCatalogItem[] {
52
- return [...servers].sort((a, b) => {
53
- switch (sort) {
54
- case 'name-asc':
55
- return a.name.localeCompare(b.name);
56
- case 'name-desc':
57
- return b.name.localeCompare(a.name);
58
- case 'tools-desc':
59
- return b.tools.length - a.tools.length;
60
- default:
61
- return 0;
62
- }
63
- });
64
- }
65
-
66
- interface McpServersPanelProps {
67
- projectPath: string | null;
68
- mcpSearchQuery: string;
69
- mcpSearch: (query: string) => void;
70
- mcpSearchResults: McpCatalogItem[];
71
- mcpSearchLoading: boolean;
72
- mcpSearchWarnings: string[];
73
- selectedMcpServerId: string | null;
74
- setSelectedMcpServerId: (id: string | null) => void;
75
- cliStatus?: Pick<
76
- CliInstallationStatus,
77
- | 'installed'
78
- | 'authLoggedIn'
79
- | 'binaryPath'
80
- | 'launchError'
81
- | 'flavor'
82
- | 'displayName'
83
- | 'providers'
84
- > | null;
85
- cliStatusLoading?: boolean;
86
- }
87
-
88
- export const McpServersPanel = ({
89
- projectPath,
90
- mcpSearchQuery,
91
- mcpSearch,
92
- mcpSearchResults,
93
- mcpSearchLoading,
94
- mcpSearchWarnings,
95
- selectedMcpServerId,
96
- setSelectedMcpServerId,
97
- cliStatus: cliStatusOverride,
98
- cliStatusLoading: cliStatusLoadingOverride,
99
- }: McpServersPanelProps): React.JSX.Element => {
100
- const projectStateKey = getMcpProjectStateKey(projectPath);
101
- const {
102
- browseCatalog,
103
- browseNextCursor,
104
- browseLoading,
105
- browseError,
106
- mcpBrowse,
107
- installedServersByProjectPath,
108
- installedServersFallback,
109
- fetchMcpGitHubStars,
110
- mcpDiagnosticsByProjectPath,
111
- mcpDiagnosticsFallback,
112
- mcpDiagnosticsLoadingByProjectPath,
113
- mcpDiagnosticsLoadingFallback,
114
- mcpDiagnosticsErrorByProjectPath,
115
- mcpDiagnosticsErrorFallback,
116
- mcpDiagnosticsLastCheckedAtByProjectPath,
117
- mcpDiagnosticsLastCheckedAtFallback,
118
- runMcpDiagnostics,
119
- uninstallMcpServer,
120
- mcpInstallProgress,
121
- } = useStore(
122
- useShallow((s) => ({
123
- browseCatalog: s.mcpBrowseCatalog,
124
- browseNextCursor: s.mcpBrowseNextCursor,
125
- browseLoading: s.mcpBrowseLoading,
126
- browseError: s.mcpBrowseError,
127
- mcpBrowse: s.mcpBrowse,
128
- installedServersByProjectPath: s.mcpInstalledServersByProjectPath,
129
- installedServersFallback: s.mcpInstalledServers,
130
- fetchMcpGitHubStars: s.fetchMcpGitHubStars,
131
- mcpDiagnosticsByProjectPath: s.mcpDiagnosticsByProjectPath,
132
- mcpDiagnosticsFallback: s.mcpDiagnostics,
133
- mcpDiagnosticsLoadingByProjectPath: s.mcpDiagnosticsLoadingByProjectPath,
134
- mcpDiagnosticsLoadingFallback: s.mcpDiagnosticsLoading,
135
- mcpDiagnosticsErrorByProjectPath: s.mcpDiagnosticsErrorByProjectPath,
136
- mcpDiagnosticsErrorFallback: s.mcpDiagnosticsError,
137
- mcpDiagnosticsLastCheckedAtByProjectPath: s.mcpDiagnosticsLastCheckedAtByProjectPath,
138
- mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt,
139
- runMcpDiagnostics: s.runMcpDiagnostics,
140
- uninstallMcpServer: s.uninstallMcpServer,
141
- mcpInstallProgress: s.mcpInstallProgress,
142
- }))
143
- );
144
- const storedCliStatus = useStore((s) => s.cliStatus);
145
- const storedCliStatusLoading = useStore((s) => s.cliStatusLoading);
146
- const cliStatus = cliStatusOverride ?? storedCliStatus;
147
- const cliStatusLoading = cliStatusLoadingOverride ?? storedCliStatusLoading;
148
- const installedServers =
149
- installedServersByProjectPath?.[projectStateKey] ?? installedServersFallback ?? [];
150
- const mcpDiagnostics =
151
- mcpDiagnosticsByProjectPath?.[projectStateKey] ?? mcpDiagnosticsFallback ?? {};
152
- const mcpDiagnosticsLoading =
153
- mcpDiagnosticsLoadingByProjectPath?.[projectStateKey] ?? mcpDiagnosticsLoadingFallback ?? false;
154
- const mcpDiagnosticsError =
155
- mcpDiagnosticsErrorByProjectPath?.[projectStateKey] ?? mcpDiagnosticsErrorFallback ?? null;
156
- const mcpDiagnosticsLastCheckedAt =
157
- mcpDiagnosticsLastCheckedAtByProjectPath?.[projectStateKey] ??
158
- mcpDiagnosticsLastCheckedAtFallback ??
159
- null;
160
-
161
- const [mcpSort, setMcpSort] = useState<McpSortValue>('name-asc');
162
-
163
- // Load initial browse data
164
- useEffect(() => {
165
- if (browseCatalog.length === 0 && !browseLoading && !browseError) {
166
- void mcpBrowse();
167
- }
168
- }, [browseCatalog.length, browseError, browseLoading, mcpBrowse]);
169
-
170
- const diagnosticsDisableReason = useMemo(() => {
171
- if (cliStatus === null || typeof cliStatus === 'undefined') {
172
- return cliStatusLoading ? '正在检查运行时状态...' : '正在检查运行时可用性...';
173
- }
174
-
175
- if (cliStatus?.installed === false) {
176
- if (cliStatus.binaryPath && cliStatus.launchError) {
177
- return '已找到配置的运行时,但启动失败。请前往首页修复或重新安装。';
178
- }
179
- return '需要配置运行时。请前往首页安装或修复。';
180
- }
181
-
182
- return null;
183
- }, [cliStatus, cliStatusLoading]);
184
-
185
- useEffect(() => {
186
- if (diagnosticsDisableReason) {
187
- return;
188
- }
189
- void runMcpDiagnostics(projectPath ?? undefined);
190
- }, [diagnosticsDisableReason, projectPath, runMcpDiagnostics]);
191
-
192
- // Fetch GitHub stars after catalog loads (fire-and-forget)
193
- useEffect(() => {
194
- const urls = browseCatalog.map((s) => s.repositoryUrl).filter((u): u is string => !!u);
195
- if (urls.length > 0) {
196
- fetchMcpGitHubStars(urls);
197
- }
198
- }, [browseCatalog, fetchMcpGitHubStars]);
199
-
200
- // Decide which list to show: search results or browse
201
- const isSearching = mcpSearchQuery.trim().length > 0;
202
- const rawServers = isSearching ? mcpSearchResults : browseCatalog;
203
- const isLoading = isSearching ? mcpSearchLoading : browseLoading;
204
- const warnings = isSearching ? mcpSearchWarnings : [];
205
-
206
- // Installed lookup set (lowercase CLI names)
207
- const installedNames = useMemo(
208
- () => new Set(installedServers.map((s) => s.name.toLowerCase())),
209
- [installedServers]
210
- );
211
-
212
- const installedEntriesByName = useMemo(() => {
213
- const entriesByName = new Map<string, InstalledMcpEntry[]>();
214
- for (const entry of installedServers) {
215
- const key = entry.name.toLowerCase();
216
- entriesByName.set(key, [...(entriesByName.get(key) ?? []), entry]);
217
- }
218
- return entriesByName;
219
- }, [installedServers]);
220
-
221
- /** Check if a catalog server is installed by comparing sanitized names */
222
- const isServerInstalled = (server: McpCatalogItem): boolean =>
223
- installedNames.has(sanitizeMcpServerName(server.name));
224
-
225
- const getInstalledEntries = (server: McpCatalogItem): InstalledMcpEntry[] =>
226
- installedEntriesByName.get(sanitizeMcpServerName(server.name)) ?? [];
227
-
228
- const getInstalledEntry = (server: McpCatalogItem): InstalledMcpEntry | null =>
229
- getPreferredMcpInstallationEntry(getInstalledEntries(server));
230
-
231
- const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => {
232
- const installedEntry = getInstalledEntry(server);
233
- return installedEntry
234
- ? (mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name, installedEntry.scope)] ??
235
- mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name)] ??
236
- mcpDiagnostics[installedEntry.name] ??
237
- null)
238
- : null;
239
- };
240
-
241
- const allDiagnostics = useMemo(
242
- () => Object.values(mcpDiagnostics).sort((a, b) => a.name.localeCompare(b.name)),
243
- [mcpDiagnostics]
244
- );
245
-
246
- const getDiagnosticBadgeClass = (status: McpServerDiagnostic['status']): string => {
247
- switch (status) {
248
- case 'connected':
249
- return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400';
250
- case 'needs-authentication':
251
- return 'border-amber-500/30 bg-amber-500/10 text-amber-400';
252
- case 'failed':
253
- return 'border-red-500/30 bg-red-500/10 text-red-400';
254
- default:
255
- return 'border-border bg-surface-raised text-text-muted';
256
- }
257
- };
258
-
259
- // Sort displayed catalog servers
260
- const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]);
261
- const runtimeLabel = getRuntimeDisplayName(cliStatus, true);
262
-
263
- // Find selected server (search in both lists to avoid losing selection during search toggle)
264
- const selectedServer = useMemo(() => {
265
- if (!selectedMcpServerId) return null;
266
- return (
267
- displayServers.find((s) => s.id === selectedMcpServerId) ??
268
- browseCatalog.find((s) => s.id === selectedMcpServerId) ??
269
- mcpSearchResults.find((s) => s.id === selectedMcpServerId) ??
270
- null
271
- );
272
- }, [displayServers, browseCatalog, mcpSearchResults, selectedMcpServerId]);
273
-
274
- return (
275
- <div className="flex flex-col gap-4">
276
- <div className="rounded-md border border-black/10 bg-surface-raised px-4 py-3 dark:border-white/10">
277
- <div className="flex items-center justify-between gap-3">
278
- <div>
279
- <p className="text-sm font-medium text-text">MCP 健康状态</p>
280
- <p className="text-xs text-text-muted">
281
- {mcpDiagnosticsLoading ? (
282
- <>正在通过 {runtimeLabel} 检查已安装的 MCP 服务器...</>
283
- ) : diagnosticsDisableReason ? (
284
- diagnosticsDisableReason
285
- ) : mcpDiagnosticsLastCheckedAt ? (
286
- `上次检查:${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}`
287
- ) : (
288
- <>在此页面运行诊断,以验证已安装 MCP 的连接状态。</>
289
- )}
290
- </p>
291
- </div>
292
- <Button
293
- variant="outline"
294
- size="sm"
295
- onClick={() => void runMcpDiagnostics(projectPath ?? undefined)}
296
- disabled={mcpDiagnosticsLoading || Boolean(diagnosticsDisableReason)}
297
- className="whitespace-nowrap"
298
- >
299
- <RefreshCw
300
- className={`mr-1.5 size-3.5 ${mcpDiagnosticsLoading ? 'animate-spin' : ''}`}
301
- />
302
- {mcpDiagnosticsLoading ? '检查中...' : '检查状态'}
303
- </Button>
304
- </div>
305
-
306
- {(mcpDiagnosticsLoading || allDiagnostics.length > 0) && (
307
- <div className="mt-4 border-t border-black/10 pt-4 dark:border-white/10">
308
- <div className="mb-3 flex items-center justify-between gap-3">
309
- <p className="text-sm font-medium text-text">运行时 MCP 诊断</p>
310
- {allDiagnostics.length > 0 && (
311
- <span className="text-xs text-text-muted">{allDiagnostics.length} 个服务器</span>
312
- )}
313
- </div>
314
- {allDiagnostics.length > 0 ? (
315
- <div className="mcp-diagnostics-list max-h-[18.5rem] space-y-2 overflow-y-auto pr-1">
316
- {allDiagnostics.map((diagnostic) => {
317
- const opKey = getMcpOperationKey(
318
- diagnostic.name,
319
- (diagnostic.scope as InstallScope) || 'user',
320
- projectPath
321
- );
322
- const uninstalling = mcpInstallProgress[opKey] === 'pending';
323
- return (
324
- <div
325
- key={getMcpDiagnosticKey(diagnostic.name, diagnostic.scope)}
326
- className="flex items-start justify-between gap-3 rounded-md border border-black/10 px-3 py-2 dark:border-white/10"
327
- >
328
- <div className="min-w-0 flex-1">
329
- <div className="flex items-center gap-2">
330
- <p className="text-sm text-text">{diagnostic.name}</p>
331
- {diagnostic.scope && (
332
- <span className="rounded-full border border-border bg-surface-raised px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-text-muted">
333
- {diagnostic.scope}
334
- </span>
335
- )}
336
- </div>
337
- <p
338
- className="truncate font-mono text-[11px] text-text-muted"
339
- title={diagnostic.target}
340
- >
341
- {diagnostic.target}
342
- </p>
343
- </div>
344
- <div className="flex items-center gap-2">
345
- <Badge
346
- className={getDiagnosticBadgeClass(diagnostic.status)}
347
- variant="outline"
348
- >
349
- {diagnostic.statusLabel}
350
- </Badge>
351
- <Button
352
- variant="ghost"
353
- size="sm"
354
- className="size-6 p-0 text-red-300 hover:text-red-200"
355
- disabled={uninstalling}
356
- onClick={() => {
357
- void uninstallMcpServer(
358
- diagnostic.name,
359
- diagnostic.name,
360
- diagnostic.scope || undefined,
361
- projectPath ?? undefined
362
- );
363
- }}
364
- title={`卸载 ${diagnostic.name}`}
365
- >
366
- <Trash2 className="size-3.5" />
367
- </Button>
368
- </div>
369
- </div>
370
- );
371
- })}
372
- </div>
373
- ) : (
374
- <p className="text-xs text-text-muted">正在等待诊断结果...</p>
375
- )}
376
- </div>
377
- )}
378
- </div>
379
-
380
- {/* Search + sort row */}
381
- <div className="flex items-center gap-3">
382
- <div className="flex-1">
383
- <SearchInput
384
- value={mcpSearchQuery}
385
- onChange={mcpSearch}
386
- placeholder="搜索 MCP 服务器..."
387
- />
388
- </div>
389
- <Select value={mcpSort} onValueChange={(v) => setMcpSort(v as McpSortValue)}>
390
- <SelectTrigger className="w-36">
391
- <SelectValue />
392
- </SelectTrigger>
393
- <SelectContent>
394
- {MCP_SORT_OPTIONS.map((opt) => (
395
- <SelectItem key={opt.value} value={opt.value}>
396
- {opt.label}
397
- </SelectItem>
398
- ))}
399
- </SelectContent>
400
- </Select>
401
- </div>
402
-
403
- {/* Warnings */}
404
- {warnings.length > 0 && (
405
- <div className="flex flex-col gap-1">
406
- {warnings.map((w, i) => (
407
- <div
408
- key={i}
409
- className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-400"
410
- >
411
- <AlertTriangle className="size-3.5 shrink-0" />
412
- {w}
413
- </div>
414
- ))}
415
- </div>
416
- )}
417
-
418
- {/* Skeleton loading */}
419
- {isLoading && displayServers.length === 0 && (
420
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
421
- {Array.from({ length: 6 }, (_, i) => (
422
- <div
423
- key={i}
424
- className="skeleton-card flex flex-col gap-2 rounded-lg border border-border p-4"
425
- style={{ animationDelay: `${i * 80}ms` }}
426
- >
427
- <div className="flex items-start gap-2.5">
428
- <div className="size-9 rounded-lg bg-surface-raised" />
429
- <div className="flex-1 space-y-1.5">
430
- <div className="h-4 w-32 rounded bg-surface-raised" />
431
- <div className="h-3 w-16 rounded-full bg-surface-raised" />
432
- </div>
433
- </div>
434
- <div className="space-y-1.5">
435
- <div className="h-3 w-full rounded bg-surface-raised" />
436
- <div className="h-3 w-2/3 rounded bg-surface-raised" />
437
- </div>
438
- <div className="flex items-center justify-between">
439
- <div className="h-5 w-12 rounded-full bg-surface-raised" />
440
- <div className="h-7 w-16 rounded bg-surface-raised" />
441
- </div>
442
- </div>
443
- ))}
444
- </div>
445
- )}
446
-
447
- {browseError && !isSearching && (
448
- <div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
449
- {browseError}
450
- </div>
451
- )}
452
-
453
- {mcpDiagnosticsError &&
454
- (mcpDiagnosticsError.includes(CLI_NOT_FOUND_MARKER) ? (
455
- <div className="flex items-start gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
456
- <AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
457
- <div>
458
- <p className="text-sm font-medium text-amber-300">
459
- {cliStatus?.flavor === 'agent_teams_orchestrator'
460
- ? `${runtimeLabel} not available`
461
- : `${runtimeLabel} not installed`}
462
- </p>
463
- <p className="mt-0.5 text-xs text-text-muted">
464
- MCP health checks require {runtimeLabel}. Go to the Dashboard to install or repair
465
- it.
466
- </p>
467
- </div>
468
- </div>
469
- ) : (
470
- <div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
471
- {mcpDiagnosticsError}
472
- </div>
473
- ))}
474
-
475
- {/* Installed servers (catalog-style cards for custom installs) */}
476
- {installedServers.length > 0 && (
477
- <div className="space-y-3">
478
- <div className="flex items-center justify-between gap-3">
479
- <p className="text-sm font-medium text-text">已安装</p>
480
- <Badge variant="secondary" className="font-normal">
481
- {installedServers.length}
482
- </Badge>
483
- </div>
484
- <div className="mcp-servers-grid grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
485
- {installedServers.map((entry) => {
486
- // Find matching catalog item
487
- const catalogMatch = browseCatalog.find(
488
- (c) => sanitizeMcpServerName(c.name) === entry.name.toLowerCase()
489
- );
490
- const fakeItem: McpCatalogItem = catalogMatch ?? {
491
- id: `custom:${entry.name}`,
492
- name: entry.name,
493
- description: entry.transport === 'http' ? 'HTTP/SSE 服务器' : 'Stdio 服务器',
494
- source: 'official',
495
- installSpec: null,
496
- envVars: [],
497
- requiresAuth: false,
498
- tools: [],
499
- };
500
- const diagnostic =
501
- mcpDiagnostics[getMcpDiagnosticKey(entry.name, entry.scope)] ??
502
- mcpDiagnostics[getMcpDiagnosticKey(entry.name)] ??
503
- mcpDiagnostics[entry.name] ??
504
- null;
505
-
506
- return (
507
- <McpServerCard
508
- key={`${entry.name}-${entry.scope}`}
509
- server={fakeItem}
510
- isInstalled={true}
511
- installedEntry={entry}
512
- installedEntries={[entry]}
513
- diagnostic={diagnostic}
514
- diagnosticsLoading={mcpDiagnosticsLoading}
515
- onClick={setSelectedMcpServerId}
516
- cliStatus={cliStatus}
517
- cliStatusLoading={cliStatusLoading}
518
- />
519
- );
520
- })}
521
- </div>
522
- </div>
523
- )}
524
-
525
- {/* Empty state */}
526
- {!isLoading && displayServers.length === 0 && (
527
- <div className="flex flex-col items-center gap-3 rounded-sm border border-dashed border-border px-8 py-16">
528
- <div className="flex size-10 items-center justify-center rounded-lg border border-border bg-surface-raised">
529
- {isSearching ? (
530
- <Search className="size-5 text-text-muted" />
531
- ) : (
532
- <Server className="size-5 text-text-muted" />
533
- )}
534
- </div>
535
- <p className="text-sm text-text-secondary">
536
- {isSearching ? '没有找到服务器' : '暂无 MCP 服务器'}
537
- </p>
538
- <p className="text-xs text-text-muted">
539
- {isSearching ? '试试其他搜索词' : '稍后再回来查看新服务器'}
540
- </p>
541
- </div>
542
- )}
543
-
544
- {displayServers.length > 0 && (
545
- <div className="mcp-servers-grid grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
546
- {displayServers.map((server) => (
547
- <McpServerCard
548
- key={server.id}
549
- server={server}
550
- isInstalled={isServerInstalled(server)}
551
- installedEntry={getInstalledEntry(server)}
552
- installedEntries={getInstalledEntries(server)}
553
- diagnostic={getDiagnostic(server)}
554
- diagnosticsLoading={mcpDiagnosticsLoading}
555
- onClick={setSelectedMcpServerId}
556
- cliStatus={cliStatus}
557
- cliStatusLoading={cliStatusLoading}
558
- />
559
- ))}
560
- </div>
561
- )}
562
-
563
- {/* Load more for browse */}
564
- {!isSearching && browseNextCursor && (
565
- <div className="flex justify-center py-4">
566
- <Button
567
- variant="outline"
568
- size="sm"
569
- disabled={browseLoading}
570
- onClick={() => void mcpBrowse(browseNextCursor)}
571
- >
572
- Load more
573
- </Button>
574
- </div>
575
- )}
576
-
577
- {/* Detail dialog */}
578
- <McpServerDetailDialog
579
- server={selectedServer}
580
- isInstalled={selectedServer ? isServerInstalled(selectedServer) : false}
581
- installedEntry={selectedServer ? getInstalledEntry(selectedServer) : null}
582
- installedEntries={selectedServer ? getInstalledEntries(selectedServer) : []}
583
- diagnostic={selectedServer ? getDiagnostic(selectedServer) : null}
584
- diagnosticsLoading={mcpDiagnosticsLoading}
585
- projectPath={projectPath}
586
- open={selectedMcpServerId !== null}
587
- onClose={() => setSelectedMcpServerId(null)}
588
- cliStatus={cliStatus}
589
- cliStatusLoading={cliStatusLoading}
590
- />
591
- </div>
592
- );
593
- };