@yancyyu/openhermit 1.6.28 → 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 (177) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.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-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
  32. package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.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 +907 -184
  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/session-intelligence/UsageTelemetryService.ts +33 -18
  93. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  94. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  95. package/src/main/services/team/cliFlavor.ts +54 -0
  96. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  97. package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
  98. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  99. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  100. package/src/main/services/teams-mvp/index.ts +3 -0
  101. package/src/main/utils/atomicWrite.ts +72 -0
  102. package/src/main/utils/childProcess.ts +554 -0
  103. package/src/main/utils/cliEnv.ts +54 -0
  104. package/src/main/utils/cliPathMerge.ts +97 -0
  105. package/src/main/utils/pathDecoder.ts +664 -0
  106. package/src/main/utils/pathValidation.ts +432 -0
  107. package/src/main/utils/shellEnv.ts +331 -0
  108. package/src/renderer/App.tsx +5 -0
  109. package/src/renderer/api/httpClient.ts +128 -0
  110. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  111. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  112. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  113. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  114. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  115. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  116. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  117. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  118. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  119. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  120. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  121. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  122. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  123. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  124. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  126. package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  128. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  129. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  130. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  131. package/src/renderer/components/team/TeamDetailView.tsx +55 -98
  132. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  133. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  134. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  135. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  136. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  137. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  138. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  139. package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
  140. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  141. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  142. package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
  143. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  144. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  145. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  146. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  147. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  148. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  149. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  150. package/src/renderer/store/slices/teamSlice.ts +67 -25
  151. package/src/renderer/types/tabs.ts +1 -0
  152. package/src/shared/types/api.ts +58 -0
  153. package/src/shared/types/extensions/index.ts +1 -0
  154. package/src/shared/types/extensions/mcp.ts +2 -0
  155. package/src/shared/types/extensions/plugin.ts +2 -1
  156. package/src/shared/types/extensions/skill.ts +7 -0
  157. package/src/shared/types/team.ts +104 -1
  158. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  159. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  160. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  161. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  162. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  163. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  164. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  165. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  166. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  167. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  168. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  169. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  170. package/src/features/recent-projects/main/index.ts +0 -3
  171. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  172. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  173. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  174. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  175. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  176. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  177. 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
+ }
@@ -52,6 +52,8 @@ function buildPathForTab(activeTab: Tab | null): string {
52
52
  return '/extensions';
53
53
  case 'schedules':
54
54
  return '/schedules';
55
+ case 'tasks':
56
+ return '/tasks';
55
57
  case 'dashboard':
56
58
  return '/dashboard';
57
59
  case 'session': {
@@ -149,6 +151,9 @@ function useTabPathPersistence() {
149
151
  case 'schedules':
150
152
  state.openSchedulesTab();
151
153
  break;
154
+ case 'tasks':
155
+ state.openTasksTab();
156
+ break;
152
157
  case 'dashboard':
153
158
  state.openDashboard();
154
159
  break;
@@ -99,6 +99,7 @@ import type {
99
99
  TeamAgentRuntimeSnapshot,
100
100
  CcSession,
101
101
  CcSessionDetail,
102
+ CollabTask,
102
103
  } from '@shared/types';
103
104
 
104
105
  import type {
@@ -1632,6 +1633,72 @@ export class HttpAPIClient implements ElectronAPI {
1632
1633
  this.get<CrossTeamMessage[]>(`/api/cross-team/outbox/${encodeURIComponent(teamName)}`),
1633
1634
  };
1634
1635
 
1636
+ // Collaboration board API
1637
+ collab = {
1638
+ getBoard: () => this.get<{ tasks: CollabTask[] }>('/api/collab/board'),
1639
+ getTask: (dispatchId: string) =>
1640
+ this.get<{ task: CollabTask }>(`/api/collab/board/${encodeURIComponent(dispatchId)}`),
1641
+ getEvents: (dispatchId: string) =>
1642
+ this.get<{ events: import('@shared/types/team').CollabTaskEvent[] }>(
1643
+ `/api/collab/board/${encodeURIComponent(dispatchId)}/events`
1644
+ ),
1645
+ accept: (teamSlug: string, dispatchId: string) =>
1646
+ this.post<{ ok: boolean; taskId: string }>('/api/cross-team/accept', {
1647
+ team_slug: teamSlug,
1648
+ dispatch_id: dispatchId,
1649
+ }),
1650
+ reject: (teamSlug: string, dispatchId: string, reason?: string) =>
1651
+ this.post<{ ok: boolean }>('/api/cross-team/reject', {
1652
+ team_slug: teamSlug,
1653
+ dispatch_id: dispatchId,
1654
+ reason,
1655
+ }),
1656
+ deliver: (teamSlug: string, dispatchId: string, result: string) =>
1657
+ this.post<{ ok: boolean }>('/api/cross-team/deliver', {
1658
+ team_slug: teamSlug,
1659
+ dispatch_id: dispatchId,
1660
+ result,
1661
+ }),
1662
+ approve: (teamSlug: string, dispatchId: string) =>
1663
+ this.post<{ ok: boolean }>('/api/cross-team/approve', {
1664
+ team_slug: teamSlug,
1665
+ dispatch_id: dispatchId,
1666
+ }),
1667
+ revision: (teamSlug: string, dispatchId: string, feedback: string) =>
1668
+ this.post<{ ok: boolean }>('/api/cross-team/revision', {
1669
+ team_slug: teamSlug,
1670
+ dispatch_id: dispatchId,
1671
+ feedback,
1672
+ }),
1673
+ dispatch: (
1674
+ fromTeam: string,
1675
+ toTeam: string,
1676
+ subject: string,
1677
+ opts?: {
1678
+ description?: string;
1679
+ deadlineMinutes?: number;
1680
+ needsHumanReview?: boolean;
1681
+ messageId?: string;
1682
+ sessionKey?: string;
1683
+ }
1684
+ ) =>
1685
+ this.post<{
1686
+ ok: boolean;
1687
+ dispatchId: string;
1688
+ status: string;
1689
+ message: string;
1690
+ }>('/api/cross-team/send', {
1691
+ fromTeam,
1692
+ toTeam,
1693
+ subject,
1694
+ description: opts?.description,
1695
+ deadlineMinutes: opts?.deadlineMinutes,
1696
+ needsHumanReview: opts?.needsHumanReview,
1697
+ messageId: opts?.messageId,
1698
+ sessionKey: opts?.sessionKey,
1699
+ }),
1700
+ };
1701
+
1635
1702
  // Review API
1636
1703
  review = {
1637
1704
  getAgentChanges: async (teamName: string, memberName: string) => {
@@ -2153,6 +2220,67 @@ export class HttpAPIClient implements ElectronAPI {
2153
2220
  },
2154
2221
  };
2155
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
+
2156
2284
  // ---------------------------------------------------------------------------
2157
2285
  // Workspace (file system browsing)
2158
2286
  // ---------------------------------------------------------------------------
@@ -37,7 +37,6 @@ import {
37
37
  import { resolveProjectPathById } from '@renderer/utils/projectLookup';
38
38
  import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
39
39
  import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
40
- import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
41
40
  import { getCliProviderExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
42
41
  import {
43
42
  AlertTriangle,
@@ -48,15 +47,20 @@ import {
48
47
  Puzzle,
49
48
  RefreshCw,
50
49
  Server,
50
+ Sliders,
51
51
  } from 'lucide-react';
52
52
  import { useShallow } from 'zustand/react/shallow';
53
53
 
54
54
  import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog';
55
+ import { EnvVarPanel } from './env/EnvVarPanel';
55
56
  import { McpServersPanel } from './mcp/McpServersPanel';
57
+ import { PluginsPanel } from './plugins/PluginsPanel';
56
58
  import { SkillsPanel } from './skills/SkillsPanel';
59
+ import { StoreExtensionToast } from './common/ExtensionToast';
57
60
  import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
58
61
 
59
62
  import type { CliProviderId, CliProviderStatus } from '@shared/types';
63
+ import type { ExtensionsSubTab } from '@renderer/hooks/useExtensionsTabState';
60
64
 
61
65
  const ProviderCapabilityCardSkeleton = ({
62
66
  providerId,
@@ -113,6 +117,12 @@ function isCodexSnapshotPending(
113
117
  }
114
118
 
115
119
  const EXTENSION_SUB_TABS = [
120
+ {
121
+ value: 'plugins' as const,
122
+ label: '插件',
123
+ icon: Puzzle,
124
+ description: 'Claude Code 私有扩展,增强运行时的能力与集成。',
125
+ },
116
126
  {
117
127
  value: 'mcp-servers' as const,
118
128
  label: 'MCP 服务器',
@@ -125,6 +135,12 @@ const EXTENSION_SUB_TABS = [
125
135
  icon: BookOpen,
126
136
  description: '面向常见任务的可复用指令,帮助运行时更稳定地处理重复工作。',
127
137
  },
138
+ {
139
+ value: 'env-vars' as const,
140
+ label: '环境变量',
141
+ icon: Sliders,
142
+ description: '管理运行时环境变量,启动 agent 时自动注入。',
143
+ },
128
144
  ] as const;
129
145
 
130
146
  export const ExtensionStoreView = (): React.JSX.Element => {
@@ -132,6 +148,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
132
148
  const {
133
149
  bootstrapCliStatus,
134
150
  fetchCliStatus,
151
+ fetchPluginCatalog,
135
152
  fetchSkillsCatalog,
136
153
  mcpBrowse,
137
154
  mcpFetchInstalled,
@@ -149,7 +166,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
149
166
  useShallow((s) => ({
150
167
  bootstrapCliStatus: s.bootstrapCliStatus,
151
168
  fetchCliStatus: s.fetchCliStatus,
169
+ fetchPluginCatalog: s.fetchPluginCatalog,
152
170
  fetchSkillsCatalog: s.fetchSkillsCatalog,
171
+ pluginCatalog: s.pluginCatalog,
153
172
  mcpBrowse: s.mcpBrowse,
154
173
  mcpFetchInstalled: s.mcpFetchInstalled,
155
174
  mcpBrowseLoading: s.mcpBrowseLoading,
@@ -239,6 +258,11 @@ export const ExtensionStoreView = (): React.JSX.Element => {
239
258
  void mcpFetchInstalled(projectPath ?? undefined);
240
259
  }, [mcpFetchInstalled, projectPath]);
241
260
 
261
+ // Fetch Plugin catalog on mount / project change
262
+ useEffect(() => {
263
+ void fetchPluginCatalog(projectPath ?? undefined);
264
+ }, [fetchPluginCatalog, projectPath]);
265
+
242
266
  // Fetch Skills catalog on mount / project change
243
267
  useEffect(() => {
244
268
  void fetchSkillsCatalog(projectPath ?? undefined);
@@ -265,16 +289,6 @@ export const ExtensionStoreView = (): React.JSX.Element => {
265
289
  ]);
266
290
 
267
291
  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
292
  const cliStatusBanner = useMemo(() => {
279
293
  const providers = effectiveCliStatus?.providers ?? [];
280
294
  const visibleProviders = getVisibleMultimodelProviders(providers);
@@ -504,7 +518,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
504
518
  )}
505
519
  <Tabs
506
520
  value={tabState.activeSubTab}
507
- onValueChange={(v) => tabState.setActiveSubTab(v as 'mcp-servers' | 'skills')}
521
+ onValueChange={(v) => tabState.setActiveSubTab(v as ExtensionsSubTab)}
508
522
  >
509
523
  <div className="-mx-6 flex items-end justify-between border-b border-border px-6">
510
524
  <TabsList className="gap-1 rounded-b-none">
@@ -519,28 +533,37 @@ export const ExtensionStoreView = (): React.JSX.Element => {
519
533
  ))}
520
534
  </TabsList>
521
535
  {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>
536
+ <Button
537
+ variant="outline"
538
+ size="sm"
539
+ onClick={() => setCustomMcpDialogOpen(true)}
540
+ className="mb-1 whitespace-nowrap"
541
+ >
542
+ <Plus className="mr-1 size-3.5" />
543
+ 添加自定义
544
+ </Button>
541
545
  )}
542
546
  </div>
543
547
 
548
+ <TabsContent value="plugins" className="mt-0 pt-4">
549
+ <PluginsPanel
550
+ projectPath={projectPath}
551
+ pluginFilters={tabState.pluginFilters}
552
+ pluginSort={tabState.pluginSort}
553
+ setPluginSort={tabState.setPluginSort}
554
+ selectedPluginId={tabState.selectedPluginId}
555
+ setSelectedPluginId={tabState.setSelectedPluginId}
556
+ updatePluginSearch={tabState.updatePluginSearch}
557
+ toggleCategory={tabState.toggleCategory}
558
+ toggleCapability={tabState.toggleCapability}
559
+ toggleInstalledOnly={tabState.toggleInstalledOnly}
560
+ clearFilters={tabState.clearFilters}
561
+ hasActiveFilters={tabState.hasActiveFilters}
562
+ cliStatus={effectiveCliStatus}
563
+ cliStatusLoading={effectiveCliStatusLoading}
564
+ />
565
+ </TabsContent>
566
+
544
567
  <TabsContent value="mcp-servers" className="mt-0 pt-4">
545
568
  <McpServersPanel
546
569
  projectPath={projectPath}
@@ -568,19 +591,21 @@ export const ExtensionStoreView = (): React.JSX.Element => {
568
591
  setSelectedSkillId={tabState.setSelectedSkillId}
569
592
  />
570
593
  </TabsContent>
594
+
595
+ <TabsContent value="env-vars" className="mt-0 pt-4">
596
+ <EnvVarPanel projectPath={projectPath} />
597
+ </TabsContent>
571
598
  </Tabs>
572
599
 
573
600
  {/* Custom MCP server dialog (lifted to store view level) */}
574
601
  <CustomMcpServerDialog
575
602
  open={customMcpDialogOpen}
576
603
  onClose={() => setCustomMcpDialogOpen(false)}
577
- projectPath={projectPath}
578
- cliStatus={effectiveCliStatus}
579
- cliStatusLoading={effectiveCliStatusLoading}
580
604
  />
581
605
  </div>
582
606
  </div>
583
607
  </div>
608
+ <StoreExtensionToast />
584
609
  </TooltipProvider>
585
610
  );
586
611
  };
@@ -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;