@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,143 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
getLifoSyncChannelName,
|
|
4
|
+
type LifoRuntime,
|
|
5
|
+
type LifoSyncMessage,
|
|
6
|
+
normalizeTerminalText,
|
|
7
|
+
} from "../platform";
|
|
8
|
+
|
|
9
|
+
interface UseLifoSyncOptions {
|
|
10
|
+
popoutMode: boolean;
|
|
11
|
+
lifoSessionId: string | null;
|
|
12
|
+
runtimeRef: React.RefObject<LifoRuntime | null>;
|
|
13
|
+
appendOutput: (line: string) => void;
|
|
14
|
+
setRunCount: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
+
setSessionKey: React.Dispatch<React.SetStateAction<number>>;
|
|
16
|
+
setControllerOnline: React.Dispatch<React.SetStateAction<boolean>>;
|
|
17
|
+
controllerHeartbeatAtRef: React.MutableRefObject<number>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseLifoSyncReturn {
|
|
21
|
+
broadcastSyncMessage: (message: Omit<LifoSyncMessage, "source">) => void;
|
|
22
|
+
syncChannelRef: React.RefObject<BroadcastChannel | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useLifoSync({
|
|
26
|
+
popoutMode,
|
|
27
|
+
lifoSessionId,
|
|
28
|
+
runtimeRef,
|
|
29
|
+
appendOutput,
|
|
30
|
+
setRunCount,
|
|
31
|
+
setSessionKey,
|
|
32
|
+
setControllerOnline,
|
|
33
|
+
controllerHeartbeatAtRef,
|
|
34
|
+
}: UseLifoSyncOptions): UseLifoSyncReturn {
|
|
35
|
+
const syncChannelRef = useRef<BroadcastChannel | null>(null);
|
|
36
|
+
|
|
37
|
+
const broadcastSyncMessage = useCallback(
|
|
38
|
+
(message: Omit<LifoSyncMessage, "source">) => {
|
|
39
|
+
if (!popoutMode) return;
|
|
40
|
+
syncChannelRef.current?.postMessage({
|
|
41
|
+
source: "controller",
|
|
42
|
+
...message,
|
|
43
|
+
} satisfies LifoSyncMessage);
|
|
44
|
+
},
|
|
45
|
+
[popoutMode],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
50
|
+
|
|
51
|
+
const channel = new BroadcastChannel(getLifoSyncChannelName(lifoSessionId));
|
|
52
|
+
syncChannelRef.current = channel;
|
|
53
|
+
|
|
54
|
+
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
55
|
+
let heartbeatWatchInterval: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
|
|
57
|
+
if (popoutMode) {
|
|
58
|
+
setControllerOnline(true);
|
|
59
|
+
broadcastSyncMessage({ type: "heartbeat" });
|
|
60
|
+
heartbeatInterval = setInterval(() => {
|
|
61
|
+
broadcastSyncMessage({ type: "heartbeat" });
|
|
62
|
+
}, 1000);
|
|
63
|
+
} else {
|
|
64
|
+
heartbeatWatchInterval = setInterval(() => {
|
|
65
|
+
const online = Date.now() - controllerHeartbeatAtRef.current < 3500;
|
|
66
|
+
setControllerOnline(online);
|
|
67
|
+
}, 1000);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
channel.onmessage = (event: MessageEvent<unknown>) => {
|
|
71
|
+
if (popoutMode) return;
|
|
72
|
+
const data = event.data as Partial<LifoSyncMessage> | null;
|
|
73
|
+
if (!data || data.source !== "controller") return;
|
|
74
|
+
|
|
75
|
+
if (data.type === "heartbeat") {
|
|
76
|
+
controllerHeartbeatAtRef.current = Date.now();
|
|
77
|
+
setControllerOnline(true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const runtime = runtimeRef.current;
|
|
82
|
+
if (!runtime) return;
|
|
83
|
+
|
|
84
|
+
switch (data.type) {
|
|
85
|
+
case "session-reset":
|
|
86
|
+
setSessionKey((value) => value + 1);
|
|
87
|
+
break;
|
|
88
|
+
case "command-start":
|
|
89
|
+
if (typeof data.command !== "string") return;
|
|
90
|
+
runtime.terminal.writeln(`$ ${data.command}`);
|
|
91
|
+
appendOutput(`$ ${data.command}`);
|
|
92
|
+
setRunCount((prev) => prev + 1);
|
|
93
|
+
break;
|
|
94
|
+
case "stdout":
|
|
95
|
+
if (typeof data.chunk !== "string") return;
|
|
96
|
+
runtime.terminal.write(normalizeTerminalText(data.chunk));
|
|
97
|
+
if (data.chunk.trimEnd()) appendOutput(data.chunk.trimEnd());
|
|
98
|
+
break;
|
|
99
|
+
case "stderr":
|
|
100
|
+
if (typeof data.chunk !== "string") return;
|
|
101
|
+
runtime.terminal.write(normalizeTerminalText(data.chunk));
|
|
102
|
+
if (data.chunk.trimEnd()) {
|
|
103
|
+
appendOutput(`stderr: ${data.chunk.trimEnd()}`);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "command-exit":
|
|
107
|
+
if (typeof data.exitCode !== "number") return;
|
|
108
|
+
runtime.terminal.writeln(`[exit ${data.exitCode}]`);
|
|
109
|
+
appendOutput(`[exit ${data.exitCode}]`);
|
|
110
|
+
try {
|
|
111
|
+
runtime.explorer.refresh();
|
|
112
|
+
} catch {
|
|
113
|
+
// Ignore refresh failures when mirroring popout events.
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
case "command-error":
|
|
117
|
+
if (typeof data.message !== "string") return;
|
|
118
|
+
runtime.terminal.writeln(`error: ${data.message}`);
|
|
119
|
+
appendOutput(`error: ${data.message}`);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
126
|
+
if (heartbeatWatchInterval) clearInterval(heartbeatWatchInterval);
|
|
127
|
+
syncChannelRef.current = null;
|
|
128
|
+
channel.close();
|
|
129
|
+
};
|
|
130
|
+
}, [
|
|
131
|
+
appendOutput,
|
|
132
|
+
broadcastSyncMessage,
|
|
133
|
+
controllerHeartbeatAtRef,
|
|
134
|
+
lifoSessionId,
|
|
135
|
+
popoutMode,
|
|
136
|
+
runtimeRef,
|
|
137
|
+
setControllerOnline,
|
|
138
|
+
setRunCount,
|
|
139
|
+
setSessionKey,
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
return { broadcastSyncMessage, syncChannelRef };
|
|
143
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory monitoring hook for detecting memory leaks.
|
|
3
|
+
*
|
|
4
|
+
* Uses the Performance API's memory metrics (Chrome/Edge only) to track
|
|
5
|
+
* heap usage over time and detect potential memory leaks by analyzing
|
|
6
|
+
* growth patterns.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { metrics, isLeaking, trend } = useMemoryMonitor({ enabled: true });
|
|
10
|
+
*
|
|
11
|
+
* The hook samples memory at regular intervals and calculates:
|
|
12
|
+
* - Current heap usage
|
|
13
|
+
* - Heap growth trend (MB/min)
|
|
14
|
+
* - Leak detection based on sustained growth
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
18
|
+
|
|
19
|
+
// Extend Performance interface for memory info (Chrome/Edge only)
|
|
20
|
+
interface PerformanceMemory {
|
|
21
|
+
usedJSHeapSize: number;
|
|
22
|
+
totalJSHeapSize: number;
|
|
23
|
+
jsHeapSizeLimit: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PerformanceWithMemory extends Performance {
|
|
27
|
+
memory?: PerformanceMemory;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MemoryMetrics {
|
|
31
|
+
/** Current used JS heap size in bytes */
|
|
32
|
+
usedHeapSize: number;
|
|
33
|
+
/** Total JS heap size in bytes */
|
|
34
|
+
totalHeapSize: number;
|
|
35
|
+
/** JS heap size limit in bytes */
|
|
36
|
+
heapSizeLimit: number;
|
|
37
|
+
/** Heap usage as percentage of limit */
|
|
38
|
+
usagePercent: number;
|
|
39
|
+
/** Timestamp of measurement */
|
|
40
|
+
timestamp: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MemorySample {
|
|
44
|
+
usedHeapSize: number;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MemoryTrend {
|
|
49
|
+
/** Growth rate in bytes per minute (positive = growing) */
|
|
50
|
+
bytesPerMinute: number;
|
|
51
|
+
/** Growth rate in MB per minute */
|
|
52
|
+
mbPerMinute: number;
|
|
53
|
+
/** Number of samples in the analysis window */
|
|
54
|
+
sampleCount: number;
|
|
55
|
+
/** Duration of analysis window in minutes */
|
|
56
|
+
windowMinutes: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface UseMemoryMonitorOptions {
|
|
60
|
+
/** Enable memory monitoring (default: true in dev mode) */
|
|
61
|
+
enabled?: boolean;
|
|
62
|
+
/** Sampling interval in milliseconds (default: 5000) */
|
|
63
|
+
sampleInterval?: number;
|
|
64
|
+
/** Number of samples to keep for trend analysis (default: 60) */
|
|
65
|
+
maxSamples?: number;
|
|
66
|
+
/** Threshold in MB/min to consider a leak (default: 1.0) */
|
|
67
|
+
leakThresholdMbPerMin?: number;
|
|
68
|
+
/** Minimum samples before detecting leaks (default: 12) */
|
|
69
|
+
minSamplesForDetection?: number;
|
|
70
|
+
/** Callback when a potential leak is detected */
|
|
71
|
+
onLeakDetected?: (trend: MemoryTrend, metrics: MemoryMetrics) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UseMemoryMonitorResult {
|
|
75
|
+
/** Whether memory API is supported in this browser */
|
|
76
|
+
supported: boolean;
|
|
77
|
+
/** Current memory metrics (null if not supported) */
|
|
78
|
+
metrics: MemoryMetrics | null;
|
|
79
|
+
/** Memory growth trend analysis */
|
|
80
|
+
trend: MemoryTrend | null;
|
|
81
|
+
/** Whether a potential memory leak is detected */
|
|
82
|
+
isLeaking: boolean;
|
|
83
|
+
/** Historical samples for charting */
|
|
84
|
+
samples: MemorySample[];
|
|
85
|
+
/** Force garbage collection (if available) */
|
|
86
|
+
forceGC: () => void;
|
|
87
|
+
/** Clear sample history */
|
|
88
|
+
clearHistory: () => void;
|
|
89
|
+
/** Get formatted memory stats for display */
|
|
90
|
+
getFormattedStats: () => string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DEFAULT_OPTIONS: Required<
|
|
94
|
+
Omit<UseMemoryMonitorOptions, "onLeakDetected">
|
|
95
|
+
> = {
|
|
96
|
+
enabled: Boolean(import.meta.env.DEV),
|
|
97
|
+
sampleInterval: 5000,
|
|
98
|
+
maxSamples: 60,
|
|
99
|
+
leakThresholdMbPerMin: 1.0,
|
|
100
|
+
minSamplesForDetection: 12,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function getMemoryInfo(): PerformanceMemory | null {
|
|
104
|
+
const perf = performance as PerformanceWithMemory;
|
|
105
|
+
return perf.memory ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatBytes(bytes: number): string {
|
|
109
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
110
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
111
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
112
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
113
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function calculateTrend(samples: MemorySample[]): MemoryTrend | null {
|
|
117
|
+
if (samples.length < 2) return null;
|
|
118
|
+
|
|
119
|
+
const firstSample = samples[0];
|
|
120
|
+
const lastSample = samples[samples.length - 1];
|
|
121
|
+
const durationMs = lastSample.timestamp - firstSample.timestamp;
|
|
122
|
+
|
|
123
|
+
if (durationMs <= 0) return null;
|
|
124
|
+
|
|
125
|
+
const durationMinutes = durationMs / (1000 * 60);
|
|
126
|
+
const heapChange = lastSample.usedHeapSize - firstSample.usedHeapSize;
|
|
127
|
+
const bytesPerMinute = heapChange / durationMinutes;
|
|
128
|
+
const mbPerMinute = bytesPerMinute / (1024 * 1024);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
bytesPerMinute,
|
|
132
|
+
mbPerMinute,
|
|
133
|
+
sampleCount: samples.length,
|
|
134
|
+
windowMinutes: durationMinutes,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function useMemoryMonitor(
|
|
139
|
+
options: UseMemoryMonitorOptions = {},
|
|
140
|
+
): UseMemoryMonitorResult {
|
|
141
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
142
|
+
const {
|
|
143
|
+
enabled,
|
|
144
|
+
sampleInterval,
|
|
145
|
+
maxSamples,
|
|
146
|
+
leakThresholdMbPerMin,
|
|
147
|
+
minSamplesForDetection,
|
|
148
|
+
} = opts;
|
|
149
|
+
|
|
150
|
+
const [supported, setSupported] = useState(false);
|
|
151
|
+
const [metrics, setMetrics] = useState<MemoryMetrics | null>(null);
|
|
152
|
+
const [samples, setSamples] = useState<MemorySample[]>([]);
|
|
153
|
+
const [trend, setTrend] = useState<MemoryTrend | null>(null);
|
|
154
|
+
const [isLeaking, setIsLeaking] = useState(false);
|
|
155
|
+
|
|
156
|
+
const onLeakDetectedRef = useRef(options.onLeakDetected);
|
|
157
|
+
onLeakDetectedRef.current = options.onLeakDetected;
|
|
158
|
+
const lastLeakNotifyRef = useRef(0);
|
|
159
|
+
|
|
160
|
+
// Check browser support
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const memInfo = getMemoryInfo();
|
|
163
|
+
setSupported(memInfo !== null);
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
// Sample memory at intervals
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!enabled || !supported) return;
|
|
169
|
+
|
|
170
|
+
const sample = () => {
|
|
171
|
+
const memInfo = getMemoryInfo();
|
|
172
|
+
if (!memInfo) return;
|
|
173
|
+
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const currentMetrics: MemoryMetrics = {
|
|
176
|
+
usedHeapSize: memInfo.usedJSHeapSize,
|
|
177
|
+
totalHeapSize: memInfo.totalJSHeapSize,
|
|
178
|
+
heapSizeLimit: memInfo.jsHeapSizeLimit,
|
|
179
|
+
usagePercent: (memInfo.usedJSHeapSize / memInfo.jsHeapSizeLimit) * 100,
|
|
180
|
+
timestamp: now,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
setMetrics(currentMetrics);
|
|
184
|
+
|
|
185
|
+
setSamples((prev) => {
|
|
186
|
+
const newSample: MemorySample = {
|
|
187
|
+
usedHeapSize: memInfo.usedJSHeapSize,
|
|
188
|
+
timestamp: now,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const updated = [...prev, newSample];
|
|
192
|
+
if (updated.length > maxSamples) {
|
|
193
|
+
updated.splice(0, updated.length - maxSamples);
|
|
194
|
+
}
|
|
195
|
+
return updated;
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Initial sample
|
|
200
|
+
sample();
|
|
201
|
+
|
|
202
|
+
const intervalId = setInterval(sample, sampleInterval);
|
|
203
|
+
return () => clearInterval(intervalId);
|
|
204
|
+
}, [enabled, supported, sampleInterval, maxSamples]);
|
|
205
|
+
|
|
206
|
+
// Calculate trend and detect leaks
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (samples.length < 2) {
|
|
209
|
+
setTrend(null);
|
|
210
|
+
setIsLeaking(false);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const currentTrend = calculateTrend(samples);
|
|
215
|
+
setTrend(currentTrend);
|
|
216
|
+
|
|
217
|
+
if (!currentTrend || samples.length < minSamplesForDetection) {
|
|
218
|
+
setIsLeaking(false);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Detect sustained memory growth
|
|
223
|
+
const leaking = currentTrend.mbPerMinute > leakThresholdMbPerMin;
|
|
224
|
+
setIsLeaking(leaking);
|
|
225
|
+
|
|
226
|
+
// Notify callback (throttled to once per minute)
|
|
227
|
+
if (leaking && metrics && onLeakDetectedRef.current) {
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
if (now - lastLeakNotifyRef.current > 60000) {
|
|
230
|
+
lastLeakNotifyRef.current = now;
|
|
231
|
+
onLeakDetectedRef.current(currentTrend, metrics);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}, [samples, minSamplesForDetection, leakThresholdMbPerMin, metrics]);
|
|
235
|
+
|
|
236
|
+
const forceGC = useCallback(() => {
|
|
237
|
+
// gc() is only available when Chrome is started with --js-flags="--expose-gc"
|
|
238
|
+
const win = window as Window & { gc?: () => void };
|
|
239
|
+
if (typeof win.gc === "function") {
|
|
240
|
+
win.gc();
|
|
241
|
+
}
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const clearHistory = useCallback(() => {
|
|
245
|
+
setSamples([]);
|
|
246
|
+
setTrend(null);
|
|
247
|
+
setIsLeaking(false);
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
const getFormattedStats = useCallback(() => {
|
|
251
|
+
if (!metrics) return "Memory monitoring not available";
|
|
252
|
+
|
|
253
|
+
const lines = [
|
|
254
|
+
`Heap Used: ${formatBytes(metrics.usedHeapSize)}`,
|
|
255
|
+
`Heap Total: ${formatBytes(metrics.totalHeapSize)}`,
|
|
256
|
+
`Heap Limit: ${formatBytes(metrics.heapSizeLimit)}`,
|
|
257
|
+
`Usage: ${metrics.usagePercent.toFixed(1)}%`,
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
if (trend) {
|
|
261
|
+
const trendSign = trend.mbPerMinute >= 0 ? "+" : "";
|
|
262
|
+
lines.push(
|
|
263
|
+
`Trend: ${trendSign}${trend.mbPerMinute.toFixed(2)} MB/min`,
|
|
264
|
+
`Samples: ${trend.sampleCount} (${trend.windowMinutes.toFixed(1)} min)`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (isLeaking) {
|
|
269
|
+
lines.push("WARNING: Potential memory leak detected!");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}, [metrics, trend, isLeaking]);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
supported,
|
|
277
|
+
metrics,
|
|
278
|
+
trend,
|
|
279
|
+
isLeaking,
|
|
280
|
+
samples,
|
|
281
|
+
forceGC,
|
|
282
|
+
clearHistory,
|
|
283
|
+
getFormattedStats,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Lightweight memory leak detector that logs warnings to console.
|
|
289
|
+
* Can be used as a standalone function without React.
|
|
290
|
+
*/
|
|
291
|
+
export function startMemoryLeakDetector(options?: {
|
|
292
|
+
intervalMs?: number;
|
|
293
|
+
thresholdMbPerMin?: number;
|
|
294
|
+
onLeak?: (info: { mbPerMinute: number; currentMb: number }) => void;
|
|
295
|
+
}): () => void {
|
|
296
|
+
const intervalMs = options?.intervalMs ?? 10000;
|
|
297
|
+
const thresholdMbPerMin = options?.thresholdMbPerMin ?? 2.0;
|
|
298
|
+
const samples: MemorySample[] = [];
|
|
299
|
+
const maxSamples = 30;
|
|
300
|
+
|
|
301
|
+
const intervalId = setInterval(() => {
|
|
302
|
+
const memInfo = getMemoryInfo();
|
|
303
|
+
if (!memInfo) return;
|
|
304
|
+
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
samples.push({
|
|
307
|
+
usedHeapSize: memInfo.usedJSHeapSize,
|
|
308
|
+
timestamp: now,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (samples.length > maxSamples) {
|
|
312
|
+
samples.shift();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (samples.length >= 6) {
|
|
316
|
+
const trend = calculateTrend(samples);
|
|
317
|
+
if (trend && trend.mbPerMinute > thresholdMbPerMin) {
|
|
318
|
+
const currentMb = memInfo.usedJSHeapSize / (1024 * 1024);
|
|
319
|
+
console.warn(
|
|
320
|
+
`[MemoryLeakDetector] Potential memory leak detected!\n` +
|
|
321
|
+
` Growth rate: +${trend.mbPerMinute.toFixed(2)} MB/min\n` +
|
|
322
|
+
` Current heap: ${currentMb.toFixed(1)} MB\n` +
|
|
323
|
+
` Window: ${trend.windowMinutes.toFixed(1)} min (${trend.sampleCount} samples)`,
|
|
324
|
+
);
|
|
325
|
+
options?.onLeak?.({
|
|
326
|
+
mbPerMinute: trend.mbPerMinute,
|
|
327
|
+
currentMb,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}, intervalMs);
|
|
332
|
+
|
|
333
|
+
return () => clearInterval(intervalId);
|
|
334
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
|
|
3
|
+
const THRESHOLD = 3;
|
|
4
|
+
const WINDOW_MS = 1000;
|
|
5
|
+
const IS_DEV =
|
|
6
|
+
typeof process !== "undefined" &&
|
|
7
|
+
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Development-only render-rate guard.
|
|
11
|
+
*
|
|
12
|
+
* Tracks render timestamps for the named component and logs a console warning
|
|
13
|
+
* when the component re-renders {@link THRESHOLD} or more times within
|
|
14
|
+
* {@link WINDOW_MS} ms. No-op in production builds.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```ts
|
|
18
|
+
* function MyComponent() {
|
|
19
|
+
* useRenderGuard("MyComponent");
|
|
20
|
+
* // …
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useRenderGuard(name: string): void {
|
|
25
|
+
// Always call the hook (preserve hook call order) but skip work in prod.
|
|
26
|
+
const timestamps = useRef<number[]>([]);
|
|
27
|
+
if (!IS_DEV) return;
|
|
28
|
+
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const ts = timestamps.current;
|
|
31
|
+
ts.push(now);
|
|
32
|
+
|
|
33
|
+
// Prune old entries outside the window
|
|
34
|
+
while (ts.length > 0 && ts[0] < now - WINDOW_MS) {
|
|
35
|
+
ts.shift();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ts.length >= THRESHOLD) {
|
|
39
|
+
console.warn(
|
|
40
|
+
`[RenderGuard] "${name}" rendered ${ts.length}× in the last ${WINDOW_MS}ms`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook that controls desktop native frame capture for retake.tv streaming.
|
|
3
|
+
*
|
|
4
|
+
* Uses the native desktop capture path through the renderer bridge — this captures
|
|
5
|
+
* the full compositor output including cross-origin iframes (unlike the old canvas approach
|
|
6
|
+
* which was blocked by same-origin policy).
|
|
7
|
+
*
|
|
8
|
+
* The main process handles capture + HTTP POST to /api/stream/frame directly.
|
|
9
|
+
* This hook just sends start/stop signals.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useRef } from "react";
|
|
13
|
+
import { invokeDesktopBridgeRequest } from "../bridge";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_FPS = 15;
|
|
16
|
+
const JPEG_QUALITY = 70;
|
|
17
|
+
|
|
18
|
+
export function useRetakeCapture(
|
|
19
|
+
_iframeRef: React.RefObject<HTMLIFrameElement | null>,
|
|
20
|
+
active: boolean,
|
|
21
|
+
fps = DEFAULT_FPS,
|
|
22
|
+
) {
|
|
23
|
+
const activeRef = useRef(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (active && !activeRef.current) {
|
|
27
|
+
activeRef.current = true;
|
|
28
|
+
void invokeDesktopBridgeRequest({
|
|
29
|
+
rpcMethod: "screencaptureStartFrameCapture",
|
|
30
|
+
ipcChannel: "screencapture:startFrameCapture",
|
|
31
|
+
params: {
|
|
32
|
+
fps,
|
|
33
|
+
quality: JPEG_QUALITY,
|
|
34
|
+
apiBase: window.__MILADY_API_BASE__ ?? "http://localhost:2138",
|
|
35
|
+
endpoint: "/api/stream/frame",
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
.then((result) => {
|
|
39
|
+
if (result === null) {
|
|
40
|
+
activeRef.current = false;
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
console.warn("[retake] Failed to start frame capture:", err);
|
|
45
|
+
activeRef.current = false;
|
|
46
|
+
});
|
|
47
|
+
} else if (!active && activeRef.current) {
|
|
48
|
+
activeRef.current = false;
|
|
49
|
+
void invokeDesktopBridgeRequest({
|
|
50
|
+
rpcMethod: "screencaptureStopFrameCapture",
|
|
51
|
+
ipcChannel: "screencapture:stopFrameCapture",
|
|
52
|
+
}).catch((err) => {
|
|
53
|
+
console.warn("[retake] Failed to stop frame capture:", err);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
if (activeRef.current) {
|
|
59
|
+
activeRef.current = false;
|
|
60
|
+
void invokeDesktopBridgeRequest({
|
|
61
|
+
rpcMethod: "screencaptureStopFrameCapture",
|
|
62
|
+
ipcChannel: "screencapture:stopFrameCapture",
|
|
63
|
+
}).catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [active, fps]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function getNextTabForStreamPopoutEvent<TTab extends string>(
|
|
4
|
+
_detail: unknown,
|
|
5
|
+
): TTab | null {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useStreamPopoutNavigation<TTab extends string>(
|
|
10
|
+
setTab: (tab: TTab) => void,
|
|
11
|
+
): void {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const target =
|
|
14
|
+
typeof window !== "undefined" ? window : (globalThis as EventTarget);
|
|
15
|
+
const handler = (event: Event) => {
|
|
16
|
+
const nextTab = getNextTabForStreamPopoutEvent<TTab>(
|
|
17
|
+
(event as CustomEvent).detail,
|
|
18
|
+
);
|
|
19
|
+
if (nextTab) {
|
|
20
|
+
setTab(nextTab);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
target.addEventListener("stream-popout", handler);
|
|
25
|
+
return () => target.removeEventListener("stream-popout", handler);
|
|
26
|
+
}, [setTab]);
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook to safely manage `setTimeout` calls within React components.
|
|
5
|
+
* Automatically clears the timeout if the component unmounts before it fires.
|
|
6
|
+
*
|
|
7
|
+
* Returns `{ setTimeout, clearTimeout }` which mimic the global functions.
|
|
8
|
+
*/
|
|
9
|
+
export function useTimeout() {
|
|
10
|
+
const timeoutRefs = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
|
11
|
+
|
|
12
|
+
const setSafeTimeout = useCallback((callback: () => void, ms: number) => {
|
|
13
|
+
const id = setTimeout(() => {
|
|
14
|
+
timeoutRefs.current.delete(id);
|
|
15
|
+
callback();
|
|
16
|
+
}, ms);
|
|
17
|
+
timeoutRefs.current.add(id);
|
|
18
|
+
return id;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const clearSafeTimeout = useCallback((id: ReturnType<typeof setTimeout>) => {
|
|
22
|
+
clearTimeout(id);
|
|
23
|
+
timeoutRefs.current.delete(id);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const refs = timeoutRefs;
|
|
28
|
+
return () => {
|
|
29
|
+
for (const id of refs.current) {
|
|
30
|
+
clearTimeout(id);
|
|
31
|
+
}
|
|
32
|
+
refs.current.clear();
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return { setTimeout: setSafeTimeout, clearTimeout: clearSafeTimeout };
|
|
37
|
+
}
|