@yancyyu/openhermit 1.6.29 → 1.6.30

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 (152) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DsQt4FHy.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
  20. package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-BowUl0Jb.js → index-BlOrAXp3.js} +542 -532
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-qNBNjW4K.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-vAykq1H1.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-CGKpOUF2.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +4 -1
  56. package/src/main/ipc/extensions.ts +353 -0
  57. package/src/main/server.ts +209 -6
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
  59. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
  60. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
  61. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
  62. package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
  63. package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
  64. package/src/main/services/extensions/install/McpInstallService.ts +407 -0
  65. package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
  66. package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
  67. package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
  68. package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
  69. package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
  70. package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
  71. package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
  72. package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
  73. package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
  74. package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
  75. package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
  76. package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
  77. package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
  78. package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
  79. package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
  80. package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
  81. package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
  82. package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
  83. package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
  84. package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
  85. package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
  86. package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
  87. package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
  88. package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
  89. package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
  90. package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
  91. package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
  92. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  93. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  94. package/src/main/services/team/cliFlavor.ts +54 -0
  95. package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
  96. package/src/main/utils/atomicWrite.ts +72 -0
  97. package/src/main/utils/childProcess.ts +554 -0
  98. package/src/main/utils/cliEnv.ts +54 -0
  99. package/src/main/utils/cliPathMerge.ts +97 -0
  100. package/src/main/utils/pathDecoder.ts +664 -0
  101. package/src/main/utils/pathValidation.ts +432 -0
  102. package/src/main/utils/shellEnv.ts +331 -0
  103. package/src/renderer/api/httpClient.ts +61 -0
  104. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  105. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  106. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  107. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  108. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  109. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  110. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  111. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  112. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  113. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  114. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  115. package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
  116. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  117. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  118. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  119. package/src/renderer/components/team/TeamDetailView.tsx +35 -0
  120. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  121. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  122. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  123. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  124. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  125. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  126. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  127. package/src/renderer/store/slices/teamSlice.ts +8 -2
  128. package/src/shared/types/api.ts +29 -0
  129. package/src/shared/types/extensions/index.ts +1 -0
  130. package/src/shared/types/extensions/mcp.ts +2 -0
  131. package/src/shared/types/extensions/plugin.ts +2 -1
  132. package/src/shared/types/extensions/skill.ts +7 -0
  133. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  134. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  135. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  136. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  137. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  138. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  139. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  140. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  141. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  142. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  143. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  144. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  145. package/src/features/recent-projects/main/index.ts +0 -3
  146. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  147. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  148. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  149. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  150. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  151. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  152. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,239 @@
1
+ /**
2
+ * ProjectEnvPanel — manage project-level environment variables for Skills/MCP.
3
+ * Scans required env vars from enabled MCP servers + Skills, shows fill status,
4
+ * allows editing and encrypted saving.
5
+ */
6
+
7
+ import { useCallback, useEffect, useState } from 'react';
8
+
9
+ import { api } from '@renderer/api';
10
+ import { Button } from '@renderer/components/ui/button';
11
+ import { Input } from '@renderer/components/ui/input';
12
+ import { Label } from '@renderer/components/ui/label';
13
+ import { Eye, EyeOff, Save, Shield } from 'lucide-react';
14
+
15
+ interface EnvVarEntry {
16
+ name: string;
17
+ isRequired: boolean;
18
+ description?: string;
19
+ source: string;
20
+ value?: string;
21
+ }
22
+
23
+ interface ProjectEnvPanelProps {
24
+ projectPath: string | null;
25
+ }
26
+
27
+ export const ProjectEnvPanel = ({ projectPath }: ProjectEnvPanelProps): React.JSX.Element => {
28
+ const [entries, setEntries] = useState<EnvVarEntry[]>([]);
29
+ const [editValues, setEditValues] = useState<Record<string, string>>({});
30
+ const [revealed, setRevealed] = useState<Set<string>>(new Set());
31
+ const [loading, setLoading] = useState(false);
32
+ const [saving, setSaving] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [successMsg, setSuccessMsg] = useState<string | null>(null);
35
+
36
+ const scanEnv = useCallback(async () => {
37
+ if (!projectPath || !api.credentials) return;
38
+ setLoading(true);
39
+ setError(null);
40
+ try {
41
+ // Installed MCP entries do not carry env-var requirements (InstalledMcpEntry
42
+ // only exposes name/scope/transport), so MCP env scanning is not available here.
43
+ // Required env vars are sourced from skill declarations below.
44
+ const mcpServers: {
45
+ name: string;
46
+ envVars?: { name: string; isRequired: boolean; description?: string };
47
+ }[] = [];
48
+
49
+ // Gather skills with required-env from catalog
50
+ const skillReqs: {
51
+ name: string;
52
+ envVars: { name: string; isRequired?: boolean; description?: string }[];
53
+ }[] = [];
54
+ if (api.skills) {
55
+ try {
56
+ const skills = await api.skills.list(projectPath);
57
+ for (const skill of skills) {
58
+ const reqEnv = skill.requiredEnv ?? [];
59
+ if (reqEnv.length > 0) {
60
+ skillReqs.push({
61
+ name: skill.name,
62
+ envVars: reqEnv.map((v) => ({
63
+ name: v.name,
64
+ isRequired: v.isRequired ?? true,
65
+ description: v.description,
66
+ })),
67
+ });
68
+ }
69
+ }
70
+ } catch {
71
+ /* non-critical */
72
+ }
73
+ }
74
+
75
+ const result = await api.credentials
76
+ .scanRequired(projectPath, mcpServers, skillReqs)
77
+ .catch(() => null);
78
+ const required = (result as any)?.required ?? [];
79
+ setEntries(required);
80
+
81
+ const saved = await api.credentials
82
+ .getProjectEnv(projectPath)
83
+ .catch(() => ({}) as Record<string, string>);
84
+ const initialValues: Record<string, string> = {};
85
+ for (const entry of required) {
86
+ initialValues[entry.name] = (saved as any)?.[entry.name] ?? entry.value ?? '';
87
+ }
88
+ setEditValues(initialValues);
89
+ } catch (err) {
90
+ setError(err instanceof Error ? err.message : '扫描环境变量失败');
91
+ } finally {
92
+ setLoading(false);
93
+ }
94
+ }, [projectPath]);
95
+
96
+ useEffect(() => {
97
+ void scanEnv();
98
+ }, [scanEnv]);
99
+
100
+ const handleSave = async () => {
101
+ if (!projectPath || !api.credentials) return;
102
+ setSaving(true);
103
+ setError(null);
104
+ setSuccessMsg(null);
105
+ try {
106
+ await api.credentials.saveProjectEnv(projectPath, editValues);
107
+ setSuccessMsg('环境变量已加密保存');
108
+ setTimeout(() => setSuccessMsg(null), 3000);
109
+ await scanEnv();
110
+ } catch (err) {
111
+ setError(err instanceof Error ? err.message : '保存失败');
112
+ } finally {
113
+ setSaving(false);
114
+ }
115
+ };
116
+
117
+ const toggleReveal = (name: string) => {
118
+ setRevealed((prev) => {
119
+ const next = new Set(prev);
120
+ if (next.has(name)) next.delete(name);
121
+ else next.add(name);
122
+ return next;
123
+ });
124
+ };
125
+
126
+ if (!projectPath) {
127
+ return (
128
+ <div className="flex flex-col items-center justify-center py-12 text-text-muted">
129
+ <Shield className="mb-3 size-10 opacity-40" />
130
+ <p className="text-sm">请先选择一个项目以管理环境变量。</p>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ if (loading) {
136
+ return (
137
+ <div className="flex items-center justify-center py-12 text-text-muted">
138
+ <p className="text-sm">正在扫描项目所需的环境变量...</p>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ const missingRequired = entries.filter((e) => e.isRequired && !editValues[e.name]?.trim());
144
+
145
+ return (
146
+ <div className="space-y-4 px-1">
147
+ <div className="flex items-center justify-between">
148
+ <div>
149
+ <h3 className="text-sm font-medium text-text">项目环境变量</h3>
150
+ <p className="text-xs text-text-muted">
151
+ 管理当前项目所需的环境变量,供 Skills 和 CLI 工具使用。值已加密存储。
152
+ </p>
153
+ </div>
154
+ <Button size="sm" variant="outline" onClick={() => void scanEnv()} disabled={loading}>
155
+ 刷新
156
+ </Button>
157
+ </div>
158
+
159
+ {error && (
160
+ <div className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
161
+ {error}
162
+ </div>
163
+ )}
164
+
165
+ {successMsg && (
166
+ <div className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-400">
167
+ {successMsg}
168
+ </div>
169
+ )}
170
+
171
+ {missingRequired.length > 0 && (
172
+ <div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-400">
173
+ 缺少必填变量:{missingRequired.map((e) => e.name).join(', ')}
174
+ </div>
175
+ )}
176
+
177
+ {entries.length === 0 && !loading && (
178
+ <div className="py-8 text-center text-xs text-text-muted">
179
+ 未检测到所需的环境变量。启用 MCP 服务器或 Skills 后会自动扫描。
180
+ </div>
181
+ )}
182
+
183
+ {entries.length > 0 && (
184
+ <div className="space-y-3">
185
+ {entries.map((entry) => (
186
+ <div key={entry.name} className="space-y-1.5">
187
+ <div className="flex items-center gap-2">
188
+ <Label className="font-mono text-xs text-text">{entry.name}</Label>
189
+ <span
190
+ className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${
191
+ entry.isRequired
192
+ ? 'bg-red-500/10 text-red-400'
193
+ : 'bg-surface-raised text-text-muted'
194
+ }`}
195
+ >
196
+ {entry.isRequired ? '必填' : '可选'}
197
+ </span>
198
+ <span className="text-[10px] text-text-muted">来自 {entry.source}</span>
199
+ </div>
200
+ {entry.description && (
201
+ <p className="text-[11px] text-text-muted">{entry.description}</p>
202
+ )}
203
+ <div className="flex gap-2">
204
+ <Input
205
+ type={revealed.has(entry.name) ? 'text' : 'password'}
206
+ value={editValues[entry.name] ?? ''}
207
+ onChange={(e) =>
208
+ setEditValues((prev) => ({ ...prev, [entry.name]: e.target.value }))
209
+ }
210
+ placeholder={entry.isRequired ? '必填' : '可选'}
211
+ className="h-7 flex-1 font-mono text-xs"
212
+ />
213
+ <Button
214
+ size="icon"
215
+ variant="ghost"
216
+ className="size-7 shrink-0"
217
+ onClick={() => toggleReveal(entry.name)}
218
+ >
219
+ {revealed.has(entry.name) ? (
220
+ <EyeOff className="size-3.5" />
221
+ ) : (
222
+ <Eye className="size-3.5" />
223
+ )}
224
+ </Button>
225
+ </div>
226
+ </div>
227
+ ))}
228
+
229
+ <div className="pt-2">
230
+ <Button size="sm" onClick={() => void handleSave()} disabled={saving}>
231
+ <Save className="mr-1.5 size-3.5" />
232
+ {saving ? '保存中...' : '加密保存'}
233
+ </Button>
234
+ </div>
235
+ </div>
236
+ )}
237
+ </div>
238
+ );
239
+ };
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { useEffect, useRef, useState } from 'react';
7
7
 
8
- import { api } from '@renderer/api';
9
8
  import { Button } from '@renderer/components/ui/button';
10
9
  import {
11
10
  Dialog,
@@ -24,16 +23,11 @@ import {
24
23
  SelectValue,
25
24
  } from '@renderer/components/ui/select';
26
25
  import { useStore } from '@renderer/store';
27
- import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
28
- import {
29
- getDefaultMcpSharedScope,
30
- getMcpScopeLabel,
31
- isProjectScopedMcpScope,
32
- isSharedMcpScope,
33
- } from '@shared/utils/mcpScopes';
26
+ import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
34
27
  import { Plus, Server, Trash2 } from 'lucide-react';
35
28
 
36
- import type { CliInstallationStatus } from '@shared/types';
29
+ import { HarnessSelector } from '../common/HarnessSelector';
30
+
37
31
  import type {
38
32
  McpCustomInstallRequest,
39
33
  McpHeaderDef,
@@ -45,17 +39,10 @@ const SERVER_NAME_RE = /^[\w.-]{1,100}$/;
45
39
  interface CustomMcpServerDialogProps {
46
40
  open: boolean;
47
41
  onClose: () => void;
48
- projectPath: string | null;
49
- cliStatus?: Pick<
50
- CliInstallationStatus,
51
- 'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError' | 'flavor' | 'providers'
52
- > | null;
53
- cliStatusLoading?: boolean;
54
42
  }
55
43
 
56
44
  type TransportMode = 'stdio' | 'http';
57
45
  type HttpTransport = 'streamable-http' | 'sse' | 'http';
58
- type Scope = 'local' | 'user' | 'project' | 'global';
59
46
 
60
47
  const HTTP_TRANSPORT_OPTIONS: { value: HttpTransport; label: string }[] = [
61
48
  { value: 'streamable-http', label: 'Streamable HTTP' },
@@ -63,34 +50,19 @@ const HTTP_TRANSPORT_OPTIONS: { value: HttpTransport; label: string }[] = [
63
50
  { value: 'http', label: 'HTTP' },
64
51
  ];
65
52
 
66
- interface EnvEntry {
67
- key: string;
68
- value: string;
69
- }
70
-
71
53
  export const CustomMcpServerDialog = ({
72
54
  open,
73
55
  onClose,
74
- projectPath,
75
- cliStatus: cliStatusOverride,
76
- cliStatusLoading: cliStatusLoadingOverride,
77
56
  }: CustomMcpServerDialogProps): React.JSX.Element => {
78
57
  const installCustomMcpServer = useStore((s) => s.installCustomMcpServer);
79
58
  const storedCliStatus = useStore((s) => s.cliStatus);
80
- const storedCliStatusLoading = useStore((s) => s.cliStatusLoading);
81
- const cliStatus = cliStatusOverride ?? storedCliStatus;
82
- const cliStatusLoading = cliStatusLoadingOverride ?? storedCliStatusLoading;
83
- const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
84
- const scopeOptions: { value: Scope; label: string }[] = [
85
- { value: defaultSharedScope, label: getMcpScopeLabel(defaultSharedScope, cliStatus?.flavor) },
86
- { value: 'project', label: 'Project' },
87
- { value: 'local', label: 'Local' },
88
- ];
59
+ const defaultSharedScope = getDefaultMcpSharedScope(storedCliStatus?.flavor);
60
+ const installScope = defaultSharedScope === 'global' ? 'user' : defaultSharedScope;
89
61
 
90
62
  // Form state
91
63
  const [serverName, setServerName] = useState('');
92
64
  const [transportMode, setTransportMode] = useState<TransportMode>('stdio');
93
- const [scope, setScope] = useState<Scope>(defaultSharedScope);
65
+ const [harnessType, setHarnessType] = useState('claudecode');
94
66
 
95
67
  // Stdio fields
96
68
  const [npmPackage, setNpmPackage] = useState('');
@@ -102,26 +74,9 @@ export const CustomMcpServerDialog = ({
102
74
  const [headers, setHeaders] = useState<McpHeaderDef[]>([]);
103
75
 
104
76
  // Shared
105
- const [envVars, setEnvVars] = useState<EnvEntry[]>([]);
106
77
  const [error, setError] = useState<string | null>(null);
107
78
  const [installing, setInstalling] = useState(false);
108
- const autoFilledValuesRef = useRef<Record<string, string>>({});
109
79
  const wasOpenRef = useRef(false);
110
- const previousDefaultSharedScopeRef = useRef<Scope>(defaultSharedScope);
111
- const envVarLookupNames = envVars
112
- .map((entry) => entry.key.trim())
113
- .filter(Boolean)
114
- .sort()
115
- .join('\0');
116
- const apiKeyLookupProjectPath = isProjectScopedMcpScope(scope)
117
- ? (projectPath ?? undefined)
118
- : undefined;
119
- const mutationDisableReason = getExtensionActionDisableReason({
120
- isInstalled: false,
121
- cliStatus,
122
- cliStatusLoading,
123
- section: 'mcp',
124
- });
125
80
 
126
81
  // Reset on open
127
82
  useEffect(() => {
@@ -129,107 +84,20 @@ export const CustomMcpServerDialog = ({
129
84
  if (justOpened) {
130
85
  setServerName('');
131
86
  setTransportMode('stdio');
132
- setScope(defaultSharedScope);
133
87
  setNpmPackage('');
134
88
  setNpmVersion('');
135
89
  setHttpUrl('');
136
90
  setHttpTransport('streamable-http');
137
91
  setHeaders([]);
138
- setEnvVars([]);
139
92
  setError(null);
140
93
  setInstalling(false);
141
- autoFilledValuesRef.current = {};
142
94
  }
143
95
  wasOpenRef.current = open;
144
- if (!open) {
145
- previousDefaultSharedScopeRef.current = defaultSharedScope;
146
- }
147
- }, [defaultSharedScope, open]);
148
-
149
- useEffect(() => {
150
- if (!open) {
151
- previousDefaultSharedScopeRef.current = defaultSharedScope;
152
- return;
153
- }
154
-
155
- const previousDefaultSharedScope = previousDefaultSharedScopeRef.current;
156
- if (
157
- previousDefaultSharedScope !== defaultSharedScope &&
158
- scope === previousDefaultSharedScope &&
159
- isSharedMcpScope(scope)
160
- ) {
161
- setScope(defaultSharedScope);
162
- }
163
-
164
- previousDefaultSharedScopeRef.current = defaultSharedScope;
165
- }, [defaultSharedScope, open, scope]);
166
-
167
- useEffect(() => {
168
- if (open && isProjectScopedMcpScope(scope) && !projectPath) {
169
- setScope(defaultSharedScope);
170
- }
171
- }, [defaultSharedScope, open, projectPath, scope]);
172
-
173
- // Auto-fill env vars from saved API keys
174
- useEffect(() => {
175
- if (!open || envVars.length === 0 || !api.apiKeys) return;
176
-
177
- const envVarNames = envVars.map((e) => e.key.trim()).filter(Boolean);
178
- if (envVarNames.length === 0) return;
179
-
180
- void api.apiKeys.lookup(envVarNames, apiKeyLookupProjectPath).then(
181
- (results) => {
182
- const previousAutoFilledValues = autoFilledValuesRef.current;
183
- const nextAutoFilledValues = Object.fromEntries(
184
- results.map((result) => [result.envVarName, result.value])
185
- );
186
- setEnvVars((prev) => {
187
- let changed = false;
188
- const next = prev.map((entry) => {
189
- const envVarName = entry.key.trim();
190
- if (!envVarName) {
191
- return entry;
192
- }
193
-
194
- const previousValue = previousAutoFilledValues[envVarName];
195
- const nextValue = nextAutoFilledValues[envVarName];
196
-
197
- if (!nextValue) {
198
- if (previousValue && entry.value === previousValue) {
199
- changed = true;
200
- return { ...entry, value: '' };
201
- }
202
- return entry;
203
- }
204
-
205
- if (!entry.value || entry.value === previousValue) {
206
- if (entry.value !== nextValue) {
207
- changed = true;
208
- return { ...entry, value: nextValue };
209
- }
210
- }
211
-
212
- return entry;
213
- });
214
-
215
- return changed ? next : prev;
216
- });
217
- autoFilledValuesRef.current = nextAutoFilledValues;
218
- },
219
- () => {
220
- // Silently fail
221
- }
222
- );
223
- }, [apiKeyLookupProjectPath, envVarLookupNames, open]); // eslint-disable-line react-hooks/exhaustive-deps
96
+ }, [open]);
224
97
 
225
98
  const handleInstall = async () => {
226
99
  setError(null);
227
100
 
228
- if (mutationDisableReason) {
229
- setError(mutationDisableReason);
230
- return;
231
- }
232
-
233
101
  if (!serverName.trim()) {
234
102
  setError('Server name is required');
235
103
  return;
@@ -263,20 +131,13 @@ export const CustomMcpServerDialog = ({
263
131
  };
264
132
  }
265
133
 
266
- const envValues: Record<string, string> = {};
267
- for (const entry of envVars) {
268
- if (entry.key.trim() && entry.value) {
269
- envValues[entry.key.trim()] = entry.value;
270
- }
271
- }
272
-
273
134
  const request: McpCustomInstallRequest = {
274
135
  serverName,
275
- scope,
276
- projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
136
+ scope: installScope,
277
137
  installSpec,
278
- envValues,
138
+ envValues: {},
279
139
  headers: headers.filter((h) => h.key.trim() && h.value.trim()),
140
+ harnessType,
280
141
  };
281
142
 
282
143
  setInstalling(true);
@@ -290,11 +151,6 @@ export const CustomMcpServerDialog = ({
290
151
  }
291
152
  };
292
153
 
293
- const addEnvVar = () => setEnvVars((prev) => [...prev, { key: '', value: '' }]);
294
- const removeEnvVar = (i: number) => setEnvVars((prev) => prev.filter((_, idx) => idx !== i));
295
- const updateEnvVar = (i: number, field: 'key' | 'value', val: string) =>
296
- setEnvVars((prev) => prev.map((e, idx) => (idx === i ? { ...e, [field]: val } : e)));
297
-
298
154
  const addHeader = () => setHeaders((prev) => [...prev, { key: '', value: '' }]);
299
155
  const removeHeader = (i: number) => setHeaders((prev) => prev.filter((_, idx) => idx !== i));
300
156
  const updateHeader = (i: number, field: 'key' | 'value', val: string) =>
@@ -303,13 +159,11 @@ export const CustomMcpServerDialog = ({
303
159
  const canSubmit =
304
160
  serverName.trim() &&
305
161
  (transportMode === 'stdio' ? npmPackage.trim() : httpUrl.trim()) &&
306
- !(isProjectScopedMcpScope(scope) && !projectPath) &&
307
- !mutationDisableReason &&
308
162
  !installing;
309
163
 
310
164
  return (
311
165
  <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
312
- <DialogContent className="max-h-[85vh] max-w-lg overflow-y-auto">
166
+ <DialogContent className="max-h-[85vh] w-full max-w-lg overflow-y-auto">
313
167
  <DialogHeader>
314
168
  <div className="flex items-center gap-2">
315
169
  <div className="flex size-8 items-center justify-center rounded-lg border border-border bg-surface-raised">
@@ -440,7 +294,7 @@ export const CustomMcpServerDialog = ({
440
294
  </Button>
441
295
  </div>
442
296
  {headers.length > 0 && (
443
- <div className="space-y-2">
297
+ <div className="max-h-32 space-y-2 overflow-y-auto">
444
298
  {headers.map((header, i) => (
445
299
  <div key={i} className="flex items-center gap-2">
446
300
  <Input
@@ -471,73 +325,10 @@ export const CustomMcpServerDialog = ({
471
325
  </div>
472
326
  )}
473
327
 
474
- {/* Scope */}
475
- <div className="space-y-1.5">
476
- <Label className="text-xs">作用域</Label>
477
- <Select value={scope} onValueChange={(v) => setScope(v as Scope)}>
478
- <SelectTrigger className="h-8 text-sm">
479
- <SelectValue />
480
- </SelectTrigger>
481
- <SelectContent>
482
- {scopeOptions.map((opt) => (
483
- <SelectItem
484
- key={opt.value}
485
- value={opt.value}
486
- disabled={isProjectScopedMcpScope(opt.value) && !projectPath}
487
- >
488
- {opt.label}
489
- </SelectItem>
490
- ))}
491
- </SelectContent>
492
- </Select>
493
- </div>
494
-
495
- {/* Environment variables */}
496
- <div className="space-y-1.5">
497
- <div className="flex items-center justify-between">
498
- <Label className="text-xs">环境变量</Label>
499
- <Button variant="ghost" size="sm" onClick={addEnvVar} className="h-6 px-1.5 text-xs">
500
- <Plus className="mr-1 size-3" />
501
- 添加
502
- </Button>
503
- </div>
504
- {envVars.length > 0 && (
505
- <div className="space-y-2">
506
- {envVars.map((entry, i) => (
507
- <div key={i} className="flex items-center gap-2">
508
- <Input
509
- value={entry.key}
510
- onChange={(e) => updateEnvVar(i, 'key', e.target.value)}
511
- className="h-7 w-40 font-mono text-xs"
512
- placeholder="ENV_VAR_NAME"
513
- />
514
- <Input
515
- type="password"
516
- value={entry.value}
517
- onChange={(e) => updateEnvVar(i, 'value', e.target.value)}
518
- className="h-7 flex-1 text-xs"
519
- placeholder="value"
520
- />
521
- <Button
522
- variant="ghost"
523
- size="icon"
524
- className="size-7 text-red-400 hover:bg-red-500/10"
525
- onClick={() => removeEnvVar(i)}
526
- >
527
- <Trash2 className="size-3" />
528
- </Button>
529
- </div>
530
- ))}
531
- </div>
532
- )}
533
- </div>
328
+ {/* Harness selector */}
329
+ <HarnessSelector capability="mcp" value={harnessType} onChange={setHarnessType} />
534
330
 
535
331
  {/* Error */}
536
- {mutationDisableReason && (
537
- <div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs text-amber-300">
538
- {mutationDisableReason}
539
- </div>
540
- )}
541
332
  {error && (
542
333
  <div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
543
334
  {error}
@@ -40,6 +40,7 @@ import {
40
40
  import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
41
41
 
42
42
  import { InstallButton } from '../common/InstallButton';
43
+ import { HarnessSelector } from '../common/HarnessSelector';
43
44
  import { SourceBadge } from '../common/SourceBadge';
44
45
 
45
46
  import type { CliInstallationStatus } from '@shared/types';
@@ -98,6 +99,7 @@ export const McpServerDetailDialog = ({
98
99
  );
99
100
 
100
101
  const [serverName, setServerName] = useState('');
102
+ const [harnessType, setHarnessType] = useState('claudecode');
101
103
  const [envValues, setEnvValues] = useState<Record<string, string>>({});
102
104
  const [headers, setHeaders] = useState<McpHeaderDef[]>([]);
103
105
  const [imgError, setImgError] = useState(false);
@@ -271,6 +273,7 @@ export const McpServerDetailDialog = ({
271
273
  projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
272
274
  envValues,
273
275
  headers,
276
+ harnessType,
274
277
  });
275
278
  };
276
279
 
@@ -493,6 +496,14 @@ export const McpServerDetailDialog = ({
493
496
  </Select>
494
497
  </div>
495
498
 
499
+ {/* Harness selector */}
500
+ <HarnessSelector
501
+ capability="mcp"
502
+ value={harnessType}
503
+ onChange={setHarnessType}
504
+ disabled={isInstalledForScope}
505
+ />
506
+
496
507
  {/* Environment variables */}
497
508
  {server.envVars.length > 0 && (
498
509
  <div className="space-y-1.5">
@@ -256,7 +256,7 @@ export const McpServersPanel = ({
256
256
  }
257
257
  };
258
258
 
259
- // Sort displayed servers
259
+ // Sort displayed catalog servers
260
260
  const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]);
261
261
  const runtimeLabel = getRuntimeDisplayName(cliStatus, true);
262
262
 
@@ -472,6 +472,56 @@ export const McpServersPanel = ({
472
472
  </div>
473
473
  ))}
474
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
+
475
525
  {/* Empty state */}
476
526
  {!isLoading && displayServers.length === 0 && (
477
527
  <div className="flex flex-col items-center gap-3 rounded-sm border border-dashed border-border px-8 py-16">