@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,219 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { addDays, format, isToday } from 'date-fns';
3
+
4
+ import { cn } from '@renderer/lib/utils';
5
+
6
+ import { CalendarEventBlock } from './CalendarEventBlock';
7
+ import type { CalendarOccurrence } from './types';
8
+
9
+ // =============================================================================
10
+ // Constants
11
+ // =============================================================================
12
+
13
+ const HOUR_PX = 48;
14
+ const GUTTER = 40;
15
+
16
+ const DAY_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
17
+
18
+ // =============================================================================
19
+ // CalendarWeekView
20
+ // =============================================================================
21
+
22
+ interface CalendarWeekViewProps {
23
+ occurrences: CalendarOccurrence[];
24
+ weekStart: Date;
25
+ onEventClick: (occurrence: CalendarOccurrence) => void;
26
+ onSlotClick?: (date: Date) => void;
27
+ }
28
+
29
+ export const CalendarWeekView = React.memo(function CalendarWeekView({
30
+ occurrences,
31
+ weekStart,
32
+ onEventClick,
33
+ onSlotClick,
34
+ }: CalendarWeekViewProps): React.JSX.Element {
35
+ const scrollRef = useRef<HTMLDivElement>(null);
36
+ const [now, setNow] = useState(() => Date.now());
37
+
38
+ useEffect(() => {
39
+ const t = setInterval(() => setNow(Date.now()), 60_000);
40
+ return () => clearInterval(t);
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ if (!scrollRef.current) return;
45
+ const h = new Date().getHours();
46
+ scrollRef.current.scrollTop = Math.max(0, (h - 2) * HOUR_PX);
47
+ }, []);
48
+
49
+ const days = useMemo(() => Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)), [weekStart]);
50
+
51
+ const byDay = useMemo(() => {
52
+ const m = new Map<number, CalendarOccurrence[]>();
53
+ for (const o of occurrences) {
54
+ const idx = dayDiff(o.date, weekStart);
55
+ if (idx < 0 || idx > 6) continue;
56
+ (m.get(idx) ?? m.set(idx, []).get(idx)!).push(o);
57
+ }
58
+ return m;
59
+ }, [occurrences, weekStart]);
60
+
61
+ const nowDate = new Date(now);
62
+ const nowY = ((nowDate.getHours() * 60 + nowDate.getMinutes()) / 1440) * 24 * HOUR_PX;
63
+ const todayIdx = dayDiff(nowDate, weekStart);
64
+
65
+ const handleSlotClick = useCallback(
66
+ (di: number, h: number) => {
67
+ if (!onSlotClick) return;
68
+ const d = addDays(weekStart, di);
69
+ d.setHours(h, 0, 0, 0);
70
+ onSlotClick(d);
71
+ },
72
+ [onSlotClick, weekStart],
73
+ );
74
+
75
+ const gridH = 24 * HOUR_PX;
76
+
77
+ return (
78
+ <div className="select-none">
79
+ {/* ── Day headers ── */}
80
+ <div className="flex border-b border-[var(--color-border-subtle)]" style={{ paddingLeft: GUTTER }}>
81
+ {days.map((day, i) => {
82
+ const today = isToday(day);
83
+ return (
84
+ <div key={i} className={cn('flex flex-1 items-center justify-center gap-1 py-1.5')}>
85
+ <span className={cn('text-[11px]', today ? 'text-[var(--color-accent)]' : 'text-[var(--color-text-muted)]')}>
86
+ {DAY_LABELS[i]}
87
+ </span>
88
+ <span
89
+ className={cn(
90
+ 'inline-flex size-5 items-center justify-center rounded-full text-[11px] font-semibold',
91
+ today ? 'bg-[var(--color-accent)] text-white' : 'text-[var(--color-text)]',
92
+ )}
93
+ >
94
+ {format(day, 'd')}
95
+ </span>
96
+ </div>
97
+ );
98
+ })}
99
+ </div>
100
+
101
+ {/* ── Grid body ── */}
102
+ <div ref={scrollRef} className="overflow-y-auto" style={{ maxHeight: 400 }}>
103
+ <div className="relative flex" style={{ height: gridH }}>
104
+ {/* Time gutter — only render even hours */}
105
+ <div className="relative shrink-0" style={{ width: GUTTER }}>
106
+ {Array.from({ length: 12 }, (_, i) => i * 2).map((h) => (
107
+ <div
108
+ key={h}
109
+ className="absolute right-0 flex items-start justify-end pr-1.5"
110
+ style={{ top: h * HOUR_PX, width: GUTTER }}
111
+ >
112
+ <span className="-translate-y-[6px] text-[10px] tabular-nums text-[var(--color-text-muted)]">
113
+ {String(h).padStart(2, '0')}:00
114
+ </span>
115
+ </div>
116
+ ))}
117
+ </div>
118
+
119
+ {/* 7 day columns */}
120
+ {days.map((day, di) => {
121
+ const events = byDay.get(di) ?? [];
122
+ const today = isToday(day);
123
+
124
+ return (
125
+ <div
126
+ key={di}
127
+ className={cn(
128
+ 'relative flex-1',
129
+ di > 0 && 'border-l border-[var(--color-border-subtle)]/40',
130
+ today && 'bg-[var(--color-accent)]/[0.015]',
131
+ )}
132
+ >
133
+ {/* Hour lines — very subtle, only even hours */}
134
+ {Array.from({ length: 12 }, (_, i) => i * 2).map((h) => (
135
+ <div
136
+ key={h}
137
+ className="absolute left-0 right-0 border-t"
138
+ style={{
139
+ top: h * HOUR_PX,
140
+ borderColor: 'var(--color-border-subtle)',
141
+ opacity: 0.3,
142
+ }}
143
+ />
144
+ ))}
145
+
146
+ {/* Events */}
147
+ {events.map((occ) => {
148
+ const topPx = ((occ.hour * 60 + occ.minute) / 1440) * gridH;
149
+ const hPx = Math.max((occ.durationMinutes / 1440) * gridH, 22);
150
+ const wPct = 100 / occ.totalColumns;
151
+ const lPct = occ.column * wPct;
152
+
153
+ return (
154
+ <CalendarEventBlock
155
+ key={`${occ.scheduleId}-${occ.date.toISOString()}`}
156
+ occurrence={occ}
157
+ variant="week"
158
+ className="absolute z-10"
159
+ style={{
160
+ top: topPx,
161
+ height: hPx,
162
+ width: `calc(${wPct}% - 2px)`,
163
+ left: `calc(${lPct}% + 1px)`,
164
+ }}
165
+ onClick={() => onEventClick(occ)}
166
+ />
167
+ );
168
+ })}
169
+
170
+ {/* Clickable slots */}
171
+ {onSlotClick &&
172
+ Array.from({ length: 24 }, (_, h) => (
173
+ <div
174
+ key={`s-${h}`}
175
+ className="absolute left-0 right-0 cursor-pointer hover:bg-white/[0.01]"
176
+ style={{ top: h * HOUR_PX, height: HOUR_PX }}
177
+ onClick={() => handleSlotClick(di, h)}
178
+ />
179
+ ))}
180
+ </div>
181
+ );
182
+ })}
183
+
184
+ {/* Current time line */}
185
+ {todayIdx >= 0 && todayIdx < 7 && (
186
+ <div
187
+ className="pointer-events-none absolute z-30"
188
+ style={{
189
+ top: nowY,
190
+ left: GUTTER,
191
+ right: 0,
192
+ }}
193
+ >
194
+ <div className="relative flex" style={{ height: 0 }}>
195
+ {/* Position within the correct day column */}
196
+ <div
197
+ className="absolute flex items-center"
198
+ style={{
199
+ left: `${(todayIdx / 7) * 100}%`,
200
+ width: `${100 / 7}%`,
201
+ }}
202
+ >
203
+ <span className="size-2 shrink-0 rounded-full bg-red-500" />
204
+ <span className="h-[1.5px] flex-1 bg-red-500" />
205
+ </div>
206
+ </div>
207
+ </div>
208
+ )}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ );
213
+ });
214
+
215
+ function dayDiff(d: Date, base: Date): number {
216
+ const a = new Date(d); a.setHours(0,0,0,0);
217
+ const b = new Date(base); b.setHours(0,0,0,0);
218
+ return Math.round((a.getTime() - b.getTime()) / 86_400_000);
219
+ }
@@ -0,0 +1,41 @@
1
+ import React, { useCallback } from 'react';
2
+
3
+ import type { Schedule } from '@shared/types';
4
+
5
+ import { TeamGanttView } from './TeamGanttView';
6
+ import type { CalendarViewMode } from './types';
7
+
8
+ interface ScheduleCalendarBoardProps {
9
+ schedules: Schedule[];
10
+ viewMode: CalendarViewMode;
11
+ onViewModeChange: (mode: CalendarViewMode) => void;
12
+ onEdit: (schedule: Schedule) => void;
13
+ onTeamClick: (teamName: string) => void;
14
+ getTeamColor: (teamName: string) => string;
15
+ getTeamDisplayName: (teamName: string) => string;
16
+ }
17
+
18
+ function ScheduleCalendarBoardInner({
19
+ schedules,
20
+ onEdit,
21
+ getTeamColor,
22
+ getTeamDisplayName,
23
+ }: ScheduleCalendarBoardProps) {
24
+ const handleEdit = useCallback(
25
+ (schedule: Schedule) => {
26
+ onEdit(schedule);
27
+ },
28
+ [onEdit],
29
+ );
30
+
31
+ return (
32
+ <TeamGanttView
33
+ schedules={schedules}
34
+ getTeamColor={getTeamColor}
35
+ getTeamDisplayName={getTeamDisplayName}
36
+ onEdit={handleEdit}
37
+ />
38
+ );
39
+ }
40
+
41
+ export const ScheduleCalendarBoard = React.memo(ScheduleCalendarBoardInner);
@@ -0,0 +1,405 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { addDays, addHours, format, isToday, isSameDay } from 'date-fns';
3
+ import { Cron } from 'croner';
4
+
5
+ import { cn } from '@renderer/lib/utils';
6
+ import { ChevronDown, ChevronRight } from 'lucide-react';
7
+
8
+ import { getCronDescription } from '@renderer/utils/scheduleFormatters';
9
+ import type { Schedule, ScheduleStatus } from '@shared/types';
10
+
11
+ // =============================================================================
12
+ // Config
13
+ // =============================================================================
14
+
15
+ type TimeRange = '6h' | '24h' | '3d' | '7d';
16
+
17
+ const RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
18
+ { value: '6h', label: '6h' },
19
+ { value: '24h', label: '24h' },
20
+ { value: '3d', label: '3d' },
21
+ { value: '7d', label: '7d' },
22
+ ];
23
+
24
+ const RANGE_MS: Record<TimeRange, number> = {
25
+ '6h': 6 * 3600_000,
26
+ '24h': 24 * 3600_000,
27
+ '3d': 3 * 24 * 3600_000,
28
+ '7d': 7 * 24 * 3600_000,
29
+ };
30
+
31
+ const MAX_EXPANDED_ITEMS = 5;
32
+ const MAX_CRON_HITS = 200;
33
+
34
+ // Left column width for team/schedule labels
35
+ const LEFT_W = '140px';
36
+ // Minimum width for the timeline area so it doesn't cramp
37
+ const TIMELINE_MIN_W = '500px';
38
+
39
+ // =============================================================================
40
+ // Types
41
+ // =============================================================================
42
+
43
+ interface ScheduleHit {
44
+ schedule: Schedule;
45
+ date: Date;
46
+ }
47
+
48
+ interface TeamGroup {
49
+ teamName: string;
50
+ displayName: string;
51
+ color: string;
52
+ schedules: Schedule[];
53
+ hits: ScheduleHit[];
54
+ }
55
+
56
+ // =============================================================================
57
+ // TeamGanttView
58
+ // =============================================================================
59
+
60
+ interface TeamGanttViewProps {
61
+ schedules: Schedule[];
62
+ getTeamColor: (teamName: string) => string;
63
+ getTeamDisplayName: (teamName: string) => string;
64
+ onEdit: (schedule: Schedule) => void;
65
+ }
66
+
67
+ export const TeamGanttView = React.memo(function TeamGanttView({
68
+ schedules,
69
+ getTeamColor,
70
+ getTeamDisplayName,
71
+ onEdit,
72
+ }: TeamGanttViewProps): React.JSX.Element {
73
+ const [timeRange, setTimeRange] = useState<TimeRange>('24h');
74
+ const [expandedTeams, setExpandedTeams] = useState<Set<string>>(new Set());
75
+
76
+ const now = Date.now();
77
+ const rangeMs = RANGE_MS[timeRange];
78
+ const rangeEnd = now + rangeMs;
79
+
80
+ // Tick labels for the time axis
81
+ const ticks = useMemo(() => {
82
+ let intervalMs: number;
83
+ switch (timeRange) {
84
+ case '6h': intervalMs = 3600_000; break;
85
+ case '24h': intervalMs = 3 * 3600_000; break;
86
+ case '3d': intervalMs = 12 * 3600_000; break;
87
+ case '7d': intervalMs = 24 * 3600_000; break;
88
+ default: intervalMs = 3600_000; break;
89
+ }
90
+ const result: { time: number; pct: number }[] = [];
91
+ const start = Math.floor(now / intervalMs) * intervalMs;
92
+ for (let t = start; t <= now + rangeMs; t += intervalMs) {
93
+ if (t < now) continue;
94
+ const pct = ((t - now) / rangeMs) * 100;
95
+ if (pct > 100) break;
96
+ result.push({ time: t, pct });
97
+ }
98
+ return result;
99
+ }, [now, rangeMs, timeRange]);
100
+
101
+ // Group schedules by team and compute cron hits
102
+ const teams = useMemo(() => {
103
+ const groupMap = new Map<string, TeamGroup>();
104
+ for (const schedule of schedules) {
105
+ if (schedule.status === 'disabled') continue;
106
+ const key = schedule.teamName;
107
+ if (!groupMap.has(key)) {
108
+ groupMap.set(key, {
109
+ teamName: key,
110
+ displayName: getTeamDisplayName(key),
111
+ color: getTeamColor(key),
112
+ schedules: [],
113
+ hits: [],
114
+ });
115
+ }
116
+ const group = groupMap.get(key)!;
117
+ group.schedules.push(schedule);
118
+ const hits = enumerateHits(schedule, now, rangeEnd);
119
+ group.hits.push(...hits);
120
+ }
121
+ return [...groupMap.values()].sort((a, b) => a.displayName.localeCompare(b.displayName));
122
+ }, [schedules, now, rangeEnd, getTeamColor, getTeamDisplayName]);
123
+
124
+ // Per-schedule hits for expanded rows
125
+ const scheduleHits = useMemo(() => {
126
+ const map = new Map<string, ScheduleHit[]>();
127
+ for (const team of teams) {
128
+ for (const schedule of team.schedules) {
129
+ map.set(schedule.id, enumerateHits(schedule, now, rangeEnd));
130
+ }
131
+ }
132
+ return map;
133
+ }, [teams, now, rangeEnd]);
134
+
135
+ const toggleTeam = useCallback((teamName: string) => {
136
+ setExpandedTeams((prev) => {
137
+ const next = new Set(prev);
138
+ if (next.has(teamName)) next.delete(teamName);
139
+ else next.add(teamName);
140
+ return next;
141
+ });
142
+ }, []);
143
+
144
+ return (
145
+ <div className="rounded-xl border border-[var(--color-border)]">
146
+ {/* Header */}
147
+ <div className="flex items-center justify-between border-b border-[var(--color-border)] px-3 py-2">
148
+ <span className="text-xs text-[var(--color-text-muted)]">
149
+ {teams.length} 个计划
150
+ </span>
151
+ <div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
152
+ {RANGE_OPTIONS.map((opt) => (
153
+ <button
154
+ key={opt.value}
155
+ type="button"
156
+ className={cn(
157
+ 'rounded-sm px-2 py-0.5 text-[10px] transition-colors',
158
+ timeRange === opt.value
159
+ ? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
160
+ : 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
161
+ )}
162
+ onClick={() => setTimeRange(opt.value)}
163
+ >
164
+ {opt.label}
165
+ </button>
166
+ ))}
167
+ </div>
168
+ </div>
169
+
170
+ {/* Time axis — aligned to grid, horizontally scrollable */}
171
+ <div className="overflow-x-auto border-b border-[var(--color-border)]">
172
+ <div className="grid" style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}>
173
+ <div className="px-3 py-1.5 text-[10px] text-[var(--color-text-muted)] opacity-50">
174
+ 团队
175
+ </div>
176
+ <div className="relative h-6">
177
+ {ticks.map((tick, i) => (
178
+ <span
179
+ key={i}
180
+ className="absolute top-0.5 text-[10px] tabular-nums text-[var(--color-text-muted)]"
181
+ style={{
182
+ left: `${tick.pct}%`,
183
+ transform: 'translateX(-50%)',
184
+ whiteSpace: 'nowrap',
185
+ }}
186
+ >
187
+ {formatTick(tick.time, timeRange)}
188
+ </span>
189
+ ))}
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Team rows — scrollable horizontally */}
195
+ {teams.length === 0 ? (
196
+ <div className="flex items-center justify-center py-8 text-xs text-[var(--color-text-muted)]">
197
+ 当前范围内没有计划任务
198
+ </div>
199
+ ) : (
200
+ <div className="overflow-x-auto">
201
+ {teams.map((team) => {
202
+ const expanded = expandedTeams.has(team.teamName);
203
+ const hitCount = team.hits.length;
204
+ const showSchedules = team.schedules.slice(0, MAX_EXPANDED_ITEMS);
205
+ const hiddenCount = team.schedules.length - MAX_EXPANDED_ITEMS;
206
+
207
+ return (
208
+ <div key={team.teamName} className="group/team border-b border-[var(--color-border)] last:border-b-0">
209
+ {/* Team row (collapsed) — grid aligned */}
210
+ <div
211
+ className="grid items-center hover:bg-white/[0.02] transition-colors"
212
+ style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}
213
+ >
214
+ {/* Left: team info */}
215
+ <button
216
+ type="button"
217
+ className="flex min-w-0 items-center gap-2 px-3 py-2.5 text-left"
218
+ onClick={() => toggleTeam(team.teamName)}
219
+ >
220
+ <span className="text-[var(--color-text-muted)]">
221
+ {expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
222
+ </span>
223
+ <span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: team.color }} />
224
+ <span className="truncate text-xs font-medium text-[var(--color-text)]">
225
+ {team.displayName}
226
+ </span>
227
+ </button>
228
+
229
+ {/* Right: mini timeline */}
230
+ <div className="relative h-10">
231
+ {/* Grid lines */}
232
+ {ticks.map((tick, i) => (
233
+ <div
234
+ key={i}
235
+ className="absolute bottom-0 top-0 w-px bg-[var(--color-border)] opacity-30"
236
+ style={{ left: `${tick.pct}%` }}
237
+ />
238
+ ))}
239
+ {/* Now marker */}
240
+ <div className="absolute bottom-0 top-0 w-px bg-red-500/50" style={{ left: '0%' }} />
241
+
242
+ {/* Fallback: show schedule names when no hits in range */}
243
+ {hitCount === 0 && team.schedules.map((schedule) => (
244
+ <button
245
+ key={schedule.id}
246
+ type="button"
247
+ className="absolute top-1/2 z-10 -translate-y-1/2 rounded-sm border border-[var(--color-border)] px-2 py-0.5 text-left transition-colors hover:bg-white/[0.04]"
248
+ style={{ left: '8px', transform: 'translateY(-50%)' }}
249
+ onClick={(e) => { e.stopPropagation(); onEdit(schedule); }}
250
+ >
251
+ <span className="text-[10px] font-medium text-[var(--color-text-secondary)]">
252
+ {schedule.label || getCronDescription(schedule.cronExpression)}
253
+ </span>
254
+ <span className="ml-2 text-[9px] text-[var(--color-text-muted)]">
255
+ {schedule.nextRunAt
256
+ ? `下次 ${format(new Date(schedule.nextRunAt), 'M/d HH:mm')}`
257
+ : '无计划'}
258
+ </span>
259
+ </button>
260
+ ))}
261
+
262
+ {/* Hits as labeled pills */}
263
+ {team.hits.map((hit, i) => {
264
+ const pct = ((hit.date.getTime() - now) / rangeMs) * 100;
265
+ if (pct < 0 || pct > 100) return null;
266
+ const isActive = hit.schedule.status === 'active';
267
+ return (
268
+ <button
269
+ key={`${hit.schedule.id}-${i}`}
270
+ type="button"
271
+ className={cn(
272
+ 'absolute top-1/2 z-10 -translate-y-1/2 rounded-sm px-1.5 py-0.5 text-left transition-transform hover:scale-105 hover:z-20',
273
+ isActive && 'opacity-100',
274
+ !isActive && 'opacity-40',
275
+ )}
276
+ style={{
277
+ left: `${pct}%`,
278
+ backgroundColor: team.color,
279
+ maxWidth: 120,
280
+ transform: `translate(-50%, -50%)`,
281
+ }}
282
+ onClick={(e) => { e.stopPropagation(); onEdit(hit.schedule); }}
283
+ title={`${hit.schedule.label || getCronDescription(hit.schedule.cronExpression)}\n${hit.date.toLocaleString('zh-CN')}`}
284
+ >
285
+ <span className="block truncate text-[9px] font-medium leading-tight text-white">
286
+ {format(hit.date, 'HH:mm')}
287
+ </span>
288
+ </button>
289
+ );
290
+ })}
291
+ </div>
292
+ </div>
293
+
294
+ {/* Expanded: per-schedule rows */}
295
+ {expanded && (
296
+ <div className="bg-[var(--color-surface-raised)]/30">
297
+ {showSchedules.map((schedule) => {
298
+ const hits = scheduleHits.get(schedule.id) ?? [];
299
+ const statusLabel = schedule.status === 'active' ? '运行中' : schedule.status === 'paused' ? '已暂停' : schedule.status;
300
+
301
+ return (
302
+ <div
303
+ key={schedule.id}
304
+ className="grid items-center hover:bg-white/[0.02] transition-colors"
305
+ style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}
306
+ >
307
+ {/* Left: schedule info */}
308
+ <button
309
+ type="button"
310
+ className="flex min-w-0 items-center gap-2 pl-7 pr-3 py-2 text-left"
311
+ onClick={() => onEdit(schedule)}
312
+ >
313
+ <span className="truncate text-[11px] text-[var(--color-text-secondary)]">
314
+ {schedule.label || getCronDescription(schedule.cronExpression)}
315
+ </span>
316
+ <span className={cn(
317
+ 'shrink-0 rounded-sm px-1 py-px text-[9px]',
318
+ schedule.status === 'active' && 'bg-emerald-500/15 text-emerald-400',
319
+ schedule.status === 'paused' && 'bg-amber-500/15 text-amber-400',
320
+ )}>
321
+ {statusLabel}
322
+ </span>
323
+ </button>
324
+
325
+ {/* Right: schedule timeline */}
326
+ <div className="relative h-7">
327
+ {ticks.map((tick, i) => (
328
+ <div
329
+ key={i}
330
+ className="absolute bottom-0 top-0 w-px bg-[var(--color-border)] opacity-20"
331
+ style={{ left: `${tick.pct}%` }}
332
+ />
333
+ ))}
334
+ <div className="absolute bottom-0 top-0 w-px bg-red-500/30" style={{ left: '0%' }} />
335
+
336
+ {hits.map((hit, i) => {
337
+ const pct = ((hit.date.getTime() - now) / rangeMs) * 100;
338
+ if (pct < 0 || pct > 100) return null;
339
+ return (
340
+ <span
341
+ key={i}
342
+ className="absolute top-1/2 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full"
343
+ style={{ left: `${pct}%`, backgroundColor: team.color }}
344
+ title={hit.date.toLocaleString('zh-CN')}
345
+ />
346
+ );
347
+ })}
348
+
349
+ {/* Next run label */}
350
+ {schedule.nextRunAt && (
351
+ <span
352
+ className="absolute top-1/2 right-2 -translate-y-1/2 text-[9px] tabular-nums text-[var(--color-text-muted)]"
353
+ >
354
+ 下次 {format(new Date(schedule.nextRunAt), 'HH:mm')}
355
+ </span>
356
+ )}
357
+ </div>
358
+ </div>
359
+ );
360
+ })}
361
+ {hiddenCount > 0 && (
362
+ <div className="pl-7 pr-3 py-1.5 text-[10px] text-[var(--color-text-muted)]">
363
+ +{hiddenCount} 更多
364
+ </div>
365
+ )}
366
+ </div>
367
+ )}
368
+ </div>
369
+ );
370
+ })}
371
+ </div>
372
+ )}
373
+ </div>
374
+ );
375
+ });
376
+
377
+ // =============================================================================
378
+ // Helpers
379
+ // =============================================================================
380
+
381
+ function enumerateHits(schedule: Schedule, rangeStart: number, rangeEnd: number): ScheduleHit[] {
382
+ try {
383
+ const job = new Cron(schedule.cronExpression.trim(), { timezone: schedule.timezone, paused: true });
384
+ const raw = job.nextRuns(MAX_CRON_HITS, new Date(rangeStart));
385
+ const results: ScheduleHit[] = [];
386
+ for (const d of raw) {
387
+ const dt = d instanceof Date ? d : new Date(d);
388
+ if (dt.getTime() > rangeEnd) break;
389
+ if (dt.getTime() >= rangeStart) {
390
+ results.push({ schedule, date: dt });
391
+ }
392
+ }
393
+ return results;
394
+ } catch {
395
+ return [];
396
+ }
397
+ }
398
+
399
+ function formatTick(time: number, range: TimeRange): string {
400
+ const d = new Date(time);
401
+ if (range === '6h' || range === '24h') {
402
+ return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
403
+ }
404
+ return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
405
+ }