@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
@@ -208,7 +208,8 @@ export const MessagesPanel = memo(function MessagesPanel({
208
208
  teams,
209
209
  openTeamTab,
210
210
  messages,
211
- messagesState,
211
+ hasMore,
212
+ loadingOlderMessages,
212
213
  loadOlderTeamMessages,
213
214
  refreshTeamMessagesHead,
214
215
  addOptimisticTeamMessage,
@@ -224,7 +225,13 @@ export const MessagesPanel = memo(function MessagesPanel({
224
225
  teams: s.teams,
225
226
  openTeamTab: s.openTeamTab,
226
227
  messages: selectTeamMessages(s, teamName),
227
- messagesState: teamName ? s.teamMessagesByName[teamName] : undefined,
228
+ // Subscribe to only the primitive flags the panel renders. The full
229
+ // cache entry object is rebuilt on every (even no-op) head refresh —
230
+ // selecting it wholesale would re-render this heavy panel every poll.
231
+ hasMore: teamName ? (s.teamMessagesByName[teamName]?.hasMore ?? false) : false,
232
+ loadingOlderMessages: teamName
233
+ ? (s.teamMessagesByName[teamName]?.loadingOlder ?? false)
234
+ : false,
228
235
  loadOlderTeamMessages: s.loadOlderTeamMessages,
229
236
  refreshTeamMessagesHead: s.refreshTeamMessagesHead,
230
237
  addOptimisticTeamMessage: s.addOptimisticTeamMessage,
@@ -233,16 +240,15 @@ export const MessagesPanel = memo(function MessagesPanel({
233
240
  const bootstrapHeadRefreshAttemptedForTeamRef = useRef<string | null>(null);
234
241
 
235
242
  const loadOlderMessages = useCallback(async () => {
236
- if (!messagesState?.hasMore || messagesState.loadingHead || messagesState.loadingOlder) {
243
+ // Read the live cache entry instead of subscribing to it — loadingHead
244
+ // toggles on every background head refresh and must not re-render us.
245
+ const entry = useStore.getState().teamMessagesByName[teamName];
246
+ if (!entry?.hasMore || entry.loadingHead || entry.loadingOlder) {
237
247
  return;
238
248
  }
239
249
  await loadOlderTeamMessages(teamName);
240
- }, [loadOlderTeamMessages, messagesState, teamName]);
250
+ }, [loadOlderTeamMessages, teamName]);
241
251
 
242
- const messagesLoading =
243
- (messagesState?.loadingHead ?? false) || (messagesState?.loadingOlder ?? false);
244
- const loadingOlderMessages = messagesState?.loadingOlder ?? false;
245
- const hasMore = messagesState?.hasMore ?? false;
246
252
  const effectiveMessages = messages;
247
253
  const loadedMessageCount = effectiveMessages.length;
248
254
  const autoLoadOlderLockRef = useRef(false);
@@ -271,12 +277,11 @@ export const MessagesPanel = memo(function MessagesPanel({
271
277
 
272
278
  const maybeAutoLoadOlderMessages = useCallback(
273
279
  (scrollTop: number) => {
274
- if (
275
- scrollTop > AUTO_LOAD_OLDER_SCROLL_TOP_PX ||
276
- !hasMore ||
277
- messagesState?.loadingHead ||
278
- loadingOlderMessages
279
- ) {
280
+ if (scrollTop > AUTO_LOAD_OLDER_SCROLL_TOP_PX || !hasMore || loadingOlderMessages) {
281
+ return;
282
+ }
283
+ // loadingHead is read live (not subscribed) to avoid per-poll re-renders.
284
+ if (useStore.getState().teamMessagesByName[teamName]?.loadingHead) {
280
285
  return;
281
286
  }
282
287
  if (autoLoadOlderLockRef.current) {
@@ -285,7 +290,7 @@ export const MessagesPanel = memo(function MessagesPanel({
285
290
  autoLoadOlderLockRef.current = true;
286
291
  void loadOlderMessages();
287
292
  },
288
- [hasMore, loadOlderMessages, loadingOlderMessages, messagesState?.loadingHead]
293
+ [hasMore, loadOlderMessages, loadingOlderMessages, teamName]
289
294
  );
290
295
 
291
296
  useEffect(() => {
@@ -379,7 +384,9 @@ export const MessagesPanel = memo(function MessagesPanel({
379
384
  return () => {
380
385
  cancelled = true;
381
386
  };
382
- }, [teamName]);
387
+ // Refetch when the lead session id changes (e.g. a new session is spawned)
388
+ // so the session list/selector reflects the updated id without a remount.
389
+ }, [teamName, currentLeadSessionId]);
383
390
 
384
391
  const selectedSession = useMemo(
385
392
  () => teamSessions.find((session) => session.sessionKey === selectedSessionKey) ?? null,
@@ -455,7 +462,10 @@ export const MessagesPanel = memo(function MessagesPanel({
455
462
  bootstrapHeadRefreshAttemptedForTeamRef.current = null;
456
463
  return;
457
464
  }
458
- if (messagesState?.loadingHead || messagesState?.loadingOlder) {
465
+ // Read loading flags live rather than subscribing — they toggle on every
466
+ // background head refresh and must not drive this bootstrap effect.
467
+ const entry = useStore.getState().teamMessagesByName[teamName];
468
+ if (entry?.loadingHead || entry?.loadingOlder) {
459
469
  return;
460
470
  }
461
471
  if (bootstrapHeadRefreshAttemptedForTeamRef.current === teamName) {
@@ -463,13 +473,7 @@ export const MessagesPanel = memo(function MessagesPanel({
463
473
  }
464
474
  bootstrapHeadRefreshAttemptedForTeamRef.current = teamName;
465
475
  void refreshTeamMessagesHead(teamName).catch(() => undefined);
466
- }, [
467
- effectiveMessages.length,
468
- messagesState?.loadingHead,
469
- messagesState?.loadingOlder,
470
- refreshTeamMessagesHead,
471
- teamName,
472
- ]);
476
+ }, [effectiveMessages.length, refreshTeamMessagesHead, teamName]);
473
477
 
474
478
  useLayoutEffect(() => {
475
479
  if (position !== 'sidebar') return;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * TerminalPanel — a good-looking, read-only terminal-style panel for rendering
3
+ * command / CLI output faithfully.
4
+ *
5
+ * Unlike the structured markdown renderers, this preserves the raw terminal feel:
6
+ * - full ANSI color / decoration fidelity (via `anser`)
7
+ * - monospace, whitespace-exact output
8
+ * - carriage-return (\r) overwrite handling so progress bars settle to their
9
+ * final frame instead of dumping every intermediate line
10
+ * - optional `$ command` prompt line so a Bash call reads like a real terminal
11
+ *
12
+ * It is intentionally lightweight (no xterm.js / node-pty): the session view is a
13
+ * read-only viewer of recorded output, so we only need faithful rendering, not a
14
+ * live PTY.
15
+ */
16
+
17
+ import { useMemo, useState } from 'react';
18
+
19
+ import Anser, { type AnserJsonEntry } from 'anser';
20
+ import { Check, Copy } from 'lucide-react';
21
+
22
+ interface TerminalPanelProps {
23
+ /** Raw output text, may contain ANSI escape sequences. */
24
+ text: string;
25
+ /** Optional command to render as a `$ command` prompt line above the output. */
26
+ command?: string;
27
+ /** Optional label shown in the header bar (e.g. a short description). */
28
+ title?: string;
29
+ /** Max body height in px before scrolling. Defaults to 384. */
30
+ maxHeight?: number;
31
+ className?: string;
32
+ }
33
+
34
+ /**
35
+ * Collapse carriage-return overwrites within each line and strip non-color
36
+ * escape sequences (cursor moves, screen clears, OSC) that would otherwise
37
+ * render as garbage. SGR color codes are left intact for `anser`.
38
+ */
39
+ function normalizeTerminalText(raw: string): string {
40
+ // Strip OSC sequences: ESC ] ... BEL or ESC ] ... ESC \
41
+ let out = raw.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
42
+ // Strip CSI sequences that are NOT SGR ("m"): cursor movement, erase, etc.
43
+ out = out.replace(/\x1b\[[0-9;?]*[A-Za-ln-z]/g, '');
44
+ // Collapse \r overwrites per line: later segments overwrite earlier from col 0.
45
+ out = out
46
+ .split('\n')
47
+ .map((line) => {
48
+ if (!line.includes('\r')) return line;
49
+ let acc = '';
50
+ for (const seg of line.split('\r')) {
51
+ acc = seg.length >= acc.length ? seg : seg + acc.slice(seg.length);
52
+ }
53
+ return acc;
54
+ })
55
+ .join('\n');
56
+ return out;
57
+ }
58
+
59
+ function styleForSegment(seg: AnserJsonEntry): React.CSSProperties {
60
+ const style: React.CSSProperties = {};
61
+ if (seg.fg) style.color = `rgb(${seg.fg})`;
62
+ if (seg.bg) style.backgroundColor = `rgb(${seg.bg})`;
63
+ const decorations = seg.decorations ?? [];
64
+ if (decorations.includes('bold')) style.fontWeight = 600;
65
+ if (decorations.includes('italic')) style.fontStyle = 'italic';
66
+ if (decorations.includes('underline')) style.textDecoration = 'underline';
67
+ if (decorations.includes('dim')) style.opacity = 0.6;
68
+ return style;
69
+ }
70
+
71
+ const MONO_FONT =
72
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
73
+
74
+ export const TerminalPanel = ({
75
+ text,
76
+ command,
77
+ title,
78
+ maxHeight = 384,
79
+ className,
80
+ }: TerminalPanelProps): React.JSX.Element => {
81
+ const [copied, setCopied] = useState(false);
82
+
83
+ const segments = useMemo<AnserJsonEntry[]>(
84
+ () =>
85
+ Anser.ansiToJson(normalizeTerminalText(text ?? ''), {
86
+ json: true,
87
+ use_classes: false,
88
+ remove_empty: false,
89
+ }),
90
+ [text]
91
+ );
92
+
93
+ const handleCopy = (): void => {
94
+ const payload = command ? `$ ${command}\n${text ?? ''}` : (text ?? '');
95
+ void navigator.clipboard?.writeText(payload).then(() => {
96
+ setCopied(true);
97
+ setTimeout(() => setCopied(false), 1500);
98
+ });
99
+ };
100
+
101
+ return (
102
+ <div
103
+ className={`overflow-hidden rounded-lg border ${className ?? ''}`}
104
+ style={{ borderColor: 'rgba(255,255,255,0.08)', backgroundColor: '#0c0c0f' }}
105
+ >
106
+ {/* Header / window chrome */}
107
+ <div
108
+ className="flex items-center gap-2 px-3 py-1.5"
109
+ style={{
110
+ backgroundColor: '#16161b',
111
+ borderBottom: '1px solid rgba(255,255,255,0.06)',
112
+ }}
113
+ >
114
+ <span className="flex items-center gap-1.5">
115
+ <span className="size-2.5 rounded-full" style={{ backgroundColor: '#ff5f57' }} />
116
+ <span className="size-2.5 rounded-full" style={{ backgroundColor: '#febc2e' }} />
117
+ <span className="size-2.5 rounded-full" style={{ backgroundColor: '#28c840' }} />
118
+ </span>
119
+ <span
120
+ className="ml-1 flex-1 truncate text-[11px]"
121
+ style={{ color: 'rgba(255,255,255,0.45)', fontFamily: MONO_FONT }}
122
+ >
123
+ {title ?? (command ? command : 'terminal')}
124
+ </span>
125
+ <button
126
+ type="button"
127
+ onClick={handleCopy}
128
+ className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] transition-colors"
129
+ style={{ color: 'rgba(255,255,255,0.45)' }}
130
+ title="复制"
131
+ >
132
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
133
+ {copied ? '已复制' : '复制'}
134
+ </button>
135
+ </div>
136
+
137
+ {/* Body */}
138
+ <pre
139
+ className="overflow-auto whitespace-pre-wrap break-all px-3 py-2.5 text-xs leading-relaxed"
140
+ style={{ maxHeight, fontFamily: MONO_FONT, color: '#d4d4d4', margin: 0 }}
141
+ >
142
+ {command && (
143
+ <div className="mb-1">
144
+ <span style={{ color: '#28c840' }}>$ </span>
145
+ <span style={{ color: '#e8e8e8' }}>{command}</span>
146
+ </div>
147
+ )}
148
+ {segments.map((seg, i) => (
149
+ <span key={i} style={styleForSegment(seg)}>
150
+ {seg.content}
151
+ </span>
152
+ ))}
153
+ </pre>
154
+ </div>
155
+ );
156
+ };
@@ -16,7 +16,7 @@ import type {
16
16
  PluginSortField,
17
17
  } from '@shared/types/extensions';
18
18
 
19
- export type ExtensionsSubTab = 'plugins' | 'mcp-servers' | 'skills' | 'api-keys';
19
+ export type ExtensionsSubTab = 'plugins' | 'mcp-servers' | 'skills' | 'env-vars';
20
20
  export type SkillsSortState = 'name-asc' | 'recent-desc';
21
21
 
22
22
  interface PluginSortState {
@@ -33,7 +33,7 @@ const DEFAULT_FILTERS: PluginFilters = {
33
33
 
34
34
  export function useExtensionsTabState() {
35
35
  // ── Sub-tab navigation ──
36
- const [activeSubTab, setActiveSubTab] = useState<ExtensionsSubTab>('mcp-servers');
36
+ const [activeSubTab, setActiveSubTab] = useState<ExtensionsSubTab>('plugins');
37
37
 
38
38
  // ── Plugin filters & sort ──
39
39
  const [pluginFilters, setPluginFilters] = useState<PluginFilters>(DEFAULT_FILTERS);
@@ -18,9 +18,6 @@ import { findPaneByTabId, updatePane } from '../utils/paneHelpers';
18
18
 
19
19
  import type { AppState } from '../types';
20
20
  import type {
21
- ApiKeyEntry,
22
- ApiKeySaveRequest,
23
- ApiKeyStorageStatus,
24
21
  EnrichedPlugin,
25
22
  ExtensionOperationState,
26
23
  InstalledMcpEntry,
@@ -74,12 +71,13 @@ export interface ExtensionsSlice {
74
71
  mcpInstallProgress: Record<string, ExtensionOperationState>;
75
72
  installErrors: Record<string, string>; // keyed by scoped operation key
76
73
 
77
- // ── API Keys ──
78
- apiKeys: ApiKeyEntry[];
79
- apiKeysLoading: boolean;
80
- apiKeysError: string | null;
81
- apiKeySaving: boolean;
82
- apiKeyStorageStatus: ApiKeyStorageStatus | null;
74
+ // ── Toast notifications ──
75
+ extensionToasts: Array<{
76
+ id: string;
77
+ type: 'success' | 'error' | 'warning' | 'info';
78
+ title: string;
79
+ message?: string;
80
+ }>;
83
81
 
84
82
  // ── Skills catalog cache ──
85
83
  skillsUserCatalog: SkillCatalogItem[];
@@ -111,6 +109,14 @@ export interface ExtensionsSlice {
111
109
  applySkillImport: (request: SkillImportRequest) => Promise<SkillDetail | null>;
112
110
  deleteSkill: (request: SkillDeleteRequest) => Promise<void>;
113
111
 
112
+ // ── Toast actions ──
113
+ addExtensionToast: (
114
+ type: 'success' | 'error' | 'warning' | 'info',
115
+ title: string,
116
+ message?: string
117
+ ) => void;
118
+ dismissExtensionToast: (id: string) => void;
119
+
114
120
  // ── Mutation actions ──
115
121
  installPlugin: (request: PluginInstallRequest) => Promise<void>;
116
122
  uninstallPlugin: (pluginId: string, scope?: InstallScope, projectPath?: string) => Promise<void>;
@@ -123,12 +129,6 @@ export interface ExtensionsSlice {
123
129
  projectPath?: string
124
130
  ) => Promise<void>;
125
131
 
126
- // ── API Keys actions ──
127
- fetchApiKeys: () => Promise<void>;
128
- fetchApiKeyStorageStatus: () => Promise<void>;
129
- saveApiKey: (request: ApiKeySaveRequest) => Promise<void>;
130
- deleteApiKey: (id: string) => Promise<void>;
131
-
132
132
  // ── Tab opener ──
133
133
  openExtensionsTab: () => void;
134
134
 
@@ -337,11 +337,6 @@ function getSkillsCatalogKey(projectPath?: string): string {
337
337
  return projectPath ?? USER_SKILLS_CATALOG_KEY;
338
338
  }
339
339
 
340
- function upsertApiKeyEntry(entries: ApiKeyEntry[], entry: ApiKeyEntry): ApiKeyEntry[] {
341
- const nextEntries = entries.filter((candidate) => candidate.id !== entry.id);
342
- return [entry, ...nextEntries];
343
- }
344
-
345
340
  /** Duration to show "success" state before returning to idle */
346
341
  const SUCCESS_DISPLAY_MS = 2_000;
347
342
  const PROJECT_SCOPE_REQUIRED_MESSAGE =
@@ -397,12 +392,7 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
397
392
  pluginInstallProgress: {},
398
393
  mcpInstallProgress: {},
399
394
  installErrors: {},
400
-
401
- apiKeys: [],
402
- apiKeysLoading: false,
403
- apiKeysError: null,
404
- apiKeySaving: false,
405
- apiKeyStorageStatus: null,
395
+ extensionToasts: [],
406
396
 
407
397
  skillsUserCatalog: [],
408
398
  skillsProjectCatalogByProjectPath: {},
@@ -631,6 +621,11 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
631
621
  const diagnosticsRecord = Object.fromEntries(
632
622
  diagnostics.map((entry) => [getMcpDiagnosticKey(entry.name, entry.scope), entry] as const)
633
623
  );
624
+ const failedServers = diagnostics.filter((d) => d.status === 'failed');
625
+ if (failedServers.length > 0) {
626
+ const names = failedServers.map((s) => s.name).join(', ');
627
+ get().addExtensionToast('warning', 'MCP 连接异常', `${names} 连接失败`);
628
+ }
634
629
  const checkedAt = Date.now();
635
630
  set({
636
631
  mcpDiagnostics: diagnosticsRecord,
@@ -956,6 +951,11 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
956
951
  set((prev) => ({
957
952
  pluginInstallProgress: { ...prev.pluginInstallProgress, [operationKey]: 'success' },
958
953
  }));
954
+ get().addExtensionToast(
955
+ 'success',
956
+ '插件已安装',
957
+ `已安装到 claudecode (${effectiveRequest.scope ?? 'user'})`
958
+ );
959
959
 
960
960
  // Refresh catalog to pick up new installed state
961
961
  void get().fetchPluginCatalog(
@@ -1183,6 +1183,7 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
1183
1183
  set((prev) => ({
1184
1184
  mcpInstallProgress: { ...prev.mcpInstallProgress, [progressKey]: 'success' },
1185
1185
  }));
1186
+ get().addExtensionToast('success', 'MCP 服务器已安装', `已安装 ${request.serverName}`);
1186
1187
 
1187
1188
  scheduleMcpSuccessReset(progressKey, set);
1188
1189
  } catch (err) {
@@ -1264,6 +1265,7 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
1264
1265
  set((prev) => ({
1265
1266
  mcpInstallProgress: { ...prev.mcpInstallProgress, [operationKey]: 'success' },
1266
1267
  }));
1268
+ get().addExtensionToast('success', 'MCP 服务器已卸载');
1267
1269
 
1268
1270
  scheduleMcpSuccessReset(operationKey, set);
1269
1271
  } catch (err) {
@@ -1276,92 +1278,25 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
1276
1278
  }
1277
1279
  },
1278
1280
 
1279
- // ── API Keys fetch ──
1280
- fetchApiKeys: async () => {
1281
- if (!api.apiKeys) return;
1282
-
1283
- set({ apiKeysLoading: true, apiKeysError: null });
1284
- try {
1285
- const keys = await api.apiKeys.list();
1286
- set({ apiKeys: keys, apiKeysLoading: false });
1287
- } catch (err) {
1288
- set({
1289
- apiKeysLoading: false,
1290
- apiKeysError: err instanceof Error ? err.message : 'Failed to load API keys',
1291
- });
1292
- }
1293
- },
1294
-
1295
- fetchApiKeyStorageStatus: async () => {
1296
- if (!api.apiKeys) return;
1297
- try {
1298
- const status = await api.apiKeys.getStorageStatus();
1299
- set({ apiKeyStorageStatus: status });
1300
- } catch {
1301
- // Non-critical — UI will just not show the info icon
1302
- }
1303
- },
1304
-
1305
- // ── API Key save ──
1306
- saveApiKey: async (request: ApiKeySaveRequest) => {
1307
- if (!api.apiKeys) return;
1308
-
1309
- set({ apiKeySaving: true, apiKeysError: null });
1310
- try {
1311
- const savedKey = await api.apiKeys.save(request);
1312
- const warnings: string[] = [];
1313
-
1314
- try {
1315
- const keys = await api.apiKeys.list();
1316
- set({ apiKeys: keys });
1317
- } catch (listError) {
1318
- warnings.push(
1319
- listError instanceof Error
1320
- ? `API key saved, but failed to refresh key list. ${listError.message}`
1321
- : 'API key saved, but failed to refresh key list.'
1322
- );
1281
+ // ── Toast notifications ──
1282
+ addExtensionToast: (type, title, message) => {
1283
+ const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1284
+ set((prev) => ({
1285
+ extensionToasts: [...prev.extensionToasts, { id, type, title, message }],
1286
+ }));
1287
+ if (type === 'success') {
1288
+ setTimeout(() => {
1323
1289
  set((prev) => ({
1324
- apiKeys: upsertApiKeyEntry(prev.apiKeys, savedKey),
1290
+ extensionToasts: prev.extensionToasts.filter((t) => t.id !== id),
1325
1291
  }));
1326
- }
1327
-
1328
- await refreshConfiguredCliStatus(get());
1329
- const refreshError = get().cliStatusError;
1330
- if (refreshError) {
1331
- warnings.push(`API key saved, but failed to refresh provider status. ${refreshError}`);
1332
- }
1333
- set({ apiKeySaving: false, apiKeysError: warnings.length > 0 ? warnings.join(' ') : null });
1334
- } catch (err) {
1335
- set({
1336
- apiKeySaving: false,
1337
- apiKeysError: err instanceof Error ? err.message : 'Failed to save API key',
1338
- });
1339
- throw err; // Re-throw so the dialog can show the error
1292
+ }, 3000);
1340
1293
  }
1341
1294
  },
1342
1295
 
1343
- // ── API Key delete ──
1344
- deleteApiKey: async (id: string) => {
1345
- if (!api.apiKeys) return;
1346
-
1347
- try {
1348
- await api.apiKeys.delete(id);
1349
- set((prev) => ({
1350
- apiKeys: prev.apiKeys.filter((k) => k.id !== id),
1351
- }));
1352
- await refreshConfiguredCliStatus(get());
1353
- const refreshError = get().cliStatusError;
1354
- set({
1355
- apiKeysError: refreshError
1356
- ? `API key deleted, but failed to refresh provider status. ${refreshError}`
1357
- : null,
1358
- });
1359
- } catch (err) {
1360
- set({
1361
- apiKeysError: err instanceof Error ? err.message : 'Failed to delete API key',
1362
- });
1363
- throw err;
1364
- }
1296
+ dismissExtensionToast: (id) => {
1297
+ set((prev) => ({
1298
+ extensionToasts: prev.extensionToasts.filter((t) => t.id !== id),
1299
+ }));
1365
1300
  },
1366
1301
 
1367
1302
  // ── Tab opener ──
@@ -904,7 +904,9 @@ function pruneOptimisticMessages(
904
904
  canonical: readonly InboxMessage[]
905
905
  ): InboxMessage[] {
906
906
  if (optimistic.length === 0) {
907
- return [];
907
+ // Preserve the input reference so selectTeamMessages' identity cache stays
908
+ // warm across no-op head refreshes (otherwise every poll churns `messages`).
909
+ return optimistic as InboxMessage[];
908
910
  }
909
911
 
910
912
  const canonicalIds = new Set(
@@ -913,10 +915,14 @@ function pruneOptimisticMessages(
913
915
  .filter((messageId) => messageId.length > 0)
914
916
  );
915
917
 
916
- return optimistic.filter((message) => {
918
+ const pruned = optimistic.filter((message) => {
917
919
  const messageId = typeof message.messageId === 'string' ? message.messageId.trim() : '';
918
920
  return !messageId || !canonicalIds.has(messageId);
919
921
  });
922
+
923
+ // Nothing was actually pruned — return the original reference so downstream
924
+ // identity checks (merged-messages selector) can short-circuit re-renders.
925
+ return pruned.length === optimistic.length ? (optimistic as InboxMessage[]) : pruned;
920
926
  }
921
927
 
922
928
  function clearPendingReplyRefreshTimer(teamName: string): void {
@@ -1122,6 +1122,35 @@ export interface ElectronAPI extends RecentProjectsElectronApi {
1122
1122
  // Extension Store — Skills Catalog API (Electron-only, optional)
1123
1123
  skills?: SkillsCatalogAPI;
1124
1124
 
1125
+ // Extension Store — Credentials (project env, MCP credentials)
1126
+ credentials?: {
1127
+ getStatus: () => Promise<{ encryption: string; storagePath: string } | null>;
1128
+ getProjectEnv: (projectPath: string) => Promise<Record<string, string>>;
1129
+ saveProjectEnv: (projectPath: string, vars: Record<string, string>) => Promise<void>;
1130
+ scanRequired: (
1131
+ projectPath: string,
1132
+ mcpServers: {
1133
+ name: string;
1134
+ envVars?: { name: string; isRequired: boolean; description?: string };
1135
+ }[],
1136
+ skillReqs: {
1137
+ name: string;
1138
+ envVars: { name: string; isRequired?: boolean; description?: string }[];
1139
+ }[]
1140
+ ) => Promise<{
1141
+ required: {
1142
+ name: string;
1143
+ isRequired: boolean;
1144
+ description?: string;
1145
+ source: string;
1146
+ value?: string;
1147
+ }[];
1148
+ }>;
1149
+ resolveAgentEnv: (projectPath: string) => Promise<Record<string, string>>;
1150
+ getSkillGlobalEnv: (skillFolderName: string) => Promise<Record<string, string>>;
1151
+ saveSkillGlobalEnv: (skillFolderName: string, vars: Record<string, string>) => Promise<void>;
1152
+ };
1153
+
1125
1154
  // Extension Store — API Keys Management (Electron-only, optional)
1126
1155
  apiKeys?: ApiKeysAPI;
1127
1156
 
@@ -41,6 +41,7 @@ export type {
41
41
  CreateSkillRequest,
42
42
  DeleteSkillRequest,
43
43
  SkillCatalogItem,
44
+ SkillEnvVarDef,
44
45
  SkillDeleteRequest,
45
46
  SkillDetail,
46
47
  SkillDirectoryFlags,
@@ -111,6 +111,7 @@ export interface McpInstallRequest {
111
111
  projectPath?: string; // required for 'project' scope
112
112
  envValues: Record<string, string>;
113
113
  headers: McpHeaderDef[]; // for HTTP/SSE servers (CLI --header flag)
114
+ harnessType?: string; // which harness to install to (defaults to claudecode)
114
115
  }
115
116
 
116
117
  // ── Custom install request (bypasses registry, user provides spec) ──────────
@@ -122,6 +123,7 @@ export interface McpCustomInstallRequest {
122
123
  installSpec: McpInstallSpec; // user provides directly
123
124
  envValues: Record<string, string>;
124
125
  headers: McpHeaderDef[];
126
+ harnessType?: string; // which harness to install to
125
127
  }
126
128
 
127
129
  // ── Search result wrapper ──────────────────────────────────────────────────
@@ -14,7 +14,7 @@ export interface PluginCatalogItem {
14
14
  name: string; // display name only
15
15
 
16
16
  // Metadata
17
- source: 'official';
17
+ source: 'official' | 'local'; // 'local' = discovered from a user-registered custom marketplace
18
18
  description: string;
19
19
  category: string; // open-ended string, derived from marketplace.json
20
20
  author?: { name: string; email?: string };
@@ -71,6 +71,7 @@ export interface PluginInstallRequest {
71
71
  pluginId: string; // canonical key — main resolves qualifiedName from catalog
72
72
  scope: InstallScope;
73
73
  projectPath?: string; // required for repo-scoped installs ('project' or 'local')
74
+ harnessType?: string; // which harness to install to (defaults to claudecode)
74
75
  }
75
76
 
76
77
  // ── Filters (renderer-only concern) ────────────────────────────────────────
@@ -12,6 +12,12 @@ export type SkillInvocationMode = 'auto' | 'manual-only';
12
12
 
13
13
  export type SkillIssueSeverity = 'info' | 'warning' | 'error';
14
14
 
15
+ export interface SkillEnvVarDef {
16
+ name: string;
17
+ description?: string;
18
+ isRequired?: boolean;
19
+ }
20
+
15
21
  export interface SkillDirectoryFlags {
16
22
  hasScripts: boolean;
17
23
  hasReferences: boolean;
@@ -57,6 +63,7 @@ export interface SkillCatalogItem {
57
63
  isValid: boolean;
58
64
  issues: SkillValidationIssue[];
59
65
  modifiedAt: number;
66
+ requiredEnv?: SkillEnvVarDef[];
60
67
  }
61
68
 
62
69
  export interface SkillDetail {
@@ -58,7 +58,7 @@ export function createLegacyRuntimeFallbackCliExtensionCapabilities(
58
58
  export function getCliProviderExtensionCapabilities(
59
59
  provider: Pick<CliProviderStatus, 'capabilities'> | null | undefined
60
60
  ): CliExtensionCapabilities {
61
- const fallback = createLegacyRuntimeFallbackCliExtensionCapabilities();
61
+ const fallback = createDefaultCliExtensionCapabilities();
62
62
  const extensions = provider?.capabilities?.extensions;
63
63
  if (!extensions) {
64
64
  return fallback;
@@ -1 +0,0 @@
1
- import{a6 as o,a7 as n}from"./index-BowUl0Jb.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-FhpTEPvD.js";import{_ as i}from"./index-BowUl0Jb.js";import"./chunk-FMBD7UC4-e9l2tGHG.js";import"./chunk-55IACEB6-CGGAOoXd.js";import"./chunk-QN33PNHL-DeiXVTCy.js";import"./splashScene-C8lWNnm4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-FhpTEPvD.js";import{_ as i}from"./index-BowUl0Jb.js";import"./chunk-FMBD7UC4-e9l2tGHG.js";import"./chunk-55IACEB6-CGGAOoXd.js";import"./chunk-QN33PNHL-DeiXVTCy.js";import"./splashScene-C8lWNnm4.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{b as r}from"./_baseUniq-DwPTU9lP.js";var e=4;function a(o){return r(o,e)}export{a as c};