@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,1435 @@
1
+ /**
2
+ * Skills management view — create, enable/disable, and install skills.
3
+ *
4
+ * Professional card-grid layout with search, stats, polished toggle switches,
5
+ * and a structured install modal. Follows the CSS variable design system used
6
+ * throughout the app (--bg, --card, --border, --accent, --muted, --txt, etc.).
7
+ */
8
+
9
+ import { Button, Input } from "@elizaos/ui";
10
+ import { useCallback, useEffect, useMemo, useState } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import type {
13
+ SkillInfo,
14
+ SkillMarketplaceResult,
15
+ SkillScanReportSummary,
16
+ } from "../api";
17
+ import { client } from "../api";
18
+ import { useTimeout } from "../hooks";
19
+ import { useApp } from "../state";
20
+ import { ConfirmDeleteControl } from "./confirm-delete-control";
21
+ import { StatusBadge } from "./ui-badges";
22
+ import { Switch } from "./ui-switch";
23
+
24
+ /* ── Skill Card ─────────────────────────────────────────────────────── */
25
+
26
+ function SkillCard({
27
+ skill,
28
+ skillToggleAction,
29
+ skillReviewId,
30
+ skillReviewReport,
31
+ skillReviewLoading,
32
+ onToggle,
33
+ onEdit,
34
+ onDelete,
35
+ onReview,
36
+ onAcknowledge,
37
+ onDismissReview,
38
+ }: {
39
+ skill: SkillInfo;
40
+ skillToggleAction: string;
41
+ skillReviewId: string;
42
+ skillReviewReport: ReturnType<typeof useApp>["skillReviewReport"];
43
+ skillReviewLoading: boolean;
44
+ onToggle: (id: string, enabled: boolean) => void;
45
+ onEdit: (skill: SkillInfo) => void;
46
+ onDelete: (id: string, name: string) => void;
47
+ onReview: (id: string) => void;
48
+ onAcknowledge: (id: string) => void;
49
+ onDismissReview: () => void;
50
+ }) {
51
+ const { t } = useApp();
52
+ const isQuarantined =
53
+ skill.scanStatus === "warning" || skill.scanStatus === "critical";
54
+ const isBlocked = skill.scanStatus === "blocked";
55
+ const isReviewing = skillReviewId === skill.id;
56
+
57
+ return (
58
+ <div
59
+ className={`flex flex-col border bg-[var(--card)] transition-colors ${
60
+ isQuarantined || isBlocked
61
+ ? "border-[#e74c3c]/40"
62
+ : "border-[var(--border)] hover:border-[var(--accent)]/50"
63
+ }`}
64
+ data-skill-id={skill.id}
65
+ >
66
+ {/* Main content area */}
67
+ <div className="p-4">
68
+ {/* Top row: badge + toggle */}
69
+ <div className="flex items-center justify-between mb-2.5">
70
+ <StatusBadge
71
+ label={
72
+ skill.scanStatus === "blocked" || skill.scanStatus === "critical"
73
+ ? "Blocked"
74
+ : skill.scanStatus === "warning"
75
+ ? "Warning"
76
+ : skill.enabled
77
+ ? "Active"
78
+ : "Inactive"
79
+ }
80
+ tone={
81
+ skill.scanStatus === "blocked" ||
82
+ skill.scanStatus === "critical" ||
83
+ skill.scanStatus === "warning"
84
+ ? skill.scanStatus === "warning"
85
+ ? "warning"
86
+ : "danger"
87
+ : skill.enabled
88
+ ? "success"
89
+ : "muted"
90
+ }
91
+ withDot
92
+ />
93
+ {!isBlocked && !isQuarantined && (
94
+ <Switch
95
+ checked={skill.enabled}
96
+ disabled={skillToggleAction === skill.id}
97
+ onChange={(val) => onToggle(skill.id, val)}
98
+ size="compact"
99
+ trackOnClass="bg-[var(--accent)]"
100
+ trackOffClass="bg-[var(--border)]"
101
+ knobClass="bg-white shadow-sm"
102
+ />
103
+ )}
104
+ {isQuarantined && !isReviewing && (
105
+ <Button
106
+ variant="outline"
107
+ size="sm"
108
+ className="h-6 px-2 text-[10px] font-bold bg-[#f39c12]/15 text-[#f39c12] border-[#f39c12]/30 hover:bg-[#f39c12]/25 hover:text-[#f39c12] transition-colors"
109
+ onClick={() => onReview(skill.id)}
110
+ >
111
+ {t("skillsview.ReviewFindings")}
112
+ </Button>
113
+ )}
114
+ </div>
115
+
116
+ {/* Name + description */}
117
+ <div
118
+ className="font-semibold text-sm text-[var(--txt)] mb-1 truncate"
119
+ title={skill.name}
120
+ >
121
+ {skill.name}
122
+ </div>
123
+ <div className="text-[11px] text-[var(--muted)] line-clamp-2 min-h-[2em]">
124
+ {skill.description || "No description provided"}
125
+ </div>
126
+ </div>
127
+
128
+ {/* Footer actions */}
129
+ <div className="flex items-center gap-2 px-4 py-3 border-t border-border/40 bg-black/5 mt-auto">
130
+ <Button
131
+ variant="ghost"
132
+ size="sm"
133
+ className="h-7 px-3 text-[11px] font-bold text-muted hover:text-txt transition-colors"
134
+ onClick={() => onEdit(skill)}
135
+ >
136
+ {t("triggersview.Edit")}
137
+ </Button>
138
+ <ConfirmDeleteControl
139
+ triggerClassName="h-7 px-3 text-[11px] font-bold text-danger hover:bg-danger/10 hover:text-danger-foreground transition-colors rounded-md"
140
+ confirmClassName="px-3 py-1 text-[11px] font-bold bg-danger text-danger-foreground hover:bg-danger/90 transition-colors rounded-md shadow-sm"
141
+ cancelClassName="px-3 py-1 text-[11px] font-bold text-muted border border-border/40 hover:text-txt transition-colors rounded-md"
142
+ confirmLabel="Yes"
143
+ cancelLabel="No"
144
+ onConfirm={() => onDelete(skill.id, skill.name)}
145
+ />
146
+ <span className="flex-1" />
147
+ <span
148
+ className="text-[10px] text-[var(--muted)] font-mono truncate max-w-[120px]"
149
+ title={skill.id}
150
+ >
151
+ {skill.id.length > 16 ? `${skill.id.slice(0, 16)}...` : skill.id}
152
+ </span>
153
+ </div>
154
+
155
+ {/* Inline review panel */}
156
+ {isReviewing && skillReviewReport ? (
157
+ <div className="border-t border-[var(--border)] p-4 bg-[var(--bg)]">
158
+ <div className="flex items-center gap-3 mb-3">
159
+ <span className="text-xs font-semibold text-[var(--txt)]">
160
+ {t("skillsview.ScanReport")}
161
+ </span>
162
+ <span className="text-[11px] text-[#e74c3c] font-mono">
163
+ {skillReviewReport.summary.critical} {t("skillsview.critical")}
164
+ </span>
165
+ <span className="text-[11px] text-[#f39c12] font-mono">
166
+ {skillReviewReport.summary.warn} {t("skillsview.warnings")}
167
+ </span>
168
+ </div>
169
+ {skillReviewReport.findings.length > 0 && (
170
+ <div className="max-h-40 overflow-y-auto mb-3 border border-[var(--border)] bg-[var(--card)]">
171
+ {skillReviewReport.findings.map(
172
+ (
173
+ f: SkillScanReportSummary["findings"][number],
174
+ idx: number,
175
+ ) => (
176
+ <div
177
+ key={`${f.file}:${f.line}:${f.message}`}
178
+ className={`flex items-start gap-2 px-3 py-1.5 text-[11px] font-mono ${
179
+ idx > 0 ? "border-t border-[var(--border)]" : ""
180
+ }`}
181
+ >
182
+ <span
183
+ className={`shrink-0 px-1.5 py-px font-bold text-[10px] uppercase ${
184
+ f.severity === "critical"
185
+ ? "bg-[#e74c3c]/15 text-[#e74c3c]"
186
+ : "bg-[#f39c12]/15 text-[#f39c12]"
187
+ }`}
188
+ >
189
+ {f.severity}
190
+ </span>
191
+ <span className="text-[var(--txt)] flex-1 min-w-0">
192
+ {f.message}
193
+ </span>
194
+ <span className="text-[var(--muted)] shrink-0">
195
+ {f.file}:{f.line}
196
+ </span>
197
+ </div>
198
+ ),
199
+ )}
200
+ </div>
201
+ )}
202
+ <div className="flex gap-2.5 mt-2">
203
+ <Button
204
+ variant="default"
205
+ size="sm"
206
+ className="h-7 px-3 text-[11px] font-bold tracking-wide shadow-sm"
207
+ onClick={() => onAcknowledge(skill.id)}
208
+ >
209
+ {t("skillsview.AcknowledgeAmpEn")}
210
+ </Button>
211
+ <Button
212
+ variant="ghost"
213
+ size="sm"
214
+ className="h-7 px-3 text-[11px] font-bold text-muted hover:text-txt transition-colors"
215
+ onClick={onDismissReview}
216
+ >
217
+ {t("skillsview.Dismiss")}
218
+ </Button>
219
+ </div>
220
+ </div>
221
+ ) : isReviewing && skillReviewLoading ? (
222
+ <div className="border-t border-[var(--border)] p-4 text-xs text-[var(--muted)] italic">
223
+ {t("skillsview.LoadingScanReport")}
224
+ </div>
225
+ ) : null}
226
+ </div>
227
+ );
228
+ }
229
+
230
+ /* ── Marketplace Result Card ────────────────────────────────────────── */
231
+
232
+ function MarketplaceCard({
233
+ item,
234
+ isInstalled,
235
+ skillsMarketplaceAction,
236
+ onInstall,
237
+ onUninstall,
238
+ }: {
239
+ item: SkillMarketplaceResult;
240
+ isInstalled: boolean;
241
+ skillsMarketplaceAction: string;
242
+ onInstall: (item: SkillMarketplaceResult) => void;
243
+ onUninstall: (skillId: string, name: string) => void;
244
+ }) {
245
+ const { t } = useApp();
246
+ const isInstalling = skillsMarketplaceAction === `install:${item.id}`;
247
+ const isUninstalling = skillsMarketplaceAction === `uninstall:${item.id}`;
248
+ const sourceLabel = item.repository || item.slug || item.id;
249
+
250
+ return (
251
+ <div className="flex items-start gap-4 p-4 border border-[var(--border)] bg-[var(--card)] hover:border-[var(--accent)]/50 transition-colors">
252
+ {/* Icon placeholder */}
253
+ <div className="w-10 h-10 shrink-0 flex items-center justify-center bg-[var(--accent)]/10 text-[var(--accent)] text-sm font-bold rounded">
254
+ {item.name.charAt(0).toUpperCase()}
255
+ </div>
256
+ <div className="flex-1 min-w-0">
257
+ <div className="font-semibold text-sm text-[var(--txt)]">
258
+ {item.name}
259
+ </div>
260
+ <div className="text-[11px] text-[var(--muted)] mt-0.5 line-clamp-2">
261
+ {item.description || "No description."}
262
+ </div>
263
+ <div className="flex items-center gap-2 mt-1.5 text-[10px] text-[var(--muted)]">
264
+ <span className="font-mono">{sourceLabel}</span>
265
+ {item.score != null && (
266
+ <>
267
+ <span className="text-[var(--border)]">/</span>
268
+ <span>
269
+ {t("skillsview.score")} {item.score.toFixed(2)}
270
+ </span>
271
+ </>
272
+ )}
273
+ {item.tags && item.tags.length > 0 && (
274
+ <>
275
+ <span className="text-[var(--border)]">/</span>
276
+ {item.tags.slice(0, 3).map((tag) => (
277
+ <span
278
+ key={tag}
279
+ className="px-1.5 py-px bg-[var(--accent)]/10 text-[var(--accent)]"
280
+ >
281
+ {tag}
282
+ </span>
283
+ ))}
284
+ </>
285
+ )}
286
+ </div>
287
+ </div>
288
+ {isInstalled ? (
289
+ <Button
290
+ variant="destructive"
291
+ size="sm"
292
+ className="h-8 px-4 text-[11px] font-bold tracking-wide shadow-sm shrink-0"
293
+ onClick={() => onUninstall(item.id, item.name)}
294
+ disabled={isUninstalling}
295
+ >
296
+ {isUninstalling ? "Removing..." : "Uninstall"}
297
+ </Button>
298
+ ) : (
299
+ <Button
300
+ variant="default"
301
+ size="sm"
302
+ className="h-8 px-4 text-[11px] font-bold tracking-wide shadow-sm shrink-0"
303
+ onClick={() => onInstall(item)}
304
+ disabled={isInstalling}
305
+ >
306
+ {isInstalling ? "Installing..." : "Install"}
307
+ </Button>
308
+ )}
309
+ </div>
310
+ );
311
+ }
312
+
313
+ /* ── Install Modal ──────────────────────────────────────────────────── */
314
+
315
+ type InstallTab = "search" | "url";
316
+
317
+ function InstallModal({
318
+ skills,
319
+ skillsMarketplaceQuery,
320
+ skillsMarketplaceResults,
321
+ skillsMarketplaceError,
322
+ skillsMarketplaceLoading,
323
+ skillsMarketplaceAction,
324
+ skillsMarketplaceManualGithubUrl,
325
+ searchSkillsMarketplace,
326
+ installSkillFromMarketplace,
327
+ uninstallMarketplaceSkill,
328
+ installSkillFromGithubUrl,
329
+ setState,
330
+ onClose,
331
+ }: {
332
+ skills: SkillInfo[];
333
+ skillsMarketplaceQuery: string;
334
+ skillsMarketplaceResults: SkillMarketplaceResult[];
335
+ skillsMarketplaceError: string;
336
+ skillsMarketplaceLoading: boolean;
337
+ skillsMarketplaceAction: string;
338
+ skillsMarketplaceManualGithubUrl: string;
339
+ searchSkillsMarketplace: () => Promise<void>;
340
+ installSkillFromMarketplace: (item: SkillMarketplaceResult) => Promise<void>;
341
+ uninstallMarketplaceSkill: (skillId: string, name: string) => Promise<void>;
342
+ installSkillFromGithubUrl: () => Promise<void>;
343
+ setState: ReturnType<typeof useApp>["setState"];
344
+ onClose: () => void;
345
+ }) {
346
+ const { t } = useApp();
347
+ const [tab, setTab] = useState<InstallTab>("search");
348
+
349
+ return (
350
+ <div
351
+ className="fixed inset-0 z-[70] flex items-center justify-center bg-black/70"
352
+ onClick={(e) => {
353
+ if (e.target === e.currentTarget) onClose();
354
+ }}
355
+ onKeyDown={(e) => {
356
+ if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
357
+ e.preventDefault();
358
+ onClose();
359
+ }
360
+ }}
361
+ role="dialog"
362
+ aria-modal="true"
363
+ >
364
+ <div
365
+ className="w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden mx-4"
366
+ style={{
367
+ background: "var(--card)",
368
+ border: "1px solid var(--border)",
369
+ borderRadius: 16,
370
+ backdropFilter: "blur(20px) saturate(115%)",
371
+ boxShadow: "var(--shadow-lg)",
372
+ color: "var(--text)",
373
+ }}
374
+ >
375
+ {/* Header */}
376
+ <div
377
+ className="flex items-center justify-between px-5 py-4"
378
+ style={{ borderBottom: "1px solid var(--border)" }}
379
+ >
380
+ <div>
381
+ <div
382
+ style={{
383
+ fontSize: 13,
384
+ fontWeight: 800,
385
+ letterSpacing: "0.14em",
386
+ textTransform: "uppercase",
387
+ color: "var(--text)",
388
+ }}
389
+ >
390
+ Install Skill
391
+ </div>
392
+ <div
393
+ style={{
394
+ fontSize: 11,
395
+ color: "var(--muted)",
396
+ marginTop: 2,
397
+ }}
398
+ >
399
+ Add skills from the marketplace or a GitHub repository.
400
+ </div>
401
+ </div>
402
+ <button
403
+ type="button"
404
+ style={{
405
+ width: 28,
406
+ height: 28,
407
+ borderRadius: "50%",
408
+ border: "1px solid var(--border)",
409
+ background: "transparent",
410
+ color: "var(--muted)",
411
+ cursor: "pointer",
412
+ fontSize: 14,
413
+ display: "flex",
414
+ alignItems: "center",
415
+ justifyContent: "center",
416
+ }}
417
+ onClick={onClose}
418
+ >
419
+ ×
420
+ </button>
421
+ </div>
422
+
423
+ {/* Tabs */}
424
+ <div
425
+ className="flex"
426
+ style={{ borderBottom: "1px solid var(--border)" }}
427
+ >
428
+ {(
429
+ [
430
+ { id: "search" as const, label: "MARKETPLACE" },
431
+ { id: "url" as const, label: "GITHUB URL" },
432
+ ] as const
433
+ ).map((t) => (
434
+ <button
435
+ type="button"
436
+ key={t.id}
437
+ style={{
438
+ flex: 1,
439
+ padding: "10px 16px",
440
+ fontSize: 11,
441
+ fontWeight: 700,
442
+ letterSpacing: "0.1em",
443
+ background: "transparent",
444
+ border: "none",
445
+ cursor: "pointer",
446
+ borderBottom:
447
+ tab === t.id ? "2px solid #f0b232" : "2px solid transparent",
448
+ color: tab === t.id ? "#f0b232" : "var(--muted)",
449
+ transition: "color 0.2s, border-color 0.2s",
450
+ }}
451
+ onClick={() => setTab(t.id)}
452
+ >
453
+ {t.label}
454
+ </button>
455
+ ))}
456
+ </div>
457
+
458
+ {/* Body */}
459
+ <div className="flex-1 overflow-y-auto px-5 py-4">
460
+ {tab === "search" && (
461
+ <>
462
+ <div className="flex gap-2 items-center mb-4">
463
+ <input
464
+ type="text"
465
+ className="plugins-game-search-input"
466
+ style={{ flex: 1, minWidth: 200 }}
467
+ placeholder="Search skills by keyword..."
468
+ value={skillsMarketplaceQuery}
469
+ onChange={(e) =>
470
+ setState("skillsMarketplaceQuery", e.target.value)
471
+ }
472
+ onKeyDown={(e) => {
473
+ if (e.key === "Enter") void searchSkillsMarketplace();
474
+ }}
475
+ />
476
+ <button
477
+ type="button"
478
+ className="plugins-game-chip"
479
+ style={{ minHeight: 36, padding: "0 16px", fontWeight: 700 }}
480
+ onClick={() => searchSkillsMarketplace()}
481
+ disabled={skillsMarketplaceLoading}
482
+ >
483
+ {skillsMarketplaceLoading ? "Searching..." : "Search"}
484
+ </button>
485
+ </div>
486
+
487
+ {skillsMarketplaceError && (
488
+ <div
489
+ className="p-2.5 text-xs mb-3"
490
+ style={{ border: "1px solid #e74c3c", color: "#e74c3c" }}
491
+ >
492
+ {skillsMarketplaceError}
493
+ </div>
494
+ )}
495
+
496
+ {skillsMarketplaceResults.length === 0 ? (
497
+ <div className="text-center py-12">
498
+ <div
499
+ style={{
500
+ color: "var(--muted)",
501
+ fontSize: 12,
502
+ letterSpacing: "0.1em",
503
+ textTransform: "uppercase",
504
+ }}
505
+ >
506
+ Search above to discover skills.
507
+ </div>
508
+ </div>
509
+ ) : (
510
+ <div className="flex flex-col gap-2">
511
+ <div className="text-[11px] text-[var(--muted)] mb-1">
512
+ {skillsMarketplaceResults.length} {t("skillsview.result")}
513
+ {skillsMarketplaceResults.length !== 1 ? "s" : ""}
514
+ </div>
515
+ {skillsMarketplaceResults.map((item) => (
516
+ <MarketplaceCard
517
+ key={item.id}
518
+ item={item}
519
+ isInstalled={skills.some((s) => s.id === item.id)}
520
+ skillsMarketplaceAction={skillsMarketplaceAction}
521
+ onInstall={installSkillFromMarketplace}
522
+ onUninstall={uninstallMarketplaceSkill}
523
+ />
524
+ ))}
525
+ </div>
526
+ )}
527
+ </>
528
+ )}
529
+
530
+ {tab === "url" && (
531
+ <div>
532
+ <div
533
+ style={{
534
+ fontSize: 12,
535
+ fontWeight: 600,
536
+ color: "var(--text)",
537
+ marginBottom: 4,
538
+ }}
539
+ >
540
+ GitHub Repository URL
541
+ </div>
542
+ <div
543
+ style={{
544
+ fontSize: 11,
545
+ color: "var(--muted)",
546
+ marginBottom: 12,
547
+ }}
548
+ >
549
+ Paste a full GitHub repository URL to install a skill directly.
550
+ </div>
551
+ <div className="flex gap-2 items-center">
552
+ <input
553
+ type="text"
554
+ className="plugins-game-search-input"
555
+ style={{ flex: 1 }}
556
+ placeholder="https://github.com/org/repo"
557
+ value={skillsMarketplaceManualGithubUrl}
558
+ onChange={(e) =>
559
+ setState("skillsMarketplaceManualGithubUrl", e.target.value)
560
+ }
561
+ onKeyDown={(e) => {
562
+ if (e.key === "Enter") void installSkillFromGithubUrl();
563
+ }}
564
+ />
565
+ <button
566
+ type="button"
567
+ className="plugins-game-chip"
568
+ style={{ minHeight: 36, padding: "0 16px", fontWeight: 700 }}
569
+ onClick={() => installSkillFromGithubUrl()}
570
+ disabled={
571
+ skillsMarketplaceAction === "install:manual" ||
572
+ !skillsMarketplaceManualGithubUrl.trim()
573
+ }
574
+ >
575
+ {skillsMarketplaceAction === "install:manual"
576
+ ? "Installing..."
577
+ : "Install"}
578
+ </button>
579
+ </div>
580
+
581
+ {skillsMarketplaceError && (
582
+ <div
583
+ className="p-2.5 text-xs mt-3"
584
+ style={{ border: "1px solid #e74c3c", color: "#e74c3c" }}
585
+ >
586
+ {skillsMarketplaceError}
587
+ </div>
588
+ )}
589
+ </div>
590
+ )}
591
+ </div>
592
+ </div>
593
+ </div>
594
+ );
595
+ }
596
+
597
+ /* ── Create Skill Inline Form ───────────────────────────────────────── */
598
+
599
+ function CreateSkillForm({
600
+ skillCreateName,
601
+ skillCreateDescription,
602
+ skillCreating,
603
+ setState,
604
+ onCancel,
605
+ onCreate,
606
+ }: {
607
+ skillCreateName: string;
608
+ skillCreateDescription: string;
609
+ skillCreating: boolean;
610
+ setState: ReturnType<typeof useApp>["setState"];
611
+ onCancel: () => void;
612
+ onCreate: () => void;
613
+ }) {
614
+ const { t } = useApp();
615
+ return (
616
+ <div className="border border-[var(--accent)]/40 bg-[var(--card)] mb-4">
617
+ <div className="px-4 py-3 border-b border-[var(--border)]">
618
+ <div className="text-xs font-semibold text-[var(--txt)]">
619
+ {t("skillsview.CreateNewSkill")}
620
+ </div>
621
+ </div>
622
+ <div className="p-4 flex flex-col gap-3">
623
+ <div>
624
+ <span className="block text-[11px] text-[var(--muted)] mb-1 font-medium">
625
+ {t("skillsview.SkillName")}{" "}
626
+ <span className="text-[#e74c3c]">*</span>
627
+ </span>
628
+ <Input
629
+ className="w-full bg-bg/50 border-border/50 focus-visible:ring-accent"
630
+ placeholder={t("skillsview.eGMyAwesomeSkil")}
631
+ value={skillCreateName}
632
+ onChange={(e) => setState("skillCreateName", e.target.value)}
633
+ onKeyDown={(e) => {
634
+ if (e.key === "Enter" && skillCreateName.trim()) onCreate();
635
+ }}
636
+ />
637
+ </div>
638
+ <div>
639
+ <span className="block text-[11px] text-[var(--muted)] mb-1 font-medium">
640
+ {t("skillsview.Description")}
641
+ </span>
642
+ <Input
643
+ className="w-full bg-bg/50 border-border/50 focus-visible:ring-accent"
644
+ placeholder={t("skillsview.BriefDescriptionOf")}
645
+ value={skillCreateDescription}
646
+ onChange={(e) => setState("skillCreateDescription", e.target.value)}
647
+ onKeyDown={(e) => {
648
+ if (e.key === "Enter" && skillCreateName.trim()) onCreate();
649
+ }}
650
+ />
651
+ </div>
652
+ <div className="flex gap-2 justify-end pt-2">
653
+ <Button variant="ghost" size="sm" onClick={onCancel}>
654
+ {t("onboarding.cancel")}
655
+ </Button>
656
+ <Button
657
+ variant="default"
658
+ size="sm"
659
+ onClick={onCreate}
660
+ disabled={skillCreating || !skillCreateName.trim()}
661
+ >
662
+ {skillCreating ? "Creating..." : "Create Skill"}
663
+ </Button>
664
+ </div>
665
+ </div>
666
+ </div>
667
+ );
668
+ }
669
+
670
+ /* ── Edit Skill Modal ──────────────────────────────────────────────── */
671
+
672
+ function EditSkillModal({
673
+ skillId,
674
+ skillName,
675
+ onClose,
676
+ onSaved,
677
+ }: {
678
+ skillId: string;
679
+ skillName: string;
680
+ onClose: () => void;
681
+ onSaved: () => void;
682
+ }) {
683
+ const { t } = useApp();
684
+ const [content, setContent] = useState("");
685
+ const [originalContent, setOriginalContent] = useState("");
686
+ const [loading, setLoading] = useState(true);
687
+ const [saving, setSaving] = useState(false);
688
+ const [error, setError] = useState("");
689
+ const [saveSuccess, setSaveSuccess] = useState(false);
690
+
691
+ const loadSource = useCallback(async () => {
692
+ setLoading(true);
693
+ setError("");
694
+ try {
695
+ const res = await client.getSkillSource(skillId);
696
+ setContent(res.content);
697
+ setOriginalContent(res.content);
698
+ } catch (err) {
699
+ setError(
700
+ err instanceof Error ? err.message : "Failed to load skill source",
701
+ );
702
+ }
703
+ setLoading(false);
704
+ }, [skillId]);
705
+
706
+ useEffect(() => {
707
+ void loadSource();
708
+ }, [loadSource]);
709
+
710
+ const hasChanges = content !== originalContent;
711
+
712
+ const handleSave = async () => {
713
+ setSaving(true);
714
+ setError("");
715
+ setSaveSuccess(false);
716
+ try {
717
+ await client.saveSkillSource(skillId, content);
718
+ setOriginalContent(content);
719
+ setSaveSuccess(true);
720
+ onSaved();
721
+ setTimeout(() => setSaveSuccess(false), 2000);
722
+ } catch (err) {
723
+ setError(err instanceof Error ? err.message : "Failed to save");
724
+ }
725
+ setSaving(false);
726
+ };
727
+
728
+ const handleKeyDown = (e: React.KeyboardEvent) => {
729
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
730
+ e.preventDefault();
731
+ if (hasChanges && !saving) void handleSave();
732
+ }
733
+ // Allow tab to insert spaces
734
+ if (e.key === "Tab") {
735
+ e.preventDefault();
736
+ const target = e.target as HTMLTextAreaElement;
737
+ const start = target.selectionStart;
738
+ const end = target.selectionEnd;
739
+ const val = target.value;
740
+ setContent(`${val.substring(0, start)} ${val.substring(end)}`);
741
+ requestAnimationFrame(() => {
742
+ target.selectionStart = target.selectionEnd = start + 2;
743
+ });
744
+ }
745
+ };
746
+
747
+ return (
748
+ <div
749
+ className="fixed inset-0 z-[70] flex items-center justify-center bg-black/70"
750
+ onClick={(e) => {
751
+ if (e.target === e.currentTarget) onClose();
752
+ }}
753
+ onKeyDown={(e) => {
754
+ if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
755
+ e.preventDefault();
756
+ onClose();
757
+ }
758
+ }}
759
+ role="dialog"
760
+ aria-modal="true"
761
+ >
762
+ <div
763
+ className="w-full max-w-4xl h-[85vh] flex flex-col overflow-hidden mx-4 rounded-xl"
764
+ style={{
765
+ background: "color-mix(in srgb, var(--bg) 96%, transparent)",
766
+ border:
767
+ "1px solid color-mix(in srgb, var(--accent) 18%, transparent)",
768
+ backdropFilter: "blur(24px)",
769
+ boxShadow: "var(--shadow-lg)",
770
+ }}
771
+ >
772
+ {/* Header */}
773
+ <div
774
+ className="flex items-center justify-between px-5 py-3 shrink-0"
775
+ style={{ borderBottom: "1px solid var(--border)" }}
776
+ >
777
+ <div className="flex items-center gap-3 min-w-0">
778
+ <div
779
+ className="font-semibold text-sm truncate"
780
+ style={{ color: "var(--text)" }}
781
+ >
782
+ {skillName}
783
+ </div>
784
+ <span
785
+ className="text-[10px] font-mono px-1.5 py-0.5"
786
+ style={{
787
+ color: "var(--muted)",
788
+ background: "var(--bg-hover)",
789
+ border: "1px solid var(--border)",
790
+ borderRadius: 4,
791
+ }}
792
+ >
793
+ {t("skillsview.SKILLMd")}
794
+ </span>
795
+ {hasChanges && (
796
+ <span
797
+ className="text-[10px] font-medium"
798
+ style={{ color: "#f0b232" }}
799
+ >
800
+ {t("skillsview.unsaved")}
801
+ </span>
802
+ )}
803
+ </div>
804
+ <div className="flex items-center gap-2">
805
+ <span className="text-[10px]" style={{ color: "var(--muted)" }}>
806
+ {navigator.platform.includes("Mac") ? "⌘S" : "Ctrl+S"}{" "}
807
+ {t("skillsview.toSave")}
808
+ </span>
809
+ <button
810
+ type="button"
811
+ className="bg-transparent border-0 cursor-pointer text-lg px-2 transition-colors"
812
+ style={{ color: "var(--muted)" }}
813
+ onMouseEnter={(e) => {
814
+ e.currentTarget.style.color = "var(--text)";
815
+ }}
816
+ onMouseLeave={(e) => {
817
+ e.currentTarget.style.color = "var(--muted)";
818
+ }}
819
+ onClick={onClose}
820
+ >
821
+ ×
822
+ </button>
823
+ </div>
824
+ </div>
825
+
826
+ {/* Editor body */}
827
+ <div className="flex-1 overflow-hidden">
828
+ {loading ? (
829
+ <div
830
+ className="flex items-center justify-center h-full text-sm"
831
+ style={{ color: "var(--muted)" }}
832
+ >
833
+ {t("skillsview.LoadingSkillSource")}
834
+ </div>
835
+ ) : error && !content ? (
836
+ <div className="flex flex-col items-center justify-center h-full gap-3">
837
+ <div className="text-sm font-medium" style={{ color: "#ef4444" }}>
838
+ {error}
839
+ </div>
840
+ <button
841
+ type="button"
842
+ className="px-3 py-1.5 text-xs font-medium rounded cursor-pointer transition-colors"
843
+ style={{
844
+ background: "var(--bg-hover)",
845
+ border: "1px solid var(--border)",
846
+ color: "var(--text)",
847
+ }}
848
+ onClick={() => loadSource()}
849
+ >
850
+ {t("common.retry")}
851
+ </button>
852
+ </div>
853
+ ) : (
854
+ <textarea
855
+ className="w-full h-full resize-none border-0 text-[13px] leading-relaxed font-mono p-5 focus:outline-none"
856
+ style={{
857
+ background: "var(--bg-hover)",
858
+ color: "var(--text)",
859
+ }}
860
+ value={content}
861
+ onChange={(e) => setContent(e.target.value)}
862
+ onKeyDown={handleKeyDown}
863
+ spellCheck={false}
864
+ />
865
+ )}
866
+ </div>
867
+
868
+ {/* Footer */}
869
+ <div
870
+ className="flex items-center justify-between px-5 py-3 shrink-0"
871
+ style={{ borderTop: "1px solid var(--border)" }}
872
+ >
873
+ <div className="text-[11px]" style={{ color: "var(--muted)" }}>
874
+ {content ? `${content.split("\n").length} lines` : ""}
875
+ {error && content ? (
876
+ <span className="ml-3" style={{ color: "#ef4444" }}>
877
+ {error}
878
+ </span>
879
+ ) : null}
880
+ </div>
881
+ <div className="flex items-center gap-2">
882
+ <button
883
+ type="button"
884
+ className="px-3 py-1.5 text-xs font-medium rounded cursor-pointer transition-colors"
885
+ style={{
886
+ background: "transparent",
887
+ border: "1px solid var(--border)",
888
+ color: "var(--muted)",
889
+ }}
890
+ onClick={onClose}
891
+ >
892
+ {hasChanges ? "Discard" : "Close"}
893
+ </button>
894
+ <button
895
+ type="button"
896
+ className="px-3 py-1.5 text-xs font-medium rounded cursor-pointer transition-colors"
897
+ style={{
898
+ background: saveSuccess ? "#22c55e" : "#f0b232",
899
+ border: "none",
900
+ color: saveSuccess ? "#fff" : "#000",
901
+ opacity: saving || !hasChanges ? 0.5 : 1,
902
+ }}
903
+ onClick={() => handleSave()}
904
+ disabled={saving || !hasChanges}
905
+ >
906
+ {saving ? "Saving..." : saveSuccess ? "Saved" : "Save"}
907
+ </button>
908
+ </div>
909
+ </div>
910
+ </div>
911
+ </div>
912
+ );
913
+ }
914
+
915
+ /* ── Main Skills View ───────────────────────────────────────────────── */
916
+
917
+ export function SkillsView({ inModal }: { inModal?: boolean } = {}) {
918
+ if (inModal) return <SkillsModalView />;
919
+ return <SkillsFullView />;
920
+ }
921
+
922
+ /* ── Companion Modal View (sidebar + detail, reuses plugins-game-* CSS) ── */
923
+
924
+ function SkillsModalView() {
925
+ const {
926
+ skills,
927
+ skillToggleAction,
928
+ loadSkills,
929
+ handleSkillToggle,
930
+ handleDeleteSkill,
931
+ refreshSkills,
932
+ setState,
933
+ skillsMarketplaceQuery,
934
+ skillsMarketplaceResults,
935
+ skillsMarketplaceError,
936
+ skillsMarketplaceLoading,
937
+ skillsMarketplaceAction,
938
+ skillsMarketplaceManualGithubUrl,
939
+ searchSkillsMarketplace,
940
+ installSkillFromMarketplace,
941
+ uninstallMarketplaceSkill,
942
+ installSkillFromGithubUrl,
943
+ } = useApp();
944
+
945
+ const [selectedId, setSelectedId] = useState<string | null>(null);
946
+ const [filterText, setFilterText] = useState("");
947
+ const [filterTab, setFilterTab] = useState<"all" | "on" | "off">("all");
948
+ const [editingSkill, setEditingSkill] = useState<SkillInfo | null>(null);
949
+ const [installModalOpen, setInstallModalOpen] = useState(false);
950
+
951
+ useEffect(() => {
952
+ void loadSkills();
953
+ }, [loadSkills]);
954
+
955
+ const filtered = useMemo(() => {
956
+ const searchLower = filterText.toLowerCase();
957
+ return skills.filter((s) => {
958
+ if (filterTab === "on" && !s.enabled) return false;
959
+ if (filterTab === "off" && s.enabled) return false;
960
+ if (
961
+ searchLower &&
962
+ !s.name.toLowerCase().includes(searchLower) &&
963
+ !(s.description ?? "").toLowerCase().includes(searchLower)
964
+ )
965
+ return false;
966
+ return true;
967
+ });
968
+ }, [skills, filterText, filterTab]);
969
+
970
+ const effectiveSelectedId =
971
+ selectedId && filtered.find((s) => s.id === selectedId)
972
+ ? selectedId
973
+ : (filtered[0]?.id ?? null);
974
+ const selected = effectiveSelectedId
975
+ ? (skills.find((s) => s.id === effectiveSelectedId) ?? null)
976
+ : null;
977
+
978
+ const tabs: { key: typeof filterTab; label: string }[] = [
979
+ { key: "all", label: `ALL (${skills.length})` },
980
+ { key: "on", label: `ON (${skills.filter((s) => s.enabled).length})` },
981
+ { key: "off", label: `OFF (${skills.filter((s) => !s.enabled).length})` },
982
+ ];
983
+
984
+ return (
985
+ <div className="plugins-game-modal">
986
+ {/* ── Left sidebar ── */}
987
+ <div className="plugins-game-list-panel">
988
+ <div className="plugins-game-list-head">
989
+ <div className="plugins-game-section-title">Talents</div>
990
+ <div className="plugins-game-section-meta">
991
+ {skills.length} installed
992
+ </div>
993
+ </div>
994
+
995
+ {/* Search + Install */}
996
+ <div className="plugins-game-list-search">
997
+ <div className="plugins-game-list-search-row">
998
+ <input
999
+ type="text"
1000
+ placeholder="Search skills..."
1001
+ value={filterText}
1002
+ onChange={(e) => setFilterText(e.target.value)}
1003
+ className="plugins-game-search-input"
1004
+ />
1005
+ <button
1006
+ type="button"
1007
+ className="plugins-game-chip plugins-game-add-btn"
1008
+ onClick={() => setInstallModalOpen(true)}
1009
+ >
1010
+ <span className="plugins-game-add-symbol">+</span> Install
1011
+ </button>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ {/* Filter tabs */}
1016
+ <div className="plugins-game-chip-row">
1017
+ {tabs.map((tab) => (
1018
+ <button
1019
+ key={tab.key}
1020
+ type="button"
1021
+ className={`plugins-game-chip plugins-game-chip-small${filterTab === tab.key ? " is-active" : ""}`}
1022
+ onClick={() => setFilterTab(tab.key)}
1023
+ >
1024
+ {tab.label}
1025
+ </button>
1026
+ ))}
1027
+ </div>
1028
+
1029
+ {/* Skill list */}
1030
+ <div className="plugins-game-list-scroll">
1031
+ {filtered.length === 0 ? (
1032
+ <div className="plugins-game-list-empty">No skills found</div>
1033
+ ) : (
1034
+ filtered.map((skill) => (
1035
+ <button
1036
+ key={skill.id}
1037
+ type="button"
1038
+ className={`plugins-game-card${effectiveSelectedId === skill.id ? " is-selected" : ""}${!skill.enabled ? " is-disabled" : ""}`}
1039
+ onClick={() => setSelectedId(skill.id)}
1040
+ >
1041
+ <div className="plugins-game-card-icon-shell">
1042
+ <span className="plugins-game-card-icon">
1043
+ {skill.name.charAt(0).toUpperCase()}
1044
+ </span>
1045
+ </div>
1046
+ <div className="plugins-game-card-body">
1047
+ <div className="plugins-game-card-name">{skill.name}</div>
1048
+ <div className="plugins-game-card-meta">
1049
+ <span
1050
+ className={`plugins-game-badge ${skill.enabled ? "is-on" : "is-off"}`}
1051
+ >
1052
+ {skill.enabled ? "ON" : "OFF"}
1053
+ </span>
1054
+ </div>
1055
+ </div>
1056
+ </button>
1057
+ ))
1058
+ )}
1059
+ </div>
1060
+ </div>
1061
+
1062
+ {/* ── Right detail panel ── */}
1063
+ <div className="plugins-game-detail-panel">
1064
+ {selected ? (
1065
+ <>
1066
+ <div className="plugins-game-detail-head">
1067
+ <div className="plugins-game-detail-title-row">
1068
+ <div className="plugins-game-detail-icon-shell">
1069
+ <span className="plugins-game-detail-icon">
1070
+ {selected.name.charAt(0).toUpperCase()}
1071
+ </span>
1072
+ </div>
1073
+ <div className="plugins-game-detail-main">
1074
+ <div className="plugins-game-detail-name">
1075
+ {selected.name}
1076
+ </div>
1077
+ </div>
1078
+ <button
1079
+ type="button"
1080
+ className={`plugins-game-toggle ${selected.enabled ? "is-on" : "is-off"}`}
1081
+ onClick={() =>
1082
+ handleSkillToggle(selected.id, !selected.enabled)
1083
+ }
1084
+ disabled={skillToggleAction === selected.id}
1085
+ >
1086
+ {skillToggleAction === selected.id
1087
+ ? "..."
1088
+ : selected.enabled
1089
+ ? "ON"
1090
+ : "OFF"}
1091
+ </button>
1092
+ </div>
1093
+ </div>
1094
+ <div className="plugins-game-detail-description">
1095
+ {selected.description || "No description provided."}
1096
+ </div>
1097
+ <div className="plugins-game-detail-actions">
1098
+ <button
1099
+ type="button"
1100
+ className="plugins-game-action-btn"
1101
+ onClick={() => setEditingSkill(selected)}
1102
+ >
1103
+ Edit Source
1104
+ </button>
1105
+ <button
1106
+ type="button"
1107
+ className="plugins-game-action-btn"
1108
+ onClick={() => handleDeleteSkill(selected.id, selected.name)}
1109
+ >
1110
+ Delete
1111
+ </button>
1112
+ </div>
1113
+ </>
1114
+ ) : (
1115
+ <div className="plugins-game-detail-empty">
1116
+ <span className="plugins-game-detail-empty-icon">🧠</span>
1117
+ <span className="plugins-game-detail-empty-text">
1118
+ Select a talent to configure
1119
+ </span>
1120
+ </div>
1121
+ )}
1122
+ </div>
1123
+
1124
+ {/* Portal modals to body so they escape the 3D transform stacking context */}
1125
+ {editingSkill &&
1126
+ createPortal(
1127
+ <EditSkillModal
1128
+ skillId={editingSkill.id}
1129
+ skillName={editingSkill.name}
1130
+ onClose={() => setEditingSkill(null)}
1131
+ onSaved={() => void refreshSkills()}
1132
+ />,
1133
+ document.body,
1134
+ )}
1135
+
1136
+ {installModalOpen &&
1137
+ createPortal(
1138
+ <InstallModal
1139
+ skills={skills}
1140
+ skillsMarketplaceQuery={skillsMarketplaceQuery}
1141
+ skillsMarketplaceResults={skillsMarketplaceResults}
1142
+ skillsMarketplaceError={skillsMarketplaceError}
1143
+ skillsMarketplaceLoading={skillsMarketplaceLoading}
1144
+ skillsMarketplaceAction={skillsMarketplaceAction}
1145
+ skillsMarketplaceManualGithubUrl={skillsMarketplaceManualGithubUrl}
1146
+ searchSkillsMarketplace={searchSkillsMarketplace}
1147
+ installSkillFromMarketplace={installSkillFromMarketplace}
1148
+ uninstallMarketplaceSkill={uninstallMarketplaceSkill}
1149
+ installSkillFromGithubUrl={installSkillFromGithubUrl}
1150
+ setState={setState}
1151
+ onClose={() => setInstallModalOpen(false)}
1152
+ />,
1153
+ document.body,
1154
+ )}
1155
+ </div>
1156
+ );
1157
+ }
1158
+
1159
+ /* ── Full-Page Skills View ─────────────────────────────────────────── */
1160
+
1161
+ function SkillsFullView() {
1162
+ const { setTimeout: _setTimeout } = useTimeout();
1163
+
1164
+ const {
1165
+ skills,
1166
+ skillCreateFormOpen,
1167
+ skillCreateName,
1168
+ skillCreateDescription,
1169
+ skillCreating,
1170
+ skillReviewReport,
1171
+ skillReviewId,
1172
+ skillReviewLoading,
1173
+ skillToggleAction,
1174
+ skillsMarketplaceQuery,
1175
+ skillsMarketplaceResults,
1176
+ skillsMarketplaceError,
1177
+ skillsMarketplaceLoading,
1178
+ skillsMarketplaceAction,
1179
+ skillsMarketplaceManualGithubUrl,
1180
+ loadSkills,
1181
+ refreshSkills,
1182
+ handleSkillToggle,
1183
+ handleCreateSkill,
1184
+ handleDeleteSkill,
1185
+ handleReviewSkill,
1186
+ handleAcknowledgeSkill,
1187
+ searchSkillsMarketplace,
1188
+ installSkillFromMarketplace,
1189
+ uninstallMarketplaceSkill,
1190
+ installSkillFromGithubUrl,
1191
+ setState,
1192
+ } = useApp();
1193
+
1194
+ const [installModalOpen, setInstallModalOpen] = useState(false);
1195
+ const [filterText, setFilterText] = useState("");
1196
+ const [editingSkill, setEditingSkill] = useState<SkillInfo | null>(null);
1197
+
1198
+ useEffect(() => {
1199
+ void loadSkills();
1200
+ }, [loadSkills]);
1201
+
1202
+ // Group into: needs attention, active, inactive — with text filter
1203
+ const { attention, active, inactive, activeCount, totalCount } =
1204
+ useMemo(() => {
1205
+ const attention: SkillInfo[] = [];
1206
+ const active: SkillInfo[] = [];
1207
+ const inactive: SkillInfo[] = [];
1208
+ let activeCount = 0;
1209
+
1210
+ const query = filterText.toLowerCase();
1211
+
1212
+ for (const skill of skills) {
1213
+ if (
1214
+ query &&
1215
+ !skill.name.toLowerCase().includes(query) &&
1216
+ !skill.description?.toLowerCase().includes(query)
1217
+ ) {
1218
+ continue;
1219
+ }
1220
+
1221
+ if (skill.enabled) activeCount++;
1222
+
1223
+ if (
1224
+ skill.scanStatus === "warning" ||
1225
+ skill.scanStatus === "critical" ||
1226
+ skill.scanStatus === "blocked"
1227
+ ) {
1228
+ attention.push(skill);
1229
+ } else if (skill.enabled) {
1230
+ active.push(skill);
1231
+ } else {
1232
+ inactive.push(skill);
1233
+ }
1234
+ }
1235
+
1236
+ return {
1237
+ attention,
1238
+ active,
1239
+ inactive,
1240
+ activeCount,
1241
+ totalCount: skills.length,
1242
+ };
1243
+ }, [skills, filterText]);
1244
+
1245
+ const handleDismissReview = () => {
1246
+ setState("skillReviewId", "");
1247
+ setState("skillReviewReport", null);
1248
+ };
1249
+
1250
+ const handleCancelCreate = () => {
1251
+ setState("skillCreateFormOpen", false);
1252
+ setState("skillCreateName", "");
1253
+ setState("skillCreateDescription", "");
1254
+ };
1255
+
1256
+ const allVisible = [...attention, ...active, ...inactive];
1257
+
1258
+ /** Render a group of skill cards in a grid with a section header. */
1259
+ const renderGroup = (label: string, items: SkillInfo[], accent?: string) => {
1260
+ if (items.length === 0) return null;
1261
+ return (
1262
+ <div className="mb-6">
1263
+ <div
1264
+ className="text-xs uppercase tracking-wider font-semibold mb-2 flex items-center gap-2"
1265
+ style={accent ? { color: accent } : { color: "var(--muted)" }}
1266
+ >
1267
+ {label}
1268
+ <span className="text-[10px] font-mono opacity-60">
1269
+ ({items.length})
1270
+ </span>
1271
+ </div>
1272
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1273
+ {items.map((skill) => (
1274
+ <SkillCard
1275
+ key={skill.id}
1276
+ skill={skill}
1277
+ skillToggleAction={skillToggleAction}
1278
+ skillReviewId={skillReviewId}
1279
+ skillReviewReport={skillReviewReport}
1280
+ skillReviewLoading={skillReviewLoading}
1281
+ onToggle={handleSkillToggle}
1282
+ onEdit={setEditingSkill}
1283
+ onDelete={handleDeleteSkill}
1284
+ onReview={handleReviewSkill}
1285
+ onAcknowledge={handleAcknowledgeSkill}
1286
+ onDismissReview={handleDismissReview}
1287
+ />
1288
+ ))}
1289
+ </div>
1290
+ </div>
1291
+ );
1292
+ };
1293
+
1294
+ return (
1295
+ <div>
1296
+ {/* Stats bar */}
1297
+ <div className="flex items-center gap-4 mb-4 text-[11px] text-[var(--muted)]">
1298
+ <span>
1299
+ {totalCount} skill{totalCount !== 1 ? "s" : ""}
1300
+ </span>
1301
+ <span>{activeCount} active</span>
1302
+ <span>{inactive.length} inactive</span>
1303
+ {attention.length > 0 && (
1304
+ <span className="text-[#f39c12]">
1305
+ {attention.length} need{attention.length === 1 ? "s" : ""} attention
1306
+ </span>
1307
+ )}
1308
+ </div>
1309
+
1310
+ {/* Toolbar */}
1311
+ <div className="flex flex-wrap items-center gap-3 mb-6 p-3 border border-border/40 bg-card/60 backdrop-blur-md rounded-2xl shadow-sm">
1312
+ <Input
1313
+ type="text"
1314
+ placeholder="Filter skills..."
1315
+ value={filterText}
1316
+ onChange={(e) => setFilterText(e.target.value)}
1317
+ className="w-[240px] h-9 bg-bg/50 border-border/50 focus-visible:ring-accent rounded-xl text-xs"
1318
+ />
1319
+
1320
+ <span className="flex-1" />
1321
+
1322
+ <Button
1323
+ variant={skillCreateFormOpen ? "ghost" : "default"}
1324
+ size="sm"
1325
+ className={
1326
+ skillCreateFormOpen
1327
+ ? "h-9 px-4 font-bold text-muted hover:text-txt"
1328
+ : "h-9 px-4 font-bold tracking-wide shadow-sm"
1329
+ }
1330
+ onClick={() => setState("skillCreateFormOpen", !skillCreateFormOpen)}
1331
+ >
1332
+ {skillCreateFormOpen ? "Cancel" : "+ New Skill"}
1333
+ </Button>
1334
+ <Button
1335
+ variant="default"
1336
+ size="sm"
1337
+ className="h-9 px-4 font-bold tracking-wide shadow-sm"
1338
+ onClick={() => setInstallModalOpen(true)}
1339
+ >
1340
+ Browse Marketplace
1341
+ </Button>
1342
+ <Button
1343
+ variant="ghost"
1344
+ size="sm"
1345
+ className="h-9 px-4 font-bold text-muted hover:text-txt"
1346
+ onClick={() => refreshSkills()}
1347
+ title="Refresh Skills List"
1348
+ >
1349
+ Refresh
1350
+ </Button>
1351
+ </div>
1352
+
1353
+ {/* Create form */}
1354
+ {skillCreateFormOpen && (
1355
+ <CreateSkillForm
1356
+ skillCreateName={skillCreateName}
1357
+ skillCreateDescription={skillCreateDescription}
1358
+ skillCreating={skillCreating}
1359
+ setState={setState}
1360
+ onCancel={handleCancelCreate}
1361
+ onCreate={handleCreateSkill}
1362
+ />
1363
+ )}
1364
+
1365
+ {/* Skill grid — grouped by status */}
1366
+ {skills.length === 0 ? (
1367
+ <div className="text-center py-16">
1368
+ <div className="text-[var(--muted)] text-sm mb-2">
1369
+ No Skills Installed
1370
+ </div>
1371
+ <div className="text-[var(--muted)] text-[11px] mb-4">
1372
+ Install skills from the marketplace or create your own.
1373
+ </div>
1374
+ <div className="flex justify-center gap-3">
1375
+ <Button
1376
+ variant="default"
1377
+ size="sm"
1378
+ className="h-10 px-6 font-bold tracking-wide shadow-sm"
1379
+ onClick={() => setInstallModalOpen(true)}
1380
+ >
1381
+ Browse Marketplace
1382
+ </Button>
1383
+ <Button
1384
+ variant="ghost"
1385
+ size="sm"
1386
+ className="h-10 px-6 font-bold text-muted hover:text-txt"
1387
+ onClick={() => setState("skillCreateFormOpen", true)}
1388
+ >
1389
+ Create Skill
1390
+ </Button>
1391
+ </div>
1392
+ </div>
1393
+ ) : allVisible.length === 0 ? (
1394
+ <div className="text-center py-12 text-[var(--muted)] text-xs">
1395
+ No skills match filtering "{filterText}"
1396
+ </div>
1397
+ ) : (
1398
+ <div>
1399
+ {renderGroup("Needs Attention", attention, "#f39c12")}
1400
+ {renderGroup("Active", active, "var(--ok, #16a34a)")}
1401
+ {renderGroup("Inactive", inactive)}
1402
+ </div>
1403
+ )}
1404
+
1405
+ {/* Edit modal */}
1406
+ {editingSkill && (
1407
+ <EditSkillModal
1408
+ skillId={editingSkill.id}
1409
+ skillName={editingSkill.name}
1410
+ onClose={() => setEditingSkill(null)}
1411
+ onSaved={() => void refreshSkills()}
1412
+ />
1413
+ )}
1414
+
1415
+ {/* Install modal */}
1416
+ {installModalOpen && (
1417
+ <InstallModal
1418
+ skills={skills}
1419
+ skillsMarketplaceQuery={skillsMarketplaceQuery}
1420
+ skillsMarketplaceResults={skillsMarketplaceResults}
1421
+ skillsMarketplaceError={skillsMarketplaceError}
1422
+ skillsMarketplaceLoading={skillsMarketplaceLoading}
1423
+ skillsMarketplaceAction={skillsMarketplaceAction}
1424
+ skillsMarketplaceManualGithubUrl={skillsMarketplaceManualGithubUrl}
1425
+ searchSkillsMarketplace={searchSkillsMarketplace}
1426
+ installSkillFromMarketplace={installSkillFromMarketplace}
1427
+ uninstallMarketplaceSkill={uninstallMarketplaceSkill}
1428
+ installSkillFromGithubUrl={installSkillFromGithubUrl}
1429
+ setState={setState}
1430
+ onClose={() => setInstallModalOpen(false)}
1431
+ />
1432
+ )}
1433
+ </div>
1434
+ );
1435
+ }