@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,908 @@
1
+ /**
2
+ * ProviderSwitcher — Provider grid, cloud settings, and switching logic.
3
+ *
4
+ * Extracted from SettingsView.tsx for decomposition (P2 §10).
5
+ * Composes SubscriptionStatus and ApiKeyConfig sub-components.
6
+ */
7
+
8
+ import { Button, Input } from "@elizaos/ui";
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+ import { client, type OnboardingOptions, type PluginParamDef } from "../api";
11
+ import {
12
+ ConfigRenderer,
13
+ defaultRegistry,
14
+ type JsonSchemaObject,
15
+ } from "../config";
16
+ import { useTimeout } from "../hooks";
17
+ import {
18
+ getOnboardingProviderOption,
19
+ getStoredSubscriptionProvider,
20
+ getSubscriptionProviderFamily,
21
+ isSubscriptionProviderSelectionId,
22
+ normalizeSubscriptionProviderSelectionId,
23
+ SUBSCRIPTION_PROVIDER_SELECTIONS,
24
+ type SubscriptionProviderSelectionId,
25
+ } from "../providers";
26
+ import { useApp } from "../state";
27
+ import type { ConfigUiHint } from "../types";
28
+ import { ApiKeyConfig } from "./ApiKeyConfig";
29
+ import { SubscriptionStatus } from "./SubscriptionStatus";
30
+
31
+ interface PluginInfo {
32
+ id: string;
33
+ name: string;
34
+ category: string;
35
+ enabled: boolean;
36
+ configured: boolean;
37
+ parameters: PluginParamDef[];
38
+ configUiHints?: Record<string, ConfigUiHint>;
39
+ }
40
+
41
+ function normalizeAiProviderPluginId(value: string): string {
42
+ return value
43
+ .toLowerCase()
44
+ .replace(/^@[^/]+\//, "")
45
+ .replace(/^plugin-/, "");
46
+ }
47
+
48
+ interface ProviderSwitcherProps {
49
+ elizaCloudEnabled?: boolean;
50
+ elizaCloudConnected?: boolean;
51
+ elizaCloudCredits?: number | null;
52
+ elizaCloudCreditsLow?: boolean;
53
+ elizaCloudCreditsCritical?: boolean;
54
+ elizaCloudTopUpUrl?: string;
55
+ elizaCloudUserId?: string | null;
56
+ elizaCloudLoginBusy?: boolean;
57
+ elizaCloudLoginError?: string | null;
58
+ cloudDisconnecting?: boolean;
59
+ plugins?: PluginInfo[];
60
+ pluginSaving?: Set<string>;
61
+ pluginSaveSuccess?: Set<string>;
62
+ loadPlugins?: () => Promise<void>;
63
+ handlePluginToggle?: (pluginId: string, enabled: boolean) => Promise<void>;
64
+ handlePluginConfigSave?: (
65
+ pluginId: string,
66
+ values: Record<string, unknown>,
67
+ ) => void | Promise<void>;
68
+ handleCloudLogin?: () => Promise<void>;
69
+ handleCloudDisconnect?: () => Promise<void>;
70
+ setState?: (key: string, value: unknown) => void;
71
+ setTab?: (tab: string) => void;
72
+ }
73
+
74
+ export function ProviderSwitcher(props: ProviderSwitcherProps = {}) {
75
+ const { setTimeout } = useTimeout();
76
+ const app = useApp();
77
+ const t = app.t;
78
+ const elizaCloudEnabled =
79
+ props.elizaCloudEnabled ?? Boolean(app.elizaCloudEnabled);
80
+ const elizaCloudConnected =
81
+ props.elizaCloudConnected ?? Boolean(app.elizaCloudConnected);
82
+ const elizaCloudCredits = props.elizaCloudCredits ?? app.elizaCloudCredits;
83
+ const elizaCloudCreditsLow =
84
+ props.elizaCloudCreditsLow ?? Boolean(app.elizaCloudCreditsLow);
85
+ const elizaCloudCreditsCritical =
86
+ props.elizaCloudCreditsCritical ?? Boolean(app.elizaCloudCreditsCritical);
87
+ const _elizaCloudTopUpUrl =
88
+ props.elizaCloudTopUpUrl ??
89
+ (typeof app.elizaCloudTopUpUrl === "string" ? app.elizaCloudTopUpUrl : "");
90
+ const elizaCloudUserId =
91
+ props.elizaCloudUserId ??
92
+ (typeof app.elizaCloudUserId === "string" ? app.elizaCloudUserId : null);
93
+ const elizaCloudLoginBusy =
94
+ props.elizaCloudLoginBusy ?? Boolean(app.elizaCloudLoginBusy);
95
+ const elizaCloudLoginError =
96
+ props.elizaCloudLoginError ??
97
+ (typeof app.elizaCloudLoginError === "string"
98
+ ? app.elizaCloudLoginError
99
+ : null);
100
+ const cloudDisconnecting =
101
+ props.cloudDisconnecting ?? Boolean(app.elizaCloudDisconnecting);
102
+ const plugins = Array.isArray(props.plugins)
103
+ ? props.plugins
104
+ : Array.isArray(app.plugins)
105
+ ? app.plugins
106
+ : [];
107
+ const pluginSaving =
108
+ props.pluginSaving ??
109
+ (app.pluginSaving instanceof Set ? app.pluginSaving : new Set<string>());
110
+ const pluginSaveSuccess =
111
+ props.pluginSaveSuccess ??
112
+ (app.pluginSaveSuccess instanceof Set
113
+ ? app.pluginSaveSuccess
114
+ : new Set<string>());
115
+ const loadPlugins = props.loadPlugins ?? app.loadPlugins;
116
+ const handlePluginToggle = props.handlePluginToggle ?? app.handlePluginToggle;
117
+ const handlePluginConfigSave =
118
+ props.handlePluginConfigSave ?? app.handlePluginConfigSave;
119
+ const handleCloudLogin = props.handleCloudLogin ?? app.handleCloudLogin;
120
+ const handleCloudDisconnect =
121
+ props.handleCloudDisconnect ?? app.handleCloudDisconnect;
122
+ const setState = props.setState ?? app.setState;
123
+ const setTab = props.setTab ?? app.setTab;
124
+ /* ── Model selection state ─────────────────────────────────────── */
125
+ const [modelOptions, setModelOptions] = useState<
126
+ OnboardingOptions["models"] | null
127
+ >(null);
128
+ const [currentSmallModel, setCurrentSmallModel] = useState("");
129
+ const [currentLargeModel, setCurrentLargeModel] = useState("");
130
+ const [modelSaving, setModelSaving] = useState(false);
131
+ const [modelSaveSuccess, setModelSaveSuccess] = useState(false);
132
+
133
+ /* ── Subscription state ────────────────────────────────────────── */
134
+ const [subscriptionStatus, setSubscriptionStatus] = useState<
135
+ Array<{
136
+ provider: string;
137
+ configured: boolean;
138
+ valid: boolean;
139
+ expiresAt: number | null;
140
+ }>
141
+ >([]);
142
+ const [anthropicConnected, setAnthropicConnected] = useState(false);
143
+ const [openaiConnected, setOpenaiConnected] = useState(false);
144
+
145
+ /* ── Cloud inference state ─────────────────────────────────────── */
146
+ const [cloudHandlesInference, setCloudHandlesInference] = useState(false);
147
+
148
+ /* ── pi-ai state ──────────────────────────────────────────────── */
149
+ const [piAiEnabled, setPiAiEnabled] = useState(false);
150
+ const [piAiModelSpec, setPiAiModelSpec] = useState("");
151
+ const [piAiModelOptions, setPiAiModelOptions] = useState<
152
+ OnboardingOptions["piAiModels"]
153
+ >([]);
154
+ const [piAiDefaultModelSpec, setPiAiDefaultModelSpec] = useState("");
155
+ const [piAiSaving, setPiAiSaving] = useState(false);
156
+ const [piAiSaveSuccess, setPiAiSaveSuccess] = useState(false);
157
+
158
+ const loadSubscriptionStatus = useCallback(async () => {
159
+ try {
160
+ const res = await client.getSubscriptionStatus();
161
+ setSubscriptionStatus(res.providers ?? []);
162
+ } catch (err) {
163
+ console.warn("[milady] Failed to load subscription status", err);
164
+ }
165
+ }, []);
166
+
167
+ useEffect(() => {
168
+ void loadSubscriptionStatus();
169
+ void (async () => {
170
+ try {
171
+ const opts = await client.getOnboardingOptions();
172
+ setModelOptions(opts.models);
173
+ setPiAiModelOptions(opts.piAiModels ?? []);
174
+ setPiAiDefaultModelSpec(
175
+ typeof opts.piAiDefaultModel === "string"
176
+ ? opts.piAiDefaultModel
177
+ : "",
178
+ );
179
+ } catch (err) {
180
+ console.warn("[milady] Failed to load onboarding options", err);
181
+ }
182
+ try {
183
+ const cfg = await client.getConfig();
184
+ const models = cfg.models as Record<string, string> | undefined;
185
+ const cloud = cfg.cloud as Record<string, unknown> | undefined;
186
+ const elizaCloudEnabledCfg = cloud?.enabled === true;
187
+ const defaultSmall = "moonshotai/kimi-k2-turbo";
188
+ const defaultLarge = "moonshotai/kimi-k2-0905";
189
+
190
+ // Environment variables — needed both for model fallback and pi-ai
191
+ const env = cfg.env as Record<string, unknown> | undefined;
192
+ const vars = (env?.vars as Record<string, unknown> | undefined) ?? {};
193
+
194
+ // Fall back to SMALL_MODEL / LARGE_MODEL env vars when cfg.models
195
+ // is empty. Local providers (e.g. Ollama) store the active model
196
+ // names as env vars rather than in cfg.models.
197
+ const envSmall =
198
+ typeof vars.SMALL_MODEL === "string" ? vars.SMALL_MODEL : "";
199
+ const envLarge =
200
+ typeof vars.LARGE_MODEL === "string" ? vars.LARGE_MODEL : "";
201
+ setCurrentSmallModel(
202
+ models?.small ||
203
+ envSmall ||
204
+ (elizaCloudEnabledCfg ? defaultSmall : ""),
205
+ );
206
+ setCurrentLargeModel(
207
+ models?.large ||
208
+ envLarge ||
209
+ (elizaCloudEnabledCfg ? defaultLarge : ""),
210
+ );
211
+ const rawPiAi =
212
+ (typeof vars.MILADY_USE_PI_AI === "string"
213
+ ? vars.MILADY_USE_PI_AI
214
+ : undefined) ||
215
+ (typeof env?.MILADY_USE_PI_AI === "string"
216
+ ? env.MILADY_USE_PI_AI
217
+ : "");
218
+ const piAiOn = ["1", "true", "yes"].includes(
219
+ rawPiAi.trim().toLowerCase(),
220
+ );
221
+ setPiAiEnabled(piAiOn);
222
+
223
+ // Check if cloud handles inference or user has own keys
224
+ const cloudServices = cloud?.services as
225
+ | Record<string, unknown>
226
+ | undefined;
227
+ const inferenceMode =
228
+ typeof cloud?.inferenceMode === "string"
229
+ ? cloud.inferenceMode
230
+ : "cloud";
231
+ const inferenceToggle = cloudServices?.inference !== false;
232
+ const cloudHandlesInferenceCfg =
233
+ elizaCloudEnabledCfg && inferenceMode === "cloud" && inferenceToggle;
234
+ setCloudHandlesInference(cloudHandlesInferenceCfg);
235
+
236
+ const agents = cfg.agents as Record<string, unknown> | undefined;
237
+ const defaults = agents?.defaults as
238
+ | Record<string, unknown>
239
+ | undefined;
240
+ const model = defaults?.model as Record<string, unknown> | undefined;
241
+ const savedSubscriptionProvider =
242
+ normalizeSubscriptionProviderSelectionId(
243
+ defaults?.subscriptionProvider,
244
+ );
245
+ setPiAiModelSpec(
246
+ typeof model?.primary === "string" ? model.primary : "",
247
+ );
248
+ if (
249
+ !hasManualSelection.current &&
250
+ savedSubscriptionProvider &&
251
+ !piAiOn &&
252
+ !cloudHandlesInferenceCfg
253
+ ) {
254
+ setSelectedProviderId(savedSubscriptionProvider);
255
+ }
256
+ } catch (err) {
257
+ console.warn("[milady] Failed to load config", err);
258
+ }
259
+ })();
260
+ }, [loadSubscriptionStatus]);
261
+
262
+ useEffect(() => {
263
+ const anthStatus = subscriptionStatus.find(
264
+ (s) => s.provider === "anthropic-subscription",
265
+ );
266
+ const oaiStatus = subscriptionStatus.find(
267
+ (s) =>
268
+ s.provider === "openai-subscription" || s.provider === "openai-codex",
269
+ );
270
+ setAnthropicConnected(Boolean(anthStatus?.configured && anthStatus?.valid));
271
+ setOpenaiConnected(Boolean(oaiStatus?.configured && oaiStatus?.valid));
272
+ }, [subscriptionStatus]);
273
+
274
+ /* ── Derived ──────────────────────────────────────────────────── */
275
+ const allAiProviders = [
276
+ ...plugins.filter((p) => p.category === "ai-provider"),
277
+ ].sort((left, right) => {
278
+ const leftCatalog = getOnboardingProviderOption(
279
+ normalizeAiProviderPluginId(left.id),
280
+ );
281
+ const rightCatalog = getOnboardingProviderOption(
282
+ normalizeAiProviderPluginId(right.id),
283
+ );
284
+ if (leftCatalog && rightCatalog) {
285
+ return leftCatalog.order - rightCatalog.order;
286
+ }
287
+ if (leftCatalog) return -1;
288
+ if (rightCatalog) return 1;
289
+ return left.name.localeCompare(right.name);
290
+ });
291
+ const enabledAiProviders = allAiProviders.filter((p) => p.enabled);
292
+
293
+ const [selectedProviderId, setSelectedProviderId] = useState<string | null>(
294
+ () => (elizaCloudEnabled ? "__cloud__" : null),
295
+ );
296
+ const hasManualSelection = useRef(false);
297
+
298
+ useEffect(() => {
299
+ if (hasManualSelection.current) return;
300
+ if (piAiEnabled) {
301
+ if (selectedProviderId !== "pi-ai") setSelectedProviderId("pi-ai");
302
+ return;
303
+ }
304
+ // Only auto-select cloud if cloud handles inference (not just enabled)
305
+ if (cloudHandlesInference) {
306
+ if (selectedProviderId !== "__cloud__")
307
+ setSelectedProviderId("__cloud__");
308
+ }
309
+ }, [cloudHandlesInference, piAiEnabled, selectedProviderId]);
310
+
311
+ const resolvedSelectedId =
312
+ selectedProviderId === "__cloud__"
313
+ ? "__cloud__"
314
+ : selectedProviderId === "pi-ai"
315
+ ? "pi-ai"
316
+ : selectedProviderId &&
317
+ (allAiProviders.some((p) => p.id === selectedProviderId) ||
318
+ isSubscriptionProviderSelectionId(selectedProviderId))
319
+ ? selectedProviderId
320
+ : cloudHandlesInference
321
+ ? "__cloud__"
322
+ : piAiEnabled
323
+ ? "pi-ai"
324
+ : anthropicConnected
325
+ ? "anthropic-subscription"
326
+ : openaiConnected
327
+ ? "openai-subscription"
328
+ : (enabledAiProviders[0]?.id ?? null);
329
+
330
+ const selectedProvider =
331
+ resolvedSelectedId &&
332
+ resolvedSelectedId !== "__cloud__" &&
333
+ resolvedSelectedId !== "pi-ai" &&
334
+ !isSubscriptionProviderSelectionId(resolvedSelectedId)
335
+ ? (allAiProviders.find((p) => p.id === resolvedSelectedId) ?? null)
336
+ : null;
337
+
338
+ /* ── Handlers ─────────────────────────────────────────────────── */
339
+ const handleSwitchProvider = useCallback(
340
+ async (newId: string) => {
341
+ hasManualSelection.current = true;
342
+ setSelectedProviderId(newId);
343
+ const target = allAiProviders.find((p) => p.id === newId);
344
+ if (!target) return;
345
+
346
+ // Direct providers require API keys. The UI does not have access to stored
347
+ // secrets, so we avoid calling /api/provider/switch here and instead rely
348
+ // on enabling/disabling provider plugins + saving provider config.
349
+ const willTogglePlugins =
350
+ !target.enabled || enabledAiProviders.some((p) => p.id !== newId);
351
+ if (elizaCloudEnabled || piAiEnabled) {
352
+ try {
353
+ // Disable cloud inference and explicitly mark cloud as disabled
354
+ // so the cloud-status check doesn't re-enable it on restart.
355
+ await client.updateConfig({
356
+ cloud: {
357
+ enabled: false,
358
+ services: { inference: false },
359
+ inferenceMode: "byok",
360
+ },
361
+ env: { vars: { MILADY_USE_PI_AI: "" } },
362
+ });
363
+ setPiAiEnabled(false);
364
+ setCloudHandlesInference(false);
365
+ if (!willTogglePlugins) {
366
+ await client.restartAgent();
367
+ }
368
+ } catch (err) {
369
+ console.warn(
370
+ "[milady] Failed to update cloud inference config during provider switch",
371
+ err,
372
+ );
373
+ }
374
+ }
375
+ if (!target.enabled) {
376
+ await handlePluginToggle(newId, true);
377
+ }
378
+ for (const p of enabledAiProviders) {
379
+ if (p.id !== newId) {
380
+ await handlePluginToggle(p.id, false);
381
+ }
382
+ }
383
+ },
384
+ [
385
+ allAiProviders,
386
+ enabledAiProviders,
387
+ handlePluginToggle,
388
+ elizaCloudEnabled,
389
+ piAiEnabled,
390
+ ],
391
+ );
392
+
393
+ const handleSelectSubscription = useCallback(
394
+ async (providerId: SubscriptionProviderSelectionId) => {
395
+ hasManualSelection.current = true;
396
+ setSelectedProviderId(providerId);
397
+ const providerFamily = getSubscriptionProviderFamily(providerId);
398
+ const target =
399
+ allAiProviders.find((plugin) => {
400
+ const normalizedId = normalizeAiProviderPluginId(plugin.id);
401
+ const normalizedName = plugin.name.toLowerCase();
402
+ return (
403
+ normalizedId === providerFamily ||
404
+ normalizedId.startsWith(`${providerFamily}-`) ||
405
+ normalizedName.includes(providerFamily)
406
+ );
407
+ }) ?? null;
408
+
409
+ try {
410
+ // Disable cloud inference but keep cloud connected for RPC/services
411
+ await client.updateConfig({
412
+ cloud: {
413
+ services: { inference: false },
414
+ inferenceMode: "byok",
415
+ },
416
+ env: { vars: { MILADY_USE_PI_AI: "" } },
417
+ });
418
+ await client.switchProvider(getStoredSubscriptionProvider(providerId));
419
+ setCloudHandlesInference(false);
420
+ setPiAiEnabled(false);
421
+ } catch (err) {
422
+ console.warn("[milady] Provider switch failed", err);
423
+ }
424
+ if (target && !target.enabled) {
425
+ await handlePluginToggle(target.id, true);
426
+ }
427
+ for (const p of enabledAiProviders) {
428
+ if (!target || p.id !== target.id) {
429
+ await handlePluginToggle(p.id, false);
430
+ }
431
+ }
432
+ },
433
+ [allAiProviders, enabledAiProviders, handlePluginToggle],
434
+ );
435
+
436
+ const handleSelectCloud = useCallback(async () => {
437
+ hasManualSelection.current = true;
438
+ setSelectedProviderId("__cloud__");
439
+ try {
440
+ await client.updateConfig({
441
+ cloud: {
442
+ enabled: true,
443
+ services: { inference: true },
444
+ inferenceMode: "cloud",
445
+ },
446
+ env: { vars: { MILADY_USE_PI_AI: "" } },
447
+ agents: { defaults: { model: { primary: null } } },
448
+ models: {
449
+ small: currentSmallModel || "moonshotai/kimi-k2-turbo",
450
+ large: currentLargeModel || "moonshotai/kimi-k2-0905",
451
+ },
452
+ });
453
+ setState("elizaCloudEnabled", true);
454
+ setCloudHandlesInference(true);
455
+ setPiAiEnabled(false);
456
+ await client.restartAgent();
457
+ } catch (err) {
458
+ console.warn("[milady] Failed to select cloud provider", err);
459
+ }
460
+ }, [currentSmallModel, currentLargeModel, setState]);
461
+
462
+ const handlePiAiSave = useCallback(async () => {
463
+ setPiAiSaving(true);
464
+ setPiAiSaveSuccess(false);
465
+ try {
466
+ await client.updateConfig({
467
+ cloud: {
468
+ enabled: false,
469
+ services: { inference: false },
470
+ inferenceMode: "byok",
471
+ },
472
+ env: { vars: { MILADY_USE_PI_AI: "1" } },
473
+ agents: {
474
+ defaults: {
475
+ model: {
476
+ primary: piAiModelSpec.trim() || null,
477
+ },
478
+ },
479
+ },
480
+ });
481
+ setPiAiEnabled(true);
482
+ setPiAiSaveSuccess(true);
483
+ setTimeout(() => setPiAiSaveSuccess(false), 2000);
484
+ await client.restartAgent();
485
+ } catch (err) {
486
+ console.warn("[milady] Failed to enable pi-ai", err);
487
+ } finally {
488
+ setPiAiSaving(false);
489
+ }
490
+ }, [piAiModelSpec, setTimeout]);
491
+
492
+ const handleSelectPiAi = useCallback(async () => {
493
+ hasManualSelection.current = true;
494
+ setSelectedProviderId("pi-ai");
495
+ await handlePiAiSave();
496
+ }, [handlePiAiSave]);
497
+
498
+ const normalizedPiAiModelSpec = piAiModelSpec.trim();
499
+ const hasKnownPiAiModel = (piAiModelOptions ?? []).some(
500
+ (model) => model.id === normalizedPiAiModelSpec,
501
+ );
502
+ const piAiModelSelectValue =
503
+ normalizedPiAiModelSpec.length === 0
504
+ ? ""
505
+ : hasKnownPiAiModel
506
+ ? normalizedPiAiModelSpec
507
+ : "__custom__";
508
+
509
+ /* ── Render ───────────────────────────────────────────────────── */
510
+ const totalCols =
511
+ allAiProviders.length + 2 + SUBSCRIPTION_PROVIDER_SELECTIONS.length;
512
+ const isCloudSelected = resolvedSelectedId === "__cloud__";
513
+ const isPiAiSelected = resolvedSelectedId === "pi-ai";
514
+ const isSubscriptionSelected =
515
+ isSubscriptionProviderSelectionId(resolvedSelectedId);
516
+ const providerChoices = [
517
+ {
518
+ id: "__cloud__",
519
+ label: t("providerswitcher.elizaCloud"),
520
+ disabled: false,
521
+ },
522
+ { id: "pi-ai", label: t("providerswitcher.piAi"), disabled: false },
523
+ ...SUBSCRIPTION_PROVIDER_SELECTIONS.map((provider) => ({
524
+ id: provider.id,
525
+ label: t(provider.labelKey),
526
+ disabled: false,
527
+ })),
528
+ ...allAiProviders.map((provider) => ({
529
+ id: provider.id,
530
+ label:
531
+ getOnboardingProviderOption(normalizeAiProviderPluginId(provider.id))
532
+ ?.name ?? provider.name,
533
+ disabled: false,
534
+ })),
535
+ ];
536
+
537
+ if (totalCols === 0) {
538
+ return (
539
+ <div className="p-4 border border-[var(--warning,#f39c12)] bg-[var(--card)]">
540
+ <div className="text-xs text-[var(--warning,#f39c12)]">
541
+ {t("providerswitcher.noAiProvidersAvailable")}{" "}
542
+ <Button
543
+ variant="link"
544
+ size="sm"
545
+ className="settings-compact-button text-txt underline p-0 h-auto"
546
+ onClick={() => {
547
+ setTab("plugins");
548
+ }}
549
+ >
550
+ {t("providerswitcher.plugins")}
551
+ </Button>{" "}
552
+ {t("providerswitcher.page")}
553
+ </div>
554
+ </div>
555
+ );
556
+ }
557
+
558
+ return (
559
+ <>
560
+ {/* Provider dropdown - works for all screen sizes */}
561
+ <div className="mb-3">
562
+ <label
563
+ htmlFor="provider-switcher-select"
564
+ className="block text-xs font-semibold mb-1.5 text-[var(--muted)]"
565
+ >
566
+ {t("providerswitcher.selectAIProvider")}
567
+ </label>
568
+ <select
569
+ id="provider-switcher-select"
570
+ className="w-full px-3 pr-8 py-2.5 border border-[var(--border)] bg-[var(--card)] text-[13px] rounded-lg transition-all duration-200 focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent)]/20 focus:outline-none hover:border-[var(--border-hover)]"
571
+ value={resolvedSelectedId ?? "__cloud__"}
572
+ onChange={(e) => {
573
+ const nextId = e.target.value;
574
+ if (nextId === "__cloud__") {
575
+ void handleSelectCloud();
576
+ return;
577
+ }
578
+ if (nextId === "pi-ai") {
579
+ void handleSelectPiAi();
580
+ return;
581
+ }
582
+ if (isSubscriptionProviderSelectionId(nextId)) {
583
+ void handleSelectSubscription(nextId);
584
+ return;
585
+ }
586
+ void handleSwitchProvider(nextId);
587
+ }}
588
+ >
589
+ {providerChoices.map((choice) => (
590
+ <option
591
+ key={choice.id}
592
+ value={choice.id}
593
+ disabled={choice.disabled}
594
+ >
595
+ {choice.label}
596
+ </option>
597
+ ))}
598
+ </select>
599
+ <p className="text-[11px] text-[var(--muted)] mt-1.5">
600
+ {t("providerswitcher.chooseYourPreferredProvider")}
601
+ </p>
602
+ </div>
603
+
604
+ {/* Cloud settings */}
605
+ {isCloudSelected && (
606
+ <div className="mt-4 pt-4 border-t border-[var(--border)]">
607
+ {elizaCloudConnected ? (
608
+ <div>
609
+ <div className="flex justify-between items-center mb-3">
610
+ <div className="flex items-center gap-2">
611
+ <span className="inline-block w-2 h-2 rounded-full bg-[var(--ok,#16a34a)]" />
612
+ <span className="text-xs font-semibold">
613
+ {t("providerswitcher.loggedIntoElizaCloud")}
614
+ </span>
615
+ </div>
616
+ <Button
617
+ variant="outline"
618
+ size="sm"
619
+ className="!mt-0"
620
+ onClick={() => void handleCloudDisconnect()}
621
+ disabled={cloudDisconnecting}
622
+ >
623
+ {cloudDisconnecting
624
+ ? t("providerswitcher.disconnecting")
625
+ : t("providerswitcher.disconnect")}
626
+ </Button>
627
+ </div>
628
+
629
+ <div className="text-xs mb-4">
630
+ {elizaCloudUserId && (
631
+ <span className="text-[var(--muted)] mr-3">
632
+ <code className="font-[var(--mono)] text-[11px]">
633
+ {elizaCloudUserId}
634
+ </code>
635
+ </span>
636
+ )}
637
+ {elizaCloudCredits !== null && (
638
+ <span>
639
+ <span className="text-[var(--muted)]">
640
+ {t("providerswitcher.credits")}
641
+ </span>{" "}
642
+ <span
643
+ className={
644
+ elizaCloudCreditsCritical
645
+ ? "text-[var(--danger,#e74c3c)] font-bold"
646
+ : elizaCloudCreditsLow
647
+ ? "rounded-md bg-[var(--warn-subtle)] px-1.5 py-0.5 text-[var(--text)] font-bold"
648
+ : ""
649
+ }
650
+ >
651
+ ${elizaCloudCredits.toFixed(2)}
652
+ </span>
653
+ <button
654
+ type="button"
655
+ onClick={() => {
656
+ setState("cloudDashboardView", "billing");
657
+ setTab("settings");
658
+ }}
659
+ className="ml-2 bg-transparent border-0 p-0 cursor-pointer text-[11px] text-[var(--text)] underline decoration-[var(--accent)] underline-offset-2 hover:opacity-80"
660
+ >
661
+ {t("providerswitcher.topUp")}
662
+ </button>
663
+ </span>
664
+ )}
665
+ </div>
666
+
667
+ {modelOptions &&
668
+ (() => {
669
+ const modelSchema = {
670
+ type: "object" as const,
671
+ properties: {
672
+ small: {
673
+ type: "string",
674
+ enum: modelOptions.small.map((m) => m.id),
675
+ description: t(
676
+ "providerswitcher.smallModelDescription",
677
+ ),
678
+ },
679
+ large: {
680
+ type: "string",
681
+ enum: modelOptions.large.map((m) => m.id),
682
+ description: t(
683
+ "providerswitcher.largeModelDescription",
684
+ ),
685
+ },
686
+ },
687
+ required: [] as string[],
688
+ };
689
+ const modelHints: Record<string, ConfigUiHint> = {
690
+ small: {
691
+ label: t("providerswitcher.smallModelLabel"),
692
+ width: "half",
693
+ },
694
+ large: {
695
+ label: t("providerswitcher.largeModelLabel"),
696
+ width: "half",
697
+ },
698
+ };
699
+ const modelValues: Record<string, unknown> = {};
700
+ const modelSetKeys = new Set<string>();
701
+ if (currentSmallModel) {
702
+ modelValues.small = currentSmallModel;
703
+ modelSetKeys.add("small");
704
+ }
705
+ if (currentLargeModel) {
706
+ modelValues.large = currentLargeModel;
707
+ modelSetKeys.add("large");
708
+ }
709
+
710
+ return (
711
+ <ConfigRenderer
712
+ schema={modelSchema as JsonSchemaObject}
713
+ hints={modelHints}
714
+ values={modelValues}
715
+ setKeys={modelSetKeys}
716
+ registry={defaultRegistry}
717
+ onChange={(key, value) => {
718
+ const val = String(value);
719
+ if (key === "small") setCurrentSmallModel(val);
720
+ if (key === "large") setCurrentLargeModel(val);
721
+ const updated = {
722
+ small: key === "small" ? val : currentSmallModel,
723
+ large: key === "large" ? val : currentLargeModel,
724
+ };
725
+ void (async () => {
726
+ setModelSaving(true);
727
+ try {
728
+ await client.updateConfig({ models: updated });
729
+ setModelSaveSuccess(true);
730
+ setTimeout(() => setModelSaveSuccess(false), 2000);
731
+ await client.restartAgent();
732
+ } catch (err) {
733
+ console.warn(
734
+ "[milady] Failed to save cloud model config",
735
+ err,
736
+ );
737
+ }
738
+ setModelSaving(false);
739
+ })();
740
+ }}
741
+ />
742
+ );
743
+ })()}
744
+
745
+ <div className="flex items-center justify-end gap-2 mt-3">
746
+ {modelSaving && (
747
+ <span className="text-[11px] text-[var(--muted)]">
748
+ {t("providerswitcher.savingRestarting")}
749
+ </span>
750
+ )}
751
+ {modelSaveSuccess && (
752
+ <span className="text-[11px] text-[var(--ok,#16a34a)]">
753
+ {t("providerswitcher.savedRestartingAgent")}
754
+ </span>
755
+ )}
756
+ </div>
757
+ </div>
758
+ ) : (
759
+ <div>
760
+ {elizaCloudLoginBusy ? (
761
+ <div className="text-xs text-[var(--muted)]">
762
+ {t("providerswitcher.waitingForBrowser")}
763
+ </div>
764
+ ) : (
765
+ <>
766
+ {elizaCloudLoginError && (
767
+ <div className="text-xs text-[var(--danger,#e74c3c)] mb-2">
768
+ {elizaCloudLoginError}
769
+ </div>
770
+ )}
771
+ <Button
772
+ variant="default"
773
+ size="sm"
774
+ className="!mt-0 font-bold"
775
+ onClick={() => void handleCloudLogin()}
776
+ >
777
+ {t("providerswitcher.logInToElizaCloud")}
778
+ </Button>
779
+ <div className="text-[11px] text-[var(--muted)] mt-1.5">
780
+ {t("providerswitcher.opensABrowserWindow")}
781
+ </div>
782
+ </>
783
+ )}
784
+ </div>
785
+ )}
786
+ </div>
787
+ )}
788
+
789
+ {/* Subscription provider settings */}
790
+ {isSubscriptionSelected && (
791
+ <SubscriptionStatus
792
+ resolvedSelectedId={resolvedSelectedId}
793
+ subscriptionStatus={subscriptionStatus}
794
+ anthropicConnected={anthropicConnected}
795
+ setAnthropicConnected={setAnthropicConnected}
796
+ openaiConnected={openaiConnected}
797
+ setOpenaiConnected={setOpenaiConnected}
798
+ handleSelectSubscription={handleSelectSubscription}
799
+ loadSubscriptionStatus={loadSubscriptionStatus}
800
+ />
801
+ )}
802
+
803
+ {/* pi-ai settings */}
804
+ {!isCloudSelected && isPiAiSelected && (
805
+ <div className="mt-4 pt-4 border-t border-[var(--border)]">
806
+ <div className="text-xs font-semibold mb-2">
807
+ {t("providerswitcher.piSettings")}
808
+ </div>
809
+ <div className="text-[11px] text-[var(--muted)] mb-2">
810
+ {t("providerswitcher.usesLocalCredentials")}
811
+ </div>
812
+ <label
813
+ htmlFor="pi-ai-model-override"
814
+ className="block text-[11px] text-[var(--muted)] mb-1"
815
+ >
816
+ {t("providerswitcher.primaryModelOverride")}
817
+ </label>
818
+
819
+ {piAiModelOptions && piAiModelOptions.length > 0 ? (
820
+ <>
821
+ <select
822
+ id="pi-ai-model-override"
823
+ value={piAiModelSelectValue}
824
+ onChange={(e) => {
825
+ const next = e.target.value;
826
+ if (next === "__custom__") {
827
+ if (piAiModelSelectValue !== "__custom__") {
828
+ setPiAiModelSpec("");
829
+ }
830
+ return;
831
+ }
832
+ setPiAiModelSpec(next);
833
+ }}
834
+ className="w-full px-2.5 py-[8px] border border-[var(--border)] bg-[var(--card)] text-[13px] transition-colors focus:border-[var(--accent)] focus:outline-none"
835
+ >
836
+ <option value="">
837
+ {t("providerswitcher.usePiDefaultModel")}
838
+ {piAiDefaultModelSpec ? ` (${piAiDefaultModelSpec})` : ""}
839
+ </option>
840
+ {piAiModelOptions.map((model) => (
841
+ <option key={model.id} value={model.id}>
842
+ {model.name} ({model.provider})
843
+ </option>
844
+ ))}
845
+ <option value="__custom__">
846
+ {t("providerswitcher.customModelSpec")}
847
+ </option>
848
+ </select>
849
+
850
+ {piAiModelSelectValue === "__custom__" && (
851
+ <Input
852
+ type="text"
853
+ value={piAiModelSpec}
854
+ onChange={(e) => setPiAiModelSpec(e.target.value)}
855
+ placeholder={t("providerswitcher.providerModelPlaceholder")}
856
+ className="mt-2 bg-card text-[13px]"
857
+ />
858
+ )}
859
+ </>
860
+ ) : (
861
+ <Input
862
+ id="pi-ai-model-override"
863
+ type="text"
864
+ value={piAiModelSpec}
865
+ onChange={(e) => setPiAiModelSpec(e.target.value)}
866
+ placeholder={t("providerswitcher.providerModelPlaceholder")}
867
+ className="bg-card text-[13px]"
868
+ />
869
+ )}
870
+ <div className="flex items-center justify-end gap-2 mt-3">
871
+ {piAiSaving && (
872
+ <span className="text-[11px] text-[var(--muted)]">
873
+ {t("providerswitcher.savingRestarting")}
874
+ </span>
875
+ )}
876
+ {piAiSaveSuccess && (
877
+ <span className="text-[11px] text-[var(--ok,#16a34a)]">
878
+ {t("providerswitcher.savedRestartingAgent")}
879
+ </span>
880
+ )}
881
+ <Button
882
+ variant="default"
883
+ size="sm"
884
+ className="!mt-0"
885
+ onClick={() => void handlePiAiSave()}
886
+ disabled={piAiSaving}
887
+ >
888
+ {piAiSaving
889
+ ? t("providerswitcher.saveInProgress")
890
+ : t("providerswitcher.save")}
891
+ </Button>
892
+ </div>
893
+ </div>
894
+ )}
895
+
896
+ {/* Local provider settings (API keys) */}
897
+ {!isCloudSelected && (
898
+ <ApiKeyConfig
899
+ selectedProvider={selectedProvider}
900
+ pluginSaving={pluginSaving}
901
+ pluginSaveSuccess={pluginSaveSuccess}
902
+ handlePluginConfigSave={handlePluginConfigSave}
903
+ loadPlugins={loadPlugins}
904
+ />
905
+ )}
906
+ </>
907
+ );
908
+ }