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.
Files changed (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /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 { currentWorkflow } = useAppStore();
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 a membership check
11
- * for the Component Palette.
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
- * The backend decides whether to filter (show_all flag); the frontend only
14
- * checks list membership. While the response is loading, all nodes are shown
15
- * to avoid a palette flash.
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({ show_all: true, enabled_nodes: [] });
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
- const isVisible = useCallback(
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
- return { isVisible };
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 TOTAL_STEPS = 5;
15
+ const DEFAULT_TOTAL_STEPS = 5;
16
16
 
17
- export const useOnboarding = (reopenTrigger?: number) => {
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 >= TOTAL_STEPS) {
81
- saveProgress(TOTAL_STEPS, true);
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(TOTAL_STEPS, true);
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: TOTAL_STEPS,
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 { selectedNode, setSelectedNode } = useAppStore();
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 { selectedNode, setSelectedNode } = useAppStore();
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
- currentWorkflow,
8
- hasUnsavedChanges,
9
- updateWorkflow,
10
- saveWorkflow,
11
- loadWorkflow,
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
+ }