@yancyyu/openhermit 1.6.37 → 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.
- package/dist-renderer/assets/{ProjectEditorOverlay-Va_Vz-zz.js → ProjectEditorOverlay-lJZi-9Hp.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-DYT3bwFR.js → TeamGraphOverlay-ZEDfZyHb.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Dbt_EU-e.js → _basePickBy-CIhniz70.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DWo68sXI.js → _baseUniq-cKAW4Q8I.js} +1 -1
- package/dist-renderer/assets/{arc-DXH1iZQK.js → arc-YmNsoDXW.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-cjffS2Qr.js → architectureDiagram-VXUJARFQ-DHEls2sX.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BKdZF02Y.js → blockDiagram-VD42YOAC-Bpwf1Sbg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CN27pqaI.js → c4Diagram-YG6GDRKO-B0IaQ4w5.js} +1 -1
- package/dist-renderer/assets/channel-yIlSKy0e.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CXPCI7g_.js → chunk-4BX2VUAB-DLk-hcFc.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-BGAXQZRC.js → chunk-55IACEB6-1XRmX_Zm.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-TPDaA_KQ.js → chunk-B4BG7PRW-1waH1DAD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-D1ADe_tq.js → chunk-DI55MBZ5-BqpZBtrN.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Beimtg3a.js → chunk-FMBD7UC4-Bly7vVym.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-OjNBu854.js → chunk-QN33PNHL-Ci2QWBAs.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DinqvbH8.js → chunk-QZHKN3VN-YCqFW7d-.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BfFtlPSZ.js → chunk-TZMSLE5B-B0xGXInl.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +1 -0
- package/dist-renderer/assets/clone-BTNuUva-.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-D9z9Dgt7.js → cose-bilkent-S5V4N54A-DxcFNQKT.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-n1g-DhEE.js → dagre-6UL2VRFP-DPo_RfZY.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BvxFq-BE.js → diagram-PSM6KHXK-U3hQsFe4.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-wVnJuwza.js → diagram-QEK2KX5R-OrwrAy0V.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-B707WJQw.js → diagram-S2PKOQOG-CXATPWVw.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-C-_1dGHs.js → erDiagram-Q2GNP2WA-B0e8AfMF.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CMTSi3H6.js → flowDiagram-NV44I4VS-CXfzA4jJ.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DZ0bNrAA.js → ganttDiagram-JELNMOA3-CMr08qVl.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DNVfGooQ.js → gitGraphDiagram-V2S2FVAM-vYFHpPmy.js} +1 -1
- package/dist-renderer/assets/{graph-865j_tM_.js → graph-DOe5j8dH.js} +1 -1
- package/dist-renderer/assets/{index-2EW-eu3q.js → index-B2Dy7M2G.js} +1 -1
- package/dist-renderer/assets/index-Bi6nrZ4z.css +1 -0
- package/dist-renderer/assets/{index-C_F9N5x-.js → index-BySQS7AB.js} +1 -1
- package/dist-renderer/assets/{index-4dEMStJj.js → index-C_okzZXP.js} +1 -1
- package/dist-renderer/assets/{index-DuUaf8at.js → index-CzWxVCRL.js} +1 -1
- package/dist-renderer/assets/{index-LwDIsXJN.js → index-V7dAKPqd.js} +571 -607
- package/dist-renderer/assets/{index-BTx1nc4T.js → index-VJ-MM9xa.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-CyqtElLq.js → infoDiagram-HS3SLOUP-D_WubR0B.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BvjQ0Hm0.js → journeyDiagram-XKPGCS4Q-w9ca-1TI.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CJJ-k0zT.js → kanban-definition-3W4ZIXB7-Jg9p6_pN.js} +1 -1
- package/dist-renderer/assets/{layout-CnV6rQAG.js → layout-B-z3y17c.js} +1 -1
- package/dist-renderer/assets/{linear-Cw3UQgyX.js → linear-D-RTX5UW.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-C5tDaGSK.js → mindmap-definition-VGOIOE7T-CDQmHOYP.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-CiIpPsau.js → pieDiagram-ADFJNKIX-D_odsQL7.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C3gtowNj.js → quadrantDiagram-AYHSOK5B-BRsmYWSA.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CXBTrAnU.js → requirementDiagram-UZGBJVZJ-ChNE_BOV.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-wziX77xG.js → sankeyDiagram-TZEHDZUN-C8FtpwKc.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-sYqopcrj.js → sequenceDiagram-WL72ISMW-DmLCzNcc.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bl1-0_Cp.js → stateDiagram-FKZM4ZOC-WJBm4bhu.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-DOYYvDbi.js → stateDiagram-v2-4FDKWEC3-_m6iPPUR.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CIRjJUBo.js → timeline-definition-IT6M3QCI-BXs_hOJs.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVPuNe1n.js → treemap-GDKQZRPO-o04MA0G9.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-3nT9yHwp.js → xychartDiagram-PRI3JC2R-Czj69XRd.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/ipc/extensions.ts +29 -50
- package/src/main/server.ts +17 -26
- package/src/main/services/extensions/ExtensionFacadeService.ts +2 -51
- package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
- package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -7
- package/src/renderer/api/httpClient.ts +108 -22
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -96
- package/src/renderer/components/extensions/plugins/PluginCard.tsx +8 -0
- package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +13 -8
- package/src/renderer/components/team/TeamDetailView.tsx +15 -0
- package/src/renderer/components/team/tools/AddMcpInline.tsx +47 -0
- package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
- package/src/renderer/components/team/tools/McpChip.tsx +42 -0
- package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
- package/src/renderer/components/team/tools/ToolsSection.tsx +208 -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/utils/extensionNormalizers.ts +22 -0
- 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/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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* to run in a regular browser connected to an HTTP server.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
|
10
|
+
|
|
9
11
|
import type { DashboardRecentProjectsPayload } from '@features/recent-projects/contracts';
|
|
10
12
|
import type {
|
|
11
13
|
AddMemberRequest,
|
|
@@ -17,10 +19,13 @@ import type {
|
|
|
17
19
|
BoardTaskExactLogSummariesResponse,
|
|
18
20
|
BoardTaskLogStreamResponse,
|
|
19
21
|
BoardTaskLogStreamSummary,
|
|
22
|
+
CcSession,
|
|
23
|
+
CcSessionDetail,
|
|
20
24
|
ClaudeMdFileInfo,
|
|
21
25
|
ClaudeRootFolderSelection,
|
|
22
26
|
ClaudeRootInfo,
|
|
23
27
|
CliInstallerAPI,
|
|
28
|
+
CollabTask,
|
|
24
29
|
ConfigAPI,
|
|
25
30
|
ContextInfo,
|
|
26
31
|
ConversationGroup,
|
|
@@ -39,6 +44,7 @@ import type {
|
|
|
39
44
|
MachineRuntimeProcess,
|
|
40
45
|
MemberFullStats,
|
|
41
46
|
MemberLogSummary,
|
|
47
|
+
MemberSpawnStatusesSnapshot,
|
|
42
48
|
NotificationsAPI,
|
|
43
49
|
NotificationTrigger,
|
|
44
50
|
PaginatedSessionsResult,
|
|
@@ -65,6 +71,7 @@ import type {
|
|
|
65
71
|
SshLastConnection,
|
|
66
72
|
SubagentDetail,
|
|
67
73
|
TaskComment,
|
|
74
|
+
TeamAgentRuntimeSnapshot,
|
|
68
75
|
TeamChangeEvent,
|
|
69
76
|
TeamClaudeLogsQuery,
|
|
70
77
|
TeamClaudeLogsResponse,
|
|
@@ -95,26 +102,9 @@ import type {
|
|
|
95
102
|
UpdateSchedulePatch,
|
|
96
103
|
WaterfallData,
|
|
97
104
|
WslClaudeRootCandidate,
|
|
98
|
-
MemberSpawnStatusesSnapshot,
|
|
99
|
-
TeamAgentRuntimeSnapshot,
|
|
100
|
-
CcSession,
|
|
101
|
-
CcSessionDetail,
|
|
102
|
-
CollabTask,
|
|
103
105
|
} from '@shared/types';
|
|
104
|
-
|
|
105
|
-
import type {
|
|
106
|
-
AgentChangeSet,
|
|
107
|
-
ApplyReviewResult,
|
|
108
|
-
ChangeStats,
|
|
109
|
-
ConflictCheckResult,
|
|
110
|
-
FileChangeWithContent,
|
|
111
|
-
HunkDecision,
|
|
112
|
-
RejectResult,
|
|
113
|
-
TaskChangeSetV2,
|
|
114
|
-
} from '@shared/types/review';
|
|
115
|
-
import type { CliProviderStatus } from '@shared/types/cliInstaller';
|
|
116
106
|
import type { AgentConfig } from '@shared/types/api';
|
|
117
|
-
import type {
|
|
107
|
+
import type { CliProviderStatus } from '@shared/types/cliInstaller';
|
|
118
108
|
import type { EditorAPI, ProjectAPI, WorkspaceListResponse } from '@shared/types/editor';
|
|
119
109
|
import type {
|
|
120
110
|
EnrichedPlugin,
|
|
@@ -122,6 +112,10 @@ import type {
|
|
|
122
112
|
McpCatalogItem,
|
|
123
113
|
McpCustomInstallRequest,
|
|
124
114
|
McpInstallRequest,
|
|
115
|
+
McpLibraryEntry,
|
|
116
|
+
McpLibraryImportRequest,
|
|
117
|
+
McpLibraryImportResult,
|
|
118
|
+
McpLibraryUpsertRequest,
|
|
125
119
|
McpSearchResult,
|
|
126
120
|
McpServerDiagnostic,
|
|
127
121
|
OperationResult,
|
|
@@ -136,8 +130,19 @@ import type {
|
|
|
136
130
|
SkillUpsertRequest,
|
|
137
131
|
SkillWatcherEvent,
|
|
138
132
|
} from '@shared/types/extensions';
|
|
133
|
+
import type {
|
|
134
|
+
AgentChangeSet,
|
|
135
|
+
ApplyReviewResult,
|
|
136
|
+
ChangeStats,
|
|
137
|
+
ConflictCheckResult,
|
|
138
|
+
FileChangeWithContent,
|
|
139
|
+
HunkDecision,
|
|
140
|
+
RejectResult,
|
|
141
|
+
TaskChangeSetV2,
|
|
142
|
+
} from '@shared/types/review';
|
|
139
143
|
import type { ApplyReviewRequest } from '@shared/types/review';
|
|
140
144
|
import type { TerminalAPI } from '@shared/types/terminal';
|
|
145
|
+
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';
|
|
141
146
|
|
|
142
147
|
export class HttpAPIClient implements ElectronAPI {
|
|
143
148
|
private baseUrl: string;
|
|
@@ -166,7 +171,7 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
166
171
|
};
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
|
|
174
|
+
|
|
170
175
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- event callbacks have varying signatures
|
|
171
176
|
private addEventListener(channel: string, callback: (...args: any[]) => void): () => void {
|
|
172
177
|
this.initEventSource();
|
|
@@ -1873,7 +1878,41 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
1873
1878
|
}
|
|
1874
1879
|
})
|
|
1875
1880
|
);
|
|
1876
|
-
|
|
1881
|
+
// The cc-connect sidecar backend does not implement per-provider status
|
|
1882
|
+
// endpoints (they return an empty array), so drop any malformed entries
|
|
1883
|
+
// and only keep real provider objects.
|
|
1884
|
+
const validProviders = providerResults.filter(
|
|
1885
|
+
(p): p is CliProviderStatus =>
|
|
1886
|
+
p != null &&
|
|
1887
|
+
typeof p === 'object' &&
|
|
1888
|
+
!Array.isArray(p) &&
|
|
1889
|
+
typeof (p).providerId === 'string'
|
|
1890
|
+
);
|
|
1891
|
+
|
|
1892
|
+
// When the backend reports no provider capability data, fall back to a
|
|
1893
|
+
// sane Anthropic provider so extension management (plugins/MCP/skills)
|
|
1894
|
+
// is not falsely gated off. The backend remains the source of truth and
|
|
1895
|
+
// will reject an install if the runtime genuinely cannot perform it.
|
|
1896
|
+
const providers =
|
|
1897
|
+
validProviders.length > 0
|
|
1898
|
+
? validProviders
|
|
1899
|
+
: [
|
|
1900
|
+
{
|
|
1901
|
+
providerId: 'anthropic',
|
|
1902
|
+
displayName: 'Anthropic',
|
|
1903
|
+
supported: true,
|
|
1904
|
+
authenticated: true,
|
|
1905
|
+
authMethod: null,
|
|
1906
|
+
verificationState: 'verified',
|
|
1907
|
+
models: [],
|
|
1908
|
+
canLoginFromUi: true,
|
|
1909
|
+
capabilities: {
|
|
1910
|
+
teamLaunch: true,
|
|
1911
|
+
oneShot: true,
|
|
1912
|
+
extensions: createDefaultCliExtensionCapabilities(),
|
|
1913
|
+
},
|
|
1914
|
+
} satisfies CliProviderStatus,
|
|
1915
|
+
];
|
|
1877
1916
|
|
|
1878
1917
|
return {
|
|
1879
1918
|
flavor: 'agent_teams_orchestrator',
|
|
@@ -1887,7 +1926,7 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
1887
1926
|
launchError: null,
|
|
1888
1927
|
latestVersion: null,
|
|
1889
1928
|
updateAvailable: false,
|
|
1890
|
-
authLoggedIn: result.authenticated,
|
|
1929
|
+
authLoggedIn: result.authenticated ?? true,
|
|
1891
1930
|
authStatusChecking: true,
|
|
1892
1931
|
authMethod: null,
|
|
1893
1932
|
providers,
|
|
@@ -1914,7 +1953,21 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
1914
1953
|
},
|
|
1915
1954
|
getProviderStatus: async (providerId: string): Promise<CliProviderStatus | null> => {
|
|
1916
1955
|
try {
|
|
1917
|
-
|
|
1956
|
+
const result = await this.get<unknown>(
|
|
1957
|
+
`/api/cli/provider/${encodeURIComponent(providerId)}/status`
|
|
1958
|
+
);
|
|
1959
|
+
// The cc-connect sidecar returns an empty array for unimplemented
|
|
1960
|
+
// provider endpoints. Treat any malformed payload as "no data" (null)
|
|
1961
|
+
// so it does not overwrite the synthesized provider in getStatus().
|
|
1962
|
+
if (
|
|
1963
|
+
result == null ||
|
|
1964
|
+
typeof result !== 'object' ||
|
|
1965
|
+
Array.isArray(result) ||
|
|
1966
|
+
typeof (result as CliProviderStatus).providerId !== 'string'
|
|
1967
|
+
) {
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
return result as CliProviderStatus;
|
|
1918
1971
|
} catch {
|
|
1919
1972
|
return null;
|
|
1920
1973
|
}
|
|
@@ -2074,6 +2127,39 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
2074
2127
|
if (!result.success) return {};
|
|
2075
2128
|
return result.data ?? {};
|
|
2076
2129
|
},
|
|
2130
|
+
libraryList: async () => {
|
|
2131
|
+
const result = await this.get<{
|
|
2132
|
+
success: boolean;
|
|
2133
|
+
data?: McpLibraryEntry[];
|
|
2134
|
+
error?: string;
|
|
2135
|
+
}>('/api/extensions/mcp/library');
|
|
2136
|
+
if (!result.success) return [];
|
|
2137
|
+
return result.data ?? [];
|
|
2138
|
+
},
|
|
2139
|
+
libraryUpsert: async (request: McpLibraryUpsertRequest) => {
|
|
2140
|
+
const result = await this.post<{
|
|
2141
|
+
success: boolean;
|
|
2142
|
+
data?: McpLibraryEntry;
|
|
2143
|
+
error?: string;
|
|
2144
|
+
}>('/api/extensions/mcp/library', request);
|
|
2145
|
+
if (!result.success || !result.data) throw new Error(result.error ?? 'Save failed');
|
|
2146
|
+
return result.data;
|
|
2147
|
+
},
|
|
2148
|
+
libraryDelete: async (id: string) => {
|
|
2149
|
+
const result = await this.delete<{ success: boolean; error?: string }>(
|
|
2150
|
+
`/api/extensions/mcp/library/${encodeURIComponent(id)}`
|
|
2151
|
+
);
|
|
2152
|
+
if (!result.success) throw new Error(result.error ?? 'Delete failed');
|
|
2153
|
+
},
|
|
2154
|
+
libraryImport: async (request: McpLibraryImportRequest) => {
|
|
2155
|
+
const result = await this.post<{
|
|
2156
|
+
success: boolean;
|
|
2157
|
+
data?: McpLibraryImportResult;
|
|
2158
|
+
error?: string;
|
|
2159
|
+
}>('/api/extensions/mcp/library/import', request);
|
|
2160
|
+
if (!result.success) throw new Error(result.error ?? 'Import failed');
|
|
2161
|
+
return result.data ?? { imported: [], skipped: [] };
|
|
2162
|
+
},
|
|
2077
2163
|
};
|
|
2078
2164
|
|
|
2079
2165
|
skills = {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Global catalog data comes from Zustand store.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useCallback, useEffect, useMemo
|
|
7
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
8
8
|
|
|
9
9
|
// Stubs for removed codex-account feature
|
|
10
10
|
function useCodexAccountSnapshot(_opts: { enabled: boolean; includeRateLimits?: boolean }) {
|
|
@@ -41,22 +41,16 @@ import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
|
|
|
41
41
|
import { getCliProviderExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
|
42
42
|
import {
|
|
43
43
|
AlertTriangle,
|
|
44
|
-
BookOpen,
|
|
45
44
|
Info,
|
|
46
45
|
Loader2,
|
|
47
|
-
Plus,
|
|
48
46
|
Puzzle,
|
|
49
47
|
RefreshCw,
|
|
50
|
-
Server,
|
|
51
48
|
Sliders,
|
|
52
49
|
} from 'lucide-react';
|
|
53
50
|
import { useShallow } from 'zustand/react/shallow';
|
|
54
51
|
|
|
55
|
-
import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog';
|
|
56
52
|
import { EnvVarPanel } from './env/EnvVarPanel';
|
|
57
|
-
import { McpServersPanel } from './mcp/McpServersPanel';
|
|
58
53
|
import { PluginsPanel } from './plugins/PluginsPanel';
|
|
59
|
-
import { SkillsPanel } from './skills/SkillsPanel';
|
|
60
54
|
import { StoreExtensionToast } from './common/ExtensionToast';
|
|
61
55
|
import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
|
|
62
56
|
|
|
@@ -124,18 +118,6 @@ const EXTENSION_SUB_TABS = [
|
|
|
124
118
|
icon: Puzzle,
|
|
125
119
|
description: 'Claude Code 私有扩展,增强运行时的能力与集成。',
|
|
126
120
|
},
|
|
127
|
-
{
|
|
128
|
-
value: 'mcp-servers' as const,
|
|
129
|
-
label: 'MCP 服务器',
|
|
130
|
-
icon: Server,
|
|
131
|
-
description: '连接外部工具和应用,让运行时可以读取数据或执行本应用之外的操作。',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
value: 'skills' as const,
|
|
135
|
-
label: '技能',
|
|
136
|
-
icon: BookOpen,
|
|
137
|
-
description: '面向常见任务的可复用指令,帮助运行时更稳定地处理重复工作。',
|
|
138
|
-
},
|
|
139
121
|
{
|
|
140
122
|
value: 'env-vars' as const,
|
|
141
123
|
label: '环境变量',
|
|
@@ -145,16 +127,10 @@ const EXTENSION_SUB_TABS = [
|
|
|
145
127
|
] as const;
|
|
146
128
|
|
|
147
129
|
export const ExtensionStoreView = (): React.JSX.Element => {
|
|
148
|
-
const tabId = useTabIdOptional();
|
|
149
130
|
const {
|
|
150
131
|
bootstrapCliStatus,
|
|
151
132
|
fetchCliStatus,
|
|
152
133
|
fetchPluginCatalog,
|
|
153
|
-
fetchSkillsCatalog,
|
|
154
|
-
mcpBrowse,
|
|
155
|
-
mcpFetchInstalled,
|
|
156
|
-
mcpBrowseLoading,
|
|
157
|
-
skillsLoading,
|
|
158
134
|
cliStatus,
|
|
159
135
|
cliStatusLoading,
|
|
160
136
|
cliProviderStatusLoading,
|
|
@@ -168,12 +144,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
168
144
|
bootstrapCliStatus: s.bootstrapCliStatus,
|
|
169
145
|
fetchCliStatus: s.fetchCliStatus,
|
|
170
146
|
fetchPluginCatalog: s.fetchPluginCatalog,
|
|
171
|
-
fetchSkillsCatalog: s.fetchSkillsCatalog,
|
|
172
147
|
pluginCatalog: s.pluginCatalog,
|
|
173
|
-
mcpBrowse: s.mcpBrowse,
|
|
174
|
-
mcpFetchInstalled: s.mcpFetchInstalled,
|
|
175
|
-
mcpBrowseLoading: s.mcpBrowseLoading,
|
|
176
|
-
skillsLoading: s.skillsLoading,
|
|
177
148
|
cliStatus: s.cliStatus,
|
|
178
149
|
cliStatusLoading: s.cliStatusLoading,
|
|
179
150
|
cliProviderStatusLoading: s.cliProviderStatusLoading,
|
|
@@ -229,6 +200,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
229
200
|
const runtimeDisplayName = getRuntimeDisplayName(effectiveCliStatus, multimodelEnabled);
|
|
230
201
|
const cliInstalled = effectiveCliStatus?.installed ?? true;
|
|
231
202
|
const hasOngoingSessions = sessions.some((sess) => sess.isOngoing);
|
|
203
|
+
|
|
204
|
+
const tabState = useExtensionsTabState();
|
|
205
|
+
const tabId = useTabIdOptional();
|
|
232
206
|
const extensionsTabProjectId = useStore((s) =>
|
|
233
207
|
tabId
|
|
234
208
|
? (s.paneLayout.panes.flatMap((pane) => pane.tabs).find((tab) => tab.id === tabId)
|
|
@@ -236,8 +210,6 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
236
210
|
: null
|
|
237
211
|
);
|
|
238
212
|
|
|
239
|
-
const tabState = useExtensionsTabState();
|
|
240
|
-
const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false);
|
|
241
213
|
const resolvedProject = useMemo(
|
|
242
214
|
() => resolveProjectPathById(extensionsTabProjectId, projects, repositoryGroups),
|
|
243
215
|
[extensionsTabProjectId, projects, repositoryGroups]
|
|
@@ -254,42 +226,25 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
254
226
|
});
|
|
255
227
|
}, [bootstrapCliStatus, fetchCliStatus, multimodelEnabled]);
|
|
256
228
|
|
|
257
|
-
// Fetch MCP installed state on mount
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
void mcpFetchInstalled(projectPath ?? undefined);
|
|
260
|
-
}, [mcpFetchInstalled, projectPath]);
|
|
261
|
-
|
|
262
229
|
// Fetch Plugin catalog on mount / project change
|
|
263
230
|
useEffect(() => {
|
|
264
231
|
void fetchPluginCatalog(projectPath ?? undefined);
|
|
265
232
|
}, [fetchPluginCatalog, projectPath]);
|
|
266
233
|
|
|
267
|
-
//
|
|
268
|
-
useEffect(() => {
|
|
269
|
-
void fetchSkillsCatalog(projectPath ?? undefined);
|
|
270
|
-
}, [fetchSkillsCatalog, projectPath]);
|
|
271
|
-
|
|
272
|
-
// Refresh all data (MCP + skills + runtime status)
|
|
234
|
+
// Refresh all data
|
|
273
235
|
const handleRefresh = useCallback(() => {
|
|
274
236
|
void refreshCliStatusForCurrentMode({
|
|
275
237
|
multimodelEnabled,
|
|
276
238
|
bootstrapCliStatus,
|
|
277
239
|
fetchCliStatus,
|
|
278
240
|
});
|
|
279
|
-
void mcpBrowse(); // re-fetch first page
|
|
280
|
-
void mcpFetchInstalled(projectPath ?? undefined);
|
|
281
|
-
void fetchSkillsCatalog(projectPath ?? undefined);
|
|
282
241
|
}, [
|
|
283
242
|
bootstrapCliStatus,
|
|
284
243
|
fetchCliStatus,
|
|
285
|
-
fetchSkillsCatalog,
|
|
286
244
|
multimodelEnabled,
|
|
287
|
-
mcpBrowse,
|
|
288
|
-
mcpFetchInstalled,
|
|
289
|
-
projectPath,
|
|
290
245
|
]);
|
|
291
246
|
|
|
292
|
-
const isRefreshing = effectiveCliStatusLoading
|
|
247
|
+
const isRefreshing = effectiveCliStatusLoading;
|
|
293
248
|
const cliStatusBanner = useMemo(() => {
|
|
294
249
|
const providers = effectiveCliStatus?.providers ?? [];
|
|
295
250
|
const visibleProviders = filterExtensionStoreProviders(
|
|
@@ -535,17 +490,6 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
535
490
|
/>
|
|
536
491
|
))}
|
|
537
492
|
</TabsList>
|
|
538
|
-
{tabState.activeSubTab === 'mcp-servers' && (
|
|
539
|
-
<Button
|
|
540
|
-
variant="outline"
|
|
541
|
-
size="sm"
|
|
542
|
-
onClick={() => setCustomMcpDialogOpen(true)}
|
|
543
|
-
className="mb-1 whitespace-nowrap"
|
|
544
|
-
>
|
|
545
|
-
<Plus className="mr-1 size-3.5" />
|
|
546
|
-
添加自定义
|
|
547
|
-
</Button>
|
|
548
|
-
)}
|
|
549
493
|
</div>
|
|
550
494
|
|
|
551
495
|
<TabsContent value="plugins" className="mt-0 pt-4">
|
|
@@ -567,44 +511,10 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|
|
567
511
|
/>
|
|
568
512
|
</TabsContent>
|
|
569
513
|
|
|
570
|
-
<TabsContent value="mcp-servers" className="mt-0 pt-4">
|
|
571
|
-
<McpServersPanel
|
|
572
|
-
projectPath={projectPath}
|
|
573
|
-
mcpSearchQuery={tabState.mcpSearchQuery}
|
|
574
|
-
mcpSearch={tabState.mcpSearch}
|
|
575
|
-
mcpSearchResults={tabState.mcpSearchResults}
|
|
576
|
-
mcpSearchLoading={tabState.mcpSearchLoading}
|
|
577
|
-
mcpSearchWarnings={tabState.mcpSearchWarnings}
|
|
578
|
-
selectedMcpServerId={tabState.selectedMcpServerId}
|
|
579
|
-
setSelectedMcpServerId={tabState.setSelectedMcpServerId}
|
|
580
|
-
cliStatus={effectiveCliStatus}
|
|
581
|
-
cliStatusLoading={effectiveCliStatusLoading}
|
|
582
|
-
/>
|
|
583
|
-
</TabsContent>
|
|
584
|
-
|
|
585
|
-
<TabsContent value="skills" className="mt-0 pt-4">
|
|
586
|
-
<SkillsPanel
|
|
587
|
-
projectPath={projectPath}
|
|
588
|
-
projectLabel={projectLabel}
|
|
589
|
-
skillsSearchQuery={tabState.skillsSearchQuery}
|
|
590
|
-
setSkillsSearchQuery={tabState.setSkillsSearchQuery}
|
|
591
|
-
skillsSort={tabState.skillsSort}
|
|
592
|
-
setSkillsSort={tabState.setSkillsSort}
|
|
593
|
-
selectedSkillId={tabState.selectedSkillId}
|
|
594
|
-
setSelectedSkillId={tabState.setSelectedSkillId}
|
|
595
|
-
/>
|
|
596
|
-
</TabsContent>
|
|
597
|
-
|
|
598
514
|
<TabsContent value="env-vars" className="mt-0 pt-4">
|
|
599
515
|
<EnvVarPanel projectPath={projectPath} />
|
|
600
516
|
</TabsContent>
|
|
601
517
|
</Tabs>
|
|
602
|
-
|
|
603
|
-
{/* Custom MCP server dialog (lifted to store view level) */}
|
|
604
|
-
<CustomMcpServerDialog
|
|
605
|
-
open={customMcpDialogOpen}
|
|
606
|
-
onClose={() => setCustomMcpDialogOpen(false)}
|
|
607
|
-
/>
|
|
608
518
|
</div>
|
|
609
519
|
</div>
|
|
610
520
|
</div>
|
|
@@ -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';
|
|
@@ -77,6 +78,13 @@ export const PluginCard = ({
|
|
|
77
78
|
</div>
|
|
78
79
|
</div>
|
|
79
80
|
)}
|
|
81
|
+
{isEssentialPlugin(plugin) && (
|
|
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-amber-500/90 text-center text-[9px] font-semibold leading-[18px] text-white shadow-sm">
|
|
84
|
+
⭐ 必装
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
80
88
|
|
|
81
89
|
{/* Header: name + status/meta */}
|
|
82
90
|
<div className="flex items-start justify-between gap-2">
|
|
@@ -16,7 +16,11 @@ 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
|
+
normalizeCategory,
|
|
23
|
+
} from '@shared/utils/extensionNormalizers';
|
|
20
24
|
import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
|
|
21
25
|
import { ArrowUpDown, Filter, Puzzle, Search } from 'lucide-react';
|
|
22
26
|
import { useShallow } from 'zustand/react/shallow';
|
|
@@ -100,9 +104,15 @@ function selectFilteredPlugins(
|
|
|
100
104
|
result = result.filter((p) => p.isInstalled);
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
// Sort
|
|
107
|
+
// Sort. Essential plugins (oh-my-claudecode, codex) are pinned to the top of
|
|
108
|
+
// the grid — installed or not — so the recommendation lives inside the store
|
|
109
|
+
// list itself (rather than a separate banner). Within each group the chosen
|
|
110
|
+
// sort field still applies.
|
|
104
111
|
const direction = sort.order === 'asc' ? 1 : -1;
|
|
112
|
+
const recommendRank = (p: EnrichedPlugin): number => (isEssentialPlugin(p) ? 0 : 1);
|
|
105
113
|
result = [...result].sort((a, b) => {
|
|
114
|
+
const rankDelta = recommendRank(a) - recommendRank(b);
|
|
115
|
+
if (rankDelta !== 0) return rankDelta;
|
|
106
116
|
switch (sort.field) {
|
|
107
117
|
case 'popularity':
|
|
108
118
|
return (a.installCount - b.installCount) * direction;
|
|
@@ -134,12 +144,7 @@ export const PluginsPanel = ({
|
|
|
134
144
|
cliStatus: cliStatusOverride,
|
|
135
145
|
cliStatusLoading,
|
|
136
146
|
}: PluginsPanelProps): React.JSX.Element => {
|
|
137
|
-
const {
|
|
138
|
-
catalog,
|
|
139
|
-
loading,
|
|
140
|
-
error,
|
|
141
|
-
cliStatus: storedCliStatus,
|
|
142
|
-
} = useStore(
|
|
147
|
+
const { catalog, loading, error, cliStatus: storedCliStatus } = useStore(
|
|
143
148
|
useShallow((s) => ({
|
|
144
149
|
catalog: s.pluginCatalog,
|
|
145
150
|
loading: s.pluginCatalogLoading,
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
MoreHorizontal,
|
|
74
74
|
Shield,
|
|
75
75
|
Users,
|
|
76
|
+
Wrench,
|
|
76
77
|
} from 'lucide-react';
|
|
77
78
|
import { useShallow } from 'zustand/react/shallow';
|
|
78
79
|
|
|
@@ -106,6 +107,7 @@ import {
|
|
|
106
107
|
} from './sidebar/teamSidebarUiState';
|
|
107
108
|
import { CollapsibleTeamSection } from './CollapsibleTeamSection';
|
|
108
109
|
import { ProcessesSection } from './ProcessesSection';
|
|
110
|
+
import { ToolsSection } from './tools/ToolsSection';
|
|
109
111
|
import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from './provisioningSteps';
|
|
110
112
|
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
|
|
111
113
|
import {
|
|
@@ -2279,6 +2281,19 @@ export const TeamDetailView = ({
|
|
|
2279
2281
|
/>
|
|
2280
2282
|
</CollapsibleTeamSection>
|
|
2281
2283
|
|
|
2284
|
+
<CollapsibleTeamSection
|
|
2285
|
+
sectionId="tools"
|
|
2286
|
+
title="工具"
|
|
2287
|
+
icon={<Wrench size={14} />}
|
|
2288
|
+
defaultOpen={false}
|
|
2289
|
+
>
|
|
2290
|
+
<ToolsSection
|
|
2291
|
+
teamName={teamName}
|
|
2292
|
+
projectPath={data.config.projectPath ?? null}
|
|
2293
|
+
harnessType={data.harness}
|
|
2294
|
+
/>
|
|
2295
|
+
</CollapsibleTeamSection>
|
|
2296
|
+
|
|
2282
2297
|
<CollapsibleTeamSection
|
|
2283
2298
|
sectionId="kanban"
|
|
2284
2299
|
title="外部派单"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddMcpInline — opens CustomMcpServerDialog for adding a custom MCP server.
|
|
3
|
+
* Refreshes parent list when dialog closes (install may have occurred).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Button } from '@renderer/components/ui/button';
|
|
9
|
+
import { CustomMcpServerDialog } from '@renderer/components/extensions/mcp/CustomMcpServerDialog';
|
|
10
|
+
|
|
11
|
+
interface AddMcpInlineProps {
|
|
12
|
+
projectPath: string | null;
|
|
13
|
+
harnessType: string;
|
|
14
|
+
onAdded: () => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AddMcpInline = ({
|
|
19
|
+
onAdded,
|
|
20
|
+
onCancel,
|
|
21
|
+
}: AddMcpInlineProps): React.JSX.Element => {
|
|
22
|
+
const [dialogOpen, setDialogOpen] = useState(true);
|
|
23
|
+
|
|
24
|
+
const handleClose = () => {
|
|
25
|
+
setDialogOpen(false);
|
|
26
|
+
// Always refresh when dialog closes — install may have happened
|
|
27
|
+
onAdded();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs" onClick={() => setDialogOpen(true)}>
|
|
34
|
+
添加自定义 MCP
|
|
35
|
+
</Button>
|
|
36
|
+
<Button variant="ghost" size="sm" onClick={onCancel} className="h-7 text-xs">
|
|
37
|
+
取消
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<CustomMcpServerDialog
|
|
42
|
+
open={dialogOpen}
|
|
43
|
+
onClose={handleClose}
|
|
44
|
+
/>
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddSkillInline — inline compact form for adding a skill.
|
|
3
|
+
* Opens the existing SkillEditorDialog in create mode.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Button } from '@renderer/components/ui/button';
|
|
9
|
+
import { SkillEditorDialog } from '@renderer/components/extensions/skills/SkillEditorDialog';
|
|
10
|
+
|
|
11
|
+
interface AddSkillInlineProps {
|
|
12
|
+
projectPath: string | null;
|
|
13
|
+
projectLabel?: string | null;
|
|
14
|
+
onAdded: () => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AddSkillInline = ({
|
|
19
|
+
projectPath,
|
|
20
|
+
projectLabel,
|
|
21
|
+
onAdded,
|
|
22
|
+
onCancel,
|
|
23
|
+
}: AddSkillInlineProps): React.JSX.Element => {
|
|
24
|
+
const [editorOpen, setEditorOpen] = useState(true);
|
|
25
|
+
|
|
26
|
+
const handleClose = () => {
|
|
27
|
+
setEditorOpen(false);
|
|
28
|
+
onCancel();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleSaved = () => {
|
|
32
|
+
setEditorOpen(false);
|
|
33
|
+
onAdded();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center gap-2">
|
|
38
|
+
<Button
|
|
39
|
+
variant="outline"
|
|
40
|
+
size="sm"
|
|
41
|
+
className="h-7 gap-1 text-xs"
|
|
42
|
+
onClick={() => setEditorOpen(true)}
|
|
43
|
+
>
|
|
44
|
+
创建新 Skill
|
|
45
|
+
</Button>
|
|
46
|
+
<Button variant="ghost" size="sm" onClick={onCancel} className="h-7 text-xs">
|
|
47
|
+
取消
|
|
48
|
+
</Button>
|
|
49
|
+
|
|
50
|
+
<SkillEditorDialog
|
|
51
|
+
open={editorOpen}
|
|
52
|
+
onClose={handleClose}
|
|
53
|
+
onSaved={handleSaved}
|
|
54
|
+
mode="create"
|
|
55
|
+
projectPath={projectPath ?? null}
|
|
56
|
+
projectLabel={projectLabel ?? null}
|
|
57
|
+
detail={null}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpChip — compact chip for an installed MCP server.
|
|
3
|
+
* Shows server name, status dot, and remove button on hover.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { X } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
import type { InstalledMcpEntry, McpServerDiagnostic } from '@shared/types/extensions';
|
|
9
|
+
|
|
10
|
+
interface McpChipProps {
|
|
11
|
+
entry: InstalledMcpEntry;
|
|
12
|
+
diagnostic?: McpServerDiagnostic;
|
|
13
|
+
onRemove: (entry: InstalledMcpEntry) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const McpChip = ({ entry, diagnostic, onRemove }: McpChipProps): React.JSX.Element => {
|
|
17
|
+
// Default to green ("connected"). Only show a problem color when diagnostics
|
|
18
|
+
// explicitly report a failure or an auth requirement; absent/unknown status
|
|
19
|
+
// (e.g. diagnostics endpoint unavailable) is treated as healthy.
|
|
20
|
+
const statusColor =
|
|
21
|
+
diagnostic?.status === 'failed'
|
|
22
|
+
? 'bg-red-500'
|
|
23
|
+
: diagnostic?.status === 'needs-authentication'
|
|
24
|
+
? 'bg-amber-500'
|
|
25
|
+
: 'bg-emerald-500';
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="group inline-flex items-center gap-1.5 rounded-full bg-[var(--color-bg-secondary)] px-2.5 py-1 text-xs transition-colors hover:bg-[var(--color-bg-secondary-hover)]">
|
|
29
|
+
<span className={`size-2 shrink-0 rounded-full ${statusColor}`} title={diagnostic?.status ?? 'unknown'} />
|
|
30
|
+
<span className="max-w-[120px] truncate text-[var(--color-text)]">{entry.name}</span>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className="shrink-0 rounded-full p-0.5 opacity-0 transition-opacity hover:bg-red-500/20 group-hover:opacity-100"
|
|
34
|
+
onClick={() => onRemove(entry)}
|
|
35
|
+
aria-label={`删除 ${entry.name}`}
|
|
36
|
+
title="点击删除"
|
|
37
|
+
>
|
|
38
|
+
<X size={10} className="text-[var(--color-text-muted)] hover:text-red-400" />
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|