@elizaos/app-core 2.0.0-alpha.10

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 (399) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +90 -0
  3. package/src/App.tsx +472 -0
  4. package/src/actions/character.test.ts +139 -0
  5. package/src/actions/character.ts +152 -0
  6. package/src/actions/chat-helpers.ts +100 -0
  7. package/src/actions/cloud.ts +59 -0
  8. package/src/actions/index.ts +12 -0
  9. package/src/actions/lifecycle.ts +175 -0
  10. package/src/actions/onboarding.ts +46 -0
  11. package/src/actions/triggers.ts +190 -0
  12. package/src/ambient.d.ts +16 -0
  13. package/src/api/client.ts +5516 -0
  14. package/src/api/index.ts +1 -0
  15. package/src/autonomy/index.ts +477 -0
  16. package/src/bridge/capacitor-bridge.ts +295 -0
  17. package/src/bridge/electrobun-rpc.ts +58 -0
  18. package/src/bridge/electrobun-runtime.ts +28 -0
  19. package/src/bridge/index.ts +5 -0
  20. package/src/bridge/native-plugins.ts +134 -0
  21. package/src/bridge/plugin-bridge.ts +352 -0
  22. package/src/bridge/storage-bridge.ts +162 -0
  23. package/src/chat/index.ts +250 -0
  24. package/src/coding/index.ts +43 -0
  25. package/src/components/AdvancedPageView.tsx +362 -0
  26. package/src/components/AgentActivityBox.tsx +49 -0
  27. package/src/components/ApiKeyConfig.tsx +224 -0
  28. package/src/components/AppsPageView.tsx +52 -0
  29. package/src/components/AppsView.tsx +293 -0
  30. package/src/components/AvatarLoader.tsx +86 -0
  31. package/src/components/AvatarSelector.tsx +223 -0
  32. package/src/components/BscTradePanel.tsx +549 -0
  33. package/src/components/BugReportModal.tsx +499 -0
  34. package/src/components/CharacterView.tsx +1645 -0
  35. package/src/components/ChatAvatar.test.ts +96 -0
  36. package/src/components/ChatAvatar.tsx +147 -0
  37. package/src/components/ChatComposer.tsx +330 -0
  38. package/src/components/ChatMessage.tsx +448 -0
  39. package/src/components/ChatModalView.test.tsx +118 -0
  40. package/src/components/ChatModalView.tsx +125 -0
  41. package/src/components/ChatView.tsx +992 -0
  42. package/src/components/CloudSourceControls.tsx +80 -0
  43. package/src/components/CodingAgentSettingsSection.tsx +536 -0
  44. package/src/components/CommandPalette.tsx +284 -0
  45. package/src/components/CompanionSceneHost.tsx +497 -0
  46. package/src/components/CompanionShell.tsx +31 -0
  47. package/src/components/CompanionView.tsx +109 -0
  48. package/src/components/ConfigPageView.tsx +758 -0
  49. package/src/components/ConfigSaveFooter.tsx +41 -0
  50. package/src/components/ConfirmModal.tsx +379 -0
  51. package/src/components/ConnectionFailedBanner.tsx +91 -0
  52. package/src/components/ConnectorsPageView.tsx +13 -0
  53. package/src/components/ConversationsSidebar.tsx +279 -0
  54. package/src/components/CustomActionEditor.tsx +1125 -0
  55. package/src/components/CustomActionsPanel.tsx +288 -0
  56. package/src/components/CustomActionsView.tsx +322 -0
  57. package/src/components/DatabasePageView.tsx +55 -0
  58. package/src/components/DatabaseView.tsx +814 -0
  59. package/src/components/ElizaCloudDashboard.tsx +1696 -0
  60. package/src/components/EmotePicker.tsx +529 -0
  61. package/src/components/ErrorBoundary.tsx +76 -0
  62. package/src/components/FineTuningView.tsx +1077 -0
  63. package/src/components/GameView.tsx +552 -0
  64. package/src/components/GameViewOverlay.tsx +133 -0
  65. package/src/components/GlobalEmoteOverlay.tsx +155 -0
  66. package/src/components/Header.test.tsx +413 -0
  67. package/src/components/Header.tsx +403 -0
  68. package/src/components/HeartbeatsView.tsx +1003 -0
  69. package/src/components/InventoryView.tsx +385 -0
  70. package/src/components/KnowledgeView.tsx +1128 -0
  71. package/src/components/LanguageDropdown.tsx +188 -0
  72. package/src/components/LifoMonitorPanel.tsx +196 -0
  73. package/src/components/LifoSandboxView.tsx +499 -0
  74. package/src/components/LoadingScreen.tsx +77 -0
  75. package/src/components/LogsPageView.tsx +17 -0
  76. package/src/components/LogsView.tsx +239 -0
  77. package/src/components/MediaGalleryView.tsx +433 -0
  78. package/src/components/MediaSettingsSection.tsx +893 -0
  79. package/src/components/MessageContent.tsx +815 -0
  80. package/src/components/OnboardingWizard.test.tsx +107 -0
  81. package/src/components/OnboardingWizard.tsx +189 -0
  82. package/src/components/PairingView.tsx +110 -0
  83. package/src/components/PermissionsSection.tsx +1186 -0
  84. package/src/components/PluginsPageView.tsx +9 -0
  85. package/src/components/PluginsView.tsx +3157 -0
  86. package/src/components/ProviderSwitcher.tsx +908 -0
  87. package/src/components/RestartBanner.tsx +76 -0
  88. package/src/components/RuntimeView.tsx +460 -0
  89. package/src/components/SaveCommandModal.tsx +211 -0
  90. package/src/components/SecretsView.tsx +569 -0
  91. package/src/components/SettingsView.tsx +825 -0
  92. package/src/components/ShellOverlays.tsx +41 -0
  93. package/src/components/ShortcutsOverlay.tsx +155 -0
  94. package/src/components/SkillsView.tsx +1435 -0
  95. package/src/components/StartupFailureView.tsx +63 -0
  96. package/src/components/StreamView.tsx +483 -0
  97. package/src/components/StripeEmbeddedCheckout.tsx +155 -0
  98. package/src/components/SubscriptionStatus.tsx +640 -0
  99. package/src/components/SystemWarningBanner.tsx +71 -0
  100. package/src/components/ThemeToggle.tsx +100 -0
  101. package/src/components/TrajectoriesView.tsx +526 -0
  102. package/src/components/TrajectoryDetailView.tsx +426 -0
  103. package/src/components/TriggersView.tsx +1 -0
  104. package/src/components/VectorBrowserView.tsx +1633 -0
  105. package/src/components/VoiceConfigView.tsx +675 -0
  106. package/src/components/VrmStage.test.ts +219 -0
  107. package/src/components/VrmStage.tsx +432 -0
  108. package/src/components/WhatsAppQrOverlay.tsx +230 -0
  109. package/src/components/__tests__/chainConfig.test.ts +220 -0
  110. package/src/components/apps/AppDetailPane.tsx +242 -0
  111. package/src/components/apps/AppsCatalogGrid.tsx +137 -0
  112. package/src/components/apps/extensions/HyperscapeAppDetailPanel.tsx +577 -0
  113. package/src/components/apps/extensions/registry.ts +16 -0
  114. package/src/components/apps/extensions/types.ts +9 -0
  115. package/src/components/apps/helpers.ts +44 -0
  116. package/src/components/avatar/VrmAnimationLoader.test.ts +164 -0
  117. package/src/components/avatar/VrmAnimationLoader.ts +151 -0
  118. package/src/components/avatar/VrmBlinkController.ts +118 -0
  119. package/src/components/avatar/VrmCameraManager.ts +407 -0
  120. package/src/components/avatar/VrmEngine.ts +2678 -0
  121. package/src/components/avatar/VrmFootShadow.ts +96 -0
  122. package/src/components/avatar/VrmViewer.tsx +421 -0
  123. package/src/components/avatar/__tests__/VrmCameraManager.test.ts +168 -0
  124. package/src/components/avatar/__tests__/VrmEngine.test.ts +1574 -0
  125. package/src/components/avatar/mixamoVRMRigMap.ts +62 -0
  126. package/src/components/avatar/retargetMixamoFbxToVrm.ts +144 -0
  127. package/src/components/avatar/retargetMixamoGltfToVrm.ts +119 -0
  128. package/src/components/chainConfig.ts +380 -0
  129. package/src/components/companion/CompanionHeader.tsx +47 -0
  130. package/src/components/companion/CompanionSceneHost.tsx +1 -0
  131. package/src/components/companion/VrmStage.tsx +2 -0
  132. package/src/components/companion/__tests__/walletUtils.test.ts +742 -0
  133. package/src/components/companion/walletUtils.ts +290 -0
  134. package/src/components/companion-shell-styles.test.ts +142 -0
  135. package/src/components/companion-shell-styles.ts +270 -0
  136. package/src/components/confirm-delete-control.tsx +69 -0
  137. package/src/components/conversations/ConversationListItem.tsx +185 -0
  138. package/src/components/conversations/conversation-utils.ts +151 -0
  139. package/src/components/format.ts +131 -0
  140. package/src/components/index.ts +94 -0
  141. package/src/components/inventory/CopyableAddress.tsx +41 -0
  142. package/src/components/inventory/InventoryToolbar.tsx +142 -0
  143. package/src/components/inventory/NftGrid.tsx +99 -0
  144. package/src/components/inventory/TokenLogo.tsx +71 -0
  145. package/src/components/inventory/TokensTable.tsx +216 -0
  146. package/src/components/inventory/constants.ts +170 -0
  147. package/src/components/inventory/index.ts +29 -0
  148. package/src/components/inventory/media-url.test.ts +38 -0
  149. package/src/components/inventory/media-url.ts +36 -0
  150. package/src/components/inventory/useInventoryData.ts +460 -0
  151. package/src/components/knowledge-upload-image.ts +215 -0
  152. package/src/components/labels.ts +46 -0
  153. package/src/components/onboarding/ActivateStep.tsx +30 -0
  154. package/src/components/onboarding/ConnectionStep.tsx +1530 -0
  155. package/src/components/onboarding/IdentityStep.tsx +147 -0
  156. package/src/components/onboarding/OnboardingPanel.tsx +39 -0
  157. package/src/components/onboarding/OnboardingStepNav.tsx +31 -0
  158. package/src/components/onboarding/PermissionsStep.tsx +20 -0
  159. package/src/components/onboarding/RpcStep.tsx +402 -0
  160. package/src/components/onboarding/WakeUpStep.tsx +184 -0
  161. package/src/components/permissions/PermissionIcon.tsx +25 -0
  162. package/src/components/permissions/StreamingPermissions.tsx +413 -0
  163. package/src/components/plugins/showcase-data.ts +481 -0
  164. package/src/components/shared/ShellHeaderControls.tsx +193 -0
  165. package/src/components/shared-companion-scene-context.ts +15 -0
  166. package/src/components/skeletons.tsx +88 -0
  167. package/src/components/stream/ActivityFeed.tsx +113 -0
  168. package/src/components/stream/AvatarPip.tsx +10 -0
  169. package/src/components/stream/ChatContent.tsx +126 -0
  170. package/src/components/stream/ChatTicker.tsx +55 -0
  171. package/src/components/stream/IdleContent.tsx +73 -0
  172. package/src/components/stream/StatusBar.tsx +469 -0
  173. package/src/components/stream/StreamSettings.tsx +506 -0
  174. package/src/components/stream/StreamTerminal.tsx +94 -0
  175. package/src/components/stream/StreamVoiceConfig.tsx +160 -0
  176. package/src/components/stream/helpers.ts +134 -0
  177. package/src/components/stream/overlays/OverlayLayer.tsx +75 -0
  178. package/src/components/stream/overlays/built-in/ActionTickerWidget.tsx +64 -0
  179. package/src/components/stream/overlays/built-in/AlertPopupWidget.tsx +87 -0
  180. package/src/components/stream/overlays/built-in/BrandingWidget.tsx +51 -0
  181. package/src/components/stream/overlays/built-in/CustomHtmlWidget.tsx +105 -0
  182. package/src/components/stream/overlays/built-in/PeonGlassWidget.tsx +265 -0
  183. package/src/components/stream/overlays/built-in/PeonHudWidget.tsx +247 -0
  184. package/src/components/stream/overlays/built-in/PeonSakuraWidget.tsx +278 -0
  185. package/src/components/stream/overlays/built-in/ThoughtBubbleWidget.tsx +77 -0
  186. package/src/components/stream/overlays/built-in/ViewerCountWidget.tsx +46 -0
  187. package/src/components/stream/overlays/built-in/index.ts +13 -0
  188. package/src/components/stream/overlays/registry.ts +22 -0
  189. package/src/components/stream/overlays/types.ts +90 -0
  190. package/src/components/stream/overlays/useOverlayLayout.ts +218 -0
  191. package/src/components/trajectory-format.ts +50 -0
  192. package/src/components/ui-badges.tsx +109 -0
  193. package/src/components/ui-switch.tsx +57 -0
  194. package/src/components/vector-browser-three.ts +27 -0
  195. package/src/config/config-catalog.ts +1092 -0
  196. package/src/config/config-field.tsx +1901 -0
  197. package/src/config/config-renderer.tsx +730 -0
  198. package/src/config/index.ts +11 -0
  199. package/src/config/ui-renderer.tsx +1751 -0
  200. package/src/config/ui-spec.ts +256 -0
  201. package/src/events/index.ts +89 -0
  202. package/src/hooks/index.ts +13 -0
  203. package/src/hooks/useBugReport.tsx +43 -0
  204. package/src/hooks/useCanvasWindow.ts +372 -0
  205. package/src/hooks/useChatAvatarVoice.ts +111 -0
  206. package/src/hooks/useContextMenu.ts +127 -0
  207. package/src/hooks/useKeyboardShortcuts.ts +86 -0
  208. package/src/hooks/useLifoSync.ts +143 -0
  209. package/src/hooks/useMemoryMonitor.ts +334 -0
  210. package/src/hooks/useRenderGuard.ts +43 -0
  211. package/src/hooks/useRetakeCapture.ts +67 -0
  212. package/src/hooks/useStreamPopoutNavigation.ts +27 -0
  213. package/src/hooks/useTimeout.ts +37 -0
  214. package/src/hooks/useVoiceChat.ts +1441 -0
  215. package/src/hooks/useWhatsAppPairing.ts +123 -0
  216. package/src/i18n/index.ts +76 -0
  217. package/src/i18n/locales/en.json +1194 -0
  218. package/src/i18n/locales/es.json +1194 -0
  219. package/src/i18n/locales/ko.json +1194 -0
  220. package/src/i18n/locales/pt.json +1194 -0
  221. package/src/i18n/locales/zh-CN.json +1194 -0
  222. package/src/i18n/messages.ts +21 -0
  223. package/src/index.ts +6 -0
  224. package/src/navigation/index.ts +282 -0
  225. package/src/navigation.test.ts +189 -0
  226. package/src/onboarding-config.test.ts +104 -0
  227. package/src/onboarding-config.ts +114 -0
  228. package/src/platform/browser-launch.test.ts +94 -0
  229. package/src/platform/browser-launch.ts +149 -0
  230. package/src/platform/index.ts +58 -0
  231. package/src/platform/init.ts +236 -0
  232. package/src/platform/lifo.ts +215 -0
  233. package/src/providers/index.ts +99 -0
  234. package/src/state/AppContext.tsx +5846 -0
  235. package/src/state/index.ts +6 -0
  236. package/src/state/internal.ts +86 -0
  237. package/src/state/onboarding-resume.test.ts +135 -0
  238. package/src/state/onboarding-resume.ts +263 -0
  239. package/src/state/parsers.test.ts +124 -0
  240. package/src/state/parsers.ts +308 -0
  241. package/src/state/persistence.ts +321 -0
  242. package/src/state/shell-routing.ts +32 -0
  243. package/src/state/types.ts +701 -0
  244. package/src/state/ui-preferences.ts +3 -0
  245. package/src/state/useApp.ts +23 -0
  246. package/src/state/vrm.ts +76 -0
  247. package/src/stories/AppMockProvider.tsx +32 -0
  248. package/src/stories/ChatEmptyState.stories.tsx +27 -0
  249. package/src/stories/ChatMessage.stories.tsx +115 -0
  250. package/src/stories/CompanionHeader.stories.tsx +74 -0
  251. package/src/stories/CompanionView.stories.tsx +33 -0
  252. package/src/stories/ConversationListItem.stories.tsx +102 -0
  253. package/src/stories/TypingIndicator.stories.tsx +28 -0
  254. package/src/styles/anime.css +6324 -0
  255. package/src/styles/base.css +196 -0
  256. package/src/styles/onboarding-game.css +738 -0
  257. package/src/styles/styles.css +2087 -0
  258. package/src/styles/xterm.css +241 -0
  259. package/src/types/index.ts +715 -0
  260. package/src/types/react-test-renderer.d.ts +45 -0
  261. package/src/utils/asset-url.ts +110 -0
  262. package/src/utils/assistant-text.ts +172 -0
  263. package/src/utils/clipboard.ts +41 -0
  264. package/src/utils/desktop-dialogs.ts +80 -0
  265. package/src/utils/index.ts +6 -0
  266. package/src/utils/number-parsing.ts +125 -0
  267. package/src/utils/openExternalUrl.ts +20 -0
  268. package/src/utils/spoken-text.ts +65 -0
  269. package/src/utils/streaming-text.ts +120 -0
  270. package/src/voice/index.ts +1 -0
  271. package/src/voice/types.ts +197 -0
  272. package/src/wallet-rpc.ts +176 -0
  273. package/test/app/AppContext.pty-sessions.test.tsx +143 -0
  274. package/test/app/MessageContent.test.tsx +326 -0
  275. package/test/app/PermissionsOnboarding.test.tsx +356 -0
  276. package/test/app/PermissionsSection.test.tsx +573 -0
  277. package/test/app/advanced-trajectory-fine-tuning.e2e.test.ts +393 -0
  278. package/test/app/agent-activity-box.test.tsx +132 -0
  279. package/test/app/agent-transfer-lock.test.ts +274 -0
  280. package/test/app/api-client-electron-fallback.test.ts +139 -0
  281. package/test/app/api-client-timeout.test.ts +75 -0
  282. package/test/app/api-client-ws.test.ts +98 -0
  283. package/test/app/api-client.ws-max-reconnect.test.ts +139 -0
  284. package/test/app/api-client.ws-reconnect.test.ts +157 -0
  285. package/test/app/app-context-autonomy-events.test.ts +478 -0
  286. package/test/app/apps-page-view.test.ts +114 -0
  287. package/test/app/apps-view.test.ts +769 -0
  288. package/test/app/autonomous-workflows.e2e.test.ts +765 -0
  289. package/test/app/autonomy-events.test.ts +150 -0
  290. package/test/app/avatar-selector.test.tsx +52 -0
  291. package/test/app/bsc-trade-panel.test.tsx +134 -0
  292. package/test/app/bug-report-modal.test.tsx +353 -0
  293. package/test/app/character-customization.e2e.test.ts +1199 -0
  294. package/test/app/chat-advanced-features.e2e.test.ts +706 -0
  295. package/test/app/chat-composer.test.tsx +181 -0
  296. package/test/app/chat-language-header.test.ts +64 -0
  297. package/test/app/chat-message.test.tsx +222 -0
  298. package/test/app/chat-modal-view.test.tsx +191 -0
  299. package/test/app/chat-routine-filter.test.ts +96 -0
  300. package/test/app/chat-send-lock.test.ts +1465 -0
  301. package/test/app/chat-stream-api-client.test.tsx +390 -0
  302. package/test/app/chat-view-game-modal.test.tsx +661 -0
  303. package/test/app/chat-view.test.tsx +877 -0
  304. package/test/app/cloud-api.e2e.test.ts +258 -0
  305. package/test/app/cloud-login-flow.e2e.test.ts +494 -0
  306. package/test/app/cloud-login-lock.test.ts +411 -0
  307. package/test/app/command-palette.test.tsx +184 -0
  308. package/test/app/command-registry.test.ts +75 -0
  309. package/test/app/companion-greeting-wave.test.tsx +425 -0
  310. package/test/app/companion-stale-conversation.test.tsx +447 -0
  311. package/test/app/companion-view.test.tsx +686 -0
  312. package/test/app/confirm-delete-control.test.ts +79 -0
  313. package/test/app/confirm-modal.test.tsx +219 -0
  314. package/test/app/connectors-ui.e2e.test.ts +508 -0
  315. package/test/app/conversations-sidebar-game-modal.test.tsx +260 -0
  316. package/test/app/conversations-sidebar.test.tsx +160 -0
  317. package/test/app/custom-actions-smoke.test.ts +387 -0
  318. package/test/app/custom-avatar-api-client.test.ts +207 -0
  319. package/test/app/desktop-utils.test.ts +145 -0
  320. package/test/app/electrobun-rpc-bridge.test.ts +83 -0
  321. package/test/app/events.test.ts +88 -0
  322. package/test/app/export-import-flows.e2e.test.ts +700 -0
  323. package/test/app/fine-tuning-view.test.ts +471 -0
  324. package/test/app/game-view-auth-session.test.tsx +186 -0
  325. package/test/app/game-view.test.ts +444 -0
  326. package/test/app/global-emote-overlay.test.tsx +106 -0
  327. package/test/app/header-status.test.tsx +149 -0
  328. package/test/app/i18n.test.ts +152 -0
  329. package/test/app/inventory-bsc-view.test.ts +940 -0
  330. package/test/app/knowledge-ui.e2e.test.ts +762 -0
  331. package/test/app/knowledge-upload-helpers.test.ts +124 -0
  332. package/test/app/lifecycle-lock.test.ts +267 -0
  333. package/test/app/lifo-popout-utils.test.ts +208 -0
  334. package/test/app/lifo-safe-endpoint.test.ts +34 -0
  335. package/test/app/loading-screen.test.tsx +45 -0
  336. package/test/app/memory-monitor.test.ts +332 -0
  337. package/test/app/navigation.test.tsx +22 -0
  338. package/test/app/onboarding-finish-lock.test.ts +663 -0
  339. package/test/app/onboarding-language.test.tsx +160 -0
  340. package/test/app/onboarding-steps.test.tsx +375 -0
  341. package/test/app/open-external-url.test.ts +65 -0
  342. package/test/app/pages-navigation-smoke.e2e.test.ts +633 -0
  343. package/test/app/pairing-lock.test.ts +260 -0
  344. package/test/app/pairing-view.test.tsx +74 -0
  345. package/test/app/permissions-section.test.ts +432 -0
  346. package/test/app/plugin-bridge.test.ts +109 -0
  347. package/test/app/plugins-ui.e2e.test.ts +605 -0
  348. package/test/app/plugins-view-game-modal.test.tsx +650 -0
  349. package/test/app/plugins-view-toggle-restart.test.ts +129 -0
  350. package/test/app/provider-dropdown-default.test.tsx +302 -0
  351. package/test/app/restart-banner.test.tsx +197 -0
  352. package/test/app/retake-capture.test.ts +84 -0
  353. package/test/app/sandbox-api-client.test.ts +108 -0
  354. package/test/app/save-command-modal.test.tsx +109 -0
  355. package/test/app/secrets-view.test.tsx +92 -0
  356. package/test/app/settings-control-styles.test.tsx +142 -0
  357. package/test/app/settings-reset.e2e.test.ts +726 -0
  358. package/test/app/settings-sections.e2e.test.ts +614 -0
  359. package/test/app/shared-format.test.ts +44 -0
  360. package/test/app/shared-switch.test.ts +69 -0
  361. package/test/app/shell-mode-switching.e2e.test.ts +829 -0
  362. package/test/app/shell-mode-tab-memory.test.tsx +58 -0
  363. package/test/app/shell-overlays.test.tsx +50 -0
  364. package/test/app/shortcuts-overlay.test.tsx +111 -0
  365. package/test/app/sse-interruption.test.ts +122 -0
  366. package/test/app/startup-asset-missing.e2e.test.ts +126 -0
  367. package/test/app/startup-backend-missing.e2e.test.ts +118 -0
  368. package/test/app/startup-chat.e2e.test.ts +305 -0
  369. package/test/app/startup-conversation-restore.test.tsx +344 -0
  370. package/test/app/startup-failure-view.test.tsx +103 -0
  371. package/test/app/startup-onboarding.e2e.test.ts +618 -0
  372. package/test/app/startup-timeout.test.tsx +80 -0
  373. package/test/app/startup-token-401.e2e.test.ts +103 -0
  374. package/test/app/stream-helpers.test.ts +46 -0
  375. package/test/app/stream-popout-navigation.test.tsx +41 -0
  376. package/test/app/stream-status-bar.test.tsx +89 -0
  377. package/test/app/theme-toggle.test.tsx +33 -0
  378. package/test/app/training-api-client.test.ts +128 -0
  379. package/test/app/trajectories-view.test.tsx +220 -0
  380. package/test/app/triggers-api-client.test.ts +77 -0
  381. package/test/app/triggers-navigation.test.ts +113 -0
  382. package/test/app/triggers-view.e2e.test.ts +674 -0
  383. package/test/app/update-channel-lock.test.ts +259 -0
  384. package/test/app/vector-browser.async-cleanup.test.tsx +367 -0
  385. package/test/app/vector-browser.e2e.test.ts +653 -0
  386. package/test/app/vrm-stage.test.tsx +351 -0
  387. package/test/app/vrm-viewer.test.tsx +298 -0
  388. package/test/app/wallet-api-save-lock.test.ts +298 -0
  389. package/test/app/wallet-hooks.test.ts +405 -0
  390. package/test/app/wallet-ui-flows.e2e.test.ts +556 -0
  391. package/test/avatar/asset-url.test.ts +90 -0
  392. package/test/avatar/avatar-selector.test.ts +173 -0
  393. package/test/avatar/mixamo-vrm-rig-map.test.ts +111 -0
  394. package/test/avatar/voice-chat-streaming-text.test.ts +96 -0
  395. package/test/avatar/voice-chat.test.ts +391 -0
  396. package/test/ui/command-palette-commands.test.ts +57 -0
  397. package/test/ui/ui-renderer.test.ts +39 -0
  398. package/tsconfig.build.json +19 -0
  399. package/tsconfig.json +20 -0
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { CharacterData } from "../api/client";
4
+ import { prepareDraftForSave } from "./character";
5
+
6
+ describe("prepareDraftForSave", () => {
7
+ it("builds a strict payload and preserves message actions", () => {
8
+ const draft = {
9
+ name: "Sakuya",
10
+ username: "sakuya-clockwork",
11
+ system: "Act composed.",
12
+ bio: " first line \n\n second line ",
13
+ adjectives: ["precise", "", "calm"],
14
+ topics: ["time", "", "duty"],
15
+ postExamples: ["Mission report", ""],
16
+ style: {
17
+ all: ["Be exact"],
18
+ chat: [],
19
+ post: ["Stay brief"],
20
+ },
21
+ messageExamples: [
22
+ {
23
+ examples: [
24
+ {
25
+ name: "{{user1}}",
26
+ content: {
27
+ text: "status?",
28
+ legacy: "remove",
29
+ },
30
+ },
31
+ {
32
+ name: "Sakuya",
33
+ content: {
34
+ text: "On track.",
35
+ actions: ["REPORT_STATUS"],
36
+ debug: true,
37
+ },
38
+ },
39
+ {
40
+ name: "",
41
+ content: { text: "skip me" },
42
+ },
43
+ {
44
+ name: "Sakuya",
45
+ content: { text: " " },
46
+ },
47
+ ],
48
+ },
49
+ { examples: [] },
50
+ ],
51
+ extraField: "drop me",
52
+ } as unknown as CharacterData;
53
+
54
+ expect(prepareDraftForSave(draft)).toEqual({
55
+ name: "Sakuya",
56
+ username: "sakuya-clockwork",
57
+ system: "Act composed.",
58
+ bio: ["first line", "second line"],
59
+ adjectives: ["precise", "calm"],
60
+ topics: ["time", "duty"],
61
+ postExamples: ["Mission report"],
62
+ style: {
63
+ all: ["Be exact"],
64
+ post: ["Stay brief"],
65
+ },
66
+ messageExamples: [
67
+ {
68
+ examples: [
69
+ {
70
+ name: "{{user1}}",
71
+ content: { text: "status?" },
72
+ },
73
+ {
74
+ name: "Sakuya",
75
+ content: {
76
+ text: "On track.",
77
+ actions: ["REPORT_STATUS"],
78
+ },
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ });
84
+ });
85
+
86
+ it("omits empty collections and invalid message examples", () => {
87
+ const draft = {
88
+ bio: " \n ",
89
+ adjectives: ["", " "],
90
+ postExamples: [" "],
91
+ style: {
92
+ all: [],
93
+ chat: [],
94
+ post: [],
95
+ },
96
+ messageExamples: [
97
+ {
98
+ examples: [
99
+ { name: "", content: { text: "missing speaker" } },
100
+ { name: "User", content: { text: " " } },
101
+ ],
102
+ },
103
+ ],
104
+ } as CharacterData;
105
+
106
+ expect(prepareDraftForSave(draft)).toEqual({});
107
+ });
108
+
109
+ it("preserves array bios and populated style groups", () => {
110
+ const draft: CharacterData = {
111
+ name: "Reimu",
112
+ bio: ["Line one", "Line two"],
113
+ style: {
114
+ chat: ["Stay direct"],
115
+ },
116
+ };
117
+
118
+ expect(prepareDraftForSave(draft)).toEqual({
119
+ name: "Reimu",
120
+ username: "Reimu",
121
+ bio: ["Line one", "Line two"],
122
+ style: {
123
+ chat: ["Stay direct"],
124
+ },
125
+ });
126
+ });
127
+
128
+ it("falls back username to name when no username is provided", () => {
129
+ const draft: CharacterData = {
130
+ name: "Marisa",
131
+ username: " ",
132
+ };
133
+
134
+ expect(prepareDraftForSave(draft)).toEqual({
135
+ name: "Marisa",
136
+ username: "Marisa",
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Character action helpers — extracted from AppContext.
3
+ *
4
+ * Pure functions for character CRUD and draft management.
5
+ */
6
+
7
+ import type { CharacterData, MiladyClient } from "../api/client";
8
+
9
+ export interface CharacterActionContext {
10
+ client: MiladyClient;
11
+ setCharacterData: (data: CharacterData | null) => void;
12
+ setCharacterDraft: (
13
+ fn: CharacterData | ((prev: CharacterData) => CharacterData),
14
+ ) => void;
15
+ setCharacterLoading: (loading: boolean) => void;
16
+ setCharacterSaving: (saving: boolean) => void;
17
+ setCharacterSaveError: (error: string | null) => void;
18
+ setCharacterSaveSuccess: (message: string | null) => void;
19
+ }
20
+
21
+ export async function loadCharacter(
22
+ ctx: CharacterActionContext,
23
+ ): Promise<void> {
24
+ ctx.setCharacterLoading(true);
25
+ ctx.setCharacterSaveError(null);
26
+ ctx.setCharacterSaveSuccess(null);
27
+ try {
28
+ const { character } = await ctx.client.getCharacter();
29
+ ctx.setCharacterData(character);
30
+ ctx.setCharacterDraft({
31
+ name: character.name ?? "",
32
+ username: character.username ?? "",
33
+ bio: Array.isArray(character.bio)
34
+ ? character.bio.join("\n")
35
+ : (character.bio ?? ""),
36
+ system: character.system ?? "",
37
+ adjectives: character.adjectives ?? [],
38
+ topics: character.topics ?? [],
39
+ style: {
40
+ all: character.style?.all ?? [],
41
+ chat: character.style?.chat ?? [],
42
+ post: character.style?.post ?? [],
43
+ },
44
+ messageExamples: character.messageExamples ?? [],
45
+ postExamples: character.postExamples ?? [],
46
+ });
47
+ } catch {
48
+ ctx.setCharacterData(null);
49
+ ctx.setCharacterDraft({});
50
+ }
51
+ ctx.setCharacterLoading(false);
52
+ }
53
+
54
+ export function prepareDraftForSave(
55
+ draft: CharacterData,
56
+ ): Record<string, unknown> {
57
+ // Only pick fields the API schema accepts (.strict() rejects unknown keys)
58
+ const result: Record<string, unknown> = {};
59
+
60
+ if (draft.name?.trim()) {
61
+ result.name = draft.name.trim();
62
+ }
63
+
64
+ if (draft.username?.trim()) {
65
+ result.username = draft.username.trim();
66
+ } else if (typeof result.name === "string") {
67
+ result.username = result.name;
68
+ }
69
+ if (draft.system) result.system = draft.system;
70
+
71
+ if (typeof draft.bio === "string") {
72
+ const lines = draft.bio
73
+ .split("\n")
74
+ .map((l: string) => l.trim())
75
+ .filter((l: string) => l.length > 0);
76
+ if (lines.length > 0) result.bio = lines;
77
+ } else if (Array.isArray(draft.bio) && draft.bio.length > 0) {
78
+ result.bio = draft.bio;
79
+ }
80
+
81
+ const adjectives = (draft.adjectives ?? []).filter(
82
+ (s) => s.trim().length > 0,
83
+ );
84
+ if (adjectives.length > 0) result.adjectives = adjectives;
85
+
86
+ const topics = (draft.topics ?? []).filter((s) => s.trim().length > 0);
87
+ if (topics.length > 0) result.topics = topics;
88
+
89
+ const postExamples = (draft.postExamples ?? []).filter(
90
+ (s) => s.trim().length > 0,
91
+ );
92
+ if (postExamples.length > 0) result.postExamples = postExamples;
93
+
94
+ if (
95
+ Array.isArray(draft.messageExamples) &&
96
+ draft.messageExamples.length > 0
97
+ ) {
98
+ // Strip extra fields from content (schema is .strict() — only text + actions allowed)
99
+ const cleaned = draft.messageExamples
100
+ .map((group) => ({
101
+ examples: (group.examples ?? [])
102
+ .filter((msg) => msg.name?.trim() && msg.content?.text?.trim())
103
+ .map((msg) => ({
104
+ name: msg.name,
105
+ content: {
106
+ text: msg.content.text,
107
+ ...(msg.content.actions ? { actions: msg.content.actions } : {}),
108
+ },
109
+ })),
110
+ }))
111
+ .filter((group) => group.examples.length > 0);
112
+ if (cleaned.length > 0) result.messageExamples = cleaned;
113
+ }
114
+
115
+ if (draft.style) {
116
+ const style: Record<string, string[]> = {};
117
+ if (draft.style.all?.length) style.all = draft.style.all;
118
+ if (draft.style.chat?.length) style.chat = draft.style.chat;
119
+ if (draft.style.post?.length) style.post = draft.style.post;
120
+ if (Object.keys(style).length > 0) result.style = style;
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ export function parseMessageExamplesInput(value: string): Array<{
127
+ examples: Array<{ name: string; content: { text: string } }>;
128
+ }> {
129
+ if (!value.trim()) return [];
130
+ const blocks = value.split(/\n\s*\n/).filter((b) => b.trim().length > 0);
131
+ return blocks.map((block) => {
132
+ const lines = block.split("\n").filter((l) => l.trim().length > 0);
133
+ const examples = lines.map((line) => {
134
+ const colonIdx = line.indexOf(":");
135
+ if (colonIdx > 0) {
136
+ return {
137
+ name: line.slice(0, colonIdx).trim(),
138
+ content: { text: line.slice(colonIdx + 1).trim() },
139
+ };
140
+ }
141
+ return { name: "User", content: { text: line.trim() } };
142
+ });
143
+ return { examples };
144
+ });
145
+ }
146
+
147
+ export function parseArrayInput(value: string): string[] {
148
+ return value
149
+ .split("\n")
150
+ .map((s) => s.trim())
151
+ .filter((s) => s.length > 0);
152
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Chat helpers — streaming delta computation, formatting, and common patterns.
3
+ *
4
+ * Extracted from AppContext to be reusable across providers.
5
+ */
6
+
7
+ import {
8
+ computeStreamingDelta as computeStreamingDeltaInternal,
9
+ mergeStreamingText,
10
+ } from "../utils/streaming-text";
11
+
12
+ export { mergeStreamingText };
13
+
14
+ /**
15
+ * Compute the streaming delta between the accumulated text and the new token.
16
+ * The SSE endpoint may emit the full text so far or just the delta — this
17
+ * function handles both by returning only the new characters.
18
+ */
19
+ export function computeStreamingDelta(
20
+ accumulated: string,
21
+ token: string,
22
+ ): string {
23
+ return computeStreamingDeltaInternal(accumulated, token);
24
+ }
25
+
26
+ /**
27
+ * Determine whether the final stream response text should replace the
28
+ * accumulated text in the UI. This handles cases where the final text
29
+ * differs from what was streamed (e.g., post-processing on the server).
30
+ */
31
+ export function shouldApplyFinalStreamText(
32
+ streamed: string,
33
+ final: string,
34
+ ): boolean {
35
+ if (!final) return false;
36
+ return final !== streamed;
37
+ }
38
+
39
+ /**
40
+ * Format a search result section with a label and bullet items.
41
+ */
42
+ export function formatSearchBullet(label: string, items: string[]): string {
43
+ if (items.length === 0) return `**${label}**: (none)`;
44
+ return `**${label}**:\n${items.map((item) => `• ${item}`).join("\n")}`;
45
+ }
46
+
47
+ /**
48
+ * Parse slash command input into a command name and raw arguments.
49
+ */
50
+ export function parseSlashCommandInput(
51
+ rawText: string,
52
+ ): { name: string; argsRaw: string } | null {
53
+ if (!rawText.startsWith("/")) return null;
54
+ const rest = rawText.slice(1);
55
+ const spaceIdx = rest.indexOf(" ");
56
+ if (spaceIdx < 0) {
57
+ return { name: rest.toLowerCase(), argsRaw: "" };
58
+ }
59
+ return {
60
+ name: rest.slice(0, spaceIdx).toLowerCase(),
61
+ argsRaw: rest.slice(spaceIdx + 1),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Normalize a custom action name for slash command matching.
67
+ */
68
+ export function normalizeCustomActionName(name: string): string {
69
+ return name.trim().toLowerCase().replace(/\s+/g, "-");
70
+ }
71
+
72
+ /**
73
+ * Parse custom action params from a slash command args string.
74
+ */
75
+ export function parseCustomActionParams(
76
+ action: { params?: Array<{ name: string; required?: boolean }> },
77
+ argsRaw: string,
78
+ ): { params: Record<string, string>; missingRequired: string[] } {
79
+ const params: Record<string, string> = {};
80
+ const missingRequired: string[] = [];
81
+
82
+ if (!action.params || action.params.length === 0) {
83
+ if (argsRaw.trim()) {
84
+ params.input = argsRaw.trim();
85
+ }
86
+ return { params, missingRequired };
87
+ }
88
+
89
+ const tokens = argsRaw.trim().split(/\s+/);
90
+ for (let i = 0; i < action.params.length; i++) {
91
+ const param = action.params[i];
92
+ if (i < tokens.length && tokens[i]) {
93
+ params[param.name] = tokens[i];
94
+ } else if (param.required) {
95
+ missingRequired.push(param.name);
96
+ }
97
+ }
98
+
99
+ return { params, missingRequired };
100
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Cloud action helpers — extracted from AppContext.
3
+ *
4
+ * Pure functions for Eliza Cloud status polling and credit management.
5
+ */
6
+
7
+ import type { MiladyClient } from "../api/client";
8
+
9
+ export interface CloudStatusResult {
10
+ enabled: boolean;
11
+ connected: boolean;
12
+ userId: string | null;
13
+ topUpUrl: string | null;
14
+ credits: number | null;
15
+ creditsLow: boolean;
16
+ creditsCritical: boolean;
17
+ }
18
+
19
+ export async function pollCloudStatus(
20
+ client: MiladyClient,
21
+ ): Promise<CloudStatusResult> {
22
+ const cloudStatus = await client.getCloudStatus().catch(() => null);
23
+ if (!cloudStatus) {
24
+ return {
25
+ enabled: false,
26
+ connected: false,
27
+ userId: null,
28
+ topUpUrl: null,
29
+ credits: null,
30
+ creditsLow: false,
31
+ creditsCritical: false,
32
+ };
33
+ }
34
+
35
+ const isConnected = Boolean(cloudStatus.connected || cloudStatus.hasApiKey);
36
+ const result: CloudStatusResult = {
37
+ enabled: Boolean(cloudStatus.enabled ?? false),
38
+ connected: isConnected,
39
+ userId: cloudStatus.userId ?? null,
40
+ topUpUrl: cloudStatus.topUpUrl ?? null,
41
+ credits: null,
42
+ creditsLow: false,
43
+ creditsCritical: false,
44
+ };
45
+
46
+ if (isConnected) {
47
+ const credits = await client.getCloudCredits().catch(() => null);
48
+ if (credits && typeof credits.balance === "number") {
49
+ result.credits = credits.balance;
50
+ result.creditsLow = credits.low ?? false;
51
+ result.creditsCritical = credits.critical ?? false;
52
+ if (credits.topUpUrl) result.topUpUrl = credits.topUpUrl;
53
+ } else if (credits?.topUpUrl) {
54
+ result.topUpUrl = credits.topUpUrl;
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Domain action modules — barrel export.
3
+ *
4
+ * Re-exports all action helpers for use by providers and components.
5
+ */
6
+
7
+ export * from "./character";
8
+ export * from "./chat-helpers";
9
+ export * from "./cloud";
10
+ export * from "./lifecycle";
11
+ export * from "./onboarding";
12
+ export * from "./triggers";
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Lifecycle action helpers — extracted from AppContext.
3
+ *
4
+ * Provides pure-logic functions for agent lifecycle operations
5
+ * (start, stop, restart, reset) that can be
6
+ * called from any provider implementation.
7
+ */
8
+
9
+ import type { MiladyClient } from "../api/client";
10
+
11
+ export type LifecycleAction = "start" | "stop" | "restart" | "reset";
12
+
13
+ export interface LifecycleMessages {
14
+ progress: string;
15
+ success: string;
16
+ verb: string;
17
+ inProgress: string;
18
+ }
19
+
20
+ export const LIFECYCLE_MESSAGES: Record<LifecycleAction, LifecycleMessages> = {
21
+ start: {
22
+ progress: "Starting agent...",
23
+ success: "Agent started.",
24
+ verb: "start",
25
+ inProgress: "starting",
26
+ },
27
+ stop: {
28
+ progress: "Stopping agent...",
29
+ success: "Agent stopped.",
30
+ verb: "stop",
31
+ inProgress: "stopping",
32
+ },
33
+
34
+ restart: {
35
+ progress: "Restarting agent...",
36
+ success: "Agent restarted.",
37
+ verb: "restart",
38
+ inProgress: "restarting",
39
+ },
40
+ reset: {
41
+ progress: "Resetting agent...",
42
+ success: "Agent reset successfully.",
43
+ verb: "reset",
44
+ inProgress: "resetting",
45
+ },
46
+ };
47
+
48
+ export interface LifecycleActionContext {
49
+ client: MiladyClient;
50
+ isBusy: () => boolean;
51
+ setBusy: (busy: boolean) => void;
52
+ setNotice: (message: string, type: string, duration?: number) => void;
53
+ }
54
+
55
+ export async function executeLifecycleAction(
56
+ action: LifecycleAction,
57
+ ctx: LifecycleActionContext,
58
+ ): Promise<ReturnType<MiladyClient["getStatus"]> | null> {
59
+ if (ctx.isBusy()) {
60
+ ctx.setNotice(
61
+ `Agent action already in progress. Please wait.`,
62
+ "info",
63
+ 2800,
64
+ );
65
+ return null;
66
+ }
67
+ ctx.setBusy(true);
68
+ ctx.setNotice(LIFECYCLE_MESSAGES[action].progress, "info", 3000);
69
+
70
+ try {
71
+ let result: Awaited<ReturnType<MiladyClient["getStatus"]>>;
72
+ switch (action) {
73
+ case "start":
74
+ result = await ctx.client.startAgent();
75
+ break;
76
+ case "stop":
77
+ result = await ctx.client.stopAgent();
78
+ break;
79
+
80
+ case "restart":
81
+ result = await ctx.client.restartAgent();
82
+ break;
83
+ default:
84
+ throw new Error(`Unknown lifecycle action: ${action}`);
85
+ }
86
+ ctx.setNotice(LIFECYCLE_MESSAGES[action].success, "success", 2400);
87
+ return result;
88
+ } catch (err) {
89
+ ctx.setNotice(
90
+ `Failed to ${LIFECYCLE_MESSAGES[action].verb} agent: ${
91
+ err instanceof Error ? err.message : "unknown error"
92
+ }`,
93
+ "error",
94
+ 4200,
95
+ );
96
+ return null;
97
+ } finally {
98
+ ctx.setBusy(false);
99
+ }
100
+ }
101
+
102
+ // ── Startup helpers ─────────────────────────────────────────────────
103
+
104
+ export const AGENT_READY_TIMEOUT_MS = 120_000;
105
+ const DEFAULT_BACKEND_STARTUP_TIMEOUT_MS = 60_000;
106
+
107
+ export function getBackendStartupTimeoutMs(): number {
108
+ try {
109
+ const envVal = (globalThis as Record<string, unknown>)
110
+ .MILADY_STARTUP_TIMEOUT;
111
+ if (typeof envVal === "number" && envVal > 0) return envVal;
112
+ } catch {
113
+ /* ignore */
114
+ }
115
+ return DEFAULT_BACKEND_STARTUP_TIMEOUT_MS;
116
+ }
117
+
118
+ export interface StartupErrorState {
119
+ reason:
120
+ | "backend-unreachable"
121
+ | "backend-timeout"
122
+ | "agent-timeout"
123
+ | "agent-error";
124
+ phase: "starting-backend" | "initializing-agent";
125
+ message: string;
126
+ detail?: string;
127
+ status?: number;
128
+ path?: string;
129
+ }
130
+
131
+ export function formatStartupErrorDetail(err: unknown): string {
132
+ if (!err) return "";
133
+ if (err instanceof Error) return err.message;
134
+ if (typeof err === "string") return err;
135
+ try {
136
+ return JSON.stringify(err);
137
+ } catch {
138
+ return String(err);
139
+ }
140
+ }
141
+
142
+ export interface ApiLikeError {
143
+ kind: "http" | "timeout" | "network";
144
+ status?: number;
145
+ path?: string;
146
+ message?: string;
147
+ }
148
+
149
+ export function asApiLikeError(err: unknown): ApiLikeError | null {
150
+ if (!err || typeof err !== "object") return null;
151
+ const candidate = err as Record<string, unknown>;
152
+ if (typeof candidate.status === "number") {
153
+ return {
154
+ kind: "http",
155
+ status: candidate.status,
156
+ path: typeof candidate.path === "string" ? candidate.path : undefined,
157
+ message:
158
+ typeof candidate.message === "string" ? candidate.message : undefined,
159
+ };
160
+ }
161
+ if (
162
+ candidate.name === "AbortError" ||
163
+ (typeof candidate.message === "string" &&
164
+ candidate.message.includes("timeout"))
165
+ ) {
166
+ return {
167
+ kind: "timeout",
168
+ message:
169
+ typeof candidate.message === "string" ? candidate.message : undefined,
170
+ };
171
+ }
172
+ return null;
173
+ }
174
+
175
+ export const AGENT_TRANSFER_MIN_PASSWORD_LENGTH = 8;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Onboarding step constants and helpers — extracted from AppContext.
3
+ */
4
+
5
+ export type OnboardingStep =
6
+ | "wakeUp"
7
+ | "language"
8
+ | "identity"
9
+ | "connection"
10
+ | "senses"
11
+ | "activate";
12
+
13
+ export const ONBOARDING_STEP_ORDER: OnboardingStep[] = [
14
+ "wakeUp",
15
+ "language",
16
+ "identity",
17
+ "connection",
18
+ "senses",
19
+ "activate",
20
+ ];
21
+
22
+ export interface OnboardingNextOptions {
23
+ allowPermissionBypass?: boolean;
24
+ }
25
+
26
+ export const ONBOARDING_PERMISSION_LABELS: Record<string, string> = {
27
+ accessibility: "Accessibility",
28
+ "screen-recording": "Screen Recording",
29
+ microphone: "Microphone",
30
+ };
31
+
32
+ export function getNextOnboardingStep(
33
+ current: OnboardingStep,
34
+ ): OnboardingStep | null {
35
+ const idx = ONBOARDING_STEP_ORDER.indexOf(current);
36
+ if (idx < 0 || idx >= ONBOARDING_STEP_ORDER.length - 1) return null;
37
+ return ONBOARDING_STEP_ORDER[idx + 1];
38
+ }
39
+
40
+ export function getPreviousOnboardingStep(
41
+ current: OnboardingStep,
42
+ ): OnboardingStep | null {
43
+ const idx = ONBOARDING_STEP_ORDER.indexOf(current);
44
+ if (idx <= 0) return null;
45
+ return ONBOARDING_STEP_ORDER[idx - 1];
46
+ }