@yancyyu/openhermit 1.6.38 → 1.6.40

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 (243) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CemDOX-3.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-hPY770Db.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-BHHrJT1i.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CWErBtke.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-C_o2_Uv8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-DUW0LI3t.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-CWbCE9hQ.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-BjLadrfV.js} +1 -1
  9. package/dist-renderer/assets/channel-DyP9YlCF.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-CPnvjZl9.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-OlL47yXQ.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-DTasjbm8.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-C5_Xaqkk.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-NdoM4DMR.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-C8Fybejy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-E98TYFXJ.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-h4lFgkIq.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-BqffFTae.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BqffFTae.js +1 -0
  20. package/dist-renderer/assets/clone-MPcKWs2O.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-DtQ7fkrs.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-CN-nL_z4.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DVJtqmm-.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-DlxHxyXh.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-7dpzO6x6.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-GP1TqsHi.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-C7ZLETuH.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-CvPB68dH.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5yOm3w7.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-smeyY1YZ.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BJx8XvG1.js} +1 -1
  32. package/dist-renderer/assets/{index-C_okzZXP.js → index-CQaXUAua.js} +1 -1
  33. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-CajRpxO2.js} +1 -1
  34. package/dist-renderer/assets/{index-V7dAKPqd.js → index-ChG4rE-E.js} +587 -705
  35. package/dist-renderer/assets/index-DUd0uw9C.css +32 -0
  36. package/dist-renderer/assets/{index-CzWxVCRL.js → index-IhmXZWqf.js} +1 -1
  37. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-x_JkoDRH.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-D-hWRQGY.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-Bb6W8rUG.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-CnHdUX0q.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-pqss_zkI.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-B1mFITNh.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-DTD9q7-D.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-Df3mhrn7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-B1FZ09vH.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-aEO78thZ.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-6Ui--jp-.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-DF4Q1cAM.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-BqA2BI8C.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Cs2ZtUD2.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DoOkw_A8.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DUe26QdD.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-BKCnj5Xn.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/chat/ChatHistoryItem.tsx +1 -1
  88. package/src/renderer/components/chat/items/SubagentItem.tsx +2 -2
  89. package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +2 -2
  90. package/src/renderer/components/common/ErrorBoundary.tsx +1 -1
  91. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  92. package/src/renderer/components/dashboard/CliStatusBanner.tsx +7 -7
  93. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  94. package/src/renderer/components/extensions/ExtensionStoreView.tsx +7 -126
  95. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  96. package/src/renderer/components/extensions/common/ExtensionToast.tsx +3 -3
  97. package/src/renderer/components/extensions/common/SourceBadge.tsx +1 -1
  98. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  99. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  100. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  101. package/src/renderer/components/extensions/plugins/CategoryChips.tsx +1 -1
  102. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  103. package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +2 -2
  104. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  105. package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +1 -1
  106. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  107. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  108. package/src/renderer/components/layout/PaneResizeHandle.tsx +2 -2
  109. package/src/renderer/components/layout/Sidebar.tsx +13 -56
  110. package/src/renderer/components/layout/SortableTab.tsx +22 -33
  111. package/src/renderer/components/layout/TabBar.tsx +1 -1
  112. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  113. package/src/renderer/components/report/sections/CostSection.tsx +2 -2
  114. package/src/renderer/components/report/sections/InsightsSection.tsx +1 -1
  115. package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +2 -2
  116. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  117. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  118. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  119. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  120. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  121. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  122. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  123. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  124. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  125. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  126. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  127. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  128. package/src/renderer/components/search/CommandPalette.tsx +4 -4
  129. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  130. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  131. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  132. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  133. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  134. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  135. package/src/renderer/components/settings/sections/CliStatusSection.tsx +72 -113
  136. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  137. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  138. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  139. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  140. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  141. package/src/renderer/components/settings/sections/index.ts +0 -1
  142. package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +1 -1
  143. package/src/renderer/components/sidebar/SessionItem.tsx +3 -3
  144. package/src/renderer/components/sidebar/SidebarSessions.tsx +184 -6
  145. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  146. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +40 -5
  147. package/src/renderer/components/splash/splashScene.ts +121 -929
  148. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  149. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  150. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  151. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  152. package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +1 -1
  153. package/src/renderer/components/team/ClaudeLogsPanel.tsx +1 -1
  154. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  155. package/src/renderer/components/team/ProcessesSection.tsx +2 -2
  156. package/src/renderer/components/team/TaskTooltip.tsx +2 -2
  157. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  158. package/src/renderer/components/team/TeamListFilterPopover.tsx +1 -1
  159. package/src/renderer/components/team/TeamListView.tsx +109 -124
  160. package/src/renderer/components/team/TeamSessionsSection.tsx +6 -6
  161. package/src/renderer/components/team/UnreadCommentsBadge.tsx +1 -1
  162. package/src/renderer/components/team/activity/ActivityItem.tsx +9 -9
  163. package/src/renderer/components/team/activity/ActivityTimeline.tsx +5 -5
  164. package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +3 -3
  165. package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +4 -4
  166. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +4 -4
  167. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  168. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  169. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  170. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +18 -16
  171. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  172. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +8 -1
  173. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +5 -5
  174. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  175. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +6 -6
  176. package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +6 -6
  177. package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +1 -1
  178. package/src/renderer/components/team/dialogs/TaskAttachments.tsx +1 -1
  179. package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +6 -6
  180. package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +4 -4
  181. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +3 -3
  182. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  183. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  184. package/src/renderer/components/team/editor/EditorFileTree.tsx +4 -4
  185. package/src/renderer/components/team/editor/EditorSearchPanel.tsx +1 -1
  186. package/src/renderer/components/team/editor/MarkdownSplitView.tsx +1 -1
  187. package/src/renderer/components/team/editor/NewFileDialog.tsx +1 -1
  188. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +1 -1
  189. package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +1 -1
  190. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  191. package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +4 -4
  192. package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +1 -1
  193. package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +5 -5
  194. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +4 -4
  195. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  196. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  197. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  198. package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
  199. package/src/renderer/components/team/members/MemberStatsTab.tsx +2 -2
  200. package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +1 -1
  201. package/src/renderer/components/team/messages/MessageComposer.tsx +10 -112
  202. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +1 -1
  203. package/src/renderer/components/team/messages/MessagesPanel.tsx +136 -119
  204. package/src/renderer/components/team/review/ChangeReviewDialog.tsx +1 -1
  205. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +3 -3
  206. package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +4 -4
  207. package/src/renderer/components/team/tasks/TaskRow.tsx +1 -1
  208. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  209. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  210. package/src/renderer/components/team/tools/SkillChip.tsx +3 -3
  211. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  212. package/src/renderer/components/ui/MemberSelect.tsx +2 -2
  213. package/src/renderer/components/ui/MentionSuggestionList.tsx +2 -2
  214. package/src/renderer/components/ui/MentionableTextarea.tsx +3 -3
  215. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  216. package/src/renderer/index.css +56 -39
  217. package/src/renderer/index.html +17 -50
  218. package/src/renderer/store/index.ts +2 -1
  219. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  220. package/src/renderer/store/slices/teamSlice.ts +45 -168
  221. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  222. package/src/renderer/utils/memberHelpers.ts +5 -17
  223. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  224. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  225. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  226. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  227. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  228. package/src/shared/types/api.ts +121 -2
  229. package/src/shared/types/ccConnect.ts +2 -0
  230. package/src/shared/types/index.ts +3 -0
  231. package/src/shared/types/systemManager.ts +49 -0
  232. package/src/shared/types/team.ts +29 -0
  233. package/src/shared/types/terminal.ts +4 -2
  234. package/src/shared/utils/extensionNormalizers.ts +15 -8
  235. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  236. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  237. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  238. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  239. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  240. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  241. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  242. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  243. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -1,4 +1,9 @@
1
- import { PARTICIPANT_AVATAR_URLS } from '@renderer/utils/memberAvatarCatalog';
1
+ /**
2
+ * splashScene — Terminal-style loading splash.
3
+ *
4
+ * Replaces the heavy canvas robot animation with a clean,
5
+ * Yume-inspired terminal boot sequence.
6
+ */
2
7
 
3
8
  export interface SplashSceneHandle {
4
9
  stop: () => void;
@@ -16,964 +21,151 @@ declare global {
16
21
  }
17
22
  }
18
23
 
19
- interface Point {
20
- x: number;
21
- y: number;
22
- }
23
-
24
- interface RobotNode extends Point {
25
- teamIndex: number;
26
- robotIndex: number;
27
- color: string;
28
- size: number;
29
- bob: number;
30
- receivePulse: number;
31
- avatarUrl: string;
32
- }
33
-
34
- interface TeamNode {
35
- index: number;
36
- center: Point;
37
- color: string;
38
- radius: number;
39
- robots: RobotNode[];
40
- }
41
-
42
- interface MessageFlightState {
43
- progress: number;
44
- motionSpeed: number;
45
- bubbleScale: number;
46
- bubbleAlpha: number;
47
- }
48
-
49
- interface DepthParticle {
50
- x: number;
51
- y: number;
52
- size: number;
53
- speed: number;
54
- phase: number;
55
- alpha: number;
56
- }
57
-
58
- interface Palette {
59
- isLight: boolean;
60
- centerGlow: string;
61
- teamColors: string[];
62
- robotBody: string;
63
- robotShade: string;
64
- robotEye: string;
65
- messageAccent: string;
66
- particle: string;
67
- }
68
-
69
- const TAU = Math.PI * 2;
70
- const TEAM_MEMBER_COUNTS = [4, 3, 5] as const;
71
- const TEAM_MEMBER_OFFSETS = [0, 4, 7] as const;
72
- const TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'] as const;
73
- const MAX_DPR = 2;
74
- const avatarCache = new Map<string, HTMLImageElement>();
75
- const avatarLoading = new Map<string, Promise<HTMLImageElement | null>>();
24
+ const BOOT_LINES = [
25
+ '🦀 hermit v1.6.38',
26
+ 'connecting harness…',
27
+ 'loading team configs…',
28
+ 'scanning session history…',
29
+ 'indexing project files…',
30
+ 'ready.',
31
+ ];
76
32
 
77
33
  export function startSplashScene(
78
34
  splash: HTMLElement,
79
35
  options: SplashSceneOptions = {}
80
36
  ): SplashSceneHandle {
81
37
  const existingScene = window.__claudeTeamsSplashScene;
82
- if (existingScene && splash.querySelector('#splash-enhanced-canvas')) {
38
+ if (existingScene && splash.querySelector('#splash-terminal')) {
83
39
  return existingScene;
84
40
  }
85
41
 
86
- const ready = preloadAvatarImages();
87
- const previousCanvas = splash.querySelector<HTMLCanvasElement>('#splash-enhanced-canvas');
88
- previousCanvas?.remove();
89
-
90
- const canvas = document.createElement('canvas');
91
- canvas.id = 'splash-enhanced-canvas';
92
- canvas.setAttribute('aria-hidden', 'true');
93
- splash.appendChild(canvas);
94
-
95
- const ctx = canvas.getContext('2d', { alpha: true });
96
- if (!ctx) {
97
- const emptyHandle = {
98
- stop: () => {
99
- canvas.remove();
100
- },
101
- ready,
102
- };
103
- return emptyHandle;
104
- }
105
-
106
42
  const reducedMotion =
107
43
  options.reducedMotion ?? window.matchMedia('(prefers-reduced-motion: reduce)').matches;
108
- const state = {
109
- width: 1,
110
- height: 1,
111
- dpr: 1,
112
- particles: [] as DepthParticle[],
113
- running: true,
114
- frameId: 0,
115
- startedAt: performance.now(),
116
- };
117
44
 
118
- const resize = (): void => {
119
- const rect = splash.getBoundingClientRect();
120
- const width = Math.max(1, Math.round(rect.width));
121
- const height = Math.max(1, Math.round(rect.height));
122
- const dpr = Math.min(MAX_DPR, window.devicePixelRatio || 1);
123
-
124
- if (state.width === width && state.height === height && state.dpr === dpr) {
125
- return;
45
+ // Remove old canvas if present
46
+ splash.querySelector('#splash-enhanced-canvas')?.remove();
47
+
48
+ // Hide the original HTML splash content (logo, text, tagline)
49
+ const splashCopy = splash.querySelector('#splash-copy');
50
+ if (splashCopy instanceof HTMLElement) {
51
+ splashCopy.style.display = 'none';
52
+ }
53
+
54
+ const container = document.createElement('div');
55
+ container.id = 'splash-terminal';
56
+ container.style.cssText = `
57
+ position: absolute;
58
+ inset: 0;
59
+ display: flex;
60
+ flex-direction: column;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 8px;
64
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
65
+ font-size: 13px;
66
+ line-height: 1.8;
67
+ color: rgba(148, 163, 184, 0.8);
68
+ padding: 24px;
69
+ opacity: 0;
70
+ z-index: 5;
71
+ animation: splash-term-in 0.4s ease-out 0.1s forwards;
72
+ `;
73
+
74
+ // Boot lines appear one by one
75
+ const linesContainer = document.createElement('div');
76
+ linesContainer.style.cssText = 'text-align: left; width: 100%; max-width: 320px;';
77
+ container.appendChild(linesContainer);
78
+
79
+ // Cursor
80
+ const cursor = document.createElement('span');
81
+ cursor.textContent = '█';
82
+ cursor.style.cssText = reducedMotion
83
+ ? 'opacity: 0.6;'
84
+ : 'animation: splash-cursor-blink 1s step-end infinite;';
85
+ cursor.setAttribute('aria-hidden', 'true');
86
+
87
+ splash.appendChild(container);
88
+
89
+ let running = true;
90
+ let lineIndex = 0;
91
+
92
+ function addNextLine(): void {
93
+ if (!running || lineIndex >= BOOT_LINES.length) return;
94
+
95
+ const line = BOOT_LINES[lineIndex];
96
+ if (!line) return;
97
+
98
+ const lineEl = document.createElement('div');
99
+ lineEl.style.cssText = 'opacity: 0; transform: translateY(4px); transition: opacity 0.3s, transform 0.3s;';
100
+
101
+ if (lineIndex === 0) {
102
+ // First line — brand with crab
103
+ const brand = document.createElement('span');
104
+ brand.textContent = '> ';
105
+ brand.style.color = 'rgba(148, 163, 184, 0.4)';
106
+ lineEl.appendChild(brand);
107
+
108
+ const cmd = document.createElement('span');
109
+ cmd.textContent = line;
110
+ cmd.style.color = 'rgba(226, 232, 240, 0.9)';
111
+ cmd.style.fontWeight = '500';
112
+ lineEl.appendChild(cmd);
113
+ } else if (line === 'ready.') {
114
+ const ready = document.createElement('span');
115
+ ready.textContent = '✓ ';
116
+ ready.style.color = 'rgba(52, 211, 153, 0.7)';
117
+ lineEl.appendChild(ready);
118
+
119
+ const text = document.createElement('span');
120
+ text.textContent = line;
121
+ text.style.color = 'rgba(52, 211, 153, 0.6)';
122
+ lineEl.appendChild(text);
123
+ } else {
124
+ const arrow = document.createElement('span');
125
+ arrow.textContent = ' ';
126
+ lineEl.appendChild(arrow);
127
+
128
+ const text = document.createElement('span');
129
+ text.textContent = line;
130
+ lineEl.appendChild(text);
126
131
  }
127
132
 
128
- state.width = width;
129
- state.height = height;
130
- state.dpr = dpr;
131
- canvas.width = Math.ceil(width * dpr);
132
- canvas.height = Math.ceil(height * dpr);
133
- canvas.style.width = `${width}px`;
134
- canvas.style.height = `${height}px`;
135
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
136
- state.particles = createDepthParticles(width, height);
137
- };
133
+ linesContainer.appendChild(lineEl);
138
134
 
139
- const render = (now: number): void => {
140
- if (!state.running) return;
135
+ // Animate in
136
+ requestAnimationFrame(() => {
137
+ lineEl.style.opacity = '1';
138
+ lineEl.style.transform = 'translateY(0)';
139
+ });
141
140
 
142
- resize();
143
- const time = (now - state.startedAt) / 1000;
144
- drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion);
141
+ lineIndex++;
145
142
 
146
- if (!reducedMotion) {
147
- state.frameId = window.requestAnimationFrame(render);
143
+ if (lineIndex < BOOT_LINES.length) {
144
+ setTimeout(addNextLine, reducedMotion ? 80 : 280);
145
+ } else {
146
+ // Append cursor after last line
147
+ const lastLine = linesContainer.lastElementChild;
148
+ if (lastLine) {
149
+ lastLine.appendChild(cursor);
150
+ }
148
151
  }
149
- };
152
+ }
150
153
 
151
- const onResize = (): void => resize();
152
- window.addEventListener('resize', onResize);
153
- resize();
154
- render(performance.now());
154
+ // Start boot sequence after a brief delay
155
+ setTimeout(addNextLine, reducedMotion ? 50 : 120);
155
156
 
156
157
  const handle: SplashSceneHandle = {
157
158
  stop: () => {
158
- state.running = false;
159
- window.cancelAnimationFrame(state.frameId);
160
- window.removeEventListener('resize', onResize);
161
- canvas.remove();
159
+ running = false;
160
+ container.remove();
162
161
  if (window.__claudeTeamsSplashScene === handle) {
163
162
  window.__claudeTeamsSplashScene = undefined;
164
163
  window.__claudeTeamsSplashEnhancedStartedAt = undefined;
165
164
  }
166
165
  },
167
- ready,
168
166
  };
169
167
  window.__claudeTeamsSplashScene = handle;
170
168
  window.__claudeTeamsSplashEnhancedStartedAt = performance.now();
171
169
 
172
170
  return handle;
173
171
  }
174
-
175
- function drawScene(
176
- ctx: CanvasRenderingContext2D,
177
- width: number,
178
- height: number,
179
- time: number,
180
- particles: DepthParticle[],
181
- reducedMotion: boolean
182
- ): void {
183
- ctx.clearRect(0, 0, width, height);
184
- const palette = resolvePalette();
185
- const mobile = width < 560 || height < 620;
186
- const sceneTime = reducedMotion ? 1.2 : time;
187
- const teams = buildTeams(width, height, sceneTime, mobile, palette);
188
- const center = getCenter(width, height, mobile);
189
-
190
- drawAmbientField(ctx, width, height, sceneTime, particles, palette, mobile);
191
- drawCenterAura(ctx, center, sceneTime, palette, mobile);
192
- drawCrossTeamGuides(ctx, teams, sceneTime, palette);
193
-
194
- for (const team of teams) {
195
- drawTeamHalo(ctx, team, sceneTime, palette);
196
- }
197
-
198
- drawMessages(ctx, teams, sceneTime, palette, mobile);
199
-
200
- for (const team of teams) {
201
- for (const robot of team.robots) {
202
- drawRobot(ctx, robot, sceneTime, palette);
203
- }
204
- }
205
-
206
- for (const team of teams) {
207
- drawTeamLabel(ctx, team, palette, mobile);
208
- }
209
- }
210
-
211
- function resolvePalette(): Palette {
212
- const isLight = document.documentElement.classList.contains('light');
213
- return isLight
214
- ? {
215
- isLight,
216
- centerGlow: '#4f46e5',
217
- teamColors: ['#0369a1', '#047857', '#b45309'],
218
- robotBody: '#eef2ff',
219
- robotShade: '#dbe4ff',
220
- robotEye: '#ffffff',
221
- messageAccent: '#7c3aed',
222
- particle: '#312e81',
223
- }
224
- : {
225
- isLight,
226
- centerGlow: '#7c83f7',
227
- teamColors: ['#24a8d8', '#23b488', '#d58a19'],
228
- robotBody: '#0f1724',
229
- robotShade: '#1a2438',
230
- robotEye: '#d8f3ff',
231
- messageAccent: '#8b5cf6',
232
- particle: '#a6a4d6',
233
- };
234
- }
235
-
236
- function getCenter(width: number, height: number, mobile: boolean): Point {
237
- return {
238
- x: width / 2,
239
- y: height * (mobile ? 0.47 : 0.49),
240
- };
241
- }
242
-
243
- function buildTeams(
244
- width: number,
245
- height: number,
246
- time: number,
247
- mobile: boolean,
248
- palette: Palette
249
- ): TeamNode[] {
250
- const center = getCenter(width, height, mobile);
251
- const spreadX = mobile ? Math.min(width * 0.36, 148) : Math.min(width * 0.34, 380);
252
- const spreadY = mobile ? Math.min(height * 0.22, 154) : Math.min(height * 0.22, 220);
253
- const teamRadius = mobile
254
- ? clamp(Math.min(width, height) * 0.092, 31, 42)
255
- : clamp(Math.min(width, height) * 0.072, 42, 62);
256
- const robotSize = mobile ? 9.8 : 11.8;
257
- const centers: Point[] = [
258
- {
259
- x: center.x - spreadX,
260
- y: center.y - spreadY * (mobile ? 0.66 : 0.58),
261
- },
262
- {
263
- x: center.x + spreadX,
264
- y: center.y - spreadY * (mobile ? 0.66 : 0.58),
265
- },
266
- {
267
- x: center.x,
268
- y: center.y + spreadY * (mobile ? 1.34 : 1.18),
269
- },
270
- ];
271
-
272
- return centers.map((teamCenter, teamIndex) => {
273
- const drift = Math.sin(time * 0.75 + teamIndex * 1.7) * (mobile ? 2.2 : 4.2);
274
- const centerWithDrift = {
275
- x: teamCenter.x + Math.cos(teamIndex * 2.1 + time * 0.35) * (mobile ? 1.4 : 2.8),
276
- y: teamCenter.y + drift,
277
- };
278
- const color = palette.teamColors[teamIndex % palette.teamColors.length] ?? palette.centerGlow;
279
- const memberCount = TEAM_MEMBER_COUNTS[teamIndex] ?? 3;
280
- const robots = Array.from({ length: memberCount }, (_, robotIndex) => {
281
- const baseAngle =
282
- -Math.PI / 2 + robotIndex * (TAU / memberCount) + (teamIndex === 2 ? TAU / 20 : 0);
283
- const orbit = baseAngle + Math.sin(time * 0.55 + teamIndex + robotIndex) * 0.07;
284
- const orbitRadius =
285
- teamRadius * (0.94 + (memberCount > 4 ? 0.07 : 0) + 0.03 * Math.sin(time + robotIndex));
286
- return {
287
- teamIndex,
288
- robotIndex,
289
- color,
290
- size: memberCount > 4 ? robotSize * 0.88 : robotSize,
291
- bob: Math.sin(time * 2.2 + teamIndex * 0.8 + robotIndex * 1.1),
292
- receivePulse: 0,
293
- avatarUrl:
294
- PARTICIPANT_AVATAR_URLS[(TEAM_MEMBER_OFFSETS[teamIndex] ?? 0) + robotIndex] ??
295
- PARTICIPANT_AVATAR_URLS[0],
296
- x: centerWithDrift.x + Math.cos(orbit) * orbitRadius,
297
- y: centerWithDrift.y + Math.sin(orbit) * orbitRadius,
298
- };
299
- });
300
-
301
- return {
302
- index: teamIndex,
303
- center: centerWithDrift,
304
- color,
305
- radius: teamRadius,
306
- robots,
307
- };
308
- });
309
- }
310
-
311
- function drawAmbientField(
312
- ctx: CanvasRenderingContext2D,
313
- width: number,
314
- height: number,
315
- time: number,
316
- particles: DepthParticle[],
317
- palette: Palette,
318
- mobile: boolean
319
- ): void {
320
- const visibleParticles = mobile ? Math.floor(particles.length * 0.6) : particles.length;
321
- for (let i = 0; i < visibleParticles; i++) {
322
- const particle = particles[i];
323
- if (!particle) continue;
324
- const y = (particle.y + time * particle.speed) % (height + 24);
325
- const x = particle.x + Math.sin(time * 0.45 + particle.phase) * 8;
326
- const pulse = 0.78 + Math.sin(time * 1.8 + particle.phase) * 0.22;
327
- ctx.beginPath();
328
- ctx.fillStyle = withAlpha(palette.particle, particle.alpha * pulse);
329
- ctx.arc(x, y - 12, particle.size, 0, TAU);
330
- ctx.fill();
331
- }
332
- }
333
-
334
- function drawCenterAura(
335
- ctx: CanvasRenderingContext2D,
336
- center: Point,
337
- time: number,
338
- palette: Palette,
339
- mobile: boolean
340
- ): void {
341
- const radius = mobile ? 86 : 128;
342
- const glow = ctx.createRadialGradient(center.x, center.y, 20, center.x, center.y, radius);
343
- glow.addColorStop(0, withAlpha(palette.centerGlow, palette.isLight ? 0.1 : 0.14));
344
- glow.addColorStop(0.48, withAlpha(palette.messageAccent, palette.isLight ? 0.04 : 0.07));
345
- glow.addColorStop(1, withAlpha(palette.centerGlow, 0));
346
- ctx.fillStyle = glow;
347
- ctx.beginPath();
348
- ctx.arc(center.x, center.y, radius, 0, TAU);
349
- ctx.fill();
350
-
351
- for (let i = 0; i < 3; i++) {
352
- const ringRadius = radius * (0.42 + i * 0.18) + Math.sin(time * 1.1 + i) * 3;
353
- ctx.beginPath();
354
- ctx.strokeStyle = withAlpha(palette.centerGlow, 0.07 - i * 0.014);
355
- ctx.lineWidth = 1;
356
- ctx.setLineDash([8 + i * 2, 12 + i * 3]);
357
- ctx.lineDashOffset = -time * (18 + i * 8);
358
- ctx.arc(center.x, center.y, ringRadius, 0, TAU);
359
- ctx.stroke();
360
- }
361
- ctx.setLineDash([]);
362
- }
363
-
364
- function drawCrossTeamGuides(
365
- ctx: CanvasRenderingContext2D,
366
- teams: TeamNode[],
367
- time: number,
368
- palette: Palette
369
- ): void {
370
- for (let i = 0; i < teams.length; i++) {
371
- const from = teams[i];
372
- const to = teams[(i + 1) % teams.length];
373
- if (!from || !to) continue;
374
- ctx.beginPath();
375
- ctx.moveTo(from.center.x, from.center.y);
376
- ctx.lineTo(to.center.x, to.center.y);
377
- ctx.strokeStyle = withAlpha(palette.messageAccent, palette.isLight ? 0.14 : 0.18);
378
- ctx.lineWidth = 1.05;
379
- ctx.setLineDash([7, 12]);
380
- ctx.lineDashOffset = -time * 34;
381
- ctx.stroke();
382
- }
383
- ctx.setLineDash([]);
384
- }
385
-
386
- function drawTeamHalo(
387
- ctx: CanvasRenderingContext2D,
388
- team: TeamNode,
389
- time: number,
390
- palette: Palette
391
- ): void {
392
- const pulse = 1 + Math.sin(time * 1.8 + team.index) * 0.035;
393
- const radiusX = team.radius * 1.56 * pulse;
394
- const radiusY = team.radius * 1.14 * pulse;
395
- const glow = ctx.createRadialGradient(
396
- team.center.x,
397
- team.center.y,
398
- team.radius * 0.35,
399
- team.center.x,
400
- team.center.y,
401
- team.radius * 2
402
- );
403
- glow.addColorStop(0, withAlpha(team.color, palette.isLight ? 0.045 : 0.065));
404
- glow.addColorStop(1, withAlpha(team.color, 0));
405
- ctx.fillStyle = glow;
406
- ctx.beginPath();
407
- ctx.ellipse(team.center.x, team.center.y, team.radius * 1.82, team.radius * 1.36, 0, 0, TAU);
408
- ctx.fill();
409
-
410
- ctx.beginPath();
411
- ctx.ellipse(team.center.x, team.center.y, radiusX, radiusY, time * 0.08, 0, TAU);
412
- ctx.strokeStyle = withAlpha(team.color, palette.isLight ? 0.2 : 0.24);
413
- ctx.lineWidth = 1;
414
- ctx.setLineDash([12, 10]);
415
- ctx.lineDashOffset = -time * (22 + team.index * 4);
416
- ctx.stroke();
417
- ctx.setLineDash([]);
418
- }
419
-
420
- function drawMessages(
421
- ctx: CanvasRenderingContext2D,
422
- teams: TeamNode[],
423
- time: number,
424
- palette: Palette,
425
- mobile: boolean
426
- ): void {
427
- for (const team of teams) {
428
- drawLocalMessages(ctx, team, time, palette, mobile);
429
- }
430
- drawCrossTeamMessages(ctx, teams, time, palette, mobile);
431
- }
432
-
433
- function drawLocalMessages(
434
- ctx: CanvasRenderingContext2D,
435
- team: TeamNode,
436
- time: number,
437
- palette: Palette,
438
- mobile: boolean
439
- ): void {
440
- const pairs = getLocalMessagePairs(team.index, team.robots.length);
441
- const activeWindow = 0.76;
442
- const period = 2.15 + team.index * 0.12;
443
-
444
- for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
445
- const [fromIndex, toIndex] = pairs[pairIndex] ?? [0, 1];
446
- const from = team.robots[fromIndex];
447
- const to = team.robots[toIndex];
448
- if (!from || !to) continue;
449
- const raw = positiveModulo(time + team.index * 0.7 + pairIndex * 0.36, period) / period;
450
- applyReceivePulse(to, getReceivePulse(raw, activeWindow));
451
- const flightState = getMessageFlightState(raw, activeWindow, 0.12);
452
- if (!flightState) continue;
453
- const curve = makeLocalCurve(from, to, team.center, team.radius * 0.42);
454
- drawMessageFlight(ctx, curve, flightState, team.color, mobile ? 4.6 : 5.8, palette);
455
- }
456
- }
457
-
458
- function drawCrossTeamMessages(
459
- ctx: CanvasRenderingContext2D,
460
- teams: TeamNode[],
461
- time: number,
462
- palette: Palette,
463
- mobile: boolean
464
- ): void {
465
- const activeWindow = 0.64;
466
- const period = 4.25;
467
- const routes = [
468
- { fromTeam: 0, fromRobot: 3, toTeam: 1, toRobot: 1, delay: 0 },
469
- { fromTeam: 1, fromRobot: 2, toTeam: 2, toRobot: 0, delay: 1.34, accent: true },
470
- { fromTeam: 2, fromRobot: 4, toTeam: 0, toRobot: 1, delay: 2.68 },
471
- ];
472
-
473
- for (const route of routes) {
474
- const fromTeam = teams[route.fromTeam];
475
- const toTeam = teams[route.toTeam];
476
- if (!fromTeam || !toTeam) continue;
477
- const raw = positiveModulo(time + route.delay, period) / period;
478
-
479
- const from = fromTeam.robots[route.fromRobot % fromTeam.robots.length];
480
- const to = toTeam.robots[route.toRobot % toTeam.robots.length];
481
- if (!from || !to) continue;
482
- applyReceivePulse(to, getReceivePulse(raw, activeWindow) * 0.88);
483
- const flightState = getMessageFlightState(raw, activeWindow, 0.1);
484
- if (!flightState) continue;
485
- const curve = makeStraightCurve(from, to);
486
- drawMessageFlight(
487
- ctx,
488
- curve,
489
- flightState,
490
- route.accent ? palette.messageAccent : fromTeam.color,
491
- mobile ? 5.2 : 6.8,
492
- palette,
493
- true
494
- );
495
- }
496
- }
497
-
498
- function drawMessageFlight(
499
- ctx: CanvasRenderingContext2D,
500
- curve: [Point, Point, Point, Point],
501
- state: MessageFlightState,
502
- color: string,
503
- size: number,
504
- palette: Palette,
505
- crossTeam = false
506
- ): void {
507
- const [p0, p1, p2, p3] = curve;
508
- ctx.save();
509
-
510
- const progress = state.progress;
511
- const speed = clamp(state.motionSpeed, 0, 1);
512
- if (speed > 0.045) {
513
- drawSpeedTrail(ctx, curve, progress, speed, color, size, palette, crossTeam);
514
- }
515
-
516
- const position = cubicPoint(p0, p1, p2, p3, progress);
517
- const tangent = cubicTangent(p0, p1, p2, p3, progress);
518
- const angle = Math.atan2(tangent.y, tangent.x);
519
- drawMessageBubble(
520
- ctx,
521
- position,
522
- angle,
523
- size,
524
- color,
525
- palette,
526
- crossTeam,
527
- state.bubbleScale,
528
- state.bubbleAlpha
529
- );
530
- ctx.restore();
531
- }
532
-
533
- function drawSpeedTrail(
534
- ctx: CanvasRenderingContext2D,
535
- curve: [Point, Point, Point, Point],
536
- progress: number,
537
- speed: number,
538
- color: string,
539
- size: number,
540
- palette: Palette,
541
- crossTeam: boolean
542
- ): void {
543
- const [p0, p1, p2, p3] = curve;
544
- const trailLength = (crossTeam ? 0.26 : 0.21) * (0.24 + speed * 1.08);
545
- const segmentCount = Math.round(9 + speed * 10);
546
- const alphaBase = (palette.isLight ? 0.22 : 0.32) * speed;
547
-
548
- ctx.save();
549
- ctx.lineCap = 'round';
550
- ctx.lineJoin = 'round';
551
- ctx.shadowColor = withAlpha(color, alphaBase * 0.58);
552
- ctx.shadowBlur = size * (0.78 + speed * 1.28);
553
-
554
- for (let segment = 0; segment < segmentCount; segment++) {
555
- const startRatio = segment / segmentCount;
556
- const endRatio = (segment + 1) / segmentCount;
557
- const t0 = progress - trailLength * (1 - startRatio);
558
- const t1 = progress - trailLength * (1 - endRatio);
559
- if (t1 <= 0) continue;
560
-
561
- const from = cubicPoint(p0, p1, p2, p3, Math.max(0, t0));
562
- const to = cubicPoint(p0, p1, p2, p3, Math.max(0, t1));
563
- const headWeight = endRatio * endRatio;
564
- const width = size * (0.12 + headWeight * 0.48) * (0.9 + speed * 0.45);
565
- const alpha = alphaBase * headWeight;
566
-
567
- ctx.beginPath();
568
- ctx.moveTo(from.x, from.y);
569
- ctx.lineTo(to.x, to.y);
570
- ctx.strokeStyle = withAlpha(color, alpha * 0.34);
571
- ctx.lineWidth = width * 2.35;
572
- ctx.stroke();
573
-
574
- ctx.beginPath();
575
- ctx.moveTo(from.x, from.y);
576
- ctx.lineTo(to.x, to.y);
577
- ctx.strokeStyle = withAlpha(color, alpha);
578
- ctx.lineWidth = width;
579
- ctx.stroke();
580
- }
581
-
582
- ctx.restore();
583
- }
584
-
585
- function getMessageFlightState(
586
- raw: number,
587
- activeWindow: number,
588
- settleWindow: number
589
- ): MessageFlightState | null {
590
- if (raw > activeWindow + settleWindow) return null;
591
-
592
- if (raw <= activeWindow) {
593
- const phase = raw / activeWindow;
594
- return {
595
- progress: easeInOutCubic(phase),
596
- motionSpeed: getEasedMotionSpeed(phase),
597
- bubbleScale: 1,
598
- bubbleAlpha: 1,
599
- };
600
- }
601
-
602
- const settlePhase = (raw - activeWindow) / settleWindow;
603
- const eased = easeOutCubic(settlePhase);
604
- return {
605
- progress: 1,
606
- motionSpeed: 0,
607
- bubbleScale: Math.max(0.12, 1 - eased * 0.88),
608
- bubbleAlpha: Math.max(0, 1 - eased),
609
- };
610
- }
611
-
612
- function applyReceivePulse(robot: RobotNode, pulse: number): void {
613
- robot.receivePulse = Math.max(robot.receivePulse, pulse);
614
- }
615
-
616
- function getReceivePulse(raw: number, activeWindow: number): number {
617
- const previousStart = activeWindow * 0.78;
618
- const previousEnd = Math.min(0.96, activeWindow + 0.11);
619
- const duration = (previousEnd - previousStart) / 3;
620
- const start = activeWindow - duration * 0.62;
621
- const end = activeWindow + duration * 0.38;
622
- if (raw < start || raw > end) return 0;
623
-
624
- const phase = (raw - start) / (end - start);
625
- return Math.sin(phase * Math.PI) * (1 - phase * 0.28);
626
- }
627
-
628
- function getEasedMotionSpeed(value: number): number {
629
- const t = clamp(value, 0, 1);
630
- const derivative = t < 0.5 ? 12 * t * t : 12 * (1 - t) * (1 - t);
631
- return clamp(derivative / 3, 0, 1);
632
- }
633
-
634
- function easeOutCubic(value: number): number {
635
- const t = clamp(value, 0, 1);
636
- return 1 - Math.pow(1 - t, 3);
637
- }
638
-
639
- function drawMessageBubble(
640
- ctx: CanvasRenderingContext2D,
641
- position: Point,
642
- angle: number,
643
- size: number,
644
- color: string,
645
- palette: Palette,
646
- crossTeam: boolean,
647
- scale = 1,
648
- alpha = 1
649
- ): void {
650
- if (scale <= 0.02 || alpha <= 0.01) return;
651
-
652
- ctx.save();
653
- ctx.translate(position.x, position.y);
654
- ctx.rotate(angle * 0.08);
655
- ctx.scale(scale, scale);
656
- ctx.globalAlpha = alpha;
657
- ctx.shadowColor = withAlpha(color, (palette.isLight ? 0.16 : 0.3) * alpha);
658
- ctx.shadowBlur = (crossTeam ? 12 : 8) * (0.5 + scale * 0.5);
659
-
660
- const width = size * (crossTeam ? 2.28 : 2.06);
661
- const height = size * 1.42;
662
- roundRectPath(ctx, -width / 2, -height / 2, width, height, size * 0.28);
663
- ctx.fillStyle = withAlpha(color, palette.isLight ? 0.82 : 0.9);
664
- ctx.fill();
665
-
666
- ctx.beginPath();
667
- ctx.moveTo(-width * 0.24, height * 0.42);
668
- ctx.lineTo(-width * 0.32, height * 0.68);
669
- ctx.lineTo(-width * 0.03, height * 0.42);
670
- ctx.closePath();
671
- ctx.fill();
672
-
673
- ctx.shadowBlur = 0;
674
- ctx.fillStyle = palette.robotEye;
675
- for (let i = -1; i <= 1; i++) {
676
- ctx.beginPath();
677
- ctx.arc(i * size * 0.4, -size * 0.02, size * 0.095, 0, TAU);
678
- ctx.fill();
679
- }
680
- ctx.restore();
681
- }
682
-
683
- function drawTeamLabel(
684
- ctx: CanvasRenderingContext2D,
685
- team: TeamNode,
686
- palette: Palette,
687
- mobile: boolean
688
- ): void {
689
- const label = TEAM_LABELS[team.index] ?? '';
690
- if (!label) return;
691
-
692
- const fontSize = mobile ? 7.5 : 8.5;
693
- const y = team.center.y + team.radius * (mobile ? 1.65 : 1.58);
694
- ctx.save();
695
- ctx.font = `600 ${fontSize}px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
696
- ctx.textAlign = 'center';
697
- ctx.textBaseline = 'middle';
698
-
699
- const metrics = ctx.measureText(label);
700
- const paddingX = mobile ? 4 : 5;
701
- const paddingY = mobile ? 2 : 2.5;
702
- const width = metrics.width + paddingX * 2;
703
- const height = fontSize + paddingY * 2;
704
- const x = team.center.x - width / 2;
705
- const rectY = y - height / 2;
706
-
707
- roundRectPath(ctx, x, rectY, width, height, height / 2);
708
- ctx.fillStyle = withAlpha(palette.isLight ? '#ffffff' : '#090a14', palette.isLight ? 0.36 : 0.24);
709
- ctx.fill();
710
- ctx.strokeStyle = withAlpha(team.color, palette.isLight ? 0.18 : 0.24);
711
- ctx.lineWidth = 0.75;
712
- ctx.stroke();
713
-
714
- ctx.shadowColor = withAlpha(team.color, palette.isLight ? 0.12 : 0.22);
715
- ctx.shadowBlur = mobile ? 4 : 6;
716
- ctx.fillStyle = withAlpha(palette.isLight ? '#3f3f46' : '#e4e4e7', palette.isLight ? 0.58 : 0.66);
717
- ctx.fillText(label, team.center.x, y + 0.2);
718
- ctx.restore();
719
- }
720
-
721
- function drawRobot(
722
- ctx: CanvasRenderingContext2D,
723
- robot: RobotNode,
724
- time: number,
725
- palette: Palette
726
- ): void {
727
- const size = robot.size;
728
- const x = robot.x;
729
- const y = robot.y + robot.bob * 0.9 - robot.receivePulse * size * 0.24;
730
- const tilt = Math.sin(time * 1.5 + robot.teamIndex + robot.robotIndex * 0.8) * 0.045;
731
- const img = getAvatarImage(robot.avatarUrl);
732
- const avatarSize = size * 2.65;
733
-
734
- ctx.save();
735
- ctx.translate(x, y);
736
- ctx.rotate(tilt);
737
- ctx.scale(1 + robot.receivePulse * 0.065, 1 + robot.receivePulse * 0.065);
738
- ctx.shadowColor = withAlpha(robot.color, palette.isLight ? 0.2 : 0.34);
739
- ctx.shadowBlur = size * (1.25 + robot.receivePulse * 0.72);
740
-
741
- if (img) {
742
- ctx.globalAlpha = palette.isLight ? 0.92 : 0.86;
743
- ctx.drawImage(img, -avatarSize / 2, -avatarSize / 2, avatarSize, avatarSize);
744
- ctx.globalAlpha = 1;
745
- } else {
746
- drawAvatarFallback(ctx, size, robot.color, palette);
747
- }
748
- ctx.restore();
749
- }
750
-
751
- function getAvatarImage(url: string): HTMLImageElement | null {
752
- const cached = avatarCache.get(url);
753
- if (cached) {
754
- avatarCache.delete(url);
755
- avatarCache.set(url, cached);
756
- return cached;
757
- }
758
-
759
- void loadAvatarImage(url);
760
- return null;
761
- }
762
-
763
- function preloadAvatarImages(): Promise<void> {
764
- return Promise.allSettled(PARTICIPANT_AVATAR_URLS.map((url) => loadAvatarImage(url))).then(
765
- () => undefined
766
- );
767
- }
768
-
769
- function loadAvatarImage(url: string): Promise<HTMLImageElement | null> {
770
- const cached = avatarCache.get(url);
771
- if (cached) return Promise.resolve(cached);
772
-
773
- const loading = avatarLoading.get(url);
774
- if (loading) return loading;
775
-
776
- const promise = new Promise<HTMLImageElement | null>((resolve) => {
777
- const img = new Image();
778
- img.decoding = 'async';
779
- img.onload = () => {
780
- const finish = (): void => {
781
- avatarCache.set(url, img);
782
- avatarLoading.delete(url);
783
- resolve(img);
784
- };
785
-
786
- if (typeof img.decode === 'function') {
787
- void img.decode().then(finish, finish);
788
- } else {
789
- finish();
790
- }
791
- };
792
- img.onerror = () => {
793
- avatarLoading.delete(url);
794
- resolve(null);
795
- };
796
- img.src = url;
797
- });
798
-
799
- avatarLoading.set(url, promise);
800
- return promise;
801
- }
802
-
803
- function drawAvatarFallback(
804
- ctx: CanvasRenderingContext2D,
805
- size: number,
806
- color: string,
807
- palette: Palette
808
- ): void {
809
- ctx.strokeStyle = withAlpha(color, palette.isLight ? 0.44 : 0.56);
810
- ctx.lineWidth = Math.max(1, size * 0.08);
811
- ctx.beginPath();
812
- ctx.moveTo(0, -size * 0.72);
813
- ctx.lineTo(0, -size * 1.0);
814
- ctx.stroke();
815
- ctx.fillStyle = withAlpha(color, palette.isLight ? 0.64 : 0.78);
816
- ctx.beginPath();
817
- ctx.arc(0, -size * 1.08, size * 0.13, 0, TAU);
818
- ctx.fill();
819
- ctx.fillStyle = palette.robotEye;
820
- ctx.beginPath();
821
- ctx.arc(-size * 0.24, -size * 0.13, size * 0.095, 0, TAU);
822
- ctx.arc(size * 0.24, -size * 0.13, size * 0.095, 0, TAU);
823
- ctx.fill();
824
- }
825
-
826
- function getLocalMessagePairs(teamIndex: number, memberCount: number): [number, number][] {
827
- const routeMap: [number, number][][] = [
828
- [
829
- [0, 2],
830
- [3, 1],
831
- [1, 0],
832
- ],
833
- [
834
- [2, 0],
835
- [0, 1],
836
- [1, 2],
837
- ],
838
- [
839
- [4, 1],
840
- [0, 3],
841
- [2, 4],
842
- [3, 0],
843
- ],
844
- ];
845
- return (routeMap[teamIndex] ?? routeMap[0]).filter(
846
- ([fromIndex, toIndex]) => fromIndex < memberCount && toIndex < memberCount
847
- );
848
- }
849
-
850
- function makeLocalCurve(
851
- from: Point,
852
- to: Point,
853
- center: Point,
854
- lift: number
855
- ): [Point, Point, Point, Point] {
856
- const mid = mix(from, to, 0.5);
857
- const away = normalize({ x: mid.x - center.x, y: mid.y - center.y });
858
- const control = {
859
- x: mid.x + away.x * lift,
860
- y: mid.y + away.y * lift,
861
- };
862
- return [from, mix(from, control, 0.72), mix(to, control, 0.72), to];
863
- }
864
-
865
- function makeStraightCurve(from: Point, to: Point): [Point, Point, Point, Point] {
866
- return [from, mix(from, to, 0.33), mix(from, to, 0.66), to];
867
- }
868
-
869
- function createDepthParticles(width: number, height: number): DepthParticle[] {
870
- const count = width < 560 ? 46 : 78;
871
- return Array.from({ length: count }, (_, index) => {
872
- const seed = index * 97.13;
873
- return {
874
- x: pseudoRandom(seed) * width,
875
- y: pseudoRandom(seed + 12.4) * (height + 24),
876
- size: 0.45 + pseudoRandom(seed + 22.8) * 1.15,
877
- speed: 8 + pseudoRandom(seed + 31.2) * 18,
878
- phase: pseudoRandom(seed + 48.7) * TAU,
879
- alpha: 0.06 + pseudoRandom(seed + 72.1) * 0.16,
880
- };
881
- });
882
- }
883
-
884
- function pseudoRandom(seed: number): number {
885
- const value = Math.sin(seed * 12.9898) * 43758.5453;
886
- return value - Math.floor(value);
887
- }
888
-
889
- function cubicPoint(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
890
- const clamped = clamp(t, 0, 1);
891
- const mt = 1 - clamped;
892
- const mt2 = mt * mt;
893
- const t2 = clamped * clamped;
894
- return {
895
- x: mt2 * mt * p0.x + 3 * mt2 * clamped * p1.x + 3 * mt * t2 * p2.x + t2 * clamped * p3.x,
896
- y: mt2 * mt * p0.y + 3 * mt2 * clamped * p1.y + 3 * mt * t2 * p2.y + t2 * clamped * p3.y,
897
- };
898
- }
899
-
900
- function cubicTangent(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
901
- const clamped = clamp(t, 0, 1);
902
- const mt = 1 - clamped;
903
- return {
904
- x:
905
- 3 * mt * mt * (p1.x - p0.x) +
906
- 6 * mt * clamped * (p2.x - p1.x) +
907
- 3 * clamped * clamped * (p3.x - p2.x),
908
- y:
909
- 3 * mt * mt * (p1.y - p0.y) +
910
- 6 * mt * clamped * (p2.y - p1.y) +
911
- 3 * clamped * clamped * (p3.y - p2.y),
912
- };
913
- }
914
-
915
- function mix(from: Point, to: Point, amount: number): Point {
916
- return {
917
- x: from.x + (to.x - from.x) * amount,
918
- y: from.y + (to.y - from.y) * amount,
919
- };
920
- }
921
-
922
- function normalize(point: Point): Point {
923
- const length = Math.hypot(point.x, point.y) || 1;
924
- return {
925
- x: point.x / length,
926
- y: point.y / length,
927
- };
928
- }
929
-
930
- function easeInOutCubic(value: number): number {
931
- const t = clamp(value, 0, 1);
932
- return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
933
- }
934
-
935
- function positiveModulo(value: number, divisor: number): number {
936
- return ((value % divisor) + divisor) % divisor;
937
- }
938
-
939
- function clamp(value: number, min: number, max: number): number {
940
- return Math.min(max, Math.max(min, value));
941
- }
942
-
943
- function roundRectPath(
944
- ctx: CanvasRenderingContext2D,
945
- x: number,
946
- y: number,
947
- width: number,
948
- height: number,
949
- radius: number
950
- ): void {
951
- const r = Math.min(radius, width / 2, height / 2);
952
- ctx.beginPath();
953
- ctx.moveTo(x + r, y);
954
- ctx.lineTo(x + width - r, y);
955
- ctx.quadraticCurveTo(x + width, y, x + width, y + r);
956
- ctx.lineTo(x + width, y + height - r);
957
- ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
958
- ctx.lineTo(x + r, y + height);
959
- ctx.quadraticCurveTo(x, y + height, x, y + height - r);
960
- ctx.lineTo(x, y + r);
961
- ctx.quadraticCurveTo(x, y, x + r, y);
962
- ctx.closePath();
963
- }
964
-
965
- function withAlpha(hex: string, alpha: number): string {
966
- const normalized = normalizeHex(hex);
967
- const r = Number.parseInt(normalized.slice(1, 3), 16);
968
- const g = Number.parseInt(normalized.slice(3, 5), 16);
969
- const b = Number.parseInt(normalized.slice(5, 7), 16);
970
- return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
971
- }
972
-
973
- function normalizeHex(hex: string): string {
974
- if (/^#[0-9a-fA-F]{6}$/.test(hex)) return hex;
975
- if (/^#[0-9a-fA-F]{3}$/.test(hex)) {
976
- return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
977
- }
978
- return '#ffffff';
979
- }