@yancyyu/openhermit 1.6.37 → 1.6.39

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 (205) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-DYT3bwFR.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-Dbt_EU-e.js → _basePickBy-B7kSYPxr.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DWo68sXI.js → _baseUniq-CnjxqwAk.js} +1 -1
  5. package/dist-renderer/assets/{arc-DXH1iZQK.js → arc-CLeZuINP.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-cjffS2Qr.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BKdZF02Y.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CN27pqaI.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
  9. package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CXPCI7g_.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-BGAXQZRC.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-TPDaA_KQ.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-D1ADe_tq.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Beimtg3a.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-OjNBu854.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DinqvbH8.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BfFtlPSZ.js → chunk-TZMSLE5B-Ds1hInvp.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-CBYCBVRl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CBYCBVRl.js +1 -0
  20. package/dist-renderer/assets/clone-DcMF6Psb.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-D9z9Dgt7.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-n1g-DhEE.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BvxFq-BE.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-wVnJuwza.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-B707WJQw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-C-_1dGHs.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CMTSi3H6.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DZ0bNrAA.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DNVfGooQ.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
  30. package/dist-renderer/assets/{graph-865j_tM_.js → graph-CsoEwUhS.js} +1 -1
  31. package/dist-renderer/assets/{index-C_F9N5x-.js → index-BWPWmJNo.js} +1 -1
  32. package/dist-renderer/assets/{index-LwDIsXJN.js → index-Bu2R-Se7.js} +586 -740
  33. package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
  34. package/dist-renderer/assets/{index-DuUaf8at.js → index-D-3KgskL.js} +1 -1
  35. package/dist-renderer/assets/{index-BTx1nc4T.js → index-DGEBzLNT.js} +1 -1
  36. package/dist-renderer/assets/{index-2EW-eu3q.js → index-NhHNs2Oo.js} +1 -1
  37. package/dist-renderer/assets/{index-4dEMStJj.js → index-h17WuEyf.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-CyqtElLq.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BvjQ0Hm0.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CJJ-k0zT.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
  41. package/dist-renderer/assets/{layout-CnV6rQAG.js → layout-C3XWrpwo.js} +1 -1
  42. package/dist-renderer/assets/{linear-Cw3UQgyX.js → linear-OEEcn8KN.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-C5tDaGSK.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-CiIpPsau.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-C3gtowNj.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CXBTrAnU.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-wziX77xG.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-sYqopcrj.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bl1-0_Cp.js → stateDiagram-FKZM4ZOC-B2FY5qqi.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DcoMiR8H.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CIRjJUBo.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-CVPuNe1n.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-3nT9yHwp.js → xychartDiagram-PRI3JC2R-J6VVJzRq.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +30 -50
  58. package/src/main/server.ts +890 -247
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +4 -56
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/extensions/library/McpLibraryService.ts +243 -0
  62. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  63. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  64. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  65. package/src/main/services/session-intelligence/UsageTelemetryService.ts +14 -1
  66. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  67. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  68. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  69. package/src/main/services/teams-mvp/TaskDispatchService.ts +32 -8
  70. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  71. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  72. package/src/main/utils/teamProjectResolution.ts +15 -0
  73. package/src/renderer/App.tsx +8 -4
  74. package/src/renderer/api/httpClient.ts +174 -38
  75. package/src/renderer/api/providers.ts +23 -2
  76. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  87. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  88. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  89. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  90. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  91. package/src/renderer/components/extensions/ExtensionStoreView.tsx +12 -221
  92. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  93. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  94. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  95. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  96. package/src/renderer/components/extensions/plugins/PluginCard.tsx +10 -2
  97. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +40 -22
  98. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  99. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  100. package/src/renderer/components/layout/Sidebar.tsx +11 -54
  101. package/src/renderer/components/layout/SortableTab.tsx +20 -31
  102. package/src/renderer/components/layout/TabBar.tsx +1 -1
  103. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  104. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  105. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  106. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  107. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  108. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  109. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  110. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  111. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  112. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  113. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  114. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  115. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  116. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  117. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  118. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  119. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  120. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  121. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  122. package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
  123. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  124. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  126. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  127. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  128. package/src/renderer/components/settings/sections/index.ts +0 -1
  129. package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
  130. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  131. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
  132. package/src/renderer/components/splash/splashScene.ts +121 -929
  133. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  134. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  135. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  136. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  137. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  138. package/src/renderer/components/team/TeamDetailView.tsx +325 -114
  139. package/src/renderer/components/team/TeamListView.tsx +108 -123
  140. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
  141. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  142. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  143. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  144. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
  145. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  146. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
  147. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
  148. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  149. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  150. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  151. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  152. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  153. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  154. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  155. package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
  156. package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
  157. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
  158. package/src/renderer/components/team/tools/AddMcpInline.tsx +57 -0
  159. package/src/renderer/components/team/tools/AddSkillInline.tsx +61 -0
  160. package/src/renderer/components/team/tools/McpChip.tsx +45 -0
  161. package/src/renderer/components/team/tools/SkillChip.tsx +35 -0
  162. package/src/renderer/components/team/tools/ToolsSection.tsx +556 -0
  163. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  164. package/src/renderer/index.css +39 -22
  165. package/src/renderer/index.html +17 -50
  166. package/src/renderer/store/index.ts +2 -1
  167. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  168. package/src/renderer/store/slices/teamSlice.ts +45 -168
  169. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  170. package/src/renderer/utils/memberHelpers.ts +5 -17
  171. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  172. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  173. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  174. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  175. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  176. package/src/shared/types/api.ts +121 -2
  177. package/src/shared/types/ccConnect.ts +2 -0
  178. package/src/shared/types/extensions/api.ts +9 -0
  179. package/src/shared/types/extensions/index.ts +4 -0
  180. package/src/shared/types/extensions/mcp.ts +41 -0
  181. package/src/shared/types/index.ts +3 -0
  182. package/src/shared/types/systemManager.ts +49 -0
  183. package/src/shared/types/team.ts +29 -0
  184. package/src/shared/types/terminal.ts +4 -2
  185. package/src/shared/utils/extensionNormalizers.ts +29 -0
  186. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  187. package/dist-renderer/assets/ProjectEditorOverlay-Va_Vz-zz.js +0 -52
  188. package/dist-renderer/assets/channel-5dJIx68e.js +0 -1
  189. package/dist-renderer/assets/classDiagram-2ON5EDUG-BMGXWJ2d.js +0 -1
  190. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BMGXWJ2d.js +0 -1
  191. package/dist-renderer/assets/clone-D7FWfGY9.js +0 -1
  192. package/dist-renderer/assets/index-B2z_IyRH.css +0 -1
  193. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  194. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DOYYvDbi.js +0 -1
  195. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +0 -190
  196. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +0 -150
  197. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +0 -381
  198. package/src/main/services/extensions/install/McpInstallService.ts +0 -407
  199. package/src/main/services/extensions/state/McpInstallationStateService.ts +0 -42
  200. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +0 -314
  201. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +0 -765
  202. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +0 -593
  203. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +0 -372
  204. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +0 -343
  205. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +0 -659
@@ -0,0 +1,122 @@
1
+ import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import type {
6
+ SystemManagerConfig,
7
+ SystemManagerConfigPatch,
8
+ SystemManagerStatus,
9
+ } from '@shared/types/systemManager';
10
+
11
+ const CONFIG_FILE = 'system-manager.json';
12
+
13
+ function hermitHome(): string {
14
+ return process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
15
+ }
16
+
17
+ function expandHome(input: string): string {
18
+ const normalized = input.trim().replace(/^~/, '~');
19
+ if (normalized === '~') return os.homedir();
20
+ if (normalized.startsWith('~/')) return path.join(os.homedir(), normalized.slice(2));
21
+ return normalized;
22
+ }
23
+
24
+ async function isDirectory(dirPath: string): Promise<boolean> {
25
+ try {
26
+ return (await stat(dirPath)).isDirectory();
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function commandExists(command: string): Promise<boolean> {
33
+ const paths = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
34
+ for (const dir of paths) {
35
+ try {
36
+ await access(path.join(dir, command));
37
+ return true;
38
+ } catch {
39
+ // keep looking
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export class SystemManagerConfigService {
46
+ private readonly configPath = path.join(hermitHome(), CONFIG_FILE);
47
+
48
+ constructor(private readonly defaultWorkDir: string) {}
49
+
50
+ async getConfig(): Promise<SystemManagerConfig> {
51
+ try {
52
+ const raw = await readFile(this.configPath, 'utf-8');
53
+ const parsed = JSON.parse(raw) as Partial<SystemManagerConfig>;
54
+ const selectedWorkDir = await this.normalizeDirectory(
55
+ parsed.selectedWorkDir || this.defaultWorkDir,
56
+ 'selectedWorkDir'
57
+ );
58
+ const workflowFolder = parsed.workflowFolder
59
+ ? await this.normalizeDirectory(parsed.workflowFolder, 'workflowFolder')
60
+ : undefined;
61
+ return {
62
+ schemaVersion: 1,
63
+ selectedWorkDir,
64
+ ...(workflowFolder ? { workflowFolder } : {}),
65
+ updatedAt:
66
+ typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
67
+ };
68
+ } catch {
69
+ return {
70
+ schemaVersion: 1,
71
+ selectedWorkDir: this.defaultWorkDir,
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ }
75
+ }
76
+
77
+ async updateConfig(patch: SystemManagerConfigPatch): Promise<SystemManagerConfig> {
78
+ const current = await this.getConfig();
79
+ const next: SystemManagerConfig = {
80
+ ...current,
81
+ updatedAt: new Date().toISOString(),
82
+ };
83
+
84
+ if (typeof patch.selectedWorkDir === 'string') {
85
+ next.selectedWorkDir = await this.normalizeDirectory(
86
+ patch.selectedWorkDir,
87
+ 'selectedWorkDir'
88
+ );
89
+ }
90
+ if (patch.workflowFolder === null) {
91
+ delete next.workflowFolder;
92
+ } else if (typeof patch.workflowFolder === 'string') {
93
+ next.workflowFolder = await this.normalizeDirectory(patch.workflowFolder, 'workflowFolder');
94
+ }
95
+
96
+ await mkdir(path.dirname(this.configPath), { recursive: true });
97
+ await writeFile(this.configPath, JSON.stringify(next, null, 2), 'utf-8');
98
+ return next;
99
+ }
100
+
101
+ async getStatus(): Promise<SystemManagerStatus> {
102
+ const config = await this.getConfig();
103
+ const hasClaude = await commandExists('claude');
104
+ return {
105
+ displayName: '控制台',
106
+ defaultWorkDir: this.defaultWorkDir,
107
+ selectedWorkDir: config.selectedWorkDir,
108
+ ...(config.workflowFolder ? { workflowFolder: config.workflowFolder } : {}),
109
+ claudeCommand: 'claude',
110
+ localStatus: hasClaude ? 'ready' : 'missing-claude',
111
+ ...(hasClaude ? {} : { error: '未在 PATH 中找到 claude 命令' }),
112
+ };
113
+ }
114
+
115
+ private async normalizeDirectory(input: string, fieldName: string): Promise<string> {
116
+ const resolved = path.resolve(expandHome(input));
117
+ if (!(await isDirectory(resolved))) {
118
+ throw new Error(`${fieldName} 不是有效目录: ${resolved}`);
119
+ }
120
+ return resolved;
121
+ }
122
+ }
@@ -0,0 +1,233 @@
1
+ import { spawn as spawnChild } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { EventEmitter } from 'node:events';
4
+ import { stat } from 'node:fs/promises';
5
+
6
+ import type { PtySpawnOptions } from '@shared/types/terminal';
7
+ import * as pty from 'node-pty';
8
+
9
+ const PYTHON_PTY_BRIDGE = String.raw`
10
+ import errno
11
+ import os
12
+ import pty
13
+ import select
14
+ import signal
15
+ import sys
16
+
17
+ cmd = sys.argv[1:]
18
+ child_pid = None
19
+
20
+
21
+ def terminate(signum, _frame):
22
+ if child_pid:
23
+ try:
24
+ os.kill(child_pid, signum)
25
+ except OSError:
26
+ pass
27
+ sys.exit(128 + signum)
28
+
29
+
30
+ signal.signal(signal.SIGTERM, terminate)
31
+ signal.signal(signal.SIGINT, terminate)
32
+
33
+ child_pid, fd = pty.fork()
34
+ if child_pid == 0:
35
+ os.execvpe(cmd[0], cmd, os.environ)
36
+
37
+ while True:
38
+ try:
39
+ readable, _, _ = select.select([fd, sys.stdin.fileno()], [], [])
40
+ except OSError:
41
+ break
42
+
43
+ if fd in readable:
44
+ try:
45
+ data = os.read(fd, 4096)
46
+ except OSError:
47
+ break
48
+ if not data:
49
+ break
50
+ os.write(sys.stdout.fileno(), data)
51
+
52
+ if sys.stdin.fileno() in readable:
53
+ try:
54
+ data = os.read(sys.stdin.fileno(), 4096)
55
+ except OSError as err:
56
+ if err.errno == errno.EIO:
57
+ data = b''
58
+ else:
59
+ raise
60
+ if data:
61
+ os.write(fd, data)
62
+
63
+ try:
64
+ _, status = os.waitpid(child_pid, 0)
65
+ except ChildProcessError:
66
+ sys.exit(0)
67
+ sys.exit(os.waitstatus_to_exitcode(status))
68
+ `;
69
+
70
+ interface ManagedProcess {
71
+ id: string;
72
+ pid: number;
73
+ command: string;
74
+ args: string[];
75
+ cwd: string;
76
+ createdAt: string;
77
+ write(data: string): void;
78
+ resize(cols: number, rows: number): void;
79
+ kill(signal?: NodeJS.Signals): void;
80
+ }
81
+
82
+ const TERMINAL_KILL_TIMEOUT_MS = 1_500;
83
+ const TERMINAL_FORCE_KILL_TIMEOUT_MS = 1_500;
84
+
85
+ export type TerminalDataEvent = { ptyId: string; data: string };
86
+ export type TerminalExitEvent = { ptyId: string; exitCode: number };
87
+
88
+ export class SystemManagerPtyService extends EventEmitter {
89
+ private readonly sessions = new Map<string, ManagedProcess>();
90
+
91
+ async spawn(options: PtySpawnOptions = {}): Promise<string> {
92
+ const command = options.command || 'claude';
93
+ const args = options.args ?? [];
94
+ const cwd = options.cwd || process.cwd();
95
+ const cwdStat = await stat(cwd);
96
+ if (!cwdStat.isDirectory()) {
97
+ throw new Error(`cwd 不是有效目录: ${cwd}`);
98
+ }
99
+
100
+ const id = `pty-${randomUUID()}`;
101
+ const env = {
102
+ ...process.env,
103
+ TERM: 'xterm-256color',
104
+ COLORTERM: 'truecolor',
105
+ ...(options.env ?? {}),
106
+ } as Record<string, string>;
107
+
108
+ try {
109
+ const proc = pty.spawn(command, args, {
110
+ name: 'xterm-256color',
111
+ cols: options.cols ?? 120,
112
+ rows: options.rows ?? 34,
113
+ cwd,
114
+ env,
115
+ });
116
+
117
+ this.sessions.set(id, {
118
+ id,
119
+ pid: proc.pid,
120
+ command,
121
+ args,
122
+ cwd,
123
+ createdAt: new Date().toISOString(),
124
+ write: (data) => proc.write(data),
125
+ resize: (cols, rows) => proc.resize(Math.max(20, cols), Math.max(5, rows)),
126
+ kill: (signal) => proc.kill(signal),
127
+ });
128
+
129
+ proc.onData((data) => {
130
+ this.emit('data', { ptyId: id, data } satisfies TerminalDataEvent);
131
+ });
132
+ proc.onExit(({ exitCode }) => {
133
+ if (!this.sessions.delete(id)) return;
134
+ this.emit('exit', { ptyId: id, exitCode } satisfies TerminalExitEvent);
135
+ });
136
+ } catch (err) {
137
+ const child = spawnChild('python3', ['-u', '-c', PYTHON_PTY_BRIDGE, command, ...args], {
138
+ cwd,
139
+ env,
140
+ stdio: 'pipe',
141
+ });
142
+ this.sessions.set(id, {
143
+ id,
144
+ pid: child.pid ?? -1,
145
+ command,
146
+ args,
147
+ cwd,
148
+ createdAt: new Date().toISOString(),
149
+ write: (data) => child.stdin.write(data),
150
+ resize: () => {},
151
+ kill: (signal) => child.kill(signal),
152
+ });
153
+ this.emit('data', {
154
+ ptyId: id,
155
+ data: `[Hermit] node-pty unavailable (${err instanceof Error ? err.message : String(err)}); using python PTY fallback.\r\n`,
156
+ } satisfies TerminalDataEvent);
157
+ child.stdout.on('data', (data) => {
158
+ this.emit('data', { ptyId: id, data: data.toString() } satisfies TerminalDataEvent);
159
+ });
160
+ child.stderr.on('data', (data) => {
161
+ this.emit('data', { ptyId: id, data: data.toString() } satisfies TerminalDataEvent);
162
+ });
163
+ child.on('error', (error) => {
164
+ if (!this.sessions.delete(id)) return;
165
+ this.emit('data', {
166
+ ptyId: id,
167
+ data: `[Hermit] failed to start process: ${error.message}\r\n`,
168
+ } satisfies TerminalDataEvent);
169
+ this.emit('exit', { ptyId: id, exitCode: 1 } satisfies TerminalExitEvent);
170
+ });
171
+ child.on('exit', (exitCode) => {
172
+ if (!this.sessions.delete(id)) return;
173
+ this.emit('exit', { ptyId: id, exitCode: exitCode ?? 0 } satisfies TerminalExitEvent);
174
+ });
175
+ }
176
+
177
+ return id;
178
+ }
179
+
180
+ write(ptyId: string, data: string): void {
181
+ const session = this.sessions.get(ptyId);
182
+ if (!session) throw new Error(`PTY 不存在: ${ptyId}`);
183
+ session.write(data);
184
+ }
185
+
186
+ resize(ptyId: string, cols: number, rows: number): void {
187
+ const session = this.sessions.get(ptyId);
188
+ if (!session) return;
189
+ session.resize(cols, rows);
190
+ }
191
+
192
+ async kill(ptyId: string): Promise<void> {
193
+ const session = this.sessions.get(ptyId);
194
+ if (!session) return;
195
+
196
+ await new Promise<void>((resolve) => {
197
+ let settled = false;
198
+ let forceTimer: ReturnType<typeof setTimeout> | null = null;
199
+ const finish = (): void => {
200
+ if (settled) return;
201
+ settled = true;
202
+ if (forceTimer) clearTimeout(forceTimer);
203
+ this.off('exit', onExit);
204
+ resolve();
205
+ };
206
+ const onExit = (event: TerminalExitEvent): void => {
207
+ if (event.ptyId === ptyId) finish();
208
+ };
209
+
210
+ this.on('exit', onExit);
211
+ session.kill();
212
+ forceTimer = setTimeout(() => {
213
+ if (!this.sessions.has(ptyId)) {
214
+ finish();
215
+ return;
216
+ }
217
+ session.kill('SIGKILL');
218
+ }, TERMINAL_KILL_TIMEOUT_MS);
219
+ setTimeout(() => {
220
+ if (this.sessions.delete(ptyId)) {
221
+ this.emit('exit', { ptyId, exitCode: 0 } satisfies TerminalExitEvent);
222
+ }
223
+ finish();
224
+ }, TERMINAL_KILL_TIMEOUT_MS + TERMINAL_FORCE_KILL_TIMEOUT_MS);
225
+ });
226
+ }
227
+
228
+ killAll(): void {
229
+ for (const id of [...this.sessions.keys()]) {
230
+ void this.kill(id);
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,75 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import type {
7
+ WorkflowPromptContentResponse,
8
+ WorkflowPromptListResponse,
9
+ WorkflowPromptSummary,
10
+ } from '@shared/types/systemManager';
11
+
12
+ const SUPPORTED_EXTENSIONS = new Set(['.md', '.txt', '.prompt', '.workflow']);
13
+ const MAX_PROMPT_BYTES = 256 * 1024;
14
+
15
+ function expandHome(input: string): string {
16
+ const normalized = input.trim().replace(/^~/, '~');
17
+ if (normalized === '~') return os.homedir();
18
+ if (normalized.startsWith('~/')) return path.join(os.homedir(), normalized.slice(2));
19
+ return normalized;
20
+ }
21
+
22
+ function promptId(filePath: string): string {
23
+ return createHash('sha256').update(filePath).digest('hex').slice(0, 16);
24
+ }
25
+
26
+ function labelFromFilename(filename: string): string {
27
+ return path.basename(filename, path.extname(filename)).replace(/[-_]+/g, ' ').trim() || filename;
28
+ }
29
+
30
+ export class WorkflowPromptService {
31
+ async list(folderInput: string): Promise<WorkflowPromptListResponse> {
32
+ const folder = path.resolve(expandHome(folderInput));
33
+ const folderStat = await stat(folder);
34
+ if (!folderStat.isDirectory()) {
35
+ throw new Error(`workflow folder 不是有效目录: ${folder}`);
36
+ }
37
+
38
+ const warnings: string[] = [];
39
+ const prompts: WorkflowPromptSummary[] = [];
40
+ const entries = await readdir(folder, { withFileTypes: true });
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.isFile() || entry.name.startsWith('.')) continue;
44
+ const ext = path.extname(entry.name).toLowerCase();
45
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
46
+ const filePath = path.join(folder, entry.name);
47
+ const fileStat = await stat(filePath);
48
+ if (fileStat.size > MAX_PROMPT_BYTES) {
49
+ warnings.push(`${entry.name} 超过 256 KiB,已跳过`);
50
+ continue;
51
+ }
52
+ prompts.push({
53
+ id: promptId(filePath),
54
+ label: labelFromFilename(entry.name),
55
+ filename: entry.name,
56
+ path: filePath,
57
+ sizeBytes: fileStat.size,
58
+ updatedAt: fileStat.mtime.toISOString(),
59
+ });
60
+ }
61
+
62
+ prompts.sort((a, b) => a.filename.localeCompare(b.filename));
63
+ return { folder, prompts, warnings };
64
+ }
65
+
66
+ async read(folderInput: string, id: string): Promise<WorkflowPromptContentResponse> {
67
+ const list = await this.list(folderInput);
68
+ const prompt = list.prompts.find((item) => item.id === id || item.filename === id);
69
+ if (!prompt) {
70
+ throw new Error(`未找到 workflow: ${id}`);
71
+ }
72
+ const content = await readFile(prompt.path, 'utf-8');
73
+ return { prompt, content };
74
+ }
75
+ }
@@ -79,6 +79,7 @@ export class TaskDispatchService {
79
79
  this.stopHeartbeat();
80
80
  this.stopConsumers();
81
81
  this.stopResponseConsumers();
82
+ this.collabBoard.setRedis(null);
82
83
  this.redis?.disconnect();
83
84
  this.redisSub?.disconnect();
84
85
  this.redis = null;
@@ -757,27 +758,50 @@ export class TaskDispatchService {
757
758
 
758
759
  private async connectRedis(): Promise<void> {
759
760
  if (!this.config?.redis) return;
761
+ let redis: Redis | null = null;
762
+ let redisSub: Redis | null = null;
760
763
  try {
761
764
  const ioredis = await import('ioredis');
762
765
  const { host, port, password, db } = this.config.redis;
763
- const opts = { host, port, password: password || undefined, db: db ?? 0 };
766
+ const opts = {
767
+ host,
768
+ port,
769
+ password: password || undefined,
770
+ db: db ?? 0,
771
+ lazyConnect: true,
772
+ maxRetriesPerRequest: 0,
773
+ retryStrategy: () => null,
774
+ };
764
775
 
765
- this.redis = new ioredis.default(opts);
766
- this.redisSub = new ioredis.default(opts);
776
+ redis = new ioredis.default(opts);
777
+ redisSub = new ioredis.default(opts);
767
778
 
768
- this.redis.on('error', (err: Error) => {
769
- console.error('[TaskDispatchService] Redis error:', err.message);
779
+ redis.on('error', () => {
780
+ /* handled by task bus connection status */
781
+ });
782
+ redisSub.on('error', () => {
783
+ /* handled by task bus connection status */
770
784
  });
771
785
 
772
- await this.redis.ping();
786
+ await redis.connect();
787
+ await redisSub.connect();
788
+ await redis.ping();
773
789
 
790
+ this.redis = redis;
791
+ this.redisSub = redisSub;
792
+ redis = null;
793
+ redisSub = null;
774
794
  this.collabBoard.setRedis(this.redis);
775
795
  this.startHeartbeat();
776
796
  this.startConsumers();
777
797
  this.startResponseConsumers();
778
798
  this.subscribeStatus();
779
- } catch (err) {
780
- console.error('[TaskDispatchService] Redis connect failed:', err);
799
+ } catch {
800
+ this.collabBoard.setRedis(null);
801
+ redis?.disconnect();
802
+ redisSub?.disconnect();
803
+ this.redis?.disconnect();
804
+ this.redisSub?.disconnect();
781
805
  this.redis = null;
782
806
  this.redisSub = null;
783
807
  }
@@ -45,6 +45,35 @@ function removeManagedTeamInstructions(content: string): string {
45
45
  return next.replace(/\n{3,}/g, '\n\n').trimEnd();
46
46
  }
47
47
 
48
+ async function injectHermitTasksMcpConfig(workDir: string): Promise<void> {
49
+ const settingsPath = path.join(workDir, '.claude', 'settings.json');
50
+ let settings: Record<string, unknown> = {};
51
+ try {
52
+ const raw = await fs.promises.readFile(settingsPath, 'utf8');
53
+ settings = JSON.parse(raw) as Record<string, unknown>;
54
+ } catch (err) {
55
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
56
+ throw err;
57
+ }
58
+ }
59
+
60
+ const existingMcpServers =
61
+ settings.mcpServers && typeof settings.mcpServers === 'object'
62
+ ? (settings.mcpServers as Record<string, unknown>)
63
+ : {};
64
+ const port = process.env.PORT ?? '5680';
65
+ settings.mcpServers = {
66
+ ...existingMcpServers,
67
+ 'hermit-tasks': {
68
+ type: 'sse',
69
+ url: `http://127.0.0.1:${port}/mcp`,
70
+ },
71
+ };
72
+
73
+ await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
74
+ await fs.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2));
75
+ }
76
+
48
77
  export class TeamProvisioningService {
49
78
  private readonly workspace: TeamWorkspaceService;
50
79
 
@@ -68,12 +97,16 @@ export class TeamProvisioningService {
68
97
  * 4. 触发 cc-connect restart 激活 project
69
98
  */
70
99
  async createTeam(
71
- input: CreateTeamInput & { createCcProject?: boolean }
100
+ input: CreateTeamInput & { createCcProject?: boolean; injectInstructions?: boolean }
72
101
  ): Promise<{ slug: string; manifest: TeamManifest }> {
73
- const { createCcProject = true, ...workspaceInput } = input;
102
+ const { createCcProject = true, injectInstructions = true, ...workspaceInput } = input;
74
103
 
75
104
  const { slug, manifest } = await this.workspace.createTeam(workspaceInput);
76
105
 
106
+ if (injectInstructions && manifest.harness === 'claudecode') {
107
+ await injectHermitTasksMcpConfig(manifest.workDir);
108
+ }
109
+
77
110
  if (createCcProject) {
78
111
  try {
79
112
  const platformType = manifest.platform ?? 'bridge';
@@ -118,6 +151,7 @@ export class TeamProvisioningService {
118
151
  Pick<
119
152
  TeamManifest,
120
153
  | 'displayName'
154
+ | 'bindProject'
121
155
  | 'color'
122
156
  | 'description'
123
157
  | 'collaboration'
@@ -130,7 +164,10 @@ export class TeamProvisioningService {
130
164
  | 'injectSender'
131
165
  | 'managedSources'
132
166
  | 'disabledCommands'
167
+ | 'platform'
168
+ | 'platformOptions'
133
169
  | 'platformAllowFrom'
170
+ | 'platformAllowChat'
134
171
  >
135
172
  >
136
173
  ): Promise<TeamManifest> {
@@ -45,6 +45,8 @@ export interface TeamManifest {
45
45
  managedSources?: string;
46
46
  disabledCommands?: string[];
47
47
  platformAllowFrom?: Record<string, string>;
48
+ /** 群聊允许的 chat ID(按平台) */
49
+ platformAllowChat?: Record<string, string>;
48
50
  pendingDelete?: boolean;
49
51
  restartRequired?: boolean;
50
52
  /**
@@ -159,9 +161,9 @@ async function readJson<T>(p: string, fallback: T): Promise<T> {
159
161
  try {
160
162
  const raw = await fs.promises.readFile(p, 'utf8');
161
163
  return JSON.parse(raw) as T;
162
- } catch (err) {
163
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return fallback;
164
- throw err;
164
+ } catch {
165
+ // File missing or corrupted JSON return fallback silently
166
+ return fallback;
165
167
  }
166
168
  }
167
169
 
@@ -185,6 +187,17 @@ export class TeamWorkspaceService {
185
187
  return match?.slug ?? teamSlug;
186
188
  }
187
189
 
190
+ private async createUniqueStorageSlug(displayName: string): Promise<string> {
191
+ const baseSlug = toSlug(displayName);
192
+ let slug = baseSlug;
193
+ let suffix = 2;
194
+ while (await pathExists(path.join(teamRoot(slug), 'team.json'))) {
195
+ slug = `${baseSlug}-${suffix}`;
196
+ suffix += 1;
197
+ }
198
+ return slug;
199
+ }
200
+
188
201
  async createTeam(
189
202
  input: CreateTeamInput
190
203
  ): Promise<{ slug: string; root: string; manifest: TeamManifest }> {
@@ -192,7 +205,7 @@ export class TeamWorkspaceService {
192
205
  if (!input.bindProject) throw new Error('bindProject is required');
193
206
  if (!input.workDir) throw new Error('workDir is required');
194
207
 
195
- const slug = toSlug(input.bindProject);
208
+ const slug = await this.createUniqueStorageSlug(input.displayName);
196
209
  const root = teamRoot(slug);
197
210
 
198
211
  await fs.promises.mkdir(root, { recursive: true });
@@ -263,6 +276,7 @@ export class TeamWorkspaceService {
263
276
  const out: TeamManifest[] = [];
264
277
  for (const e of entries) {
265
278
  if (!e.isDirectory()) continue;
279
+ if (e.name.startsWith('.')) continue;
266
280
  try {
267
281
  out.push(await this.readTeamManifest(e.name));
268
282
  } catch {
@@ -278,6 +292,7 @@ export class TeamWorkspaceService {
278
292
  Pick<
279
293
  TeamManifest,
280
294
  | 'displayName'
295
+ | 'bindProject'
281
296
  | 'color'
282
297
  | 'description'
283
298
  | 'collaboration'
@@ -290,7 +305,10 @@ export class TeamWorkspaceService {
290
305
  | 'injectSender'
291
306
  | 'managedSources'
292
307
  | 'disabledCommands'
308
+ | 'platform'
309
+ | 'platformOptions'
293
310
  | 'platformAllowFrom'
311
+ | 'platformAllowChat'
294
312
  | 'pendingDelete'
295
313
  | 'restartRequired'
296
314
  >
@@ -0,0 +1,15 @@
1
+ import type { TeamManifest } from '@main/services/teams-mvp/TeamWorkspaceService';
2
+
3
+ export async function resolveCcProjectName(
4
+ routeTeamName: string,
5
+ readTeamManifest: (teamName: string) => Promise<TeamManifest>
6
+ ): Promise<string> {
7
+ try {
8
+ const manifest = await readTeamManifest(routeTeamName);
9
+ const bindProject = manifest.bindProject?.trim();
10
+ if (bindProject) return bindProject;
11
+ } catch {
12
+ // routeTeamName may already be a cc-connect project name.
13
+ }
14
+ return routeTeamName;
15
+ }
@@ -41,6 +41,7 @@ function buildPathForTab(activeTab: Tab | null): string {
41
41
  }
42
42
  switch (activeTab.type) {
43
43
  case 'team':
44
+ if (activeTab.teamName === 'system-manager') return '/system-manager';
44
45
  return activeTab.teamName
45
46
  ? `/team/${encodeURIComponent(activeTab.teamName)}`
46
47
  : DEFAULT_APP_PATH;
@@ -142,6 +143,9 @@ function useTabPathPersistence() {
142
143
  case 'teams':
143
144
  state.openTeamsTab();
144
145
  break;
146
+ case 'system-manager':
147
+ void state.openSystemManager();
148
+ break;
145
149
  case 'settings':
146
150
  state.openSettingsTab();
147
151
  break;
@@ -208,11 +212,11 @@ declare global {
208
212
  }
209
213
  }
210
214
 
211
- const SPLASH_MIN_DURATION_MS = 1600;
212
- const SPLASH_ENHANCED_HOLD_MS = 600;
215
+ const SPLASH_MIN_DURATION_MS = 2800;
216
+ const SPLASH_ENHANCED_HOLD_MS = 800;
213
217
  const SPLASH_FADE_MS = 480;
214
- const SPLASH_REDUCED_MIN_DURATION_MS = 320;
215
- const SPLASH_REDUCED_HOLD_MS = 120;
218
+ const SPLASH_REDUCED_MIN_DURATION_MS = 600;
219
+ const SPLASH_REDUCED_HOLD_MS = 200;
216
220
  const SPLASH_REDUCED_FADE_MS = 180;
217
221
  const SPLASH_AVATAR_READY_MAX_WAIT_MS = 900;
218
222
  const SPLASH_REDUCED_AVATAR_READY_MAX_WAIT_MS = 160;