machinaos 0.0.76 → 0.0.78
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/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
25
|
-
import { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
|
|
25
|
+
import { useQuery, useQueryClient, type QueryClient, type UseQueryResult } from '@tanstack/react-query';
|
|
26
26
|
import { get as idbGet, set as idbSet } from 'idb-keyval';
|
|
27
27
|
|
|
28
28
|
import { useWebSocket } from '../contexts/WebSocketContext';
|
|
@@ -45,7 +45,17 @@ export interface ServerFieldDef {
|
|
|
45
45
|
type?: 'string' | 'password';
|
|
46
46
|
secret?: boolean;
|
|
47
47
|
placeholder?: string;
|
|
48
|
+
/** Initial value pre-populated when the user has nothing stored yet
|
|
49
|
+
(e.g. canonical local-LLM Base URL — http://localhost:1234/v1).
|
|
50
|
+
Distinct from `placeholder`: placeholder is a UI ghost-text hint;
|
|
51
|
+
default actually fills the field so the user can click Fetch
|
|
52
|
+
without typing. */
|
|
53
|
+
default?: string;
|
|
48
54
|
required?: boolean;
|
|
55
|
+
/** Multi-line description shown under the input (e.g. why this field
|
|
56
|
+
exists, where to find the value). Different from `placeholder` —
|
|
57
|
+
this is always visible. */
|
|
58
|
+
help?: string;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
/** Raw status-row descriptor — uses a string `ok_field` instead of a callable. */
|
|
@@ -104,6 +114,8 @@ export interface ServerProviderConfig {
|
|
|
104
114
|
usage_service?: string;
|
|
105
115
|
/** Server-resolved: whether a key/token is stored in the credentials DB. */
|
|
106
116
|
stored?: boolean;
|
|
117
|
+
/** Connected account identifier (email or display name) for OAuth providers. */
|
|
118
|
+
account_label?: string | null;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
export interface CatalogueResponse {
|
|
@@ -134,6 +146,33 @@ function isUnchanged(response: WsResponse): response is UnchangedResponse {
|
|
|
134
146
|
|
|
135
147
|
export const CATALOGUE_QUERY_KEY = ['credentialCatalogue'] as const;
|
|
136
148
|
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Debounced invalidation
|
|
151
|
+
//
|
|
152
|
+
// 8 broadcast handlers in WebSocketContext (api_key_status, whatsapp_status,
|
|
153
|
+
// twitter_oauth_complete, google_oauth_complete, google_status,
|
|
154
|
+
// telegram_status, credential_catalogue_updated, initial_status) all want to
|
|
155
|
+
// refetch the catalogue. During init-burst or multi-service reconnect, those
|
|
156
|
+
// fire within the same tick and would trigger N back-to-back roundtrips.
|
|
157
|
+
// Coalesce them onto a single invalidate at the trailing edge of a 300ms
|
|
158
|
+
// quiet window. Trailing-edge debounce so the freshest state always wins.
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
const CATALOGUE_INVALIDATE_DEBOUNCE_MS = 300;
|
|
162
|
+
let _catalogueInvalidateTimer: ReturnType<typeof setTimeout> | null = null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Request a catalogue refetch, coalesced across rapid bursts of broadcasts.
|
|
166
|
+
* Replaces direct `queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY })`.
|
|
167
|
+
*/
|
|
168
|
+
export function invalidateCatalogue(queryClient: QueryClient): void {
|
|
169
|
+
if (_catalogueInvalidateTimer) clearTimeout(_catalogueInvalidateTimer);
|
|
170
|
+
_catalogueInvalidateTimer = setTimeout(() => {
|
|
171
|
+
_catalogueInvalidateTimer = null;
|
|
172
|
+
void queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
|
|
173
|
+
}, CATALOGUE_INVALIDATE_DEBOUNCE_MS);
|
|
174
|
+
}
|
|
175
|
+
|
|
137
176
|
/** IDB key — we only store the current version, overwritten on each update. */
|
|
138
177
|
const IDB_STORAGE_KEY = 'credentials:catalogue:current';
|
|
139
178
|
|
|
@@ -312,3 +351,30 @@ export function useCatalogueQuery(): UseCatalogueQueryResult {
|
|
|
312
351
|
|
|
313
352
|
return useMemo(() => ({ ...query, refresh }), [query, refresh]);
|
|
314
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Single-provider "is stored?" selector derived from the catalogue.
|
|
357
|
+
*
|
|
358
|
+
* Replaces the retired ``apiKeyStatuses[id].hasKey`` duplication —
|
|
359
|
+
* `provider.stored` on the server-driven catalogue is the canonical
|
|
360
|
+
* answer (computed from `auth_service.has_valid_key` on every catalogue
|
|
361
|
+
* read). Consumers re-render only when this provider's `stored` flag
|
|
362
|
+
* actually flips, not on every credential mutation, because TanStack
|
|
363
|
+
* Query produces a new array reference per refetch and React's
|
|
364
|
+
* referential-equality short-circuits the boolean derivation.
|
|
365
|
+
*/
|
|
366
|
+
export function useProviderStored(providerId: string | null | undefined): boolean {
|
|
367
|
+
const { data } = useCatalogueQuery();
|
|
368
|
+
if (!providerId || !data?.providers) return false;
|
|
369
|
+
return Boolean(data.providers.find((p) => p.id === providerId)?.stored);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Count of providers with a stored credential (any kind — API key,
|
|
374
|
+
* OAuth, paired QR). Read from the catalogue's `stored` flag.
|
|
375
|
+
*/
|
|
376
|
+
export function useStoredProviderCount(): number {
|
|
377
|
+
const { data } = useCatalogueQuery();
|
|
378
|
+
if (!data?.providers) return 0;
|
|
379
|
+
return data.providers.filter((p) => p.stored).length;
|
|
380
|
+
}
|
|
@@ -26,7 +26,7 @@ interface DragVariableHookReturn {
|
|
|
26
26
|
* @param targetNodeId - The ID of the node receiving the drag (target of edge)
|
|
27
27
|
*/
|
|
28
28
|
export const useDragVariable = (_targetNodeId: string): DragVariableHookReturn => {
|
|
29
|
-
const
|
|
29
|
+
const currentWorkflow = useAppStore((s) => s.currentWorkflow);
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Get template variable name for a source node.
|
|
@@ -4,15 +4,51 @@ import { useWebSocket } from '../contexts/WebSocketContext';
|
|
|
4
4
|
interface NodeAllowlistResponse {
|
|
5
5
|
show_all: boolean;
|
|
6
6
|
enabled_nodes: string[];
|
|
7
|
+
/** Mode-independent blocklist by backend group (e.g. 'android' hides
|
|
8
|
+
* every plugin in the android group + androidTool). Empty array if
|
|
9
|
+
* the backend doesn't ship the field (older deployments). */
|
|
10
|
+
disabled_groups: string[];
|
|
11
|
+
/** Mode-independent blocklist by exact node type. Use for one-off
|
|
12
|
+
* types whose group label doesn't match (e.g. 'android_agent' is in
|
|
13
|
+
* the 'agent' group, not 'android'). */
|
|
14
|
+
disabled_nodes: string[];
|
|
15
|
+
/** Mode-independent blocklist for the Credentials Modal by category
|
|
16
|
+
* key (matches `category` in server/config/credential_providers.json,
|
|
17
|
+
* e.g. 'android' / 'ai' / 'social'). Empty array on older
|
|
18
|
+
* deployments. */
|
|
19
|
+
disabled_credential_categories: string[];
|
|
20
|
+
/** Mode-independent blocklist for the Master Skill folder
|
|
21
|
+
* dropdown — every entry hides the matching subfolder under
|
|
22
|
+
* server/skills/. Empty array on older deployments. */
|
|
23
|
+
disabled_skill_folders: string[];
|
|
7
24
|
}
|
|
8
25
|
|
|
9
26
|
/**
|
|
10
|
-
* Fetches the node allowlist from the backend and exposes
|
|
11
|
-
*
|
|
27
|
+
* Fetches the node allowlist from the backend and exposes the membership
|
|
28
|
+
* checks every UI surface that lists nodes uses (Component Palette,
|
|
29
|
+
* dropdowns, AI tool selectors, master skill folder, etc.).
|
|
12
30
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
31
|
+
* Two independent checks:
|
|
32
|
+
*
|
|
33
|
+
* `isBlocked(nodeType, groups?)` — absolute blocklist. Mode-independent.
|
|
34
|
+
* Driven by `disabled_groups` + `disabled_nodes` from the JSON
|
|
35
|
+
* config. Use to turn off entire groups (e.g. android) or specific
|
|
36
|
+
* types (e.g. android_agent) so they're hidden in BOTH normal and
|
|
37
|
+
* dev mode. Pass the node's group array so disabled_groups can
|
|
38
|
+
* fire — without groups, only exact-type matches catch.
|
|
39
|
+
*
|
|
40
|
+
* `isAllowed(nodeType)` — positive allowlist. Driven by `enabled_nodes`.
|
|
41
|
+
* `show_all=true` (empty list) returns true for everything. Call
|
|
42
|
+
* sites typically gate this on `!proMode` so dev mode bypasses
|
|
43
|
+
* the allowlist.
|
|
44
|
+
*
|
|
45
|
+
* `isVisible(nodeType, groups?)` — convenience: `!isBlocked && isAllowed`.
|
|
46
|
+
* Use when proMode doesn't matter (every surface SHOULD respect
|
|
47
|
+
* both layers).
|
|
48
|
+
*
|
|
49
|
+
* While the response is loading, all checks return permissively (no
|
|
50
|
+
* palette flash). Both blocklists default to empty when the backend
|
|
51
|
+
* doesn't ship the fields (older deployments).
|
|
16
52
|
*/
|
|
17
53
|
export const useNodeAllowlist = () => {
|
|
18
54
|
const { sendRequest, isConnected } = useWebSocket();
|
|
@@ -28,15 +64,44 @@ export const useNodeAllowlist = () => {
|
|
|
28
64
|
setConfig({
|
|
29
65
|
show_all: response?.show_all ?? true,
|
|
30
66
|
enabled_nodes: response?.enabled_nodes ?? [],
|
|
67
|
+
disabled_groups: response?.disabled_groups ?? [],
|
|
68
|
+
disabled_nodes: response?.disabled_nodes ?? [],
|
|
69
|
+
disabled_credential_categories: response?.disabled_credential_categories ?? [],
|
|
70
|
+
disabled_skill_folders: response?.disabled_skill_folders ?? [],
|
|
31
71
|
});
|
|
32
72
|
})
|
|
33
73
|
.catch((error) => {
|
|
34
74
|
console.error('[NodeAllowlist] Failed to fetch:', error);
|
|
35
|
-
setConfig({
|
|
75
|
+
setConfig({
|
|
76
|
+
show_all: true,
|
|
77
|
+
enabled_nodes: [],
|
|
78
|
+
disabled_groups: [],
|
|
79
|
+
disabled_nodes: [],
|
|
80
|
+
disabled_credential_categories: [],
|
|
81
|
+
disabled_skill_folders: [],
|
|
82
|
+
});
|
|
36
83
|
});
|
|
37
84
|
}, [isConnected, sendRequest]);
|
|
38
85
|
|
|
39
|
-
|
|
86
|
+
/** Absolute blocklist check (mode-independent). False while loading
|
|
87
|
+
* so UI doesn't pre-hide nodes during the fetch round-trip. */
|
|
88
|
+
const isBlocked = useCallback(
|
|
89
|
+
(nodeType: string, groups?: string[] | readonly string[]): boolean => {
|
|
90
|
+
if (!config) return false;
|
|
91
|
+
if (config.disabled_nodes.includes(nodeType)) return true;
|
|
92
|
+
if (groups && groups.length > 0) {
|
|
93
|
+
for (const g of groups) {
|
|
94
|
+
if (config.disabled_groups.includes(g)) return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
[config]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/** Positive allowlist check. True while loading so UI doesn't hide
|
|
103
|
+
* during the fetch. show_all=true returns true for every node. */
|
|
104
|
+
const isAllowed = useCallback(
|
|
40
105
|
(nodeType: string): boolean => {
|
|
41
106
|
if (!config) return true;
|
|
42
107
|
if (config.show_all) return true;
|
|
@@ -45,5 +110,47 @@ export const useNodeAllowlist = () => {
|
|
|
45
110
|
[config]
|
|
46
111
|
);
|
|
47
112
|
|
|
48
|
-
|
|
113
|
+
/** Convenience: hidden if blocked OR not allowed. Honors both layers
|
|
114
|
+
* unconditionally — call sites that want to bypass the allowlist
|
|
115
|
+
* in dev mode should call isBlocked directly and short-circuit
|
|
116
|
+
* the allowlist when proMode is true. */
|
|
117
|
+
const isVisible = useCallback(
|
|
118
|
+
(nodeType: string, groups?: string[] | readonly string[]): boolean => {
|
|
119
|
+
if (isBlocked(nodeType, groups)) return false;
|
|
120
|
+
return isAllowed(nodeType);
|
|
121
|
+
},
|
|
122
|
+
[isBlocked, isAllowed]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/** True if the credential category is in the absolute blocklist —
|
|
126
|
+
* use to filter the Credentials Modal's category list (e.g. hide
|
|
127
|
+
* the entire 'android' panel without removing it from the backend
|
|
128
|
+
* catalogue). False while loading so the modal doesn't pre-hide. */
|
|
129
|
+
const isCredentialCategoryDisabled = useCallback(
|
|
130
|
+
(categoryKey: string): boolean => {
|
|
131
|
+
if (!config) return false;
|
|
132
|
+
return config.disabled_credential_categories.includes(categoryKey);
|
|
133
|
+
},
|
|
134
|
+
[config]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/** True if the skill folder is in the absolute blocklist — use to
|
|
138
|
+
* filter the Master Skill folder dropdown (e.g. hide
|
|
139
|
+
* 'android_agent' when the android feature is disabled). False
|
|
140
|
+
* while loading so the dropdown doesn't pre-hide. */
|
|
141
|
+
const isSkillFolderDisabled = useCallback(
|
|
142
|
+
(folderName: string): boolean => {
|
|
143
|
+
if (!config) return false;
|
|
144
|
+
return config.disabled_skill_folders.includes(folderName);
|
|
145
|
+
},
|
|
146
|
+
[config]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
isVisible,
|
|
151
|
+
isBlocked,
|
|
152
|
+
isAllowed,
|
|
153
|
+
isCredentialCategoryDisabled,
|
|
154
|
+
isSkillFolderDisabled,
|
|
155
|
+
};
|
|
49
156
|
};
|
|
@@ -12,9 +12,21 @@ export interface OnboardingState {
|
|
|
12
12
|
hasChecked: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const DEFAULT_TOTAL_STEPS = 5;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Onboarding wizard state hook.
|
|
19
|
+
*
|
|
20
|
+
* @param reopenTrigger - bump from SettingsPanel to replay the wizard.
|
|
21
|
+
* @param totalSteps - number of steps the wizard will render. Caller
|
|
22
|
+
* owns the step list (`STEPS.length` in OnboardingWizard); the hook
|
|
23
|
+
* uses this only to detect last-step completion. Defaults to 5 for
|
|
24
|
+
* backwards compatibility.
|
|
25
|
+
*/
|
|
26
|
+
export const useOnboarding = (
|
|
27
|
+
reopenTrigger?: number,
|
|
28
|
+
totalSteps: number = DEFAULT_TOTAL_STEPS,
|
|
29
|
+
) => {
|
|
18
30
|
const settingsQuery = useUserSettingsQuery();
|
|
19
31
|
const saveSettings = useSaveUserSettingsMutation();
|
|
20
32
|
const [state, setState] = useState<OnboardingState>({
|
|
@@ -77,14 +89,14 @@ export const useOnboarding = (reopenTrigger?: number) => {
|
|
|
77
89
|
const nextStep = useCallback(() => {
|
|
78
90
|
setState((prev) => {
|
|
79
91
|
const next = prev.currentStep + 1;
|
|
80
|
-
if (next >=
|
|
81
|
-
saveProgress(
|
|
92
|
+
if (next >= totalSteps) {
|
|
93
|
+
saveProgress(totalSteps, true);
|
|
82
94
|
return { ...prev, currentStep: next, isCompleted: true, isVisible: false };
|
|
83
95
|
}
|
|
84
96
|
saveProgress(next, false);
|
|
85
97
|
return { ...prev, currentStep: next };
|
|
86
98
|
});
|
|
87
|
-
}, [saveProgress]);
|
|
99
|
+
}, [saveProgress, totalSteps]);
|
|
88
100
|
|
|
89
101
|
const prevStep = useCallback(() => {
|
|
90
102
|
setState((prev) => {
|
|
@@ -100,13 +112,13 @@ export const useOnboarding = (reopenTrigger?: number) => {
|
|
|
100
112
|
}, [saveProgress, state.currentStep]);
|
|
101
113
|
|
|
102
114
|
const complete = useCallback(() => {
|
|
103
|
-
saveProgress(
|
|
115
|
+
saveProgress(totalSteps, true);
|
|
104
116
|
setState((prev) => ({ ...prev, isVisible: false, isCompleted: true }));
|
|
105
|
-
}, [saveProgress]);
|
|
117
|
+
}, [saveProgress, totalSteps]);
|
|
106
118
|
|
|
107
119
|
return {
|
|
108
120
|
...state,
|
|
109
|
-
totalSteps
|
|
121
|
+
totalSteps,
|
|
110
122
|
nextStep,
|
|
111
123
|
prevStep,
|
|
112
124
|
skip,
|
|
@@ -23,7 +23,8 @@ function defaultsForNodeType(nodeType: string): Record<string, any> {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const useParameterPanel = () => {
|
|
26
|
-
const
|
|
26
|
+
const selectedNode = useAppStore((s) => s.selectedNode);
|
|
27
|
+
const setSelectedNode = useAppStore((s) => s.setSelectedNode);
|
|
27
28
|
const { sendRequest, isConnected } = useWebSocket();
|
|
28
29
|
|
|
29
30
|
const nodeId = selectedNode?.id;
|
|
@@ -10,7 +10,8 @@ interface UseReactFlowNodesProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const useReactFlowNodes = ({ setNodes, setEdges }: UseReactFlowNodesProps) => {
|
|
13
|
-
const
|
|
13
|
+
const selectedNode = useAppStore((s) => s.selectedNode);
|
|
14
|
+
const setSelectedNode = useAppStore((s) => s.setSelectedNode);
|
|
14
15
|
|
|
15
16
|
// Helper function to get node inputs/outputs for both enhanced and legacy nodes
|
|
16
17
|
const getNodeInputs = (nodeType: string): INodeInputDefinition[] => {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSound — React glue for the per-theme WebAudio engine.
|
|
3
|
+
*
|
|
4
|
+
* `useSoundSync()` mounts once at the Dashboard root. It:
|
|
5
|
+
* - mirrors the `soundEnabled` Zustand slice into `Sounds.setEnabled()`
|
|
6
|
+
* - reads `--sound-pack` from `:root` after every theme change and
|
|
7
|
+
* calls `Sounds.setPack(...)` so the active pack tracks the active
|
|
8
|
+
* theme without per-component wiring
|
|
9
|
+
* - installs a global mouseenter / touchstart delegate that fires
|
|
10
|
+
* `play('hover')` for any element matching the design-handoff hover
|
|
11
|
+
* selector list (`.btn`, `.action-btn`, `.row`, `.menu-pop-item`,
|
|
12
|
+
* `.wf-card`, `.comp`, `.cmdk-item`, `[data-sound-hover]`).
|
|
13
|
+
*
|
|
14
|
+
* `useSound()` is the lightweight handle every event handler uses:
|
|
15
|
+
*
|
|
16
|
+
* const play = useSound();
|
|
17
|
+
* <button onClick={() => { play('click'); onSave(); }} />
|
|
18
|
+
*
|
|
19
|
+
* `withSound()` is the convenience wrap to spice an existing handler:
|
|
20
|
+
*
|
|
21
|
+
* <Button onClick={withSound('click', onSave)} />
|
|
22
|
+
*
|
|
23
|
+
* Adding a new sound event: extend `SoundEvent` in lib/sound.ts, add
|
|
24
|
+
* an entry per pack, and fire `play('<event>')` from the relevant
|
|
25
|
+
* handler. No additional wiring here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useEffect } from 'react';
|
|
29
|
+
import { toast } from 'sonner';
|
|
30
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
31
|
+
import { useAppStore } from '../store/useAppStore';
|
|
32
|
+
import { Sounds, type SoundPackName, type SoundEvent } from '../lib/sound';
|
|
33
|
+
|
|
34
|
+
const VALID_PACKS: ReadonlySet<SoundPackName> = new Set([
|
|
35
|
+
'none', 'parchment', 'marble', 'ink', 'clockwork', 'vibraphone',
|
|
36
|
+
'terminal', 'scrap', 'crypt', 'bell', 'telex',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Selector for the global hover delegate. Mirrors the upstream
|
|
40
|
+
* `app/sound.js` auto-capture list, modulo class additions from W15
|
|
41
|
+
* (`.action-btn`, `.wf-card`, `.comp`, `.cmdk-item`). */
|
|
42
|
+
const HOVER_SELECTOR =
|
|
43
|
+
'.btn, .action-btn, .row, .menu-pop-item, .wf-card, .comp, .cmdk-item, [data-sound-hover]';
|
|
44
|
+
|
|
45
|
+
function readSoundPack(): SoundPackName {
|
|
46
|
+
if (typeof document === 'undefined') return 'none';
|
|
47
|
+
const raw = getComputedStyle(document.documentElement)
|
|
48
|
+
.getPropertyValue('--sound-pack')
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/['"]/g, '');
|
|
51
|
+
return VALID_PACKS.has(raw as SoundPackName) ? (raw as SoundPackName) : 'none';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One-shot monkey-patch of `toast.success` / `toast.error` so every
|
|
56
|
+
* call site fires the matching per-theme sound automatically. Sonner
|
|
57
|
+
* exports `toast` as a singleton object with mutable methods, so
|
|
58
|
+
* patching once at module load is safe. Guarded by a flag so React
|
|
59
|
+
* 18+ Strict-Mode double-invocation of useSoundSync doesn't double-
|
|
60
|
+
* wrap the methods.
|
|
61
|
+
*/
|
|
62
|
+
let toastPatched = false;
|
|
63
|
+
function patchToast(): void {
|
|
64
|
+
if (toastPatched) return;
|
|
65
|
+
toastPatched = true;
|
|
66
|
+
const originalSuccess = toast.success;
|
|
67
|
+
const originalError = toast.error;
|
|
68
|
+
// `toast.success` / `toast.error` accept (message, options) and
|
|
69
|
+
// return the toast id. We preserve the return type by deferring to
|
|
70
|
+
// the original after firing the sound.
|
|
71
|
+
toast.success = ((...args: Parameters<typeof originalSuccess>) => {
|
|
72
|
+
Sounds.play('success');
|
|
73
|
+
return originalSuccess.apply(toast, args);
|
|
74
|
+
}) as typeof originalSuccess;
|
|
75
|
+
toast.error = ((...args: Parameters<typeof originalError>) => {
|
|
76
|
+
Sounds.play('error');
|
|
77
|
+
return originalError.apply(toast, args);
|
|
78
|
+
}) as typeof originalError;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Mount once at the Dashboard root. */
|
|
82
|
+
export function useSoundSync(): void {
|
|
83
|
+
const { theme } = useTheme();
|
|
84
|
+
const enabled = useAppStore((s) => s.soundEnabled);
|
|
85
|
+
|
|
86
|
+
// Patch sonner's `toast.success` / `toast.error` so every call fires
|
|
87
|
+
// the matching sound. Idempotent — safe under React Strict Mode.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
patchToast();
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
Sounds.setEnabled(enabled);
|
|
94
|
+
}, [enabled]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
Sounds.setPack(readSoundPack());
|
|
98
|
+
}, [theme]);
|
|
99
|
+
|
|
100
|
+
// Global hover delegate. Capture-phase listener so it picks up
|
|
101
|
+
// mouseenter on any matching surface without each component wiring
|
|
102
|
+
// its own onMouseEnter handler. touchstart mirrors the same selector
|
|
103
|
+
// list for mobile / hybrid devices.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const handleHover = (event: Event) => {
|
|
106
|
+
const target = event.target as Element | null;
|
|
107
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
108
|
+
if (target.closest(HOVER_SELECTOR)) {
|
|
109
|
+
Sounds.play('hover');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// mouseenter doesn't bubble, so we use the capture-phase approach
|
|
113
|
+
// via mouseover (which does bubble) plus a relatedTarget filter so
|
|
114
|
+
// a single hover only fires once per crossing-into-element.
|
|
115
|
+
const handleMouseOver = (event: MouseEvent) => {
|
|
116
|
+
const target = event.target as Element | null;
|
|
117
|
+
const related = event.relatedTarget as Element | null;
|
|
118
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
119
|
+
const matched = target.closest(HOVER_SELECTOR);
|
|
120
|
+
if (!matched) return;
|
|
121
|
+
// Only fire on enter — if the previous mouse position was already
|
|
122
|
+
// inside the same matched element, skip.
|
|
123
|
+
if (related && matched.contains(related)) return;
|
|
124
|
+
Sounds.play('hover');
|
|
125
|
+
};
|
|
126
|
+
// Wave 33: PASSIVE listener so the handler can never block scroll /
|
|
127
|
+
// input dispatch. The handler doesn't call preventDefault, so passive
|
|
128
|
+
// is safe. Bare `true` (capture-only) registers an ACTIVE listener,
|
|
129
|
+
// which means the browser must wait for the handler to finish before
|
|
130
|
+
// dispatching the next input event — on tab return, when a burst of
|
|
131
|
+
// queued mouseover events fires while the mouse is over the canvas,
|
|
132
|
+
// the active handler's closest() DOM-walks blocked first-click input
|
|
133
|
+
// dispatch by 5-15ms. Passive removes that block.
|
|
134
|
+
//
|
|
135
|
+
// removeEventListener's options bag only consults `capture` for
|
|
136
|
+
// matching (W3C spec — passive isn't part of the listener identity),
|
|
137
|
+
// so the cleanup pair below uses the same shape but TypeScript's
|
|
138
|
+
// EventListenerOptions doesn't include `passive` — pass `true` for
|
|
139
|
+
// capture-only removal which matches both add registrations.
|
|
140
|
+
document.addEventListener('mouseover', handleMouseOver, { capture: true, passive: true });
|
|
141
|
+
document.addEventListener('touchstart', handleHover, { capture: true, passive: true });
|
|
142
|
+
return () => {
|
|
143
|
+
document.removeEventListener('mouseover', handleMouseOver, true);
|
|
144
|
+
document.removeEventListener('touchstart', handleHover, true);
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
// One-shot AudioContext unlock on the first user gesture. Modern
|
|
149
|
+
// browsers (Chrome / Safari) keep the AudioContext suspended until a
|
|
150
|
+
// resume() call originates from a gesture handler — without this, the
|
|
151
|
+
// first play() can land microseconds after the gesture frame and the
|
|
152
|
+
// sound is silently dropped. `Sounds.unlock()` is idempotent so the
|
|
153
|
+
// `{ once: true }` listeners cover the lifetime fine.
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const handler = () => { Sounds.unlock(); };
|
|
156
|
+
const opts: AddEventListenerOptions = { once: true, capture: true, passive: true };
|
|
157
|
+
window.addEventListener('pointerdown', handler, opts);
|
|
158
|
+
window.addEventListener('keydown', handler, opts);
|
|
159
|
+
window.addEventListener('touchstart', handler, opts);
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener('pointerdown', handler, opts);
|
|
162
|
+
window.removeEventListener('keydown', handler, opts);
|
|
163
|
+
window.removeEventListener('touchstart', handler, opts);
|
|
164
|
+
};
|
|
165
|
+
}, []);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Play handle. Returns the same `Sounds.play` reference each render. */
|
|
169
|
+
export function useSound(): (event: SoundEvent) => void {
|
|
170
|
+
return Sounds.play;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Wrap an existing onClick / onChange handler so the matching sound
|
|
174
|
+
* fires before the underlying handler runs. Sound events are non-
|
|
175
|
+
* blocking (fire-and-forget OscillatorNode). When the engine is
|
|
176
|
+
* disabled or the active pack is `none`, this is a no-op. */
|
|
177
|
+
export function withSound<E extends ((...args: any[]) => any) | undefined>(
|
|
178
|
+
event: SoundEvent,
|
|
179
|
+
handler: E,
|
|
180
|
+
): (...args: E extends (...args: infer P) => any ? P : never[]) => void {
|
|
181
|
+
return ((...args: any[]) => {
|
|
182
|
+
Sounds.play(event);
|
|
183
|
+
handler?.(...args);
|
|
184
|
+
}) as any;
|
|
185
|
+
}
|
|
@@ -3,14 +3,12 @@ import { useAppStore } from '../store/useAppStore';
|
|
|
3
3
|
import { useWorkflowsQuery, type SavedWorkflow } from './useWorkflowsQuery';
|
|
4
4
|
|
|
5
5
|
export const useWorkflowManagement = () => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
createNewWorkflow,
|
|
13
|
-
} = useAppStore();
|
|
6
|
+
const currentWorkflow = useAppStore((s) => s.currentWorkflow);
|
|
7
|
+
const hasUnsavedChanges = useAppStore((s) => s.hasUnsavedChanges);
|
|
8
|
+
const updateWorkflow = useAppStore((s) => s.updateWorkflow);
|
|
9
|
+
const saveWorkflow = useAppStore((s) => s.saveWorkflow);
|
|
10
|
+
const loadWorkflow = useAppStore((s) => s.loadWorkflow);
|
|
11
|
+
const createNewWorkflow = useAppStore((s) => s.createNewWorkflow);
|
|
14
12
|
|
|
15
13
|
const { data: savedWorkflows = [] } = useWorkflowsQuery();
|
|
16
14
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listens for backend-pushed `workflow_ops_apply` events and applies
|
|
3
|
+
* them to the live React Flow canvas via the standard
|
|
4
|
+
* `applyOperations` reconciler.
|
|
5
|
+
*
|
|
6
|
+
* Source of events: `services/status_broadcaster.send_custom_event`
|
|
7
|
+
* called by the Agent Builder's tool functions
|
|
8
|
+
* (`server/nodes/tool/agent_builder.py`) after a successful mutation.
|
|
9
|
+
*
|
|
10
|
+
* Filtering:
|
|
11
|
+
* - Events whose `workflow_id` matches the current workflow apply
|
|
12
|
+
* to the canvas in-place.
|
|
13
|
+
* - Events for OTHER workflows (e.g. `create_workflow` returning a
|
|
14
|
+
* fresh id) surface as a sonner toast with a Switch action so the
|
|
15
|
+
* user can jump to the new workflow without losing their place.
|
|
16
|
+
*
|
|
17
|
+
* Mounted once in Dashboard.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect } from 'react';
|
|
21
|
+
import type { Node, Edge } from 'reactflow';
|
|
22
|
+
import { toast } from 'sonner';
|
|
23
|
+
|
|
24
|
+
import { useWebSocket } from '../contexts/WebSocketContext';
|
|
25
|
+
import { useAppStore } from '../store/useAppStore';
|
|
26
|
+
import { applyOperations, type WorkflowOperation } from '../lib/workflowOps';
|
|
27
|
+
|
|
28
|
+
interface WorkflowOpsApplyEvent {
|
|
29
|
+
workflow_id?: string | null;
|
|
30
|
+
caller_node_id?: string | null;
|
|
31
|
+
operations?: WorkflowOperation[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UseWorkflowOpsListenerProps {
|
|
35
|
+
nodes: Node[];
|
|
36
|
+
edges: Edge[];
|
|
37
|
+
setNodes: (updater: (ns: Node[]) => Node[]) => void;
|
|
38
|
+
setEdges: (updater: (es: Edge[]) => Edge[]) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useWorkflowOpsListener({
|
|
42
|
+
nodes,
|
|
43
|
+
edges,
|
|
44
|
+
setNodes,
|
|
45
|
+
setEdges,
|
|
46
|
+
}: UseWorkflowOpsListenerProps) {
|
|
47
|
+
const { addEventListener, saveNodeParameters } = useWebSocket();
|
|
48
|
+
const currentWorkflowId = useAppStore(s => s.currentWorkflow?.id);
|
|
49
|
+
const loadWorkflow = useAppStore(s => s.loadWorkflow);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const unsubscribe = addEventListener('workflow_ops_apply', (raw: WorkflowOpsApplyEvent) => {
|
|
53
|
+
const ops = raw?.operations ?? [];
|
|
54
|
+
if (ops.length === 0 && !raw?.workflow_id) return;
|
|
55
|
+
|
|
56
|
+
// Different workflow -- surface a toast with a switch action.
|
|
57
|
+
// Used by `create_workflow`, which persists a new workflow but
|
|
58
|
+
// doesn't try to mutate the canvas the user is currently on.
|
|
59
|
+
if (raw.workflow_id && raw.workflow_id !== currentWorkflowId) {
|
|
60
|
+
toast.message('New workflow created', {
|
|
61
|
+
description: `Workflow ${raw.workflow_id} is ready.`,
|
|
62
|
+
action: {
|
|
63
|
+
label: 'Switch',
|
|
64
|
+
onClick: () => loadWorkflow(raw.workflow_id!),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ops.length === 0) return;
|
|
71
|
+
|
|
72
|
+
void applyOperations(ops, {
|
|
73
|
+
nodes,
|
|
74
|
+
edges,
|
|
75
|
+
setNodes,
|
|
76
|
+
setEdges,
|
|
77
|
+
saveNodeParameters,
|
|
78
|
+
}).then(result => {
|
|
79
|
+
if (result.errors.length > 0) {
|
|
80
|
+
console.warn('[workflow_ops_apply] some ops failed:', result.errors);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return unsubscribe;
|
|
86
|
+
}, [
|
|
87
|
+
addEventListener, currentWorkflowId, loadWorkflow,
|
|
88
|
+
nodes, edges, setNodes, setEdges, saveNodeParameters,
|
|
89
|
+
]);
|
|
90
|
+
}
|