@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.
- package/LICENSE +21 -0
- package/package.json +90 -0
- package/src/App.tsx +472 -0
- package/src/actions/character.test.ts +139 -0
- package/src/actions/character.ts +152 -0
- package/src/actions/chat-helpers.ts +100 -0
- package/src/actions/cloud.ts +59 -0
- package/src/actions/index.ts +12 -0
- package/src/actions/lifecycle.ts +175 -0
- package/src/actions/onboarding.ts +46 -0
- package/src/actions/triggers.ts +190 -0
- package/src/ambient.d.ts +16 -0
- package/src/api/client.ts +5516 -0
- package/src/api/index.ts +1 -0
- package/src/autonomy/index.ts +477 -0
- package/src/bridge/capacitor-bridge.ts +295 -0
- package/src/bridge/electrobun-rpc.ts +58 -0
- package/src/bridge/electrobun-runtime.ts +28 -0
- package/src/bridge/index.ts +5 -0
- package/src/bridge/native-plugins.ts +134 -0
- package/src/bridge/plugin-bridge.ts +352 -0
- package/src/bridge/storage-bridge.ts +162 -0
- package/src/chat/index.ts +250 -0
- package/src/coding/index.ts +43 -0
- package/src/components/AdvancedPageView.tsx +362 -0
- package/src/components/AgentActivityBox.tsx +49 -0
- package/src/components/ApiKeyConfig.tsx +224 -0
- package/src/components/AppsPageView.tsx +52 -0
- package/src/components/AppsView.tsx +293 -0
- package/src/components/AvatarLoader.tsx +86 -0
- package/src/components/AvatarSelector.tsx +223 -0
- package/src/components/BscTradePanel.tsx +549 -0
- package/src/components/BugReportModal.tsx +499 -0
- package/src/components/CharacterView.tsx +1645 -0
- package/src/components/ChatAvatar.test.ts +96 -0
- package/src/components/ChatAvatar.tsx +147 -0
- package/src/components/ChatComposer.tsx +330 -0
- package/src/components/ChatMessage.tsx +448 -0
- package/src/components/ChatModalView.test.tsx +118 -0
- package/src/components/ChatModalView.tsx +125 -0
- package/src/components/ChatView.tsx +992 -0
- package/src/components/CloudSourceControls.tsx +80 -0
- package/src/components/CodingAgentSettingsSection.tsx +536 -0
- package/src/components/CommandPalette.tsx +284 -0
- package/src/components/CompanionSceneHost.tsx +497 -0
- package/src/components/CompanionShell.tsx +31 -0
- package/src/components/CompanionView.tsx +109 -0
- package/src/components/ConfigPageView.tsx +758 -0
- package/src/components/ConfigSaveFooter.tsx +41 -0
- package/src/components/ConfirmModal.tsx +379 -0
- package/src/components/ConnectionFailedBanner.tsx +91 -0
- package/src/components/ConnectorsPageView.tsx +13 -0
- package/src/components/ConversationsSidebar.tsx +279 -0
- package/src/components/CustomActionEditor.tsx +1125 -0
- package/src/components/CustomActionsPanel.tsx +288 -0
- package/src/components/CustomActionsView.tsx +322 -0
- package/src/components/DatabasePageView.tsx +55 -0
- package/src/components/DatabaseView.tsx +814 -0
- package/src/components/ElizaCloudDashboard.tsx +1696 -0
- package/src/components/EmotePicker.tsx +529 -0
- package/src/components/ErrorBoundary.tsx +76 -0
- package/src/components/FineTuningView.tsx +1077 -0
- package/src/components/GameView.tsx +552 -0
- package/src/components/GameViewOverlay.tsx +133 -0
- package/src/components/GlobalEmoteOverlay.tsx +155 -0
- package/src/components/Header.test.tsx +413 -0
- package/src/components/Header.tsx +403 -0
- package/src/components/HeartbeatsView.tsx +1003 -0
- package/src/components/InventoryView.tsx +385 -0
- package/src/components/KnowledgeView.tsx +1128 -0
- package/src/components/LanguageDropdown.tsx +188 -0
- package/src/components/LifoMonitorPanel.tsx +196 -0
- package/src/components/LifoSandboxView.tsx +499 -0
- package/src/components/LoadingScreen.tsx +77 -0
- package/src/components/LogsPageView.tsx +17 -0
- package/src/components/LogsView.tsx +239 -0
- package/src/components/MediaGalleryView.tsx +433 -0
- package/src/components/MediaSettingsSection.tsx +893 -0
- package/src/components/MessageContent.tsx +815 -0
- package/src/components/OnboardingWizard.test.tsx +107 -0
- package/src/components/OnboardingWizard.tsx +189 -0
- package/src/components/PairingView.tsx +110 -0
- package/src/components/PermissionsSection.tsx +1186 -0
- package/src/components/PluginsPageView.tsx +9 -0
- package/src/components/PluginsView.tsx +3157 -0
- package/src/components/ProviderSwitcher.tsx +908 -0
- package/src/components/RestartBanner.tsx +76 -0
- package/src/components/RuntimeView.tsx +460 -0
- package/src/components/SaveCommandModal.tsx +211 -0
- package/src/components/SecretsView.tsx +569 -0
- package/src/components/SettingsView.tsx +825 -0
- package/src/components/ShellOverlays.tsx +41 -0
- package/src/components/ShortcutsOverlay.tsx +155 -0
- package/src/components/SkillsView.tsx +1435 -0
- package/src/components/StartupFailureView.tsx +63 -0
- package/src/components/StreamView.tsx +483 -0
- package/src/components/StripeEmbeddedCheckout.tsx +155 -0
- package/src/components/SubscriptionStatus.tsx +640 -0
- package/src/components/SystemWarningBanner.tsx +71 -0
- package/src/components/ThemeToggle.tsx +100 -0
- package/src/components/TrajectoriesView.tsx +526 -0
- package/src/components/TrajectoryDetailView.tsx +426 -0
- package/src/components/TriggersView.tsx +1 -0
- package/src/components/VectorBrowserView.tsx +1633 -0
- package/src/components/VoiceConfigView.tsx +675 -0
- package/src/components/VrmStage.test.ts +219 -0
- package/src/components/VrmStage.tsx +432 -0
- package/src/components/WhatsAppQrOverlay.tsx +230 -0
- package/src/components/__tests__/chainConfig.test.ts +220 -0
- package/src/components/apps/AppDetailPane.tsx +242 -0
- package/src/components/apps/AppsCatalogGrid.tsx +137 -0
- package/src/components/apps/extensions/HyperscapeAppDetailPanel.tsx +577 -0
- package/src/components/apps/extensions/registry.ts +16 -0
- package/src/components/apps/extensions/types.ts +9 -0
- package/src/components/apps/helpers.ts +44 -0
- package/src/components/avatar/VrmAnimationLoader.test.ts +164 -0
- package/src/components/avatar/VrmAnimationLoader.ts +151 -0
- package/src/components/avatar/VrmBlinkController.ts +118 -0
- package/src/components/avatar/VrmCameraManager.ts +407 -0
- package/src/components/avatar/VrmEngine.ts +2678 -0
- package/src/components/avatar/VrmFootShadow.ts +96 -0
- package/src/components/avatar/VrmViewer.tsx +421 -0
- package/src/components/avatar/__tests__/VrmCameraManager.test.ts +168 -0
- package/src/components/avatar/__tests__/VrmEngine.test.ts +1574 -0
- package/src/components/avatar/mixamoVRMRigMap.ts +62 -0
- package/src/components/avatar/retargetMixamoFbxToVrm.ts +144 -0
- package/src/components/avatar/retargetMixamoGltfToVrm.ts +119 -0
- package/src/components/chainConfig.ts +380 -0
- package/src/components/companion/CompanionHeader.tsx +47 -0
- package/src/components/companion/CompanionSceneHost.tsx +1 -0
- package/src/components/companion/VrmStage.tsx +2 -0
- package/src/components/companion/__tests__/walletUtils.test.ts +742 -0
- package/src/components/companion/walletUtils.ts +290 -0
- package/src/components/companion-shell-styles.test.ts +142 -0
- package/src/components/companion-shell-styles.ts +270 -0
- package/src/components/confirm-delete-control.tsx +69 -0
- package/src/components/conversations/ConversationListItem.tsx +185 -0
- package/src/components/conversations/conversation-utils.ts +151 -0
- package/src/components/format.ts +131 -0
- package/src/components/index.ts +94 -0
- package/src/components/inventory/CopyableAddress.tsx +41 -0
- package/src/components/inventory/InventoryToolbar.tsx +142 -0
- package/src/components/inventory/NftGrid.tsx +99 -0
- package/src/components/inventory/TokenLogo.tsx +71 -0
- package/src/components/inventory/TokensTable.tsx +216 -0
- package/src/components/inventory/constants.ts +170 -0
- package/src/components/inventory/index.ts +29 -0
- package/src/components/inventory/media-url.test.ts +38 -0
- package/src/components/inventory/media-url.ts +36 -0
- package/src/components/inventory/useInventoryData.ts +460 -0
- package/src/components/knowledge-upload-image.ts +215 -0
- package/src/components/labels.ts +46 -0
- package/src/components/onboarding/ActivateStep.tsx +30 -0
- package/src/components/onboarding/ConnectionStep.tsx +1530 -0
- package/src/components/onboarding/IdentityStep.tsx +147 -0
- package/src/components/onboarding/OnboardingPanel.tsx +39 -0
- package/src/components/onboarding/OnboardingStepNav.tsx +31 -0
- package/src/components/onboarding/PermissionsStep.tsx +20 -0
- package/src/components/onboarding/RpcStep.tsx +402 -0
- package/src/components/onboarding/WakeUpStep.tsx +184 -0
- package/src/components/permissions/PermissionIcon.tsx +25 -0
- package/src/components/permissions/StreamingPermissions.tsx +413 -0
- package/src/components/plugins/showcase-data.ts +481 -0
- package/src/components/shared/ShellHeaderControls.tsx +193 -0
- package/src/components/shared-companion-scene-context.ts +15 -0
- package/src/components/skeletons.tsx +88 -0
- package/src/components/stream/ActivityFeed.tsx +113 -0
- package/src/components/stream/AvatarPip.tsx +10 -0
- package/src/components/stream/ChatContent.tsx +126 -0
- package/src/components/stream/ChatTicker.tsx +55 -0
- package/src/components/stream/IdleContent.tsx +73 -0
- package/src/components/stream/StatusBar.tsx +469 -0
- package/src/components/stream/StreamSettings.tsx +506 -0
- package/src/components/stream/StreamTerminal.tsx +94 -0
- package/src/components/stream/StreamVoiceConfig.tsx +160 -0
- package/src/components/stream/helpers.ts +134 -0
- package/src/components/stream/overlays/OverlayLayer.tsx +75 -0
- package/src/components/stream/overlays/built-in/ActionTickerWidget.tsx +64 -0
- package/src/components/stream/overlays/built-in/AlertPopupWidget.tsx +87 -0
- package/src/components/stream/overlays/built-in/BrandingWidget.tsx +51 -0
- package/src/components/stream/overlays/built-in/CustomHtmlWidget.tsx +105 -0
- package/src/components/stream/overlays/built-in/PeonGlassWidget.tsx +265 -0
- package/src/components/stream/overlays/built-in/PeonHudWidget.tsx +247 -0
- package/src/components/stream/overlays/built-in/PeonSakuraWidget.tsx +278 -0
- package/src/components/stream/overlays/built-in/ThoughtBubbleWidget.tsx +77 -0
- package/src/components/stream/overlays/built-in/ViewerCountWidget.tsx +46 -0
- package/src/components/stream/overlays/built-in/index.ts +13 -0
- package/src/components/stream/overlays/registry.ts +22 -0
- package/src/components/stream/overlays/types.ts +90 -0
- package/src/components/stream/overlays/useOverlayLayout.ts +218 -0
- package/src/components/trajectory-format.ts +50 -0
- package/src/components/ui-badges.tsx +109 -0
- package/src/components/ui-switch.tsx +57 -0
- package/src/components/vector-browser-three.ts +27 -0
- package/src/config/config-catalog.ts +1092 -0
- package/src/config/config-field.tsx +1901 -0
- package/src/config/config-renderer.tsx +730 -0
- package/src/config/index.ts +11 -0
- package/src/config/ui-renderer.tsx +1751 -0
- package/src/config/ui-spec.ts +256 -0
- package/src/events/index.ts +89 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/useBugReport.tsx +43 -0
- package/src/hooks/useCanvasWindow.ts +372 -0
- package/src/hooks/useChatAvatarVoice.ts +111 -0
- package/src/hooks/useContextMenu.ts +127 -0
- package/src/hooks/useKeyboardShortcuts.ts +86 -0
- package/src/hooks/useLifoSync.ts +143 -0
- package/src/hooks/useMemoryMonitor.ts +334 -0
- package/src/hooks/useRenderGuard.ts +43 -0
- package/src/hooks/useRetakeCapture.ts +67 -0
- package/src/hooks/useStreamPopoutNavigation.ts +27 -0
- package/src/hooks/useTimeout.ts +37 -0
- package/src/hooks/useVoiceChat.ts +1441 -0
- package/src/hooks/useWhatsAppPairing.ts +123 -0
- package/src/i18n/index.ts +76 -0
- package/src/i18n/locales/en.json +1194 -0
- package/src/i18n/locales/es.json +1194 -0
- package/src/i18n/locales/ko.json +1194 -0
- package/src/i18n/locales/pt.json +1194 -0
- package/src/i18n/locales/zh-CN.json +1194 -0
- package/src/i18n/messages.ts +21 -0
- package/src/index.ts +6 -0
- package/src/navigation/index.ts +282 -0
- package/src/navigation.test.ts +189 -0
- package/src/onboarding-config.test.ts +104 -0
- package/src/onboarding-config.ts +114 -0
- package/src/platform/browser-launch.test.ts +94 -0
- package/src/platform/browser-launch.ts +149 -0
- package/src/platform/index.ts +58 -0
- package/src/platform/init.ts +236 -0
- package/src/platform/lifo.ts +215 -0
- package/src/providers/index.ts +99 -0
- package/src/state/AppContext.tsx +5846 -0
- package/src/state/index.ts +6 -0
- package/src/state/internal.ts +86 -0
- package/src/state/onboarding-resume.test.ts +135 -0
- package/src/state/onboarding-resume.ts +263 -0
- package/src/state/parsers.test.ts +124 -0
- package/src/state/parsers.ts +308 -0
- package/src/state/persistence.ts +321 -0
- package/src/state/shell-routing.ts +32 -0
- package/src/state/types.ts +701 -0
- package/src/state/ui-preferences.ts +3 -0
- package/src/state/useApp.ts +23 -0
- package/src/state/vrm.ts +76 -0
- package/src/stories/AppMockProvider.tsx +32 -0
- package/src/stories/ChatEmptyState.stories.tsx +27 -0
- package/src/stories/ChatMessage.stories.tsx +115 -0
- package/src/stories/CompanionHeader.stories.tsx +74 -0
- package/src/stories/CompanionView.stories.tsx +33 -0
- package/src/stories/ConversationListItem.stories.tsx +102 -0
- package/src/stories/TypingIndicator.stories.tsx +28 -0
- package/src/styles/anime.css +6324 -0
- package/src/styles/base.css +196 -0
- package/src/styles/onboarding-game.css +738 -0
- package/src/styles/styles.css +2087 -0
- package/src/styles/xterm.css +241 -0
- package/src/types/index.ts +715 -0
- package/src/types/react-test-renderer.d.ts +45 -0
- package/src/utils/asset-url.ts +110 -0
- package/src/utils/assistant-text.ts +172 -0
- package/src/utils/clipboard.ts +41 -0
- package/src/utils/desktop-dialogs.ts +80 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/number-parsing.ts +125 -0
- package/src/utils/openExternalUrl.ts +20 -0
- package/src/utils/spoken-text.ts +65 -0
- package/src/utils/streaming-text.ts +120 -0
- package/src/voice/index.ts +1 -0
- package/src/voice/types.ts +197 -0
- package/src/wallet-rpc.ts +176 -0
- package/test/app/AppContext.pty-sessions.test.tsx +143 -0
- package/test/app/MessageContent.test.tsx +326 -0
- package/test/app/PermissionsOnboarding.test.tsx +356 -0
- package/test/app/PermissionsSection.test.tsx +573 -0
- package/test/app/advanced-trajectory-fine-tuning.e2e.test.ts +393 -0
- package/test/app/agent-activity-box.test.tsx +132 -0
- package/test/app/agent-transfer-lock.test.ts +274 -0
- package/test/app/api-client-electron-fallback.test.ts +139 -0
- package/test/app/api-client-timeout.test.ts +75 -0
- package/test/app/api-client-ws.test.ts +98 -0
- package/test/app/api-client.ws-max-reconnect.test.ts +139 -0
- package/test/app/api-client.ws-reconnect.test.ts +157 -0
- package/test/app/app-context-autonomy-events.test.ts +478 -0
- package/test/app/apps-page-view.test.ts +114 -0
- package/test/app/apps-view.test.ts +769 -0
- package/test/app/autonomous-workflows.e2e.test.ts +765 -0
- package/test/app/autonomy-events.test.ts +150 -0
- package/test/app/avatar-selector.test.tsx +52 -0
- package/test/app/bsc-trade-panel.test.tsx +134 -0
- package/test/app/bug-report-modal.test.tsx +353 -0
- package/test/app/character-customization.e2e.test.ts +1199 -0
- package/test/app/chat-advanced-features.e2e.test.ts +706 -0
- package/test/app/chat-composer.test.tsx +181 -0
- package/test/app/chat-language-header.test.ts +64 -0
- package/test/app/chat-message.test.tsx +222 -0
- package/test/app/chat-modal-view.test.tsx +191 -0
- package/test/app/chat-routine-filter.test.ts +96 -0
- package/test/app/chat-send-lock.test.ts +1465 -0
- package/test/app/chat-stream-api-client.test.tsx +390 -0
- package/test/app/chat-view-game-modal.test.tsx +661 -0
- package/test/app/chat-view.test.tsx +877 -0
- package/test/app/cloud-api.e2e.test.ts +258 -0
- package/test/app/cloud-login-flow.e2e.test.ts +494 -0
- package/test/app/cloud-login-lock.test.ts +411 -0
- package/test/app/command-palette.test.tsx +184 -0
- package/test/app/command-registry.test.ts +75 -0
- package/test/app/companion-greeting-wave.test.tsx +425 -0
- package/test/app/companion-stale-conversation.test.tsx +447 -0
- package/test/app/companion-view.test.tsx +686 -0
- package/test/app/confirm-delete-control.test.ts +79 -0
- package/test/app/confirm-modal.test.tsx +219 -0
- package/test/app/connectors-ui.e2e.test.ts +508 -0
- package/test/app/conversations-sidebar-game-modal.test.tsx +260 -0
- package/test/app/conversations-sidebar.test.tsx +160 -0
- package/test/app/custom-actions-smoke.test.ts +387 -0
- package/test/app/custom-avatar-api-client.test.ts +207 -0
- package/test/app/desktop-utils.test.ts +145 -0
- package/test/app/electrobun-rpc-bridge.test.ts +83 -0
- package/test/app/events.test.ts +88 -0
- package/test/app/export-import-flows.e2e.test.ts +700 -0
- package/test/app/fine-tuning-view.test.ts +471 -0
- package/test/app/game-view-auth-session.test.tsx +186 -0
- package/test/app/game-view.test.ts +444 -0
- package/test/app/global-emote-overlay.test.tsx +106 -0
- package/test/app/header-status.test.tsx +149 -0
- package/test/app/i18n.test.ts +152 -0
- package/test/app/inventory-bsc-view.test.ts +940 -0
- package/test/app/knowledge-ui.e2e.test.ts +762 -0
- package/test/app/knowledge-upload-helpers.test.ts +124 -0
- package/test/app/lifecycle-lock.test.ts +267 -0
- package/test/app/lifo-popout-utils.test.ts +208 -0
- package/test/app/lifo-safe-endpoint.test.ts +34 -0
- package/test/app/loading-screen.test.tsx +45 -0
- package/test/app/memory-monitor.test.ts +332 -0
- package/test/app/navigation.test.tsx +22 -0
- package/test/app/onboarding-finish-lock.test.ts +663 -0
- package/test/app/onboarding-language.test.tsx +160 -0
- package/test/app/onboarding-steps.test.tsx +375 -0
- package/test/app/open-external-url.test.ts +65 -0
- package/test/app/pages-navigation-smoke.e2e.test.ts +633 -0
- package/test/app/pairing-lock.test.ts +260 -0
- package/test/app/pairing-view.test.tsx +74 -0
- package/test/app/permissions-section.test.ts +432 -0
- package/test/app/plugin-bridge.test.ts +109 -0
- package/test/app/plugins-ui.e2e.test.ts +605 -0
- package/test/app/plugins-view-game-modal.test.tsx +650 -0
- package/test/app/plugins-view-toggle-restart.test.ts +129 -0
- package/test/app/provider-dropdown-default.test.tsx +302 -0
- package/test/app/restart-banner.test.tsx +197 -0
- package/test/app/retake-capture.test.ts +84 -0
- package/test/app/sandbox-api-client.test.ts +108 -0
- package/test/app/save-command-modal.test.tsx +109 -0
- package/test/app/secrets-view.test.tsx +92 -0
- package/test/app/settings-control-styles.test.tsx +142 -0
- package/test/app/settings-reset.e2e.test.ts +726 -0
- package/test/app/settings-sections.e2e.test.ts +614 -0
- package/test/app/shared-format.test.ts +44 -0
- package/test/app/shared-switch.test.ts +69 -0
- package/test/app/shell-mode-switching.e2e.test.ts +829 -0
- package/test/app/shell-mode-tab-memory.test.tsx +58 -0
- package/test/app/shell-overlays.test.tsx +50 -0
- package/test/app/shortcuts-overlay.test.tsx +111 -0
- package/test/app/sse-interruption.test.ts +122 -0
- package/test/app/startup-asset-missing.e2e.test.ts +126 -0
- package/test/app/startup-backend-missing.e2e.test.ts +118 -0
- package/test/app/startup-chat.e2e.test.ts +305 -0
- package/test/app/startup-conversation-restore.test.tsx +344 -0
- package/test/app/startup-failure-view.test.tsx +103 -0
- package/test/app/startup-onboarding.e2e.test.ts +618 -0
- package/test/app/startup-timeout.test.tsx +80 -0
- package/test/app/startup-token-401.e2e.test.ts +103 -0
- package/test/app/stream-helpers.test.ts +46 -0
- package/test/app/stream-popout-navigation.test.tsx +41 -0
- package/test/app/stream-status-bar.test.tsx +89 -0
- package/test/app/theme-toggle.test.tsx +33 -0
- package/test/app/training-api-client.test.ts +128 -0
- package/test/app/trajectories-view.test.tsx +220 -0
- package/test/app/triggers-api-client.test.ts +77 -0
- package/test/app/triggers-navigation.test.ts +113 -0
- package/test/app/triggers-view.e2e.test.ts +674 -0
- package/test/app/update-channel-lock.test.ts +259 -0
- package/test/app/vector-browser.async-cleanup.test.tsx +367 -0
- package/test/app/vector-browser.e2e.test.ts +653 -0
- package/test/app/vrm-stage.test.tsx +351 -0
- package/test/app/vrm-viewer.test.tsx +298 -0
- package/test/app/wallet-api-save-lock.test.ts +298 -0
- package/test/app/wallet-hooks.test.ts +405 -0
- package/test/app/wallet-ui-flows.e2e.test.ts +556 -0
- package/test/avatar/asset-url.test.ts +90 -0
- package/test/avatar/avatar-selector.test.ts +173 -0
- package/test/avatar/mixamo-vrm-rig-map.test.ts +111 -0
- package/test/avatar/voice-chat-streaming-text.test.ts +96 -0
- package/test/avatar/voice-chat.test.ts +391 -0
- package/test/ui/command-palette-commands.test.ts +57 -0
- package/test/ui/ui-renderer.test.ts +39 -0
- package/tsconfig.build.json +19 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageContent — Renders a chat message's content.
|
|
3
|
+
*
|
|
4
|
+
* Follows the json-render pattern: specs are rendered client-side from JSON
|
|
5
|
+
* in the agent's text response. No server-side block extraction needed.
|
|
6
|
+
*
|
|
7
|
+
* Client-side detection:
|
|
8
|
+
* 1. [CONFIG:pluginId] markers → inline plugin config form (ConfigRenderer)
|
|
9
|
+
* 2. Fenced UiSpec JSON → interactive UI (UiRenderer)
|
|
10
|
+
* 3. Everything else → plain text
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConversationMessage, PluginInfo } from "@elizaos/app-core/api";
|
|
14
|
+
import { client } from "@elizaos/app-core/api";
|
|
15
|
+
import { paramsToSchema } from "@elizaos/app-core/components";
|
|
16
|
+
import {
|
|
17
|
+
ConfigRenderer,
|
|
18
|
+
defaultRegistry,
|
|
19
|
+
type JsonSchemaObject,
|
|
20
|
+
type PatchOp,
|
|
21
|
+
UiRenderer,
|
|
22
|
+
type UiSpec,
|
|
23
|
+
} from "@elizaos/app-core/config";
|
|
24
|
+
import { useApp } from "@elizaos/app-core/state";
|
|
25
|
+
import type { ConfigUiHint } from "@elizaos/app-core/types";
|
|
26
|
+
import { stripAssistantStageDirections } from "@elizaos/app-core/utils";
|
|
27
|
+
import { Button } from "@elizaos/ui";
|
|
28
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
29
|
+
|
|
30
|
+
/** Reject prototype-pollution keys that should never be traversed or rendered. */
|
|
31
|
+
const BLOCKED_IDS = new Set(["__proto__", "constructor", "prototype"]);
|
|
32
|
+
const SAFE_PLUGIN_ID_RE = /^[\w-]+$/;
|
|
33
|
+
|
|
34
|
+
function createSafeRecord(): Record<string, unknown> {
|
|
35
|
+
return Object.create(null) as Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sanitizePatchValue(value: unknown): unknown {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value.map((item) => sanitizePatchValue(item));
|
|
41
|
+
}
|
|
42
|
+
if (!value || typeof value !== "object") {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const safe = createSafeRecord();
|
|
47
|
+
for (const [key, nestedValue] of Object.entries(
|
|
48
|
+
value as Record<string, unknown>,
|
|
49
|
+
)) {
|
|
50
|
+
if (BLOCKED_IDS.has(key)) continue;
|
|
51
|
+
safe[key] = sanitizePatchValue(nestedValue);
|
|
52
|
+
}
|
|
53
|
+
return safe;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSafeNormalizedPluginId(id: string): boolean {
|
|
57
|
+
return !BLOCKED_IDS.has(id) && SAFE_PLUGIN_ID_RE.test(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface MessageContentProps {
|
|
61
|
+
message: ConversationMessage;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Segment types ───────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
type Segment =
|
|
67
|
+
| { kind: "text"; text: string }
|
|
68
|
+
| { kind: "config"; pluginId: string }
|
|
69
|
+
| { kind: "ui-spec"; spec: UiSpec; raw: string };
|
|
70
|
+
|
|
71
|
+
// ── Detection ───────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const CONFIG_RE = /\[CONFIG:([@\w][\w@./:-]*)\]/g;
|
|
74
|
+
const FENCED_JSON_RE = /```(?:json)?\s*\n([\s\S]*?)```/g;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip ElizaOS action XML blocks (`<actions>...</actions>` and
|
|
78
|
+
* `<params>...</params>`) from displayed text. These are framework
|
|
79
|
+
* metadata, not user-facing content.
|
|
80
|
+
*/
|
|
81
|
+
const ACTION_XML_RE =
|
|
82
|
+
/\s*<actions>[\s\S]*?<\/actions>\s*|\s*<params>[\s\S]*?<\/params>\s*/g;
|
|
83
|
+
const HIDDEN_XML_BLOCK_RE =
|
|
84
|
+
/<(think|analysis|reasoning|scratchpad|tool_calls?|tools?)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi;
|
|
85
|
+
|
|
86
|
+
function extractXmlTag(
|
|
87
|
+
raw: string,
|
|
88
|
+
tag: string,
|
|
89
|
+
opts?: { allowPartial?: boolean },
|
|
90
|
+
): string | null {
|
|
91
|
+
const open = `<${tag}>`;
|
|
92
|
+
const close = `</${tag}>`;
|
|
93
|
+
const start = raw.indexOf(open);
|
|
94
|
+
if (start < 0) return null;
|
|
95
|
+
|
|
96
|
+
const contentStart = start + open.length;
|
|
97
|
+
const end = raw.indexOf(close, contentStart);
|
|
98
|
+
if (end < 0) {
|
|
99
|
+
return opts?.allowPartial ? raw.slice(contentStart) : null;
|
|
100
|
+
}
|
|
101
|
+
return raw.slice(contentStart, end);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeDisplayText(text: string): string {
|
|
105
|
+
let normalized = text;
|
|
106
|
+
|
|
107
|
+
// Hide framework-selected actions and tool params from chat bubbles.
|
|
108
|
+
normalized = normalized.replace(ACTION_XML_RE, "");
|
|
109
|
+
normalized = normalized.replace(HIDDEN_XML_BLOCK_RE, " ");
|
|
110
|
+
|
|
111
|
+
// Some prompts emit structured XML wrappers like:
|
|
112
|
+
// <response><thought>...</thought><text>...</text></response>
|
|
113
|
+
// Show only the user-facing <text>, even while it is still streaming.
|
|
114
|
+
if (normalized.includes("<response>")) {
|
|
115
|
+
const wrappedText = extractXmlTag(normalized, "text", {
|
|
116
|
+
allowPartial: true,
|
|
117
|
+
});
|
|
118
|
+
if (wrappedText !== null) {
|
|
119
|
+
normalized = wrappedText;
|
|
120
|
+
} else {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Drop any leftover wrapper tags without disturbing plain text.
|
|
126
|
+
normalized = normalized.replace(/<\/?(response|text|thought)\b[^>]*>/gi, "");
|
|
127
|
+
normalized = stripAssistantStageDirections(normalized);
|
|
128
|
+
return normalized.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function tryParse(s: string): unknown {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(s);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isUiSpec(obj: unknown): obj is UiSpec {
|
|
140
|
+
if (!obj || typeof obj !== "object") return false;
|
|
141
|
+
const c = obj as Record<string, unknown>;
|
|
142
|
+
return (
|
|
143
|
+
typeof c.root === "string" &&
|
|
144
|
+
typeof c.elements === "object" &&
|
|
145
|
+
c.elements !== null
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── JSONL patch support (Chat Mode) ─────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Quick pre-check: does this line look like a JSON patch object?
|
|
153
|
+
* Handles both compact `{"op":` and spaced `{ "op":` formats.
|
|
154
|
+
*/
|
|
155
|
+
export function looksLikePatch(trimmed: string): boolean {
|
|
156
|
+
if (!trimmed.startsWith("{")) return false;
|
|
157
|
+
return trimmed.includes('"op"') && trimmed.includes('"path"');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Try to parse a single line as an RFC 6902 JSON Patch operation. */
|
|
161
|
+
export function tryParsePatch(line: string): PatchOp | null {
|
|
162
|
+
const t = line.trim();
|
|
163
|
+
if (!looksLikePatch(t)) return null;
|
|
164
|
+
try {
|
|
165
|
+
const obj = JSON.parse(t) as Record<string, unknown>;
|
|
166
|
+
if (typeof obj.op === "string" && typeof obj.path === "string")
|
|
167
|
+
return obj as PatchOp;
|
|
168
|
+
return null;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Apply a list of RFC 6902 patches to build a UiSpec.
|
|
176
|
+
*
|
|
177
|
+
* Only handles the paths the catalog emits:
|
|
178
|
+
* /root → spec.root
|
|
179
|
+
* /elements/<id> → spec.elements[id]
|
|
180
|
+
* /state/<key> → spec.state[key]
|
|
181
|
+
* /state → spec.state (whole object)
|
|
182
|
+
*/
|
|
183
|
+
export function compilePatches(patches: PatchOp[]): UiSpec | null {
|
|
184
|
+
const spec: {
|
|
185
|
+
root?: string;
|
|
186
|
+
elements: Record<string, unknown>;
|
|
187
|
+
state: Record<string, unknown>;
|
|
188
|
+
} = { elements: {}, state: createSafeRecord() };
|
|
189
|
+
|
|
190
|
+
for (const patch of patches) {
|
|
191
|
+
if (patch.op !== "add" && patch.op !== "replace") continue;
|
|
192
|
+
const { path, value } = patch as {
|
|
193
|
+
op: string;
|
|
194
|
+
path: string;
|
|
195
|
+
value: unknown;
|
|
196
|
+
};
|
|
197
|
+
const parts = path.split("/").filter(Boolean);
|
|
198
|
+
if (parts.length === 0) continue;
|
|
199
|
+
|
|
200
|
+
if (parts[0] === "root" && parts.length === 1) {
|
|
201
|
+
spec.root = value as string;
|
|
202
|
+
} else if (parts[0] === "elements" && parts.length === 2) {
|
|
203
|
+
spec.elements[parts[1]] = value;
|
|
204
|
+
} else if (parts[0] === "state" && parts.length === 1) {
|
|
205
|
+
const nextState = sanitizePatchValue(value);
|
|
206
|
+
spec.state =
|
|
207
|
+
nextState && typeof nextState === "object" && !Array.isArray(nextState)
|
|
208
|
+
? (nextState as Record<string, unknown>)
|
|
209
|
+
: createSafeRecord();
|
|
210
|
+
} else if (parts[0] === "state" && parts.length >= 2) {
|
|
211
|
+
// Nested state path: /state/key or /state/key/subkey
|
|
212
|
+
let cursor = spec.state;
|
|
213
|
+
let blockedPath = false;
|
|
214
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
215
|
+
const k = parts[i];
|
|
216
|
+
if (BLOCKED_IDS.has(k)) {
|
|
217
|
+
blockedPath = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
!cursor[k] ||
|
|
222
|
+
typeof cursor[k] !== "object" ||
|
|
223
|
+
Array.isArray(cursor[k])
|
|
224
|
+
) {
|
|
225
|
+
cursor[k] = createSafeRecord();
|
|
226
|
+
}
|
|
227
|
+
cursor = cursor[k] as Record<string, unknown>;
|
|
228
|
+
}
|
|
229
|
+
if (blockedPath) continue;
|
|
230
|
+
const leaf = parts[parts.length - 1];
|
|
231
|
+
if (BLOCKED_IDS.has(leaf)) continue;
|
|
232
|
+
cursor[leaf] = sanitizePatchValue(value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return isUiSpec(spec) ? (spec as unknown as UiSpec) : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Scan `text` for blocks of consecutive JSONL patch lines and return
|
|
241
|
+
* their character regions plus the compiled UiSpec.
|
|
242
|
+
*
|
|
243
|
+
* A patch block is a run of lines where each non-empty line parses as a
|
|
244
|
+
* valid PatchOp. A single empty line between patch lines is allowed.
|
|
245
|
+
*/
|
|
246
|
+
export function findPatchRegions(
|
|
247
|
+
text: string,
|
|
248
|
+
): Array<{ start: number; end: number; spec: UiSpec; raw: string }> {
|
|
249
|
+
const results: Array<{
|
|
250
|
+
start: number;
|
|
251
|
+
end: number;
|
|
252
|
+
spec: UiSpec;
|
|
253
|
+
raw: string;
|
|
254
|
+
}> = [];
|
|
255
|
+
const lines = text.split("\n");
|
|
256
|
+
|
|
257
|
+
let blockStart = -1;
|
|
258
|
+
let blockEnd = 0;
|
|
259
|
+
let patches: PatchOp[] = [];
|
|
260
|
+
let rawLines: string[] = [];
|
|
261
|
+
let pos = 0;
|
|
262
|
+
|
|
263
|
+
const flush = () => {
|
|
264
|
+
if (patches.length >= 1) {
|
|
265
|
+
const spec = compilePatches(patches);
|
|
266
|
+
if (spec) {
|
|
267
|
+
results.push({
|
|
268
|
+
start: blockStart,
|
|
269
|
+
end: blockEnd,
|
|
270
|
+
spec,
|
|
271
|
+
raw: rawLines.join("\n"),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
blockStart = -1;
|
|
276
|
+
patches = [];
|
|
277
|
+
rawLines = [];
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
const line = lines[i];
|
|
282
|
+
// +1 for the newline that split() consumed (except the very last line)
|
|
283
|
+
const lineLen = line.length + (i < lines.length - 1 ? 1 : 0);
|
|
284
|
+
const trimmed = line.trim();
|
|
285
|
+
|
|
286
|
+
if (looksLikePatch(trimmed)) {
|
|
287
|
+
const patch = tryParsePatch(trimmed);
|
|
288
|
+
if (patch) {
|
|
289
|
+
if (blockStart === -1) blockStart = pos;
|
|
290
|
+
patches.push(patch);
|
|
291
|
+
rawLines.push(line);
|
|
292
|
+
blockEnd = pos + lineLen;
|
|
293
|
+
pos += lineLen;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Empty line: peek ahead to see if the next non-empty line is a patch
|
|
299
|
+
if (trimmed.length === 0 && blockStart !== -1) {
|
|
300
|
+
const nextPatch = lines.slice(i + 1).find((l) => l.trim().length > 0);
|
|
301
|
+
if (nextPatch && tryParsePatch(nextPatch) !== null) {
|
|
302
|
+
// Allow the gap and keep going
|
|
303
|
+
pos += lineLen;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Non-patch content — flush any open block
|
|
309
|
+
if (blockStart !== -1) flush();
|
|
310
|
+
pos += lineLen;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (blockStart !== -1) flush();
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Parse message text for [CONFIG:id] markers, fenced UiSpec JSON, and
|
|
319
|
+
* inline JSONL patch blocks (Chat Mode).
|
|
320
|
+
* Returns an array of segments for rendering.
|
|
321
|
+
*/
|
|
322
|
+
function parseSegments(text: string): Segment[] {
|
|
323
|
+
const cleaned = normalizeDisplayText(text);
|
|
324
|
+
if (!cleaned) return [{ kind: "text", text: "" }];
|
|
325
|
+
|
|
326
|
+
// Build a unified list of match regions sorted by position
|
|
327
|
+
const regions: Array<{ start: number; end: number; segment: Segment }> = [];
|
|
328
|
+
|
|
329
|
+
// 1. Find [CONFIG:pluginId] markers
|
|
330
|
+
CONFIG_RE.lastIndex = 0;
|
|
331
|
+
let m: RegExpExecArray | null = CONFIG_RE.exec(cleaned);
|
|
332
|
+
while (m !== null) {
|
|
333
|
+
regions.push({
|
|
334
|
+
start: m.index,
|
|
335
|
+
end: m.index + m[0].length,
|
|
336
|
+
segment: { kind: "config", pluginId: m[1] },
|
|
337
|
+
});
|
|
338
|
+
m = CONFIG_RE.exec(cleaned);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2. Find fenced JSON that is a UiSpec (Generate Mode / legacy format)
|
|
342
|
+
FENCED_JSON_RE.lastIndex = 0;
|
|
343
|
+
m = FENCED_JSON_RE.exec(cleaned);
|
|
344
|
+
while (m !== null) {
|
|
345
|
+
const json = m[1].trim();
|
|
346
|
+
const parsed = tryParse(json);
|
|
347
|
+
if (parsed && isUiSpec(parsed)) {
|
|
348
|
+
regions.push({
|
|
349
|
+
start: m.index,
|
|
350
|
+
end: m.index + m[0].length,
|
|
351
|
+
segment: { kind: "ui-spec", spec: parsed, raw: json },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
m = FENCED_JSON_RE.exec(cleaned);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 3. Find inline JSONL patch blocks (Chat Mode)
|
|
358
|
+
for (const patch of findPatchRegions(cleaned)) {
|
|
359
|
+
// Skip if this region overlaps with an already-found fenced block
|
|
360
|
+
const overlaps = regions.some(
|
|
361
|
+
(r) => patch.start < r.end && patch.end > r.start,
|
|
362
|
+
);
|
|
363
|
+
if (!overlaps) {
|
|
364
|
+
regions.push({
|
|
365
|
+
start: patch.start,
|
|
366
|
+
end: patch.end,
|
|
367
|
+
segment: { kind: "ui-spec", spec: patch.spec, raw: patch.raw },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// No special content found — return plain text
|
|
373
|
+
if (regions.length === 0) {
|
|
374
|
+
return [{ kind: "text", text: cleaned }];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Sort by start position, then interleave with text segments
|
|
378
|
+
regions.sort((a, b) => a.start - b.start);
|
|
379
|
+
const segments: Segment[] = [];
|
|
380
|
+
let cursor = 0;
|
|
381
|
+
|
|
382
|
+
for (const r of regions) {
|
|
383
|
+
// Skip overlapping regions
|
|
384
|
+
if (r.start < cursor) continue;
|
|
385
|
+
|
|
386
|
+
// Push preceding text
|
|
387
|
+
if (r.start > cursor) {
|
|
388
|
+
const t = cleaned.slice(cursor, r.start);
|
|
389
|
+
if (t.trim()) segments.push({ kind: "text", text: t });
|
|
390
|
+
}
|
|
391
|
+
segments.push(r.segment);
|
|
392
|
+
cursor = r.end;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Trailing text
|
|
396
|
+
if (cursor < cleaned.length) {
|
|
397
|
+
const t = cleaned.slice(cursor);
|
|
398
|
+
if (t.trim()) segments.push({ kind: "text", text: t });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return segments;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── InlinePluginConfig ──────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
/** Normalize plugin ID: strip @scope/plugin- prefix so both "discord" and "@elizaos/plugin-discord" resolve. */
|
|
407
|
+
export function normalizePluginId(id: string): string {
|
|
408
|
+
return id.replace(/^@[^/]+\/plugin-/, "");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function buildInlinePluginConfigModel(
|
|
412
|
+
plugin: PluginInfo | null,
|
|
413
|
+
values: Record<string, unknown>,
|
|
414
|
+
): {
|
|
415
|
+
hasConfigurableParams: boolean;
|
|
416
|
+
hints: Record<string, ConfigUiHint>;
|
|
417
|
+
mergedValues: Record<string, unknown>;
|
|
418
|
+
schema: JsonSchemaObject | null;
|
|
419
|
+
setKeys: Set<string>;
|
|
420
|
+
} {
|
|
421
|
+
const pluginParams = plugin?.parameters ?? [];
|
|
422
|
+
const hasConfigurableParams = pluginParams.length > 0;
|
|
423
|
+
if (!hasConfigurableParams || !plugin?.id) {
|
|
424
|
+
return {
|
|
425
|
+
hasConfigurableParams: false,
|
|
426
|
+
hints: {},
|
|
427
|
+
mergedValues: values,
|
|
428
|
+
schema: null,
|
|
429
|
+
setKeys: new Set<string>(),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const auto = paramsToSchema(pluginParams, plugin.id);
|
|
434
|
+
if (plugin.configUiHints) {
|
|
435
|
+
for (const [key, serverHint] of Object.entries(plugin.configUiHints)) {
|
|
436
|
+
auto.hints[key] = { ...auto.hints[key], ...serverHint };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const initialValues: Record<string, unknown> = {};
|
|
441
|
+
const setKeys = new Set<string>();
|
|
442
|
+
for (const param of pluginParams) {
|
|
443
|
+
if (param.isSet) {
|
|
444
|
+
setKeys.add(param.key);
|
|
445
|
+
}
|
|
446
|
+
if (param.isSet && !param.sensitive && param.currentValue != null) {
|
|
447
|
+
initialValues[param.key] = param.currentValue;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const [key, value] of Object.entries(values)) {
|
|
452
|
+
if (value != null && value !== "") {
|
|
453
|
+
setKeys.add(key);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
hasConfigurableParams: true,
|
|
459
|
+
hints: auto.hints,
|
|
460
|
+
mergedValues: { ...initialValues, ...values },
|
|
461
|
+
schema: auto.schema as JsonSchemaObject,
|
|
462
|
+
setKeys,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function InlinePluginConfig({ pluginId: rawPluginId }: { pluginId: string }) {
|
|
467
|
+
const pluginId = normalizePluginId(rawPluginId);
|
|
468
|
+
const [plugin, setPlugin] = useState<PluginInfo | null>(null);
|
|
469
|
+
const [loading, setLoading] = useState(true);
|
|
470
|
+
const [values, setValues] = useState<Record<string, unknown>>({});
|
|
471
|
+
const [saving, setSaving] = useState(false);
|
|
472
|
+
const [saved, setSaved] = useState(false);
|
|
473
|
+
const [enabling, setEnabling] = useState(false);
|
|
474
|
+
const [error, setError] = useState<string | null>(null);
|
|
475
|
+
const [dismissed, setDismissed] = useState(false);
|
|
476
|
+
const mountedRef = useRef(true);
|
|
477
|
+
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
478
|
+
const { setActionNotice, loadPlugins, t } = useApp();
|
|
479
|
+
|
|
480
|
+
// Track mount state — reset to true on each mount (needed for StrictMode
|
|
481
|
+
// which unmounts/remounts and would leave the ref false otherwise).
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
mountedRef.current = true;
|
|
484
|
+
return () => {
|
|
485
|
+
mountedRef.current = false;
|
|
486
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
487
|
+
};
|
|
488
|
+
}, []);
|
|
489
|
+
|
|
490
|
+
// Self-contained: fetch plugin data directly from API
|
|
491
|
+
const fetchPlugin = useCallback(async () => {
|
|
492
|
+
try {
|
|
493
|
+
const { plugins } = await client.getPlugins();
|
|
494
|
+
if (!mountedRef.current) return;
|
|
495
|
+
const found = plugins.find((p) => p.id === pluginId);
|
|
496
|
+
setPlugin(found ?? null);
|
|
497
|
+
} catch {
|
|
498
|
+
if (mountedRef.current) setError("Failed to load plugin info.");
|
|
499
|
+
} finally {
|
|
500
|
+
if (mountedRef.current) setLoading(false);
|
|
501
|
+
}
|
|
502
|
+
}, [pluginId]);
|
|
503
|
+
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
void fetchPlugin();
|
|
506
|
+
}, [fetchPlugin]);
|
|
507
|
+
|
|
508
|
+
const { hasConfigurableParams, hints, mergedValues, schema, setKeys } =
|
|
509
|
+
useMemo(
|
|
510
|
+
() => buildInlinePluginConfigModel(plugin, values),
|
|
511
|
+
[plugin, values],
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const handleChange = useCallback((key: string, value: unknown) => {
|
|
515
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
516
|
+
setSaved(false);
|
|
517
|
+
setError(null);
|
|
518
|
+
}, []);
|
|
519
|
+
|
|
520
|
+
const handleSave = useCallback(async () => {
|
|
521
|
+
setSaving(true);
|
|
522
|
+
setError(null);
|
|
523
|
+
try {
|
|
524
|
+
const patch: Record<string, string> = {};
|
|
525
|
+
for (const [k, v] of Object.entries(values)) {
|
|
526
|
+
if (v != null && v !== "") patch[k] = String(v);
|
|
527
|
+
}
|
|
528
|
+
await client.updatePlugin(pluginId, { config: patch });
|
|
529
|
+
if (mountedRef.current) setSaved(true);
|
|
530
|
+
await fetchPlugin();
|
|
531
|
+
} catch (e) {
|
|
532
|
+
if (mountedRef.current)
|
|
533
|
+
setError(e instanceof Error ? e.message : "Failed to save.");
|
|
534
|
+
} finally {
|
|
535
|
+
if (mountedRef.current) setSaving(false);
|
|
536
|
+
}
|
|
537
|
+
}, [pluginId, values, fetchPlugin]);
|
|
538
|
+
|
|
539
|
+
const handleToggle = useCallback(
|
|
540
|
+
async (enable: boolean) => {
|
|
541
|
+
setEnabling(true);
|
|
542
|
+
setError(null);
|
|
543
|
+
try {
|
|
544
|
+
// Save pending config first, then toggle — same as the Plugins page
|
|
545
|
+
if (enable) {
|
|
546
|
+
const patch: Record<string, string> = {};
|
|
547
|
+
for (const [k, v] of Object.entries(values)) {
|
|
548
|
+
if (v != null && v !== "") patch[k] = String(v);
|
|
549
|
+
}
|
|
550
|
+
if (Object.keys(patch).length > 0) {
|
|
551
|
+
await client.updatePlugin(pluginId, { config: patch });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Exact same call as the ON button in PluginsView
|
|
555
|
+
await client.updatePlugin(pluginId, { enabled: enable });
|
|
556
|
+
// Refresh shared plugin state so Plugins page shows updated status
|
|
557
|
+
await loadPlugins();
|
|
558
|
+
if (enable && mountedRef.current) {
|
|
559
|
+
const tabLabel =
|
|
560
|
+
plugin?.category === "feature"
|
|
561
|
+
? "Plugins > Features"
|
|
562
|
+
: plugin?.category === "connector"
|
|
563
|
+
? "Plugins > Connectors"
|
|
564
|
+
: "Plugins > System";
|
|
565
|
+
setActionNotice(
|
|
566
|
+
`${plugin?.name ?? pluginId} enabled! Find it in ${tabLabel}.`,
|
|
567
|
+
"success",
|
|
568
|
+
4000,
|
|
569
|
+
);
|
|
570
|
+
setDismissed(true);
|
|
571
|
+
}
|
|
572
|
+
// Wait for agent restart then refresh (with cleanup on unmount)
|
|
573
|
+
refreshTimerRef.current = setTimeout(() => void fetchPlugin(), 3000);
|
|
574
|
+
} catch (e) {
|
|
575
|
+
if (mountedRef.current) {
|
|
576
|
+
setError(
|
|
577
|
+
e instanceof Error
|
|
578
|
+
? e.message
|
|
579
|
+
: `Failed to ${enable ? "enable" : "disable"} plugin.`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
} finally {
|
|
583
|
+
if (mountedRef.current) setEnabling(false);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
[pluginId, plugin, values, fetchPlugin, loadPlugins, setActionNotice],
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
if (dismissed) {
|
|
590
|
+
return (
|
|
591
|
+
<div className="my-2 px-3 py-2 border border-ok/30 bg-ok/5 text-xs text-ok">
|
|
592
|
+
{plugin?.name ?? pluginId} {t("messagecontent.Enabled")}
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (loading) {
|
|
598
|
+
return (
|
|
599
|
+
<div className="my-2 px-3 py-2 border border-border bg-card text-xs text-muted italic">
|
|
600
|
+
{t("common.loading")} {pluginId} {t("messagecontent.configuration")}
|
|
601
|
+
</div>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!plugin) {
|
|
606
|
+
return (
|
|
607
|
+
<div className="my-2 px-3 py-2 border border-border bg-card text-xs text-muted italic">
|
|
608
|
+
{t("messagecontent.Plugin")}
|
|
609
|
+
{pluginId}
|
|
610
|
+
{t("messagecontent.NotFound")}
|
|
611
|
+
</div>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const isEnabled = plugin.enabled;
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<div className="my-2 border border-border bg-card overflow-hidden">
|
|
619
|
+
{/* Header */}
|
|
620
|
+
<div className="flex items-center justify-between px-3 py-2 bg-bg-hover border-b border-border">
|
|
621
|
+
<div className="flex items-center gap-2 text-xs font-bold text-txt">
|
|
622
|
+
{plugin.icon ? (
|
|
623
|
+
<span className="text-[13px]">{plugin.icon}</span>
|
|
624
|
+
) : (
|
|
625
|
+
<span className="text-[13px] opacity-60">{"\u2699\uFE0F"}</span>
|
|
626
|
+
)}
|
|
627
|
+
<span>
|
|
628
|
+
{plugin.name} {t("messagecontent.Configuration")}
|
|
629
|
+
</span>
|
|
630
|
+
</div>
|
|
631
|
+
<div className="flex items-center gap-2">
|
|
632
|
+
{plugin.configured && (
|
|
633
|
+
<span className="text-[10px] text-ok font-medium">
|
|
634
|
+
{t("messagecontent.Configured")}
|
|
635
|
+
</span>
|
|
636
|
+
)}
|
|
637
|
+
<span
|
|
638
|
+
className={`text-[10px] font-medium ${isEnabled ? "text-ok" : "text-muted"}`}
|
|
639
|
+
>
|
|
640
|
+
{isEnabled ? "Active" : "Inactive"}
|
|
641
|
+
</span>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
{/* Form — always shown so user can configure before enabling */}
|
|
646
|
+
{schema && hasConfigurableParams ? (
|
|
647
|
+
<div className="p-3">
|
|
648
|
+
<ConfigRenderer
|
|
649
|
+
schema={schema}
|
|
650
|
+
hints={hints}
|
|
651
|
+
values={mergedValues}
|
|
652
|
+
setKeys={setKeys}
|
|
653
|
+
registry={defaultRegistry}
|
|
654
|
+
pluginId={plugin.id}
|
|
655
|
+
onChange={handleChange}
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
658
|
+
) : (
|
|
659
|
+
<div className="px-3 py-2 text-xs text-muted italic">
|
|
660
|
+
{t("messagecontent.NoConfigurablePara")}
|
|
661
|
+
</div>
|
|
662
|
+
)}
|
|
663
|
+
|
|
664
|
+
{/* Footer */}
|
|
665
|
+
<div className="flex items-center gap-2 px-3 py-2 border-t border-border flex-wrap">
|
|
666
|
+
{schema && hasConfigurableParams && (
|
|
667
|
+
<Button
|
|
668
|
+
variant="default"
|
|
669
|
+
size="sm"
|
|
670
|
+
className="px-4 py-1.5 h-7 text-xs shadow-sm bg-accent text-accent-fg hover:opacity-90 disabled:opacity-40"
|
|
671
|
+
onClick={handleSave}
|
|
672
|
+
disabled={saving || enabling || Object.keys(values).length === 0}
|
|
673
|
+
>
|
|
674
|
+
{saving ? "Saving..." : "Save"}
|
|
675
|
+
</Button>
|
|
676
|
+
)}
|
|
677
|
+
|
|
678
|
+
{!isEnabled ? (
|
|
679
|
+
<Button
|
|
680
|
+
variant="outline"
|
|
681
|
+
size="sm"
|
|
682
|
+
className="px-4 py-1.5 h-7 text-xs border-ok/50 text-ok bg-ok/5 hover:bg-ok/10 hover:text-ok disabled:opacity-40"
|
|
683
|
+
onClick={() => void handleToggle(true)}
|
|
684
|
+
disabled={enabling || saving}
|
|
685
|
+
>
|
|
686
|
+
{enabling ? "Enabling..." : "Enable Plugin"}
|
|
687
|
+
</Button>
|
|
688
|
+
) : (
|
|
689
|
+
<Button
|
|
690
|
+
variant="outline"
|
|
691
|
+
size="sm"
|
|
692
|
+
className="px-4 py-1.5 h-7 text-xs text-muted hover:border-danger hover:text-danger disabled:opacity-40"
|
|
693
|
+
onClick={() => void handleToggle(false)}
|
|
694
|
+
disabled={enabling || saving}
|
|
695
|
+
>
|
|
696
|
+
{enabling ? "Disabling..." : "Disable"}
|
|
697
|
+
</Button>
|
|
698
|
+
)}
|
|
699
|
+
|
|
700
|
+
{saved && (
|
|
701
|
+
<span className="text-xs text-ok">{t("messagecontent.Saved")}</span>
|
|
702
|
+
)}
|
|
703
|
+
{error && <span className="text-xs text-danger">{error}</span>}
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ── UiSpec block ────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
function UiSpecBlock({ spec, raw }: { spec: UiSpec; raw: string }) {
|
|
712
|
+
const { t } = useApp();
|
|
713
|
+
const { sendActionMessage } = useApp();
|
|
714
|
+
const [showRaw, setShowRaw] = useState(false);
|
|
715
|
+
|
|
716
|
+
const handleAction = useCallback(
|
|
717
|
+
(action: string, params?: Record<string, unknown>) => {
|
|
718
|
+
const paramsStr = params ? ` ${JSON.stringify(params)}` : "";
|
|
719
|
+
void sendActionMessage(`[action:${action}]${paramsStr}`);
|
|
720
|
+
},
|
|
721
|
+
[sendActionMessage],
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<div className="my-2 border border-border overflow-hidden">
|
|
726
|
+
<div className="flex items-center justify-between px-3 py-1.5 bg-bg-hover border-b border-border">
|
|
727
|
+
<span className="text-[10px] font-semibold text-muted uppercase tracking-wider">
|
|
728
|
+
{t("messagecontent.InteractiveUI")}
|
|
729
|
+
</span>
|
|
730
|
+
<Button
|
|
731
|
+
variant="link"
|
|
732
|
+
size="sm"
|
|
733
|
+
className="h-auto p-0 text-[10px] text-txt hover:underline decoration-accent/50 underline-offset-2"
|
|
734
|
+
onClick={() => setShowRaw((v) => !v)}
|
|
735
|
+
>
|
|
736
|
+
{showRaw ? "Hide JSON" : "View JSON"}
|
|
737
|
+
</Button>
|
|
738
|
+
</div>
|
|
739
|
+
{showRaw && (
|
|
740
|
+
<div className="px-3 py-2 bg-card border-b border-border overflow-x-auto">
|
|
741
|
+
<pre className="text-[10px] text-muted font-mono whitespace-pre-wrap break-words m-0">
|
|
742
|
+
{raw}
|
|
743
|
+
</pre>
|
|
744
|
+
</div>
|
|
745
|
+
)}
|
|
746
|
+
<div className="p-3">
|
|
747
|
+
<UiRenderer spec={spec} onAction={handleAction} />
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── Main component ──────────────────────────────────────────────────
|
|
754
|
+
|
|
755
|
+
export function MessageContent({ message }: MessageContentProps) {
|
|
756
|
+
// Parse segments — memoize to avoid re-parsing on every render
|
|
757
|
+
const segments = useMemo(() => {
|
|
758
|
+
try {
|
|
759
|
+
return parseSegments(message.text);
|
|
760
|
+
} catch {
|
|
761
|
+
// If parsing fails, just show plain text
|
|
762
|
+
return [{ kind: "text" as const, text: message.text }];
|
|
763
|
+
}
|
|
764
|
+
}, [message.text]);
|
|
765
|
+
|
|
766
|
+
// Fast path: single plain-text segment (most messages)
|
|
767
|
+
if (segments.length === 1 && segments[0].kind === "text") {
|
|
768
|
+
return <div className="whitespace-pre-wrap">{segments[0].text}</div>;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return (
|
|
772
|
+
<div>
|
|
773
|
+
{(() => {
|
|
774
|
+
const keyCounts = new Map<string, number>();
|
|
775
|
+
const nextKey = (base: string) => {
|
|
776
|
+
const nextCount = (keyCounts.get(base) ?? 0) + 1;
|
|
777
|
+
keyCounts.set(base, nextCount);
|
|
778
|
+
return `${base}:${nextCount}`;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
return segments.map((seg) => {
|
|
782
|
+
const baseKey =
|
|
783
|
+
seg.kind === "text"
|
|
784
|
+
? `text:${seg.text.slice(0, 80)}`
|
|
785
|
+
: seg.kind === "config"
|
|
786
|
+
? `config:${seg.pluginId}`
|
|
787
|
+
: `ui:${seg.raw.slice(0, 80)}`;
|
|
788
|
+
const segmentKey = nextKey(baseKey);
|
|
789
|
+
|
|
790
|
+
switch (seg.kind) {
|
|
791
|
+
case "text":
|
|
792
|
+
return (
|
|
793
|
+
<div key={segmentKey} className="whitespace-pre-wrap">
|
|
794
|
+
{seg.text}
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
797
|
+
case "config":
|
|
798
|
+
if (!isSafeNormalizedPluginId(normalizePluginId(seg.pluginId))) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
return (
|
|
802
|
+
<InlinePluginConfig key={segmentKey} pluginId={seg.pluginId} />
|
|
803
|
+
);
|
|
804
|
+
case "ui-spec":
|
|
805
|
+
return (
|
|
806
|
+
<UiSpecBlock key={segmentKey} spec={seg.spec} raw={seg.raw} />
|
|
807
|
+
);
|
|
808
|
+
default:
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
})()}
|
|
813
|
+
</div>
|
|
814
|
+
);
|
|
815
|
+
}
|