@yancyyu/openhermit 1.6.29 → 1.6.31

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 (157) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DkXfi2pg.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-CHNNVraw.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-Do-Ff83V.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-nDLhSuJI.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-Bp7fA6sx.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-CPC1HdGy.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DTVKyNTO.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-XVu-AN00.js} +1 -1
  9. package/dist-renderer/assets/channel-CIwbNcUO.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-BcWmVyA-.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-Co4Z2jsE.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-C8q9gfDT.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-qDgb1gxO.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-Cm8Gu2gu.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DYji1BRS.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-DWAS568H.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-CLFzXLA8.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-04A-pvql.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-04A-pvql.js +1 -0
  20. package/dist-renderer/assets/clone-DQnvTIEM.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-CZdGhX_3.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-BVY-G6nO.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CUACvAwG.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-3SfnesSG.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-E3ksXClJ.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-aYjGXss7.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-JMHrrTQs.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-CVQ-R5rN.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-OLn9jq61.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-BAb2J0l8.js} +1 -1
  31. package/dist-renderer/assets/{index-qNBNjW4K.js → index-BSoCjBWn.js} +1 -1
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-BtG3HbqP.js} +1 -1
  33. package/dist-renderer/assets/{index-BowUl0Jb.js → index-CH8e7g1f.js} +583 -573
  34. package/dist-renderer/assets/index-CSt8DTcn.css +1 -0
  35. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-Ca4iNkRA.js} +1 -1
  36. package/dist-renderer/assets/{index-vAykq1H1.js → index-DU9PGgZJ.js} +1 -1
  37. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DtMzIS9o.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-CY_ptQNL.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-C2vuHEo_.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-mbdNfu8h.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-Do_ArEB1.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-BMlMKyiq.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-Dfntn-o2.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-LiWHsGMV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-D87St8AF.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-DAa6lHBx.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-VOUngars.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-BzwzmFr2.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-BjAQEJ52.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-BDwy4GJm.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-Y5XBZt3W.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DzkdUEow.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-D-zbvJOv.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 +63 -35
  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 +111 -15
  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 +74 -123
  120. package/src/renderer/components/team/TeamListFilterPopover.tsx +0 -16
  121. package/src/renderer/components/team/TeamListView.tsx +7 -32
  122. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  123. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +287 -418
  124. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +283 -0
  125. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  126. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  127. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  128. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  129. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  130. package/src/renderer/store/slices/teamSlice.ts +8 -2
  131. package/src/renderer/utils/multimodelProviderVisibility.ts +17 -0
  132. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +29 -9
  133. package/src/shared/types/api.ts +29 -0
  134. package/src/shared/types/extensions/index.ts +1 -0
  135. package/src/shared/types/extensions/mcp.ts +2 -0
  136. package/src/shared/types/extensions/plugin.ts +2 -1
  137. package/src/shared/types/extensions/skill.ts +7 -0
  138. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  139. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  140. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  141. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  142. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  143. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  144. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  145. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  146. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  147. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  148. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  149. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  150. package/src/features/recent-projects/main/index.ts +0 -3
  151. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  152. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  153. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  154. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  155. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  156. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  157. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Interactive shell environment resolver.
3
+ *
4
+ * Resolves the user's interactive shell environment (PATH, etc.) by spawning
5
+ * a login/interactive shell and reading its exported variables. The result is
6
+ * cached for the lifetime of the process.
7
+ *
8
+ * Extracted from TeamProvisioningService for reuse by ScheduledTaskExecutor
9
+ * and any other service that needs the user's shell environment.
10
+ */
11
+
12
+ import { getHomeDir } from '@main/utils/pathDecoder';
13
+ import { createLogger } from '@shared/utils/logger';
14
+ import { spawn } from 'child_process';
15
+
16
+ const logger = createLogger('Utils:shellEnv');
17
+
18
+ const SHELL_ENV_TIMEOUT_MS = 12_000;
19
+ const SHELL_ENV_BEST_EFFORT_TIMEOUT_MS = 5_000;
20
+ const SHELL_ENV_FAILURE_COOLDOWN_MS = 60_000;
21
+
22
+ let cachedInteractiveShellEnv: NodeJS.ProcessEnv | null = null;
23
+ let shellEnvResolvePromise: Promise<NodeJS.ProcessEnv> | null = null;
24
+ let shellEnvFailureCooldownUntil = 0;
25
+ let lastShellEnvFailureMessage: string | null = null;
26
+
27
+ export interface ShellEnvResolveProgress {
28
+ phase: string;
29
+ message: string;
30
+ source?: string;
31
+ }
32
+
33
+ export interface ShellEnvResolveOptions {
34
+ onProgress?: (progress: ShellEnvResolveProgress) => void;
35
+ /**
36
+ * Stable diagnostic label for the caller that initiated the shell probe.
37
+ * Keep this to a short feature/service id, not a filesystem path.
38
+ */
39
+ source?: string;
40
+ }
41
+
42
+ export interface ShellEnvBestEffortResolveOptions extends ShellEnvResolveOptions {
43
+ /**
44
+ * Max time to wait on the critical path before returning fallbackEnv.
45
+ * By default, the full shell resolve continues in the background and caches
46
+ * on success. Set background=false for hot paths that only want cached env
47
+ * or an immediate fallback.
48
+ */
49
+ timeoutMs?: number;
50
+ /**
51
+ * Whether a slow shell probe should continue in the background after the
52
+ * caller falls back. Disable this for startup/status hot paths where a
53
+ * delayed hard timeout would only create log noise and process pressure.
54
+ */
55
+ background?: boolean;
56
+ /**
57
+ * Returned when shell env is not ready quickly enough. This is intentionally
58
+ * not cached as a real shell env.
59
+ */
60
+ fallbackEnv?: NodeJS.ProcessEnv;
61
+ }
62
+
63
+ function emitProgress(
64
+ options: ShellEnvResolveOptions | undefined,
65
+ phase: string,
66
+ message: string
67
+ ): void {
68
+ const source = normalizeShellEnvSource(options?.source);
69
+ options?.onProgress?.(source ? { phase, message, source } : { phase, message });
70
+ }
71
+
72
+ function normalizeShellEnvSource(source: string | undefined): string | null {
73
+ const trimmed = source?.trim();
74
+ if (!trimmed) {
75
+ return null;
76
+ }
77
+ return trimmed.replace(/[^A-Za-z0-9_.:-]/g, '_').slice(0, 80);
78
+ }
79
+
80
+ function formatShellEnvSource(options: ShellEnvResolveOptions | undefined): string {
81
+ const source = normalizeShellEnvSource(options?.source);
82
+ return source ? ` source=${source}` : '';
83
+ }
84
+
85
+ function rememberShellEnvFailure(message: string): void {
86
+ lastShellEnvFailureMessage = message;
87
+ shellEnvFailureCooldownUntil = Date.now() + SHELL_ENV_FAILURE_COOLDOWN_MS;
88
+ }
89
+
90
+ function clearShellEnvFailure(): void {
91
+ lastShellEnvFailureMessage = null;
92
+ shellEnvFailureCooldownUntil = 0;
93
+ }
94
+
95
+ function parseNullSeparatedEnv(content: string): NodeJS.ProcessEnv {
96
+ const parsed: NodeJS.ProcessEnv = {};
97
+ const lines = content.split('\0');
98
+ for (const line of lines) {
99
+ if (!line) {
100
+ continue;
101
+ }
102
+ const separatorIndex = line.indexOf('=');
103
+ if (separatorIndex <= 0) {
104
+ continue;
105
+ }
106
+ const key = line.slice(0, separatorIndex);
107
+ const value = line.slice(separatorIndex + 1);
108
+ parsed[key] = value;
109
+ }
110
+ return parsed;
111
+ }
112
+
113
+ async function readShellEnv(shellPath: string, args: string[]): Promise<NodeJS.ProcessEnv> {
114
+ const envDump = await new Promise<string>((resolve, reject) => {
115
+ const child = spawn(shellPath, args, {
116
+ env: process.env,
117
+ stdio: ['ignore', 'pipe', 'ignore'],
118
+ windowsHide: true,
119
+ });
120
+ const chunks: Buffer[] = [];
121
+ let settled = false;
122
+ let timeoutHandle: NodeJS.Timeout | null = setTimeout(() => {
123
+ timeoutHandle = null;
124
+ child.kill();
125
+ // SIGKILL fallback if SIGTERM is ignored (e.g., shell stuck on .zshrc)
126
+ setTimeout(() => {
127
+ try {
128
+ child.kill('SIGKILL');
129
+ } catch {
130
+ /* already dead */
131
+ }
132
+ }, 3000);
133
+ if (!settled) {
134
+ settled = true;
135
+ reject(new Error('shell env resolve timeout'));
136
+ }
137
+ }, SHELL_ENV_TIMEOUT_MS);
138
+
139
+ child.stdout?.on('data', (chunk: Buffer) => {
140
+ chunks.push(chunk);
141
+ });
142
+ child.once('error', (error) => {
143
+ if (timeoutHandle) {
144
+ clearTimeout(timeoutHandle);
145
+ timeoutHandle = null;
146
+ }
147
+ if (!settled) {
148
+ settled = true;
149
+ reject(error);
150
+ }
151
+ });
152
+ child.once('close', (code: number | null, signal: NodeJS.Signals | null) => {
153
+ if (timeoutHandle) {
154
+ clearTimeout(timeoutHandle);
155
+ }
156
+ if (!settled) {
157
+ settled = true;
158
+ if (chunks.length === 0 && (code !== 0 || signal)) {
159
+ reject(
160
+ new Error(
161
+ signal
162
+ ? `shell env command exited with signal ${signal}`
163
+ : `shell env command exited with code ${code}`
164
+ )
165
+ );
166
+ return;
167
+ }
168
+ resolve(Buffer.concat(chunks).toString('utf8'));
169
+ }
170
+ });
171
+ });
172
+ return parseNullSeparatedEnv(envDump);
173
+ }
174
+
175
+ /**
176
+ * Resolve the user's interactive shell environment.
177
+ *
178
+ * Tries login shell first (`-lic`), falls back to interactive (`-ic`).
179
+ * On Windows returns empty object. Result is cached after first success.
180
+ */
181
+ export async function resolveInteractiveShellEnv(
182
+ options: ShellEnvResolveOptions = {}
183
+ ): Promise<NodeJS.ProcessEnv> {
184
+ if (cachedInteractiveShellEnv) {
185
+ emitProgress(options, 'shell-env-cached', 'Using cached shell environment...');
186
+ return cachedInteractiveShellEnv;
187
+ }
188
+ if (shellEnvResolvePromise) {
189
+ emitProgress(options, 'shell-env-waiting', 'Waiting for shell environment...');
190
+ return shellEnvResolvePromise;
191
+ }
192
+ if (process.platform === 'win32') {
193
+ emitProgress(options, 'shell-env-skipped', 'Skipping shell environment on Windows...');
194
+ cachedInteractiveShellEnv = {};
195
+ return cachedInteractiveShellEnv;
196
+ }
197
+
198
+ shellEnvResolvePromise = (async () => {
199
+ const shellPath = process.env.SHELL || '/bin/zsh';
200
+ try {
201
+ emitProgress(options, 'shell-env-login', 'Reading login shell environment...');
202
+ const loginEnv = await readShellEnv(shellPath, ['-lic', 'env -0']);
203
+ cachedInteractiveShellEnv = loginEnv;
204
+ clearShellEnvFailure();
205
+ return loginEnv;
206
+ } catch (loginError) {
207
+ const loginMessage = loginError instanceof Error ? loginError.message : String(loginError);
208
+ try {
209
+ emitProgress(options, 'shell-env-interactive', 'Trying interactive shell environment...');
210
+ const interactiveEnv = await readShellEnv(shellPath, ['-ic', 'env -0']);
211
+ cachedInteractiveShellEnv = interactiveEnv;
212
+ clearShellEnvFailure();
213
+ return interactiveEnv;
214
+ } catch (interactiveError) {
215
+ const interactiveMessage =
216
+ interactiveError instanceof Error ? interactiveError.message : String(interactiveError);
217
+ logger.warn(
218
+ `Failed to resolve shell env after login and interactive probes${formatShellEnvSource(
219
+ options
220
+ )}: login=${loginMessage}; interactive=${interactiveMessage}`
221
+ );
222
+ rememberShellEnvFailure(interactiveMessage);
223
+ emitProgress(options, 'shell-env-fallback', 'Using current process environment...');
224
+ return {};
225
+ }
226
+ } finally {
227
+ shellEnvResolvePromise = null;
228
+ }
229
+ })();
230
+
231
+ return shellEnvResolvePromise;
232
+ }
233
+
234
+ /**
235
+ * Resolve shell env without making the caller wait for slow prompt/plugin init.
236
+ *
237
+ * This is deliberately additive: fallbackEnv is returned only to the current
238
+ * caller, never cached. A successful background resolve still populates the
239
+ * normal interactive-shell cache used by buildMergedCliPath/buildEnrichedEnv.
240
+ */
241
+ export async function resolveInteractiveShellEnvBestEffort(
242
+ options: ShellEnvBestEffortResolveOptions = {}
243
+ ): Promise<NodeJS.ProcessEnv> {
244
+ if (cachedInteractiveShellEnv) {
245
+ emitProgress(options, 'shell-env-cached', 'Using cached shell environment...');
246
+ return cachedInteractiveShellEnv;
247
+ }
248
+
249
+ if (process.platform === 'win32') {
250
+ return resolveInteractiveShellEnv(options);
251
+ }
252
+
253
+ const fallbackEnv = options.fallbackEnv ?? {};
254
+ const timeoutMs = Math.max(0, options.timeoutMs ?? SHELL_ENV_BEST_EFFORT_TIMEOUT_MS);
255
+ const startedAt = Date.now();
256
+ if (options.background === false) {
257
+ emitProgress(options, 'shell-env-best-effort-fallback', 'Using fallback shell environment...');
258
+ return fallbackEnv;
259
+ }
260
+ if (!shellEnvResolvePromise && startedAt < shellEnvFailureCooldownUntil) {
261
+ const retryInMs = Math.max(0, shellEnvFailureCooldownUntil - startedAt);
262
+ emitProgress(
263
+ options,
264
+ 'shell-env-failure-cooldown',
265
+ lastShellEnvFailureMessage
266
+ ? `Using fallback shell environment after recent failure: ${lastShellEnvFailureMessage}`
267
+ : `Using fallback shell environment for ${retryInMs}ms after recent failure...`
268
+ );
269
+ return fallbackEnv;
270
+ }
271
+
272
+ const resolvePromise = resolveInteractiveShellEnv(options);
273
+ if (timeoutMs === 0) {
274
+ emitProgress(options, 'shell-env-best-effort-fallback', 'Using fallback shell environment...');
275
+ return fallbackEnv;
276
+ }
277
+
278
+ let timeoutHandle: NodeJS.Timeout | null = null;
279
+ const fallbackPromise = new Promise<NodeJS.ProcessEnv>((resolve) => {
280
+ timeoutHandle = setTimeout(() => {
281
+ timeoutHandle = null;
282
+ emitProgress(
283
+ options,
284
+ 'shell-env-best-effort-timeout',
285
+ 'Shell environment is still resolving; using fallback for now...'
286
+ );
287
+ resolve(fallbackEnv);
288
+ }, timeoutMs);
289
+ timeoutHandle.unref?.();
290
+ });
291
+
292
+ try {
293
+ const resolvedEnv = await Promise.race([resolvePromise, fallbackPromise]);
294
+ if (!cachedInteractiveShellEnv && shellEnvFailureCooldownUntil > startedAt) {
295
+ return fallbackEnv;
296
+ }
297
+ return resolvedEnv;
298
+ } finally {
299
+ if (timeoutHandle) {
300
+ clearTimeout(timeoutHandle);
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Clear the cached shell environment. Useful for testing.
307
+ */
308
+ export function clearShellEnvCache(): void {
309
+ cachedInteractiveShellEnv = null;
310
+ shellEnvResolvePromise = null;
311
+ clearShellEnvFailure();
312
+ }
313
+
314
+ /**
315
+ * Return the cached shell environment synchronously, or null if not yet resolved.
316
+ *
317
+ * Use this when you need the shell env but cannot afford to wait for resolution
318
+ * (e.g. synchronous PATH enrichment with async pre-warming at startup).
319
+ */
320
+ export function getCachedShellEnv(): NodeJS.ProcessEnv | null {
321
+ return cachedInteractiveShellEnv;
322
+ }
323
+
324
+ /**
325
+ * HOME from login/interactive shell when resolved, else Electron/Node home.
326
+ * Matches TeamProvisioningService so CLI reads the same ~/.claude as the terminal.
327
+ */
328
+ export function getShellPreferredHome(): string {
329
+ const fromShell = getCachedShellEnv()?.HOME?.trim();
330
+ return fromShell || getHomeDir();
331
+ }
@@ -2220,6 +2220,67 @@ export class HttpAPIClient implements ElectronAPI {
2220
2220
  },
2221
2221
  };
2222
2222
 
2223
+ // ---------------------------------------------------------------------------
2224
+ // Credentials (project env, MCP credentials)
2225
+ // ---------------------------------------------------------------------------
2226
+
2227
+ credentials = {
2228
+ getStatus: async () =>
2229
+ this.get<{ encryption: string; storagePath: string } | null>(
2230
+ '/api/extensions/credentials/status'
2231
+ ),
2232
+
2233
+ getProjectEnv: async (projectPath: string) =>
2234
+ this.get<Record<string, string>>(
2235
+ `/api/extensions/credentials/project-env?projectPath=${encodeURIComponent(projectPath)}`
2236
+ ),
2237
+
2238
+ saveProjectEnv: async (projectPath: string, vars: Record<string, string>): Promise<void> => {
2239
+ await this.post('/api/extensions/credentials/project-env', { projectPath, vars });
2240
+ },
2241
+
2242
+ scanRequired: async (
2243
+ projectPath: string,
2244
+ mcpServers: {
2245
+ name: string;
2246
+ envVars?: { name: string; isRequired: boolean; description?: string };
2247
+ }[],
2248
+ skillReqs: {
2249
+ name: string;
2250
+ envVars: { name: string; isRequired?: boolean; description?: string }[];
2251
+ }[]
2252
+ ) =>
2253
+ this.post<{
2254
+ required: {
2255
+ name: string;
2256
+ isRequired: boolean;
2257
+ description?: string;
2258
+ source: string;
2259
+ value?: string;
2260
+ }[];
2261
+ }>('/api/extensions/credentials/scan-required', { projectPath, mcpServers, skillReqs }),
2262
+
2263
+ resolveAgentEnv: async (projectPath: string) =>
2264
+ this.get<Record<string, string>>(
2265
+ `/api/extensions/credentials/resolve-agent-env?projectPath=${encodeURIComponent(projectPath)}`
2266
+ ),
2267
+
2268
+ getSkillGlobalEnv: async (skillFolderName: string) =>
2269
+ this.get<Record<string, string>>(
2270
+ `/api/extensions/credentials/skill-env?folderName=${encodeURIComponent(skillFolderName)}`
2271
+ ),
2272
+
2273
+ saveSkillGlobalEnv: async (
2274
+ skillFolderName: string,
2275
+ vars: Record<string, string>
2276
+ ): Promise<void> => {
2277
+ await this.post('/api/extensions/credentials/skill-env', {
2278
+ folderName: skillFolderName,
2279
+ vars,
2280
+ });
2281
+ },
2282
+ };
2283
+
2223
2284
  // ---------------------------------------------------------------------------
2224
2285
  // Workspace (file system browsing)
2225
2286
  // ---------------------------------------------------------------------------
@@ -30,6 +30,7 @@ import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState';
30
30
  import { useStore } from '@renderer/store';
31
31
  import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
32
32
  import {
33
+ filterExtensionStoreProviders,
33
34
  formatCliExtensionCapabilityStatus,
34
35
  getVisibleMultimodelProviders,
35
36
  isMultimodelRuntimeStatus,
@@ -37,7 +38,6 @@ import {
37
38
  import { resolveProjectPathById } from '@renderer/utils/projectLookup';
38
39
  import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
39
40
  import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
40
- import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
41
41
  import { getCliProviderExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
42
42
  import {
43
43
  AlertTriangle,
@@ -48,15 +48,20 @@ import {
48
48
  Puzzle,
49
49
  RefreshCw,
50
50
  Server,
51
+ Sliders,
51
52
  } from 'lucide-react';
52
53
  import { useShallow } from 'zustand/react/shallow';
53
54
 
54
55
  import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog';
56
+ import { EnvVarPanel } from './env/EnvVarPanel';
55
57
  import { McpServersPanel } from './mcp/McpServersPanel';
58
+ import { PluginsPanel } from './plugins/PluginsPanel';
56
59
  import { SkillsPanel } from './skills/SkillsPanel';
60
+ import { StoreExtensionToast } from './common/ExtensionToast';
57
61
  import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
58
62
 
59
63
  import type { CliProviderId, CliProviderStatus } from '@shared/types';
64
+ import type { ExtensionsSubTab } from '@renderer/hooks/useExtensionsTabState';
60
65
 
61
66
  const ProviderCapabilityCardSkeleton = ({
62
67
  providerId,
@@ -113,6 +118,12 @@ function isCodexSnapshotPending(
113
118
  }
114
119
 
115
120
  const EXTENSION_SUB_TABS = [
121
+ {
122
+ value: 'plugins' as const,
123
+ label: '插件',
124
+ icon: Puzzle,
125
+ description: 'Claude Code 私有扩展,增强运行时的能力与集成。',
126
+ },
116
127
  {
117
128
  value: 'mcp-servers' as const,
118
129
  label: 'MCP 服务器',
@@ -125,6 +136,12 @@ const EXTENSION_SUB_TABS = [
125
136
  icon: BookOpen,
126
137
  description: '面向常见任务的可复用指令,帮助运行时更稳定地处理重复工作。',
127
138
  },
139
+ {
140
+ value: 'env-vars' as const,
141
+ label: '环境变量',
142
+ icon: Sliders,
143
+ description: '管理运行时环境变量,启动 agent 时自动注入。',
144
+ },
128
145
  ] as const;
129
146
 
130
147
  export const ExtensionStoreView = (): React.JSX.Element => {
@@ -132,6 +149,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
132
149
  const {
133
150
  bootstrapCliStatus,
134
151
  fetchCliStatus,
152
+ fetchPluginCatalog,
135
153
  fetchSkillsCatalog,
136
154
  mcpBrowse,
137
155
  mcpFetchInstalled,
@@ -149,7 +167,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
149
167
  useShallow((s) => ({
150
168
  bootstrapCliStatus: s.bootstrapCliStatus,
151
169
  fetchCliStatus: s.fetchCliStatus,
170
+ fetchPluginCatalog: s.fetchPluginCatalog,
152
171
  fetchSkillsCatalog: s.fetchSkillsCatalog,
172
+ pluginCatalog: s.pluginCatalog,
153
173
  mcpBrowse: s.mcpBrowse,
154
174
  mcpFetchInstalled: s.mcpFetchInstalled,
155
175
  mcpBrowseLoading: s.mcpBrowseLoading,
@@ -239,6 +259,11 @@ export const ExtensionStoreView = (): React.JSX.Element => {
239
259
  void mcpFetchInstalled(projectPath ?? undefined);
240
260
  }, [mcpFetchInstalled, projectPath]);
241
261
 
262
+ // Fetch Plugin catalog on mount / project change
263
+ useEffect(() => {
264
+ void fetchPluginCatalog(projectPath ?? undefined);
265
+ }, [fetchPluginCatalog, projectPath]);
266
+
242
267
  // Fetch Skills catalog on mount / project change
243
268
  useEffect(() => {
244
269
  void fetchSkillsCatalog(projectPath ?? undefined);
@@ -265,19 +290,11 @@ export const ExtensionStoreView = (): React.JSX.Element => {
265
290
  ]);
266
291
 
267
292
  const isRefreshing = effectiveCliStatusLoading || mcpBrowseLoading || skillsLoading;
268
- const mcpMutationDisableReason = useMemo(
269
- () =>
270
- getExtensionActionDisableReason({
271
- isInstalled: false,
272
- cliStatus: effectiveCliStatus,
273
- cliStatusLoading: effectiveCliStatusLoading,
274
- section: 'mcp',
275
- }),
276
- [effectiveCliStatus, effectiveCliStatusLoading]
277
- );
278
293
  const cliStatusBanner = useMemo(() => {
279
294
  const providers = effectiveCliStatus?.providers ?? [];
280
- const visibleProviders = getVisibleMultimodelProviders(providers);
295
+ const visibleProviders = filterExtensionStoreProviders(
296
+ getVisibleMultimodelProviders(providers)
297
+ );
281
298
  const isMultimodel = isMultimodelRuntimeStatus(effectiveCliStatus);
282
299
  const shouldShowMultimodelProviderCards =
283
300
  isMultimodel && visibleProviders.length > 0 && effectiveCliStatus !== null;
@@ -504,7 +521,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
504
521
  )}
505
522
  <Tabs
506
523
  value={tabState.activeSubTab}
507
- onValueChange={(v) => tabState.setActiveSubTab(v as 'mcp-servers' | 'skills')}
524
+ onValueChange={(v) => tabState.setActiveSubTab(v as ExtensionsSubTab)}
508
525
  >
509
526
  <div className="-mx-6 flex items-end justify-between border-b border-border px-6">
510
527
  <TabsList className="gap-1 rounded-b-none">
@@ -519,28 +536,37 @@ export const ExtensionStoreView = (): React.JSX.Element => {
519
536
  ))}
520
537
  </TabsList>
521
538
  {tabState.activeSubTab === 'mcp-servers' && (
522
- <Tooltip>
523
- <TooltipTrigger asChild>
524
- <span tabIndex={mcpMutationDisableReason ? 0 : -1}>
525
- <Button
526
- variant="outline"
527
- size="sm"
528
- onClick={() => setCustomMcpDialogOpen(true)}
529
- className="mb-1 whitespace-nowrap"
530
- disabled={Boolean(mcpMutationDisableReason)}
531
- >
532
- <Plus className="mr-1 size-3.5" />
533
- 添加自定义
534
- </Button>
535
- </span>
536
- </TooltipTrigger>
537
- {mcpMutationDisableReason && (
538
- <TooltipContent>{mcpMutationDisableReason}</TooltipContent>
539
- )}
540
- </Tooltip>
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>
541
548
  )}
542
549
  </div>
543
550
 
551
+ <TabsContent value="plugins" className="mt-0 pt-4">
552
+ <PluginsPanel
553
+ projectPath={projectPath}
554
+ pluginFilters={tabState.pluginFilters}
555
+ pluginSort={tabState.pluginSort}
556
+ setPluginSort={tabState.setPluginSort}
557
+ selectedPluginId={tabState.selectedPluginId}
558
+ setSelectedPluginId={tabState.setSelectedPluginId}
559
+ updatePluginSearch={tabState.updatePluginSearch}
560
+ toggleCategory={tabState.toggleCategory}
561
+ toggleCapability={tabState.toggleCapability}
562
+ toggleInstalledOnly={tabState.toggleInstalledOnly}
563
+ clearFilters={tabState.clearFilters}
564
+ hasActiveFilters={tabState.hasActiveFilters}
565
+ cliStatus={effectiveCliStatus}
566
+ cliStatusLoading={effectiveCliStatusLoading}
567
+ />
568
+ </TabsContent>
569
+
544
570
  <TabsContent value="mcp-servers" className="mt-0 pt-4">
545
571
  <McpServersPanel
546
572
  projectPath={projectPath}
@@ -568,19 +594,21 @@ export const ExtensionStoreView = (): React.JSX.Element => {
568
594
  setSelectedSkillId={tabState.setSelectedSkillId}
569
595
  />
570
596
  </TabsContent>
597
+
598
+ <TabsContent value="env-vars" className="mt-0 pt-4">
599
+ <EnvVarPanel projectPath={projectPath} />
600
+ </TabsContent>
571
601
  </Tabs>
572
602
 
573
603
  {/* Custom MCP server dialog (lifted to store view level) */}
574
604
  <CustomMcpServerDialog
575
605
  open={customMcpDialogOpen}
576
606
  onClose={() => setCustomMcpDialogOpen(false)}
577
- projectPath={projectPath}
578
- cliStatus={effectiveCliStatus}
579
- cliStatusLoading={effectiveCliStatusLoading}
580
607
  />
581
608
  </div>
582
609
  </div>
583
610
  </div>
611
+ <StoreExtensionToast />
584
612
  </TooltipProvider>
585
613
  );
586
614
  };
@@ -5,7 +5,7 @@ import { Info } from 'lucide-react';
5
5
  import type { LucideIcon } from 'lucide-react';
6
6
 
7
7
  interface ExtensionsSubTabTriggerProps {
8
- value: 'mcp-servers' | 'skills';
8
+ value: 'plugins' | 'mcp-servers' | 'skills' | 'env-vars';
9
9
  label: string;
10
10
  description: string;
11
11
  icon: LucideIcon;