@yancyyu/openhermit 1.6.38 → 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 (188) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-B7kSYPxr.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CnjxqwAk.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-CLeZuINP.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
  9. package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.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-DxcFNQKT.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-CsoEwUhS.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BWPWmJNo.js} +1 -1
  32. package/dist-renderer/assets/{index-V7dAKPqd.js → index-Bu2R-Se7.js} +587 -705
  33. package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
  34. package/dist-renderer/assets/{index-CzWxVCRL.js → index-D-3KgskL.js} +1 -1
  35. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-DGEBzLNT.js} +1 -1
  36. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-NhHNs2Oo.js} +1 -1
  37. package/dist-renderer/assets/{index-C_okzZXP.js → index-h17WuEyf.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-C3XWrpwo.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-OEEcn8KN.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.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-BXs_hOJs.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.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 +2 -1
  58. package/src/main/server.ts +873 -221
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  62. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  63. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  64. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  65. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  66. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  67. package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
  68. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  69. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  70. package/src/main/utils/teamProjectResolution.ts +15 -0
  71. package/src/renderer/App.tsx +8 -4
  72. package/src/renderer/api/httpClient.ts +68 -18
  73. package/src/renderer/api/providers.ts +23 -2
  74. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  75. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  76. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  87. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  88. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  89. package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -125
  90. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  91. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  92. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  93. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  94. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  95. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  96. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  97. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  98. package/src/renderer/components/layout/Sidebar.tsx +11 -54
  99. package/src/renderer/components/layout/SortableTab.tsx +20 -31
  100. package/src/renderer/components/layout/TabBar.tsx +1 -1
  101. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  102. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  103. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  104. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  105. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  106. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  107. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  108. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  109. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  110. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  111. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  112. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  113. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  114. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  115. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  116. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  117. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  118. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  119. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  120. package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
  121. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  122. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  123. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  124. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  125. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  126. package/src/renderer/components/settings/sections/index.ts +0 -1
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
  128. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  129. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
  130. package/src/renderer/components/splash/splashScene.ts +121 -929
  131. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  132. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  133. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  134. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  135. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  136. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  137. package/src/renderer/components/team/TeamListView.tsx +108 -123
  138. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
  139. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  140. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  141. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  142. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
  143. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  144. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
  145. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
  146. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  147. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  148. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  149. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  150. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  151. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  152. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  153. package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
  154. package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
  155. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
  156. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  157. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  158. package/src/renderer/components/team/tools/SkillChip.tsx +2 -2
  159. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  160. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  161. package/src/renderer/index.css +39 -22
  162. package/src/renderer/index.html +17 -50
  163. package/src/renderer/store/index.ts +2 -1
  164. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  165. package/src/renderer/store/slices/teamSlice.ts +45 -168
  166. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  167. package/src/renderer/utils/memberHelpers.ts +5 -17
  168. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  169. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  170. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  171. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  172. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  173. package/src/shared/types/api.ts +121 -2
  174. package/src/shared/types/ccConnect.ts +2 -0
  175. package/src/shared/types/index.ts +3 -0
  176. package/src/shared/types/systemManager.ts +49 -0
  177. package/src/shared/types/team.ts +29 -0
  178. package/src/shared/types/terminal.ts +4 -2
  179. package/src/shared/utils/extensionNormalizers.ts +15 -8
  180. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  181. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  182. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  183. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  184. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  185. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  186. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  187. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  188. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -0,0 +1,163 @@
1
+ /**
2
+ * FolderBrowser — directory browser dialog for the console path input.
3
+ * Extracted from ProjectPathSelector so the SystemManagerView can reuse it.
4
+ */
5
+ import React, { useState } from 'react';
6
+
7
+ import { api } from '@renderer/api';
8
+ import { Button } from '@renderer/components/ui/button';
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from '@renderer/components/ui/dialog';
16
+ import { Input } from '@renderer/components/ui/input';
17
+ import { Check, ChevronLeft, ChevronRight, Folder, FolderOpen, Loader2 } from 'lucide-react';
18
+
19
+ interface FolderBrowserProps {
20
+ value: string;
21
+ onChange: (path: string) => void;
22
+ }
23
+
24
+ export const FolderBrowser = ({ value, onChange }: FolderBrowserProps): React.JSX.Element => {
25
+ const [open, setOpen] = useState(false);
26
+ const [currentPath, setCurrentPath] = useState(value || '');
27
+ const [dirs, setDirs] = useState<string[]>([]);
28
+ const [loading, setLoading] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
30
+
31
+ const browse = async (dirPath: string) => {
32
+ setLoading(true);
33
+ setError(null);
34
+ try {
35
+ const result = await api.config.browseFolders(dirPath || undefined);
36
+ setCurrentPath(result.path);
37
+ setDirs(result.dirs);
38
+ } catch (err) {
39
+ setError(err instanceof Error ? err.message : '无法访问目录');
40
+ setDirs([]);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ const handleOpen = () => {
47
+ setOpen(true);
48
+ void browse(value || '');
49
+ };
50
+
51
+ const handleSelect = (dir: string) => {
52
+ onChange(dir);
53
+ setOpen(false);
54
+ };
55
+
56
+ const handleConfirm = () => {
57
+ if (currentPath) {
58
+ onChange(currentPath);
59
+ }
60
+ setOpen(false);
61
+ };
62
+
63
+ const handleNavigateUp = () => {
64
+ if (currentPath) {
65
+ const parent = currentPath.split('/').slice(0, -1).join('/') || '/';
66
+ void browse(parent);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <>
72
+ <Button
73
+ size="sm"
74
+ variant="outline"
75
+ className="h-8 shrink-0 border-[var(--color-border)]"
76
+ onClick={handleOpen}
77
+ title="浏览目录"
78
+ >
79
+ <FolderOpen size={13} />
80
+ </Button>
81
+
82
+ <Dialog
83
+ open={open}
84
+ onOpenChange={(o) => {
85
+ if (!o) setOpen(false);
86
+ }}
87
+ >
88
+ <DialogContent className="max-w-md">
89
+ <DialogHeader>
90
+ <DialogTitle>选择工作目录</DialogTitle>
91
+ </DialogHeader>
92
+
93
+ {/* Path breadcrumb + manual input */}
94
+ <div className="flex items-center gap-1 text-xs text-[var(--color-text-muted)]">
95
+ <button
96
+ type="button"
97
+ className="shrink-0 hover:text-[var(--color-text)]"
98
+ onClick={handleNavigateUp}
99
+ disabled={!currentPath || currentPath === '/'}
100
+ >
101
+ <ChevronLeft size={14} />
102
+ </button>
103
+ <Input
104
+ className="h-7 flex-1 font-mono text-xs"
105
+ value={currentPath}
106
+ onChange={(e) => setCurrentPath(e.target.value)}
107
+ onKeyDown={(e) => {
108
+ if (e.key === 'Enter') void browse(currentPath);
109
+ }}
110
+ placeholder="/path/to/directory"
111
+ />
112
+ </div>
113
+
114
+ {/* Directory list */}
115
+ <div className="max-h-64 overflow-auto rounded-md border border-[var(--color-border)]">
116
+ {loading && (
117
+ <div className="flex items-center justify-center gap-2 py-6 text-xs text-[var(--color-text-muted)]">
118
+ <Loader2 className="size-4 animate-spin" />
119
+ 加载中…
120
+ </div>
121
+ )}
122
+ {error && <div className="px-3 py-4 text-xs text-red-400">{error}</div>}
123
+ {!loading && !error && dirs.length === 0 && (
124
+ <div className="px-3 py-4 text-xs text-[var(--color-text-muted)]">
125
+ 此目录下没有子目录。可手动输入路径后按 Enter。
126
+ </div>
127
+ )}
128
+ {!loading && !error && dirs.length > 0 && (
129
+ <ul className="divide-y divide-[var(--color-border)]">
130
+ {dirs.map((dir) => (
131
+ <li key={dir}>
132
+ <button
133
+ type="button"
134
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs hover:bg-[var(--color-surface-raised)]"
135
+ onClick={() => void browse(dir)}
136
+ onDoubleClick={() => handleSelect(dir)}
137
+ >
138
+ <Folder size={14} className="shrink-0 text-[var(--color-text-muted)]" />
139
+ <span className="truncate">{dir}</span>
140
+ <ChevronRight
141
+ size={14}
142
+ className="ml-auto shrink-0 text-[var(--color-text-muted)]"
143
+ />
144
+ </button>
145
+ </li>
146
+ ))}
147
+ </ul>
148
+ )}
149
+ </div>
150
+
151
+ <DialogFooter>
152
+ <Button variant="outline" onClick={() => setOpen(false)}>
153
+ 取消
154
+ </Button>
155
+ <Button onClick={handleConfirm} disabled={!currentPath}>
156
+ 选择此目录
157
+ </Button>
158
+ </DialogFooter>
159
+ </DialogContent>
160
+ </Dialog>
161
+ </>
162
+ );
163
+ };
@@ -0,0 +1,351 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import '@xterm/xterm/css/xterm.css';
4
+
5
+ import { FitAddon } from '@xterm/addon-fit';
6
+ import { WebLinksAddon } from '@xterm/addon-web-links';
7
+ import { Terminal } from '@xterm/xterm';
8
+ import { api } from '@renderer/api';
9
+ import { useStore } from '@renderer/store';
10
+ import { Button } from '@renderer/components/ui/button';
11
+ import { Input } from '@renderer/components/ui/input';
12
+ import { SYSTEM_MANAGER_DISPLAY_NAME } from '@shared/types/team';
13
+ import type {
14
+ SystemManagerConfig,
15
+ SystemManagerStatus,
16
+ WorkflowPromptSummary,
17
+ } from '@shared/types/systemManager';
18
+ import { Loader2, RefreshCw, TerminalSquare } from 'lucide-react';
19
+
20
+ import { FolderBrowser } from './FolderBrowser';
21
+
22
+ interface SystemManagerViewProps {
23
+ isPaneFocused?: boolean;
24
+ }
25
+
26
+ function formatPathForTitle(pathValue: string): string {
27
+ const home = typeof process !== 'undefined' ? process.env.HOME : undefined;
28
+ if (home && pathValue.startsWith(home)) return `~${pathValue.slice(home.length)}`;
29
+ return pathValue;
30
+ }
31
+
32
+ function joinPath(basePath: string, childPath: string): string {
33
+ const trimmedBase = basePath.replace(/[\\/]+$/, '');
34
+ return `${trimmedBase}/${childPath}`;
35
+ }
36
+
37
+ export const SystemManagerView = ({
38
+ isPaneFocused: _isPaneFocused = false,
39
+ }: SystemManagerViewProps): React.JSX.Element => {
40
+ const terminalHostRef = useRef<HTMLDivElement | null>(null);
41
+ const terminalRef = useRef<Terminal | null>(null);
42
+ const fitAddonRef = useRef<FitAddon | null>(null);
43
+ const ptyIdRef = useRef<string | null>(null);
44
+ const autoStartedRef = useRef(false);
45
+ const startClaudeRef = useRef<((workDirOverride?: string) => Promise<void>) | null>(null);
46
+
47
+ const [status, setStatus] = useState<SystemManagerStatus | null>(null);
48
+ const [config, setConfig] = useState<SystemManagerConfig | null>(null);
49
+ const [workDirInput, setWorkDirInput] = useState('');
50
+ const [workflowPrompts, setWorkflowPrompts] = useState<WorkflowPromptSummary[]>([]);
51
+ const [warnings, setWarnings] = useState<string[]>([]);
52
+ const [loading, setLoading] = useState(true);
53
+ const [starting, setStarting] = useState(false);
54
+ const [running, setRunning] = useState(false);
55
+ const [error, setError] = useState<string | null>(null);
56
+ const fetchTeams = useStore((state) => state.fetchTeams);
57
+
58
+ const writeTerminalLine = useCallback((line: string) => {
59
+ terminalRef.current?.writeln(`\x1b[90m${line}\x1b[0m`);
60
+ }, []);
61
+
62
+ const fitTerminal = useCallback(() => {
63
+ try {
64
+ fitAddonRef.current?.fit();
65
+ if (ptyIdRef.current && terminalRef.current) {
66
+ api.terminal.resize(ptyIdRef.current, terminalRef.current.cols, terminalRef.current.rows);
67
+ }
68
+ } catch {
69
+ // xterm fit can throw when the element is not measurable yet.
70
+ }
71
+ }, []);
72
+
73
+ useEffect(() => {
74
+ const host = terminalHostRef.current;
75
+ if (!host) return;
76
+
77
+ const term = new Terminal({
78
+ cursorBlink: true,
79
+ convertEol: true,
80
+ fontFamily: 'JetBrains Mono, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
81
+ fontSize: 13,
82
+ lineHeight: 1.28,
83
+ theme: {
84
+ background: 'var(--color-surface)',
85
+ foreground: 'var(--color-text)',
86
+ cursor: 'var(--color-text)',
87
+ selectionBackground: 'var(--color-border-emphasis)',
88
+ black: 'var(--color-surface-sidebar)',
89
+ red: '#f87171',
90
+ green: '#86efac',
91
+ yellow: '#fde68a',
92
+ blue: 'var(--color-accent)',
93
+ magenta: '#d8b4fe',
94
+ cyan: '#67e8f9',
95
+ white: 'var(--color-text)',
96
+ },
97
+ });
98
+ const fitAddon = new FitAddon();
99
+ term.loadAddon(fitAddon);
100
+ term.loadAddon(new WebLinksAddon());
101
+ term.open(host);
102
+ terminalRef.current = term;
103
+ fitAddonRef.current = fitAddon;
104
+ fitTerminal();
105
+ term.writeln('\x1b[90m# Hermit 控制台 · 本地 Claude Code PTY\x1b[0m');
106
+ term.writeln('\x1b[90m# 点击 Start Claude 等价于在当前目录运行 claude\x1b[0m');
107
+ term.writeln('');
108
+
109
+ const dataDispose = api.terminal.onData((_event, ptyId, data) => {
110
+ if (ptyId === ptyIdRef.current) term.write(data);
111
+ });
112
+ const exitDispose = api.terminal.onExit((_event, ptyId, exitCode) => {
113
+ if (ptyId === ptyIdRef.current) {
114
+ setRunning(false);
115
+ ptyIdRef.current = null;
116
+ term.writeln(`\r\n\x1b[90m[claude exited with code ${exitCode}]\x1b[0m`);
117
+ }
118
+ });
119
+ const inputDispose = term.onData((data) => {
120
+ if (ptyIdRef.current) api.terminal.write(ptyIdRef.current, data);
121
+ });
122
+ const resizeObserver = new ResizeObserver(() => fitTerminal());
123
+ resizeObserver.observe(host);
124
+
125
+ return () => {
126
+ dataDispose();
127
+ exitDispose();
128
+ inputDispose.dispose();
129
+ resizeObserver.disconnect();
130
+ if (ptyIdRef.current) {
131
+ void api.terminal.kill(ptyIdRef.current).catch(() => {
132
+ // Component is unmounting; there is no safe UI surface for this lifecycle error.
133
+ });
134
+ }
135
+ term.dispose();
136
+ terminalRef.current = null;
137
+ fitAddonRef.current = null;
138
+ ptyIdRef.current = null;
139
+ };
140
+ }, [fitTerminal]);
141
+
142
+ const load = useCallback(async (): Promise<SystemManagerConfig | null> => {
143
+ setLoading(true);
144
+ setError(null);
145
+ try {
146
+ const [nextStatus, nextConfig] = await Promise.all([
147
+ api.systemManager.getStatus(),
148
+ api.systemManager.getConfig(),
149
+ ]);
150
+ setStatus(nextStatus);
151
+ setConfig(nextConfig);
152
+ setWorkDirInput(nextConfig.selectedWorkDir);
153
+ const candidateFolders = [
154
+ nextConfig.workflowFolder,
155
+ joinPath(nextConfig.selectedWorkDir, 'workflows'),
156
+ ].filter((folder): folder is string => Boolean(folder));
157
+ let loadedWorkflow = false;
158
+ for (const folder of candidateFolders) {
159
+ try {
160
+ const workflowResult = await api.systemManager.listWorkflowPrompts(folder);
161
+ setConfig((current) =>
162
+ current ? { ...current, workflowFolder: workflowResult.folder } : current
163
+ );
164
+ setWorkflowPrompts(workflowResult.prompts);
165
+ setWarnings(workflowResult.warnings);
166
+ loadedWorkflow = true;
167
+ break;
168
+ } catch {
169
+ // Common commands are optional; missing folders should not interrupt opening the console.
170
+ }
171
+ }
172
+ if (!loadedWorkflow) {
173
+ setWorkflowPrompts([]);
174
+ setWarnings([]);
175
+ }
176
+ return nextConfig;
177
+ } catch (err) {
178
+ setError(err instanceof Error ? err.message : String(err));
179
+ return null;
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ }, []);
184
+
185
+ const stopClaude = useCallback(async (): Promise<boolean> => {
186
+ const ptyId = ptyIdRef.current;
187
+ if (!ptyId) {
188
+ setRunning(false);
189
+ return true;
190
+ }
191
+
192
+ try {
193
+ await api.terminal.kill(ptyId);
194
+ if (ptyIdRef.current === ptyId) {
195
+ ptyIdRef.current = null;
196
+ setRunning(false);
197
+ writeTerminalLine('[stopped]');
198
+ }
199
+ return true;
200
+ } catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ setError(message);
203
+ writeTerminalLine(`[failed to stop claude] ${message}`);
204
+ return false;
205
+ }
206
+ }, [writeTerminalLine]);
207
+
208
+ const startClaude = useCallback(async (workDirOverride?: string) => {
209
+ if (starting) return;
210
+ setStarting(true);
211
+ setError(null);
212
+ try {
213
+ const stopped = await stopClaude();
214
+ if (!stopped) return;
215
+ const nextConfig = await api.systemManager.updateConfig({
216
+ selectedWorkDir: workDirOverride ?? workDirInput,
217
+ });
218
+ setConfig(nextConfig);
219
+ void fetchTeams();
220
+ terminalRef.current?.clear();
221
+ writeTerminalLine(`# cd ${nextConfig.selectedWorkDir}`);
222
+ writeTerminalLine('$ claude');
223
+ const ptyId = await api.terminal.spawn({
224
+ cwd: nextConfig.selectedWorkDir,
225
+ cols: terminalRef.current?.cols ?? 120,
226
+ rows: terminalRef.current?.rows ?? 34,
227
+ });
228
+ ptyIdRef.current = ptyId;
229
+ setRunning(true);
230
+ terminalRef.current?.focus();
231
+ } catch (err) {
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ setError(message);
234
+ writeTerminalLine(`[failed to start claude] ${message}`);
235
+ } finally {
236
+ setStarting(false);
237
+ }
238
+ }, [fetchTeams, starting, stopClaude, workDirInput, writeTerminalLine]);
239
+
240
+ useEffect(() => {
241
+ startClaudeRef.current = startClaude;
242
+ }, [startClaude]);
243
+
244
+ useEffect(() => {
245
+ void load().then((nextConfig) => {
246
+ if (nextConfig && !autoStartedRef.current) {
247
+ autoStartedRef.current = true;
248
+ void startClaudeRef.current?.(nextConfig.selectedWorkDir);
249
+ }
250
+ });
251
+ }, [load]);
252
+
253
+ const refreshConsole = useCallback(async () => {
254
+ await load();
255
+ await startClaude(workDirInput || undefined);
256
+ }, [load, startClaude, workDirInput]);
257
+
258
+ const runWorkflowPrompt = useCallback(
259
+ async (prompt: WorkflowPromptSummary) => {
260
+ if (!config?.workflowFolder) return;
261
+ if (!ptyIdRef.current) {
262
+ await startClaude();
263
+ }
264
+ const ptyId = ptyIdRef.current;
265
+ if (!ptyId) return;
266
+ const result = await api.systemManager.readWorkflowPrompt(config.workflowFolder, prompt.id);
267
+ writeTerminalLine(`$ # workflow: ${prompt.label}`);
268
+ api.terminal.write(ptyId, `${result.content}\r`);
269
+ },
270
+ [config?.workflowFolder, startClaude, writeTerminalLine]
271
+ );
272
+
273
+ const titlePath = useMemo(
274
+ () =>
275
+ formatPathForTitle(config?.selectedWorkDir ?? (workDirInput || status?.defaultWorkDir || '')),
276
+ [config?.selectedWorkDir, status?.defaultWorkDir, workDirInput]
277
+ );
278
+
279
+ return (
280
+ <div className="flex size-full flex-col bg-[var(--color-surface)] p-4 text-[var(--color-text)]">
281
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] shadow-2xl shadow-black/20">
282
+ <div className="flex min-h-12 items-center gap-3 border-b border-[var(--color-border)] bg-[var(--color-surface-raised)] px-4 py-2">
283
+ <div className="flex shrink-0 items-center gap-2">
284
+ <span className="size-3 rounded-full bg-[#ff5f57]" />
285
+ <span className="size-3 rounded-full bg-[#febc2e]" />
286
+ <span className="size-3 rounded-full bg-[#28c840]" />
287
+ </div>
288
+ <div className="flex shrink-0 items-center gap-2 font-mono text-xs text-[var(--color-text-secondary)]">
289
+ <TerminalSquare size={14} className="text-[var(--color-text-muted)]" />
290
+ {SYSTEM_MANAGER_DISPLAY_NAME}
291
+ </div>
292
+ <Input
293
+ value={workDirInput}
294
+ onChange={(event) => setWorkDirInput(event.target.value)}
295
+ onKeyDown={(e) => { if (e.key === 'Enter') void refreshConsole(); }}
296
+ className="h-8 min-w-[220px] flex-1 border-[var(--color-border)] bg-[var(--color-surface)] font-mono text-xs text-[var(--color-text)]"
297
+ placeholder={titlePath || '工作目录'}
298
+ />
299
+ <FolderBrowser value={workDirInput} onChange={setWorkDirInput} />
300
+ <div className="shrink-0 text-[11px] text-[var(--color-text-muted)]">
301
+ {running ? 'claude running' : (status?.localStatus ?? 'starting')}
302
+ </div>
303
+ <Button
304
+ size="sm"
305
+ variant="outline"
306
+ className="h-8 shrink-0 border-[var(--color-border)]"
307
+ disabled={starting}
308
+ onClick={() => void refreshConsole()}
309
+ >
310
+ {starting ? <Loader2 size={13} className="animate-spin" /> : <RefreshCw size={13} />}
311
+ 刷新
312
+ </Button>
313
+ </div>
314
+
315
+ <div className="min-h-0 flex-1 bg-[var(--color-surface)] p-2">
316
+ <div
317
+ ref={terminalHostRef}
318
+ className="size-full overflow-hidden rounded-lg bg-[var(--color-surface)]"
319
+ />
320
+ </div>
321
+
322
+ {(workflowPrompts.length || warnings.length || error || loading) && (
323
+ <div className="border-t border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3">
324
+ {workflowPrompts.length ? (
325
+ <div className="flex flex-wrap gap-2">
326
+ {workflowPrompts.map((prompt) => (
327
+ <button
328
+ key={prompt.id}
329
+ type="button"
330
+ className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-3 py-1 font-mono text-[11px] text-[var(--color-text-secondary)] hover:border-[var(--color-border-emphasis)] hover:text-[var(--color-text)]"
331
+ disabled={starting}
332
+ onClick={() => void runWorkflowPrompt(prompt)}
333
+ >
334
+ {prompt.label}
335
+ </button>
336
+ ))}
337
+ </div>
338
+ ) : null}
339
+ {warnings.length ? (
340
+ <div className="mt-2 text-xs text-amber-300">{warnings.join(';')}</div>
341
+ ) : null}
342
+ {error ? <div className="mt-2 text-xs text-red-300">{error}</div> : null}
343
+ {loading ? (
344
+ <div className="mt-2 text-xs text-[var(--color-text-muted)]">加载控制台配置中...</div>
345
+ ) : null}
346
+ </div>
347
+ )}
348
+ </div>
349
+ </div>
350
+ );
351
+ };