@yancyyu/openhermit 1.6.37 → 1.6.39
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/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
- package/dist-renderer/assets/{TeamGraphOverlay-DYT3bwFR.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Dbt_EU-e.js → _basePickBy-B7kSYPxr.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DWo68sXI.js → _baseUniq-CnjxqwAk.js} +1 -1
- package/dist-renderer/assets/{arc-DXH1iZQK.js → arc-CLeZuINP.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-cjffS2Qr.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BKdZF02Y.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CN27pqaI.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
- package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CXPCI7g_.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-BGAXQZRC.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-TPDaA_KQ.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-D1ADe_tq.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Beimtg3a.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-OjNBu854.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DinqvbH8.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BfFtlPSZ.js → chunk-TZMSLE5B-Ds1hInvp.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CBYCBVRl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CBYCBVRl.js +1 -0
- package/dist-renderer/assets/clone-DcMF6Psb.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-D9z9Dgt7.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-n1g-DhEE.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BvxFq-BE.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-wVnJuwza.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-B707WJQw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-C-_1dGHs.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CMTSi3H6.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DZ0bNrAA.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DNVfGooQ.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
- package/dist-renderer/assets/{graph-865j_tM_.js → graph-CsoEwUhS.js} +1 -1
- package/dist-renderer/assets/{index-C_F9N5x-.js → index-BWPWmJNo.js} +1 -1
- package/dist-renderer/assets/{index-LwDIsXJN.js → index-Bu2R-Se7.js} +586 -740
- package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
- package/dist-renderer/assets/{index-DuUaf8at.js → index-D-3KgskL.js} +1 -1
- package/dist-renderer/assets/{index-BTx1nc4T.js → index-DGEBzLNT.js} +1 -1
- package/dist-renderer/assets/{index-2EW-eu3q.js → index-NhHNs2Oo.js} +1 -1
- package/dist-renderer/assets/{index-4dEMStJj.js → index-h17WuEyf.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-CyqtElLq.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BvjQ0Hm0.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CJJ-k0zT.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
- package/dist-renderer/assets/{layout-CnV6rQAG.js → layout-C3XWrpwo.js} +1 -1
- package/dist-renderer/assets/{linear-Cw3UQgyX.js → linear-OEEcn8KN.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-C5tDaGSK.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-CiIpPsau.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C3gtowNj.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CXBTrAnU.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-wziX77xG.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-sYqopcrj.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
- package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bl1-0_Cp.js → stateDiagram-FKZM4ZOC-B2FY5qqi.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DcoMiR8H.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CIRjJUBo.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVPuNe1n.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-3nT9yHwp.js → xychartDiagram-PRI3JC2R-J6VVJzRq.js} +1 -1
- package/dist-renderer/index.html +20 -53
- package/package.json +25 -18
- package/src/main/ipc/extensions.ts +30 -50
- package/src/main/server.ts +890 -247
- package/src/main/services/extensions/ExtensionFacadeService.ts +4 -56
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
- package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
- package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
- package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
- package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
- package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
- package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -8
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
- package/src/main/utils/teamProjectResolution.ts +15 -0
- package/src/renderer/App.tsx +8 -4
- package/src/renderer/api/httpClient.ts +174 -38
- package/src/renderer/api/providers.ts +23 -2
- package/src/renderer/assets/participant-avatars/01.svg +3 -0
- package/src/renderer/assets/participant-avatars/02.svg +3 -0
- package/src/renderer/assets/participant-avatars/03.svg +3 -0
- package/src/renderer/assets/participant-avatars/04.svg +3 -0
- package/src/renderer/assets/participant-avatars/05.svg +3 -0
- package/src/renderer/assets/participant-avatars/06.svg +3 -0
- package/src/renderer/assets/participant-avatars/07.svg +3 -0
- package/src/renderer/assets/participant-avatars/08.svg +3 -0
- package/src/renderer/assets/participant-avatars/09.svg +3 -0
- package/src/renderer/assets/participant-avatars/10.svg +3 -0
- package/src/renderer/assets/participant-avatars/11.svg +3 -0
- package/src/renderer/assets/participant-avatars/12.svg +3 -0
- package/src/renderer/assets/participant-avatars/13.svg +3 -0
- package/src/renderer/components/common/TerminalPane.tsx +213 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +12 -221
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
- package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
- package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
- package/src/renderer/components/extensions/plugins/PluginCard.tsx +10 -2
- package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +40 -22
- package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
- package/src/renderer/components/layout/PaneContent.tsx +8 -1
- package/src/renderer/components/layout/Sidebar.tsx +11 -54
- package/src/renderer/components/layout/SortableTab.tsx +20 -31
- package/src/renderer/components/layout/TabBar.tsx +1 -1
- package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
- package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
- package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
- package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
- package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
- package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
- package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
- package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
- package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
- package/src/renderer/components/schedules/calendar/index.ts +2 -0
- package/src/renderer/components/schedules/calendar/types.ts +44 -0
- package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
- package/src/renderer/components/settings/SettingsView.tsx +30 -35
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
- package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
- package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
- package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
- package/src/renderer/components/settings/sections/index.ts +0 -1
- package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
- package/src/renderer/components/splash/splashScene.ts +121 -929
- package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
- package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
- package/src/renderer/components/tasks/TasksView.tsx +112 -134
- package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
- package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
- package/src/renderer/components/team/TeamDetailView.tsx +325 -114
- package/src/renderer/components/team/TeamListView.tsx +108 -123
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
- package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
- package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
- package/src/renderer/components/team/members/MemberCard.tsx +14 -47
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
- package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
- package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
- package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
- package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
- package/src/renderer/components/team/tools/AddMcpInline.tsx +57 -0
- package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
- package/src/renderer/components/team/tools/McpChip.tsx +45 -0
- package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
- package/src/renderer/components/team/tools/ToolsSection.tsx +556 -0
- package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
- package/src/renderer/index.css +39 -22
- package/src/renderer/index.html +17 -50
- package/src/renderer/store/index.ts +2 -1
- package/src/renderer/store/slices/scheduleSlice.ts +1 -1
- package/src/renderer/store/slices/teamSlice.ts +45 -168
- package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
- package/src/renderer/utils/memberHelpers.ts +5 -17
- package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
- package/src/renderer/utils/providerSlashCommands.ts +0 -5
- package/src/renderer/utils/scheduleFormatters.ts +3 -1
- package/src/renderer/utils/teamMessageFiltering.ts +14 -1
- package/src/renderer/utils/teamModelAvailability.ts +18 -2
- package/src/shared/types/api.ts +121 -2
- package/src/shared/types/ccConnect.ts +2 -0
- package/src/shared/types/extensions/api.ts +9 -0
- package/src/shared/types/extensions/index.ts +4 -0
- package/src/shared/types/extensions/mcp.ts +41 -0
- package/src/shared/types/index.ts +3 -0
- package/src/shared/types/systemManager.ts +49 -0
- package/src/shared/types/team.ts +29 -0
- package/src/shared/types/terminal.ts +4 -2
- package/src/shared/utils/extensionNormalizers.ts +29 -0
- package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-Va_Vz-zz.js +0 -52
- package/dist-renderer/assets/channel-5dJIx68e.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-BMGXWJ2d.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BMGXWJ2d.js +0 -1
- package/dist-renderer/assets/clone-D7FWfGY9.js +0 -1
- package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
- package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DOYYvDbi.js +0 -1
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
- package/src/main/services/extensions/install/McpInstallService.ts +0 -407
- package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
- package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
- package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
- package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpLibraryPanel — global reusable MCP server definition library.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { api } from '@renderer/api';
|
|
8
|
+
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
|
9
|
+
import { Badge } from '@renderer/components/ui/badge';
|
|
10
|
+
import { Button } from '@renderer/components/ui/button';
|
|
11
|
+
import { useStore } from '@renderer/store';
|
|
12
|
+
import { getMcpProjectStateKey } from '@shared/utils/extensionNormalizers';
|
|
13
|
+
import { Plus, RefreshCw, Search, Server, Trash2, Upload } from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
import { SearchInput } from '../common/SearchInput';
|
|
16
|
+
|
|
17
|
+
import { McpLibraryEnableDialog } from './McpLibraryEnableDialog';
|
|
18
|
+
import { McpLibraryEntryDialog } from './McpLibraryEntryDialog';
|
|
19
|
+
|
|
20
|
+
import type { InstalledMcpEntry, McpInstallSpec, McpLibraryEntry } from '@shared/types/extensions';
|
|
21
|
+
|
|
22
|
+
interface McpLibraryPanelProps {
|
|
23
|
+
projectPath: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarizeTransport(spec: McpInstallSpec): string {
|
|
27
|
+
if (spec.type === 'stdio') {
|
|
28
|
+
return `stdio · ${spec.npmPackage}${spec.npmVersion ? `@${spec.npmVersion}` : ''}`;
|
|
29
|
+
}
|
|
30
|
+
return `${spec.transportType} · ${spec.url}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatUpdatedAt(timestamp: number): string {
|
|
34
|
+
if (!timestamp) return '未知时间';
|
|
35
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
|
36
|
+
month: '2-digit',
|
|
37
|
+
day: '2-digit',
|
|
38
|
+
hour: '2-digit',
|
|
39
|
+
minute: '2-digit',
|
|
40
|
+
}).format(new Date(timestamp));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function countBestEffortProjectInstances(
|
|
44
|
+
entry: McpLibraryEntry,
|
|
45
|
+
installedServers: InstalledMcpEntry[]
|
|
46
|
+
): number {
|
|
47
|
+
return installedServers.filter(
|
|
48
|
+
(server) => server.scope === 'project' && server.name === entry.name
|
|
49
|
+
).length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const McpLibraryPanel = ({ projectPath }: McpLibraryPanelProps): React.JSX.Element => {
|
|
53
|
+
const mcpInstalledServersByProjectPath = useStore((s) => s.mcpInstalledServersByProjectPath);
|
|
54
|
+
const mcpFetchInstalled = useStore((s) => s.mcpFetchInstalled);
|
|
55
|
+
const runMcpDiagnostics = useStore((s) => s.runMcpDiagnostics);
|
|
56
|
+
|
|
57
|
+
const [entries, setEntries] = useState<McpLibraryEntry[]>([]);
|
|
58
|
+
const [search, setSearch] = useState('');
|
|
59
|
+
const [loading, setLoading] = useState(false);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
62
|
+
const [editingEntry, setEditingEntry] = useState<McpLibraryEntry | null>(null);
|
|
63
|
+
const [importing, setImporting] = useState(false);
|
|
64
|
+
const [importMessage, setImportMessage] = useState<string | null>(null);
|
|
65
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
66
|
+
const [enablingEntry, setEnablingEntry] = useState<McpLibraryEntry | null>(null);
|
|
67
|
+
|
|
68
|
+
const projectStateKey = getMcpProjectStateKey(projectPath);
|
|
69
|
+
const projectInstalledServers = projectPath
|
|
70
|
+
? (mcpInstalledServersByProjectPath[projectStateKey] ?? [])
|
|
71
|
+
: [];
|
|
72
|
+
|
|
73
|
+
const refresh = useCallback(async (): Promise<void> => {
|
|
74
|
+
if (!api.mcpRegistry?.libraryList) {
|
|
75
|
+
setEntries([]);
|
|
76
|
+
setError('MCP 能力库 API 不可用');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setLoading(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
const libraryEntries = await api.mcpRegistry.libraryList();
|
|
84
|
+
setEntries(libraryEntries);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setError(err instanceof Error ? err.message : '加载 MCP 能力库失败');
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
void refresh();
|
|
94
|
+
}, [refresh]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!projectPath) return;
|
|
98
|
+
void mcpFetchInstalled(projectPath);
|
|
99
|
+
}, [mcpFetchInstalled, projectPath]);
|
|
100
|
+
|
|
101
|
+
const filteredEntries = useMemo(() => {
|
|
102
|
+
const query = search.trim().toLowerCase();
|
|
103
|
+
const sorted = [...entries].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
104
|
+
if (!query) return sorted;
|
|
105
|
+
return sorted.filter((entry) => {
|
|
106
|
+
const transportSummary = summarizeTransport(entry.installSpec).toLowerCase();
|
|
107
|
+
return (
|
|
108
|
+
entry.name.toLowerCase().includes(query) ||
|
|
109
|
+
(entry.description ?? '').toLowerCase().includes(query) ||
|
|
110
|
+
transportSummary.includes(query)
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
}, [entries, search]);
|
|
114
|
+
|
|
115
|
+
const openCreateDialog = (): void => {
|
|
116
|
+
setEditingEntry(null);
|
|
117
|
+
setDialogOpen(true);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const openEditDialog = (entry: McpLibraryEntry): void => {
|
|
121
|
+
setEditingEntry(entry);
|
|
122
|
+
setDialogOpen(true);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleSaved = (entry: McpLibraryEntry): void => {
|
|
126
|
+
setEntries((prev) => {
|
|
127
|
+
const exists = prev.some((item) => item.id === entry.id);
|
|
128
|
+
return exists ? prev.map((item) => (item.id === entry.id ? entry : item)) : [entry, ...prev];
|
|
129
|
+
});
|
|
130
|
+
setDialogOpen(false);
|
|
131
|
+
setEditingEntry(null);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleDelete = (entry: McpLibraryEntry): void => {
|
|
135
|
+
void (async () => {
|
|
136
|
+
if (!api.mcpRegistry?.libraryDelete) {
|
|
137
|
+
setError('MCP 能力库 API 不可用');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const confirmed = await confirm({
|
|
142
|
+
title: '删除 MCP 定义',
|
|
143
|
+
message: `确认从全局能力库删除「${entry.name}」?已安装到项目中的服务器不会被自动移除。`,
|
|
144
|
+
confirmLabel: '删除',
|
|
145
|
+
cancelLabel: '取消',
|
|
146
|
+
variant: 'danger',
|
|
147
|
+
});
|
|
148
|
+
if (!confirmed) return;
|
|
149
|
+
|
|
150
|
+
setDeletingId(entry.id);
|
|
151
|
+
setError(null);
|
|
152
|
+
try {
|
|
153
|
+
await api.mcpRegistry.libraryDelete(entry.id);
|
|
154
|
+
setEntries((prev) => prev.filter((item) => item.id !== entry.id));
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError(err instanceof Error ? err.message : '删除 MCP 定义失败');
|
|
157
|
+
} finally {
|
|
158
|
+
setDeletingId(null);
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleInstanceInstalled = (): void => {
|
|
164
|
+
setEnablingEntry(null);
|
|
165
|
+
if (projectPath) {
|
|
166
|
+
void mcpFetchInstalled(projectPath);
|
|
167
|
+
void runMcpDiagnostics(projectPath);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleImport = (): void => {
|
|
172
|
+
void (async () => {
|
|
173
|
+
if (!api.mcpRegistry?.libraryImport) {
|
|
174
|
+
setError('MCP 能力库 API 不可用');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setImporting(true);
|
|
179
|
+
setImportMessage(null);
|
|
180
|
+
setError(null);
|
|
181
|
+
try {
|
|
182
|
+
const result = await api.mcpRegistry.libraryImport({
|
|
183
|
+
projectPath: projectPath ?? undefined,
|
|
184
|
+
});
|
|
185
|
+
await refresh();
|
|
186
|
+
const imported = result.imported.length;
|
|
187
|
+
const skipped = result.skipped.length;
|
|
188
|
+
setImportMessage(`导入完成:新增 ${imported} 个,跳过 ${skipped} 个。`);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
setError(err instanceof Error ? err.message : '导入现有 MCP 定义失败');
|
|
191
|
+
} finally {
|
|
192
|
+
setImporting(false);
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const hasActiveSearch = search.trim().length > 0;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="flex flex-col gap-4">
|
|
201
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
202
|
+
<div>
|
|
203
|
+
<h2 className="text-sm font-semibold text-text">MCP 全局能力库</h2>
|
|
204
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
205
|
+
保存可复用的 MCP 服务器定义,再按需安装到团队项目。
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="flex flex-wrap gap-2">
|
|
209
|
+
<Button variant="outline" size="sm" onClick={() => void refresh()} disabled={loading}>
|
|
210
|
+
<RefreshCw className={`mr-1.5 size-3.5 ${loading ? 'animate-spin' : ''}`} />
|
|
211
|
+
刷新
|
|
212
|
+
</Button>
|
|
213
|
+
<Button variant="outline" size="sm" onClick={handleImport} disabled={importing}>
|
|
214
|
+
<Upload className="mr-1.5 size-3.5" />
|
|
215
|
+
{importing ? '导入中...' : '导入现有定义'}
|
|
216
|
+
</Button>
|
|
217
|
+
<Button size="sm" onClick={openCreateDialog}>
|
|
218
|
+
<Plus className="mr-1.5 size-3.5" />
|
|
219
|
+
添加定义
|
|
220
|
+
</Button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
225
|
+
<div className="flex-1">
|
|
226
|
+
<SearchInput value={search} onChange={setSearch} placeholder="搜索 MCP 定义..." />
|
|
227
|
+
</div>
|
|
228
|
+
<Badge variant="secondary" className="w-fit font-normal">
|
|
229
|
+
{filteredEntries.length} / {entries.length} 个定义
|
|
230
|
+
</Badge>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{importMessage && (
|
|
234
|
+
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 px-3 py-2 text-xs text-emerald-300">
|
|
235
|
+
{importMessage}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{error && (
|
|
240
|
+
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
|
|
241
|
+
{error}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{loading && (
|
|
246
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
247
|
+
{Array.from({ length: 6 }, (_, index) => (
|
|
248
|
+
<div
|
|
249
|
+
key={index}
|
|
250
|
+
className="skeleton-card flex flex-col gap-3 rounded-lg border border-border p-4"
|
|
251
|
+
style={{ animationDelay: `${index * 80}ms` }}
|
|
252
|
+
>
|
|
253
|
+
<div className="h-4 w-32 rounded bg-surface-raised" />
|
|
254
|
+
<div className="h-3 w-full rounded bg-surface-raised" />
|
|
255
|
+
<div className="h-3 w-3/4 rounded bg-surface-raised" />
|
|
256
|
+
<div className="flex gap-2">
|
|
257
|
+
<div className="h-5 w-16 rounded-full bg-surface-raised" />
|
|
258
|
+
<div className="h-5 w-20 rounded-full bg-surface-raised" />
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{!loading && !error && filteredEntries.length === 0 && (
|
|
266
|
+
<div className="flex flex-col items-center gap-3 rounded-sm border border-dashed border-border px-8 py-16">
|
|
267
|
+
<div className="flex size-10 items-center justify-center rounded-lg border border-border bg-surface-raised">
|
|
268
|
+
{hasActiveSearch ? (
|
|
269
|
+
<Search className="size-5 text-text-muted" />
|
|
270
|
+
) : (
|
|
271
|
+
<Server className="size-5 text-text-muted" />
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
<p className="text-sm text-text-secondary">
|
|
275
|
+
{hasActiveSearch ? '没有匹配搜索条件的 MCP 定义' : '暂无 MCP 定义'}
|
|
276
|
+
</p>
|
|
277
|
+
<p className="text-xs text-text-muted">
|
|
278
|
+
{hasActiveSearch ? '试着调整搜索关键词' : '添加新定义或从现有 MCP 配置导入'}
|
|
279
|
+
</p>
|
|
280
|
+
{!hasActiveSearch && (
|
|
281
|
+
<div className="flex flex-wrap justify-center gap-2">
|
|
282
|
+
<Button variant="outline" size="sm" onClick={handleImport} disabled={importing}>
|
|
283
|
+
导入现有定义
|
|
284
|
+
</Button>
|
|
285
|
+
<Button size="sm" onClick={openCreateDialog}>
|
|
286
|
+
添加定义
|
|
287
|
+
</Button>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{!loading && filteredEntries.length > 0 && (
|
|
294
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
295
|
+
{filteredEntries.map((entry) => {
|
|
296
|
+
const envCount = Object.keys(entry.envValues ?? {}).length;
|
|
297
|
+
const headerCount = entry.headers?.length ?? 0;
|
|
298
|
+
const isDeleting = deletingId === entry.id;
|
|
299
|
+
const isEnabling = enablingEntry?.id === entry.id;
|
|
300
|
+
const projectInstanceCount = countBestEffortProjectInstances(
|
|
301
|
+
entry,
|
|
302
|
+
projectInstalledServers
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div
|
|
307
|
+
key={entry.id}
|
|
308
|
+
className="hover:bg-surface-raised/40 group flex min-h-44 flex-col rounded-lg border border-border bg-surface p-4 text-left transition-all hover:-translate-y-0.5 hover:border-border-emphasis hover:shadow-lg"
|
|
309
|
+
>
|
|
310
|
+
<div className="flex items-start justify-between gap-3">
|
|
311
|
+
<div className="min-w-0">
|
|
312
|
+
<h3 className="truncate text-sm font-semibold text-text">{entry.name}</h3>
|
|
313
|
+
<p className="mt-1 line-clamp-2 text-xs text-text-muted">
|
|
314
|
+
{entry.description || '无描述'}
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
<Badge variant="outline" className="shrink-0 font-mono text-[10px] uppercase">
|
|
318
|
+
{entry.installSpec.type === 'stdio' ? 'stdio' : entry.installSpec.transportType}
|
|
319
|
+
</Badge>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div className="bg-surface-raised/40 mt-3 rounded-md border border-border px-2 py-1.5">
|
|
323
|
+
<p className="truncate font-mono text-[11px] text-text-secondary">
|
|
324
|
+
{summarizeTransport(entry.installSpec)}
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
329
|
+
<Badge variant="secondary" className="font-normal">
|
|
330
|
+
模板 · {entry.installSpec.type === 'stdio' ? 'Node / npm' : 'HTTP'}
|
|
331
|
+
</Badge>
|
|
332
|
+
{projectPath && (
|
|
333
|
+
<Badge variant="secondary" className="font-normal">
|
|
334
|
+
当前项目同名实例 {projectInstanceCount}
|
|
335
|
+
</Badge>
|
|
336
|
+
)}
|
|
337
|
+
<Badge variant="secondary" className="font-normal">
|
|
338
|
+
Env {envCount}
|
|
339
|
+
</Badge>
|
|
340
|
+
<Badge variant="secondary" className="font-normal">
|
|
341
|
+
Headers {headerCount}
|
|
342
|
+
</Badge>
|
|
343
|
+
<Badge variant="secondary" className="font-normal">
|
|
344
|
+
更新 {formatUpdatedAt(entry.updatedAt)}
|
|
345
|
+
</Badge>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div className="mt-auto flex items-center justify-end gap-2 pt-4">
|
|
349
|
+
{projectPath && (
|
|
350
|
+
<Button
|
|
351
|
+
variant={isEnabling ? 'secondary' : 'outline'}
|
|
352
|
+
size="sm"
|
|
353
|
+
className="h-7 px-2 text-xs"
|
|
354
|
+
onClick={() => setEnablingEntry(isEnabling ? null : entry)}
|
|
355
|
+
>
|
|
356
|
+
添加实例到当前项目
|
|
357
|
+
</Button>
|
|
358
|
+
)}
|
|
359
|
+
<Button
|
|
360
|
+
variant="ghost"
|
|
361
|
+
size="sm"
|
|
362
|
+
className="h-7 px-2 text-xs"
|
|
363
|
+
onClick={() => openEditDialog(entry)}
|
|
364
|
+
>
|
|
365
|
+
编辑
|
|
366
|
+
</Button>
|
|
367
|
+
<Button
|
|
368
|
+
variant="ghost"
|
|
369
|
+
size="sm"
|
|
370
|
+
className="h-7 px-2 text-xs text-red-400 hover:bg-red-500/10 hover:text-red-300"
|
|
371
|
+
disabled={isDeleting}
|
|
372
|
+
onClick={() => handleDelete(entry)}
|
|
373
|
+
>
|
|
374
|
+
<Trash2 className="mr-1 size-3" />
|
|
375
|
+
{isDeleting ? '删除中...' : '删除'}
|
|
376
|
+
</Button>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
})}
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
<McpLibraryEnableDialog
|
|
385
|
+
open={Boolean(enablingEntry)}
|
|
386
|
+
entry={enablingEntry}
|
|
387
|
+
projectPath={projectPath}
|
|
388
|
+
installedServers={projectInstalledServers}
|
|
389
|
+
onClose={() => setEnablingEntry(null)}
|
|
390
|
+
onEnabled={handleInstanceInstalled}
|
|
391
|
+
/>
|
|
392
|
+
|
|
393
|
+
<McpLibraryEntryDialog
|
|
394
|
+
open={dialogOpen}
|
|
395
|
+
entry={editingEntry}
|
|
396
|
+
onClose={() => {
|
|
397
|
+
setDialogOpen(false);
|
|
398
|
+
setEditingEntry(null);
|
|
399
|
+
}}
|
|
400
|
+
onSaved={handleSaved}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getPluginOperationKey,
|
|
11
11
|
hasInstallationInScope,
|
|
12
12
|
inferCapabilities,
|
|
13
|
+
isEssentialPlugin,
|
|
13
14
|
normalizeCategory,
|
|
14
15
|
} from '@shared/utils/extensionNormalizers';
|
|
15
16
|
import { Tag } from 'lucide-react';
|
|
@@ -70,9 +71,16 @@ export const PluginCard = ({
|
|
|
70
71
|
plugin.isInstalled ? 'border-l-2 border-border border-l-emerald-500/35' : 'border-border'
|
|
71
72
|
}`}
|
|
72
73
|
>
|
|
73
|
-
{plugin
|
|
74
|
+
{isEssentialPlugin(plugin) && (
|
|
74
75
|
<div className="pointer-events-none absolute -left-px -top-px size-16 overflow-hidden">
|
|
75
|
-
<div className="absolute left-[-24px] top-[4px] w-[80px] -rotate-45 bg-
|
|
76
|
+
<div className="absolute left-[-24px] top-[4px] w-[80px] -rotate-45 bg-amber-500/90 text-center text-[9px] font-semibold leading-[18px] text-white shadow-sm">
|
|
77
|
+
⭐ 必装
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{plugin.source === 'official' && (
|
|
82
|
+
<div className="pointer-events-none absolute -right-px -top-px size-16 overflow-hidden">
|
|
83
|
+
<div className="absolute right-[-24px] top-[4px] w-[80px] rotate-45 bg-blue-500/90 text-center text-[9px] font-semibold leading-[18px] text-white shadow-sm">
|
|
76
84
|
Official
|
|
77
85
|
</div>
|
|
78
86
|
</div>
|
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
SelectValue,
|
|
17
17
|
} from '@renderer/components/ui/select';
|
|
18
18
|
import { useStore } from '@renderer/store';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
inferCapabilities,
|
|
21
|
+
isEssentialPlugin,
|
|
22
|
+
isHiddenPluginFromStore,
|
|
23
|
+
normalizeCategory,
|
|
24
|
+
} from '@shared/utils/extensionNormalizers';
|
|
20
25
|
import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
|
|
21
26
|
import { ArrowUpDown, Filter, Puzzle, Search } from 'lucide-react';
|
|
22
27
|
import { useShallow } from 'zustand/react/shallow';
|
|
@@ -69,7 +74,7 @@ function selectFilteredPlugins(
|
|
|
69
74
|
filters: PluginFilters,
|
|
70
75
|
sort: { field: PluginSortField; order: 'asc' | 'desc' }
|
|
71
76
|
): EnrichedPlugin[] {
|
|
72
|
-
let result = catalog;
|
|
77
|
+
let result = catalog.filter((plugin) => !isHiddenPluginFromStore(plugin));
|
|
73
78
|
|
|
74
79
|
// Search
|
|
75
80
|
if (filters.search) {
|
|
@@ -100,9 +105,15 @@ function selectFilteredPlugins(
|
|
|
100
105
|
result = result.filter((p) => p.isInstalled);
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
// Sort
|
|
108
|
+
// Sort. Essential plugins (oh-my-claudecode, codex) are pinned to the top of
|
|
109
|
+
// the grid — installed or not — so the recommendation lives inside the store
|
|
110
|
+
// list itself (rather than a separate banner). Within each group the chosen
|
|
111
|
+
// sort field still applies.
|
|
104
112
|
const direction = sort.order === 'asc' ? 1 : -1;
|
|
113
|
+
const recommendRank = (p: EnrichedPlugin): number => (isEssentialPlugin(p) ? 0 : 1);
|
|
105
114
|
result = [...result].sort((a, b) => {
|
|
115
|
+
const rankDelta = recommendRank(a) - recommendRank(b);
|
|
116
|
+
if (rankDelta !== 0) return rankDelta;
|
|
106
117
|
switch (sort.field) {
|
|
107
118
|
case 'popularity':
|
|
108
119
|
return (a.installCount - b.installCount) * direction;
|
|
@@ -191,27 +202,34 @@ export const PluginsPanel = ({
|
|
|
191
202
|
}
|
|
192
203
|
return counts.size;
|
|
193
204
|
}, [catalog]);
|
|
205
|
+
const unsupportedPluginProviders = useMemo(() => {
|
|
206
|
+
if (cliStatus?.flavor !== 'agent_teams_orchestrator') {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return cliStatus.providers
|
|
211
|
+
.map((provider) => ({
|
|
212
|
+
provider,
|
|
213
|
+
capability: getCliProviderExtensionCapability(provider, 'plugins'),
|
|
214
|
+
}))
|
|
215
|
+
.filter(({ capability }) => capability.status !== 'supported');
|
|
216
|
+
}, [cliStatus]);
|
|
217
|
+
|
|
194
218
|
return (
|
|
195
219
|
<div className="flex flex-col gap-4">
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
在多模型运行时中,目前插件仅保证用于 Anthropic
|
|
210
|
-
会话。我们正在为所有智能体构建更广泛的插件支持,包括通用插件和智能体专用插件。
|
|
211
|
-
{capability.reason ? ` ${capability.reason}` : ''}
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
})()}
|
|
220
|
+
{unsupportedPluginProviders.length > 0 && (
|
|
221
|
+
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm text-amber-300">
|
|
222
|
+
多模型运行时中,部分提供商暂不支持插件管理:
|
|
223
|
+
{unsupportedPluginProviders
|
|
224
|
+
.map(({ provider, capability }) =>
|
|
225
|
+
capability.reason
|
|
226
|
+
? `${provider.displayName}(${capability.reason})`
|
|
227
|
+
: provider.displayName
|
|
228
|
+
)
|
|
229
|
+
.join('、')}
|
|
230
|
+
。
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
215
233
|
{/* Search + Sort + Installed only row */}
|
|
216
234
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
|
217
235
|
<div className="flex-1">
|