@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,283 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import { api } from '@renderer/api';
4
+ import { useStore } from '@renderer/store';
5
+ import { isTeamProvisioningActive } from '@renderer/store/slices/teamSlice';
6
+ import { useShallow } from 'zustand/react/shallow';
7
+
8
+ import type { GlobalProvider } from '@shared/types';
9
+ import type { CcAgentType } from '@shared/types/ccConnect';
10
+
11
+ type SavePhase = 'idle' | 'saving' | 'restarting' | 'done';
12
+
13
+ export interface UseTeamEditFormReturn {
14
+ loading: boolean;
15
+ isProvisioning: boolean;
16
+ name: string;
17
+ setName: (v: string) => void;
18
+ description: string;
19
+ setDescription: (v: string) => void;
20
+ agentType: string;
21
+ setAgentType: (v: string) => void;
22
+ permissionMode: string;
23
+ setPermissionMode: (v: string) => void;
24
+ workDir: string;
25
+ setWorkDir: (v: string) => void;
26
+ language: string;
27
+ setLanguage: (v: string) => void;
28
+ managedSources: string;
29
+ setManagedSources: (v: string) => void;
30
+ feishuAllowFrom: string;
31
+ setFeishuAllowFrom: (v: string) => void;
32
+ disabledCommandsInput: string;
33
+ setDisabledCommandsInput: (v: string) => void;
34
+ providerRef: string;
35
+ setProviderRef: (v: string) => void;
36
+ showContextIndicator: boolean;
37
+ setShowContextIndicator: (v: boolean) => void;
38
+ replyFooter: boolean;
39
+ setReplyFooter: (v: boolean) => void;
40
+ injectSender: boolean;
41
+ setInjectSender: (v: boolean) => void;
42
+ color: string;
43
+ compatibleProviders: GlobalProvider[];
44
+ canDelete: boolean;
45
+ /** Current phase of the save+restart lifecycle */
46
+ savePhase: SavePhase;
47
+ /** true while saving or restarting (button spinner / disable) */
48
+ saving: boolean;
49
+ error: string | null;
50
+ clearError: () => void;
51
+ handleSave: () => void;
52
+ }
53
+
54
+ const PERMISSION_MODE_OPTIONS = [
55
+ { value: 'default', label: '默认' },
56
+ { value: 'acceptEdits', label: '自动接受编辑' },
57
+ { value: 'bypassPermissions', label: '跳过权限确认' },
58
+ { value: 'plan', label: '计划模式' },
59
+ ] as const;
60
+
61
+ export { PERMISSION_MODE_OPTIONS };
62
+
63
+ export function useTeamEditForm(teamName: string, open: boolean): UseTeamEditFormReturn {
64
+ // ── Store reads ──────────────────────────────────────────────
65
+ const { data, fetchTeams, selectTeam } = useStore(
66
+ useShallow((s) => ({
67
+ data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
68
+ fetchTeams: s.fetchTeams,
69
+ selectTeam: s.selectTeam,
70
+ }))
71
+ );
72
+ const isProvisioning = useStore((s) => isTeamProvisioningActive(s, teamName));
73
+
74
+ // ── Derived defaults ─────────────────────────────────────────
75
+ const rawSettings = useMemo(
76
+ () => (data?.settings ?? {}) as Record<string, unknown>,
77
+ [data?.settings]
78
+ );
79
+
80
+ const defaults = useMemo(() => {
81
+ const cfg = data?.config;
82
+ const d = data as Record<string, unknown> | null;
83
+ return {
84
+ name: cfg?.name ?? '',
85
+ description: cfg?.description ?? '',
86
+ color: cfg?.color ?? '',
87
+ agentType: cfg?.agentType ?? (d?.harness as string | undefined) ?? 'cursor',
88
+ workDir: (d?.workDir as string | undefined) ?? cfg?.projectPath ?? '',
89
+ permissionMode: cfg?.permissionMode ?? (d?.permissionMode as string | undefined) ?? 'default',
90
+ language:
91
+ cfg?.language ?? (typeof rawSettings.language === 'string' ? rawSettings.language : 'zh'),
92
+ managedSources:
93
+ cfg?.managedSources ??
94
+ (typeof rawSettings.admin_from === 'string' ? rawSettings.admin_from : '*'),
95
+ disabledCommands: Array.isArray(cfg?.disabledCommands)
96
+ ? cfg.disabledCommands
97
+ : Array.isArray(rawSettings.disabled_commands)
98
+ ? (rawSettings.disabled_commands as unknown[]).filter(
99
+ (entry): entry is string => typeof entry === 'string' && entry.trim().length > 0
100
+ )
101
+ : [],
102
+ platformAllowFrom:
103
+ cfg?.platformAllowFrom ??
104
+ (typeof rawSettings.platform_allow_from === 'object' &&
105
+ rawSettings.platform_allow_from !== null &&
106
+ !Array.isArray(rawSettings.platform_allow_from)
107
+ ? (rawSettings.platform_allow_from as Record<string, string>)
108
+ : {}),
109
+ providerRefs: data?.providerRefs ?? [],
110
+ globalProviders: data?.globalProviders ?? [],
111
+ showContextIndicator:
112
+ cfg?.showContextIndicator ??
113
+ (typeof rawSettings.show_context_indicator === 'boolean'
114
+ ? rawSettings.show_context_indicator
115
+ : true),
116
+ replyFooter:
117
+ cfg?.replyFooter ??
118
+ (typeof rawSettings.reply_footer === 'boolean' ? rawSettings.reply_footer : true),
119
+ injectSender:
120
+ cfg?.injectSender ??
121
+ (typeof rawSettings.inject_sender === 'boolean' ? rawSettings.inject_sender : false),
122
+ };
123
+ }, [data, rawSettings]);
124
+
125
+ // ── Local form state ─────────────────────────────────────────
126
+ const [name, setName] = useState(defaults.name);
127
+ const [description, setDescription] = useState(defaults.description);
128
+ const [agentType, setAgentType] = useState(defaults.agentType);
129
+ const [permissionMode, setPermissionMode] = useState(defaults.permissionMode);
130
+ const [workDir, setWorkDir] = useState(defaults.workDir);
131
+ const [language, setLanguage] = useState(defaults.language);
132
+ const [managedSources, setManagedSources] = useState(defaults.managedSources);
133
+ const [disabledCommandsInput, setDisabledCommandsInput] = useState(
134
+ defaults.disabledCommands.join(', ')
135
+ );
136
+ const [feishuAllowFrom, setFeishuAllowFrom] = useState(defaults.platformAllowFrom.feishu ?? '*');
137
+ const [providerRef, setProviderRef] = useState(defaults.providerRefs[0] ?? '');
138
+ const [showContextIndicator, setShowContextIndicator] = useState(defaults.showContextIndicator);
139
+ const [replyFooter, setReplyFooter] = useState(defaults.replyFooter);
140
+ const [injectSender, setInjectSender] = useState(defaults.injectSender);
141
+
142
+ // ── Single async lifecycle state ─────────────────────────────
143
+ const [savePhase, setSavePhase] = useState<SavePhase>('idle');
144
+ const [error, setError] = useState<string | null>(null);
145
+ const saving = savePhase === 'saving' || savePhase === 'restarting';
146
+
147
+ // ── Refs ─────────────────────────────────────────────────────
148
+ const defaultsRef = useRef(defaults);
149
+ if (defaults.name) {
150
+ defaultsRef.current = defaults;
151
+ }
152
+
153
+ // ── Reset form when dialog opens ─────────────────────────────
154
+ const prevOpenRef = useRef(false);
155
+ useEffect(() => {
156
+ if (!open || prevOpenRef.current) {
157
+ prevOpenRef.current = open;
158
+ return;
159
+ }
160
+ prevOpenRef.current = true;
161
+ const d = defaultsRef.current;
162
+ setSavePhase('idle');
163
+ setError(null);
164
+ setName(d.name);
165
+ setDescription(d.description);
166
+ setAgentType(d.agentType);
167
+ setPermissionMode(d.permissionMode);
168
+ setWorkDir(d.workDir);
169
+ setLanguage(d.language);
170
+ setManagedSources(d.managedSources);
171
+ setDisabledCommandsInput(d.disabledCommands.join(', '));
172
+ setFeishuAllowFrom(d.platformAllowFrom.feishu ?? '*');
173
+ setProviderRef(d.providerRefs[0] ?? '');
174
+ setShowContextIndicator(d.showContextIndicator);
175
+ setReplyFooter(d.replyFooter);
176
+ setInjectSender(d.injectSender);
177
+ }, [open]);
178
+
179
+ // ── Computed values ──────────────────────────────────────────
180
+ const compatibleProviders = useMemo(
181
+ () =>
182
+ defaults.globalProviders.filter(
183
+ (p) =>
184
+ !p.agent_types ||
185
+ p.agent_types.length === 0 ||
186
+ (p.agent_types as string[]).includes(agentType)
187
+ ),
188
+ [defaults.globalProviders, agentType]
189
+ );
190
+ const canDelete = teamName !== 'default' && teamName !== 'my-project';
191
+
192
+ // ── Actions ──────────────────────────────────────────────────
193
+ const clearError = (): void => setError(null);
194
+
195
+ const handleSave = (): void => {
196
+ if (!name.trim()) {
197
+ setError('团队名称不能为空');
198
+ return;
199
+ }
200
+ if (savePhase !== 'idle') return;
201
+
202
+ const disabledCommands = disabledCommandsInput
203
+ .split(',')
204
+ .map((e) => e.trim())
205
+ .filter((e) => e.length > 0);
206
+ const feishu = feishuAllowFrom.trim();
207
+
208
+ setSavePhase('saving');
209
+ setError(null);
210
+
211
+ void (async () => {
212
+ try {
213
+ await api.teams.updateConfig(teamName, {
214
+ name: name.trim(),
215
+ description: description.trim(),
216
+ color: defaultsRef.current.color,
217
+ agentType: agentType.trim() || undefined,
218
+ workDir: workDir.trim() || undefined,
219
+ permissionMode: permissionMode.trim() || undefined,
220
+ showContextIndicator,
221
+ replyFooter,
222
+ injectSender,
223
+ language: language.trim() || undefined,
224
+ managedSources: managedSources.trim() || undefined,
225
+ disabledCommands,
226
+ platformAllowFrom: feishu ? { feishu } : {},
227
+ providerRefs: providerRef ? [providerRef] : [],
228
+ });
229
+
230
+ setSavePhase('restarting');
231
+ await api.ccSettings.restart();
232
+
233
+ // Small grace period so the user sees "restarting" feedback
234
+ await new Promise((r) => setTimeout(r, 800));
235
+
236
+ await Promise.all([fetchTeams(), selectTeam(teamName)]);
237
+ setSavePhase('done');
238
+ } catch (err) {
239
+ setError(err instanceof Error ? err.message : '保存失败');
240
+ setSavePhase('idle');
241
+ }
242
+ })();
243
+ };
244
+
245
+ return {
246
+ loading: !data,
247
+ isProvisioning,
248
+ name,
249
+ setName,
250
+ description,
251
+ setDescription,
252
+ agentType,
253
+ setAgentType,
254
+ permissionMode,
255
+ setPermissionMode,
256
+ workDir,
257
+ setWorkDir,
258
+ language,
259
+ setLanguage,
260
+ managedSources,
261
+ setManagedSources,
262
+ feishuAllowFrom,
263
+ setFeishuAllowFrom,
264
+ disabledCommandsInput,
265
+ setDisabledCommandsInput,
266
+ providerRef,
267
+ setProviderRef,
268
+ showContextIndicator,
269
+ setShowContextIndicator,
270
+ replyFooter,
271
+ setReplyFooter,
272
+ injectSender,
273
+ setInjectSender,
274
+ color: defaults.color,
275
+ compatibleProviders,
276
+ canDelete,
277
+ savePhase,
278
+ saving,
279
+ error,
280
+ clearError,
281
+ handleSave,
282
+ };
283
+ }
@@ -361,49 +361,26 @@ export const KanbanBoard = ({
361
361
  columnTasks: TeamTask[],
362
362
  compact?: boolean
363
363
  ): React.JSX.Element => {
364
- const addHandler =
365
- onAddTask && columnId === 'todo'
366
- ? () => onAddTask(false)
367
- : onAddTask && columnId === 'in_progress'
368
- ? () => onAddTask(true)
369
- : undefined;
370
-
371
- const addButton = addHandler ? (
372
- <button
373
- type="button"
374
- onClick={addHandler}
375
- className="flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)] transition-colors hover:border-[var(--color-border-emphasis)] hover:text-[var(--color-text-secondary)]"
376
- >
377
- <Plus size={13} />
378
- Add task
379
- </button>
380
- ) : null;
381
-
382
364
  if (columnTasks.length === 0) {
383
365
  return (
384
- addButton ?? (
385
- <div className="rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
386
- No tasks
387
- </div>
388
- )
366
+ <div className="rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
367
+ No tasks
368
+ </div>
389
369
  );
390
370
  }
391
371
  if (enableTaskSorting) {
392
372
  const itemIds = columnTasks.map((t) => t.id);
393
373
  return (
394
- <>
395
- <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
396
- {columnTasks.map((task) => (
397
- <SortableKanbanTaskCard
398
- key={task.id}
399
- task={task}
400
- columnId={columnId}
401
- memberColorMap={memberColorMap}
402
- />
403
- ))}
404
- </SortableContext>
405
- {addButton}
406
- </>
374
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
375
+ {columnTasks.map((task) => (
376
+ <SortableKanbanTaskCard
377
+ key={task.id}
378
+ task={task}
379
+ columnId={columnId}
380
+ memberColorMap={memberColorMap}
381
+ />
382
+ ))}
383
+ </SortableContext>
407
384
  );
408
385
  }
409
386
  return (
@@ -432,7 +409,6 @@ export const KanbanBoard = ({
432
409
  onDeleteTask={onDeleteTask}
433
410
  />
434
411
  ))}
435
- {addButton}
436
412
  </>
437
413
  );
438
414
  };
@@ -614,36 +590,22 @@ export const KanbanBoard = ({
614
590
  </div>
615
591
  ) : (
616
592
  <div className="w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden px-1 pb-6 pr-4 pt-2">
617
- <div className="flex min-w-max items-start pr-1">
618
- {visibleColumns.map((column, index) => {
593
+ <div className="flex w-full items-start gap-3">
594
+ {visibleColumns.map((column) => {
619
595
  const columnTasks = groupedOrdered.get(column.id) ?? [];
620
596
  const accent = COLUMN_ACCENTS[column.id as 'todo' | 'in_progress' | 'done'];
621
- const width = columnWidths.get(column.id) ?? 256;
622
- const handleProps = getHandleProps(column.id);
623
597
  return (
624
- <div key={column.id} className="flex shrink-0">
625
- <div style={{ width }}>
626
- <KanbanColumn
627
- title={column.title}
628
- count={columnTasks.length}
629
- icon={accent.icon}
630
- headerBg={accent.headerBg}
631
- bodyBg={accent.bodyBg}
632
- bodyClassName="max-h-none overflow-visible"
633
- >
634
- {renderCards(column.id, columnTasks, true)}
635
- </KanbanColumn>
636
- </div>
637
- {index < visibleColumns.length - 1 ? (
638
- <div
639
- className="group relative mx-0.5 flex items-center justify-center"
640
- onPointerDown={handleProps.onPointerDown}
641
- style={handleProps.style}
642
- aria-label={handleProps['aria-label']}
643
- >
644
- <div className="h-full w-px bg-[var(--color-border)] transition-colors group-hover:bg-blue-500/50 group-active:bg-blue-500" />
645
- </div>
646
- ) : null}
598
+ <div key={column.id} className="min-w-0 flex-1">
599
+ <KanbanColumn
600
+ title={column.title}
601
+ count={columnTasks.length}
602
+ icon={accent.icon}
603
+ headerBg={accent.headerBg}
604
+ bodyBg={accent.bodyBg}
605
+ bodyClassName="max-h-none overflow-visible"
606
+ >
607
+ {renderCards(column.id, columnTasks, true)}
608
+ </KanbanColumn>
647
609
  </div>
648
610
  );
649
611
  })}
@@ -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);