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
@@ -11,13 +11,16 @@
11
11
  */
12
12
 
13
13
  import React, { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from 'react';
14
+ import ReconnectingWebSocket from 'partysocket/ws';
14
15
  import { API_CONFIG } from '../config/api';
15
16
  import { useAppStore } from '../store/useAppStore';
16
17
  import { useAuth } from './AuthContext';
17
18
  import { queryClient } from '../lib/queryClient';
18
19
  import { queryKeys } from '../lib/queryConfig';
19
- import { CATALOGUE_QUERY_KEY } from '../hooks/useCatalogueQuery';
20
+ import { invalidateCatalogue } from '../hooks/useCatalogueQuery';
20
21
  import { nodeParamsQueryKey } from '../hooks/useNodeParamsQuery';
22
+ import type { WorkflowEvent } from '../types/cloudEvents';
23
+ import { WS_CLOSE, WS_RECONNECT } from '../lib/connectionConfig';
21
24
  import {
22
25
  useNodeStatusStore,
23
26
  useNodeStatusForId,
@@ -173,9 +176,17 @@ export interface RateLimitStats {
173
176
  pause_reason?: string;
174
177
  }
175
178
 
179
+ /**
180
+ * In-memory validation-result cache for an API-key provider.
181
+ *
182
+ * NOT a "do we have a stored key" answer — that comes from the
183
+ * `useCatalogueQuery['credentialCatalogue']` `provider.stored` field
184
+ * (single source of truth, derived from the encrypted DB on each
185
+ * `get_credential_catalogue` round-trip). Mixing the two flags caused
186
+ * cross-tab drift; the duplicated `hasKey` field was retired.
187
+ */
176
188
  export interface ApiKeyStatus {
177
189
  valid: boolean;
178
- hasKey?: boolean;
179
190
  message?: string;
180
191
  models?: string[];
181
192
  timestamp?: number;
@@ -311,6 +322,12 @@ interface WebSocketContextValue {
311
322
  // Generic request method
312
323
  sendRequest: <T = any>(type: string, data?: Record<string, any>) => Promise<T>;
313
324
 
325
+ // Generic broadcast subscription. Returns an unsubscribe fn.
326
+ // Use for ad-hoc backend-pushed events like `workflow_ops_apply`
327
+ // (see server/services/status_broadcaster.send_custom_event) so a
328
+ // new listener doesn't require a new switch case + state slice.
329
+ addEventListener: (type: string, handler: (data: any) => void) => () => void;
330
+
314
331
  // Node Parameters
315
332
  getNodeParameters: (nodeId: string) => Promise<NodeParameters | null>;
316
333
  getAllNodeParameters: (nodeIds: string[]) => Promise<Record<string, NodeParameters>>;
@@ -492,10 +509,20 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
492
509
  // Per-workflow compaction stats: workflow_id -> session_id -> CompactionStats (n8n pattern)
493
510
  const [allCompactionStats, setAllCompactionStats] = useState<Record<string, Record<string, CompactionStats>>>({});
494
511
 
495
- const wsRef = useRef<WebSocket | null>(null);
496
- const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
512
+ // PartySocket's `ReconnectingWebSocket` implements the native WebSocket
513
+ // surface (`send`, `close`, `readyState`, `addEventListener`, `onopen`,
514
+ // `onmessage`, `onclose`, `onerror`) so consumers — including the request
515
+ // correlation map and ping loop below — work unchanged. The ref's element
516
+ // type tightens to the library's class so any feature-specific calls
517
+ // (`shouldReconnect`, `reconnect()`) type-check.
518
+ const wsRef = useRef<ReconnectingWebSocket | null>(null);
497
519
  const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
498
520
  const pendingRequestsRef = useRef<Map<string, PendingRequest>>(new Map());
521
+ // Generic broadcast subscribers ({type -> Set<handler>}) for events
522
+ // surfaced via send_custom_event on the backend. Lets new features
523
+ // listen for ad-hoc broadcasts without growing the switch statement
524
+ // or the context state shape.
525
+ const eventListenersRef = useRef<Map<string, Set<(data: any) => void>>>(new Map());
499
526
  // Pending-send queue for backpressure + replay across reconnects.
500
527
  // Drained inside `ws.onopen` after the init burst. Source of truth
501
528
  // for currentWorkflowId is `useAppStore.getState().currentWorkflow?.id`
@@ -518,7 +545,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
518
545
 
519
546
  // Fetch deployment status for the new workflow (n8n pattern)
520
547
  // This ensures the deploy button shows correct state when switching workflows
521
- if (wsRef.current?.readyState === WebSocket.OPEN) {
548
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
522
549
  const fetchDeploymentStatus = async () => {
523
550
  try {
524
551
  const requestId = generateRequestId();
@@ -652,7 +679,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
652
679
  }
653
680
  // Catalogue 'stored' flags derive from api_keys + oauth state;
654
681
  // re-sync once after the bulk status lands.
655
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
682
+ invalidateCatalogue(queryClient);
656
683
  }
657
684
  break;
658
685
 
@@ -664,16 +691,26 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
664
691
  }));
665
692
  // Sidebar catalogue's `stored` flag depends on server-side
666
693
  // api_keys + oauth state; refresh it.
667
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
694
+ invalidateCatalogue(queryClient);
668
695
  }
669
696
  break;
670
697
 
671
- case 'credential_catalogue_updated':
672
- // Future server emitter (per useCatalogueQuery.ts TODO). Client
673
- // handler is wired now so once the server pushes it, consumers
674
- // refresh automatically.
675
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
698
+ case 'credential_catalogue_updated': {
699
+ // The backend wraps a CloudEvents v1.0 `WorkflowEvent` envelope
700
+ // inside `data` (see server/services/status_broadcaster.py and
701
+ // server/services/events/envelope.py). The legacy outer wire
702
+ // key stays as the dispatch tag for back-compat, but the
703
+ // envelope's `id` / `time` / nested `type` (e.g.
704
+ // `credential.api_key.saved`) are now statically typed for any
705
+ // future consumer that wants ordering, dedup, or fine-grained
706
+ // glob dispatch via `matchesType()`.
707
+ const event = data as WorkflowEvent<{ provider: string; customer_id?: string }>;
708
+ // Today the only action is to refetch the catalogue; the
709
+ // envelope is read for telemetry / future dispatch.
710
+ void event;
711
+ invalidateCatalogue(queryClient);
676
712
  break;
713
+ }
677
714
 
678
715
  case 'android_status':
679
716
  setAndroidStatus(data || defaultAndroidStatus);
@@ -681,7 +718,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
681
718
 
682
719
  case 'whatsapp_status':
683
720
  setWhatsappStatus(data || defaultWhatsAppStatus);
684
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
721
+ invalidateCatalogue(queryClient);
685
722
  break;
686
723
 
687
724
  case 'twitter_oauth_complete':
@@ -694,7 +731,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
694
731
  name: data.name,
695
732
  profile_image_url: data.profile_image_url,
696
733
  });
697
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
734
+ invalidateCatalogue(queryClient);
698
735
  }
699
736
  break;
700
737
 
@@ -707,7 +744,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
707
744
  name: data.name,
708
745
  profile_image_url: data.profile_image_url,
709
746
  });
710
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
747
+ invalidateCatalogue(queryClient);
711
748
  }
712
749
  break;
713
750
 
@@ -719,7 +756,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
719
756
  email: data.email || null,
720
757
  name: data.name,
721
758
  });
722
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
759
+ invalidateCatalogue(queryClient);
723
760
  }
724
761
  break;
725
762
 
@@ -733,7 +770,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
733
770
  bot_id: data.bot_id || null,
734
771
  owner_chat_id: data.owner_chat_id ?? null,
735
772
  });
736
- queryClient.invalidateQueries({ queryKey: CATALOGUE_QUERY_KEY });
773
+ invalidateCatalogue(queryClient);
737
774
  }
738
775
  break;
739
776
 
@@ -804,6 +841,53 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
804
841
  }
805
842
  break;
806
843
 
844
+ case 'agent_progress': {
845
+ // CloudEvents v1.0 envelope from broadcaster.broadcast_agent_progress.
846
+ // Inner payload: { node_id, iteration, max_iterations, phase? }.
847
+ // Routes into nodeStatusStore (same per-workflow / per-node slot
848
+ // the existing useNodeStatus consumers read) so the AI-agent body
849
+ // can render "iteration / max_iterations" live without a parallel
850
+ // store. Wire-key parity with `credential_catalogue_updated`.
851
+ const envelope = data as WorkflowEvent<{
852
+ node_id?: string;
853
+ iteration?: number;
854
+ max_iterations?: number;
855
+ phase?: string;
856
+ }> | undefined;
857
+ const inner = envelope?.data;
858
+ const targetNodeId = inner?.node_id || envelope?.subject;
859
+ const progressWorkflowId =
860
+ envelope?.workflow_id || message.workflow_id || 'unknown';
861
+ if (targetNodeId && inner) {
862
+ const store = useNodeStatusStore.getState();
863
+ const previous =
864
+ store.allStatuses[progressWorkflowId]?.[targetNodeId] ||
865
+ ({} as NodeStatus);
866
+ // Defensive: an agent_progress event implies the agent IS
867
+ // mid-loop. Set status='executing' even if no prior
868
+ // node_status broadcast arrived first (race or edge case
869
+ // where the agent finishes in a single step). Without this,
870
+ // AIAgentNode's `isExecuting && iteration != null` gate
871
+ // hides the badge entirely.
872
+ const carriedStatus =
873
+ previous.status === 'success' || previous.status === 'error'
874
+ ? previous.status
875
+ : 'executing';
876
+ store.setStatus(progressWorkflowId, targetNodeId, {
877
+ ...previous,
878
+ status: carriedStatus,
879
+ workflow_id: progressWorkflowId,
880
+ data: {
881
+ ...(previous.data || {}),
882
+ iteration: inner.iteration,
883
+ max_iterations: inner.max_iterations,
884
+ ...(inner.phase ? { phase: inner.phase } : {}),
885
+ },
886
+ });
887
+ }
888
+ break;
889
+ }
890
+
807
891
  case 'node_status_cleared':
808
892
  // Handle broadcast from server when node status is cleared
809
893
  if (node_id || message.node_id) {
@@ -889,6 +973,57 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
889
973
  break;
890
974
  }
891
975
 
976
+ case 'deployment_snapshot': {
977
+ // CloudEvents v1.0 envelope from broadcaster._send_deployment_snapshot.
978
+ // Pushed once per WS connect so the FE can reconcile its stale
979
+ // `deploymentStatus.isRunning=true` after a backend restart that
980
+ // wiped DeploymentManager._deployments. Empty list is meaningful
981
+ // and forces a reset — the prior bug was: backend restart wiped
982
+ // the deployment dict, FE never got a `stopped` broadcast (because
983
+ // there was nothing to broadcast about), so the Start button
984
+ // stayed showing "Stop" forever.
985
+ const envelope = data as WorkflowEvent<{
986
+ running_workflow_ids?: string[];
987
+ }> | undefined;
988
+ const runningIds = envelope?.data?.running_workflow_ids ?? [];
989
+ const runningSet = new Set(runningIds);
990
+ const store = useAppStore.getState();
991
+ const currentId = store.currentWorkflow?.id;
992
+
993
+ // Reconcile per-workflow execution state in workflowUIStates.
994
+ // Anything currently flagged isExecuting=true that isn't in
995
+ // the snapshot's running set gets cleared (the load-bearing
996
+ // reset for stale state after backend restart). Anything in
997
+ // the snapshot gets flagged true.
998
+ const existingStates = store.workflowUIStates ?? {};
999
+ for (const [wid, ui] of Object.entries(existingStates)) {
1000
+ if (ui?.isExecuting && !runningSet.has(wid)) {
1001
+ store.setWorkflowExecuting(wid, false);
1002
+ }
1003
+ }
1004
+ for (const wid of runningIds) {
1005
+ store.setWorkflowExecuting(wid, true);
1006
+ }
1007
+
1008
+ // Reconcile the toolbar `deploymentStatus` for the active workflow
1009
+ setDeploymentStatus(prev => {
1010
+ const next: DeploymentStatus = { ...prev };
1011
+ if (currentId && runningSet.has(currentId)) {
1012
+ next.isRunning = true;
1013
+ next.status = 'running';
1014
+ next.workflow_id = currentId;
1015
+ } else if (currentId && !runningSet.has(currentId) && prev.workflow_id === currentId) {
1016
+ // Previously thought current workflow was deployed; backend says no.
1017
+ next.isRunning = false;
1018
+ next.status = 'stopped';
1019
+ next.workflow_id = null;
1020
+ next.activeRuns = 0;
1021
+ }
1022
+ return next;
1023
+ });
1024
+ break;
1025
+ }
1026
+
892
1027
  case 'deployment_status':
893
1028
  // Handle deployment status updates (event-driven, no iterations)
894
1029
  // Per-workflow scoping (n8n pattern): Only apply updates for current workflow
@@ -1141,8 +1276,21 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1141
1276
  console.error('[WebSocket] Server error:', message.code, message.message);
1142
1277
  break;
1143
1278
 
1144
- default:
1279
+ default: {
1280
+ // Generic broadcast dispatch -- any listener registered via
1281
+ // addEventListener(type, handler) gets the message data. Lets
1282
+ // backend-only features (e.g. workflow_ops_apply) ship without
1283
+ // adding a switch case + state slice every time.
1284
+ const listeners = eventListenersRef.current.get(type);
1285
+ if (listeners && listeners.size > 0) {
1286
+ for (const handler of listeners) {
1287
+ try { handler(data); } catch (err) {
1288
+ console.error(`[WebSocket] Listener for '${type}' threw:`, err);
1289
+ }
1290
+ }
1291
+ }
1145
1292
  break;
1293
+ }
1146
1294
  }
1147
1295
  } catch (error) {
1148
1296
  console.error('[WebSocket] Failed to parse message:', error);
@@ -1153,7 +1301,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1153
1301
  // fresh request_id and a reset timeout budget; responses correlate via the
1154
1302
  // existing pendingRequestsRef map. Per-call abortController cancels the
1155
1303
  // queue-side timeout that was running while the request was waiting in line.
1156
- const drainPendingSends = useCallback((ws: WebSocket) => {
1304
+ const drainPendingSends = useCallback((ws: ReconnectingWebSocket) => {
1157
1305
  const queue = pendingSendQueueRef.current;
1158
1306
  pendingSendQueueRef.current = [];
1159
1307
  for (const queued of queue) {
@@ -1194,16 +1342,34 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1194
1342
  }
1195
1343
  }, []);
1196
1344
 
1197
- // Connect to WebSocket
1345
+ // Connect to WebSocket. Uses PartySocket's `ReconnectingWebSocket` —
1346
+ // a native-WebSocket-compatible class with built-in jittered exponential
1347
+ // backoff, message replay, and intentional-close (code 1000) handling.
1348
+ // Replaces the previous flat 3 s `setTimeout(reconnect)` loop.
1349
+ // Backoff envelope is configured via `WS_RECONNECT` in
1350
+ // `lib/connectionConfig.ts` so a future tuning pass is a one-file edit.
1351
+ // Ref: https://docs.partykit.io/reference/partysocket-api/
1198
1352
  const connect = useCallback(() => {
1199
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1353
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1200
1354
  return;
1201
1355
  }
1202
1356
 
1203
1357
  const wsUrl = getWebSocketUrl();
1204
1358
 
1205
1359
  try {
1206
- const ws = new WebSocket(wsUrl);
1360
+ const ws = new ReconnectingWebSocket(wsUrl, [], {
1361
+ minReconnectionDelay: WS_RECONNECT.MIN_DELAY_MS,
1362
+ maxReconnectionDelay: WS_RECONNECT.MAX_DELAY_MS,
1363
+ reconnectionDelayGrowFactor: WS_RECONNECT.GROW_FACTOR,
1364
+ // Reconnect indefinitely while the page is open. Intentional
1365
+ // closes via `ws.close(1000, ...)` (logout / unmount) skip the
1366
+ // reconnect path because PartySocket inspects the close code.
1367
+ maxRetries: Infinity,
1368
+ // Send-while-disconnected buffer; replayed automatically on the
1369
+ // next OPEN. Mirrors the previous `pendingSendQueueRef` cap intent
1370
+ // for opportunistic out-of-band sends.
1371
+ maxEnqueuedMessages: WS_RECONNECT.MAX_ENQUEUED_MESSAGES,
1372
+ });
1207
1373
 
1208
1374
  ws.onopen = async () => {
1209
1375
  setIsConnected(true);
@@ -1219,51 +1385,65 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1219
1385
 
1220
1386
  // Start ping interval
1221
1387
  pingIntervalRef.current = setInterval(() => {
1222
- if (ws.readyState === WebSocket.OPEN) {
1388
+ if (ws.readyState === ReconnectingWebSocket.OPEN) {
1223
1389
  ws.send(JSON.stringify({ type: 'ping' }));
1224
1390
  }
1225
1391
  }, 30000);
1226
1392
 
1227
- // Load initial API key statuses for known providers
1228
- const providers = ['openai', 'anthropic', 'gemini', 'google_maps', 'android_remote'];
1229
- for (const provider of providers) {
1230
- try {
1231
- const response = await new Promise<any>((resolve, reject) => {
1232
- const requestId = `init_${provider}_${Date.now()}`;
1233
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1234
-
1235
- const handler = (event: MessageEvent) => {
1236
- try {
1237
- const msg = JSON.parse(event.data);
1238
- if (msg.request_id === requestId) {
1239
- clearTimeout(timeout);
1240
- ws.removeEventListener('message', handler);
1241
- resolve(msg);
1242
- }
1243
- } catch {}
1244
- };
1245
-
1246
- ws.addEventListener('message', handler);
1247
- ws.send(JSON.stringify({ type: 'get_stored_api_key', provider, request_id: requestId }));
1248
- });
1249
-
1250
- if (response.hasKey) {
1251
- setApiKeyStatuses(prev => ({
1252
- ...prev,
1253
- [provider]: { hasKey: true, valid: true }
1254
- }));
1255
- }
1256
- } catch {
1257
- // Ignore errors during initial check
1258
- }
1259
- }
1393
+ // Drain any sends that were queued while the socket was reconnecting.
1394
+ // Runs BEFORE setIsReady(true) so isReady-gated callers don't race
1395
+ // an empty pendingRequestsRef. Mirrors socket.io-client's offline
1396
+ // buffer + Apollo RetryLink semantics.
1397
+ drainPendingSends(ws);
1260
1398
 
1261
- // Load terminal log history
1262
- try {
1263
- const terminalResponse = await new Promise<any>((resolve, reject) => {
1264
- const requestId = `terminal_logs_${Date.now()}`;
1265
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1399
+ // Wave 32: flip `isReady` IMMEDIATELY on socket open + queue drain.
1400
+ // The page-state restore (terminal / chat / console history) fires
1401
+ // in the BACKGROUND below its results trickle into state writes
1402
+ // when each request settles, but UI interaction is no longer blocked
1403
+ // behind a serial 5-up-to-25-second `Promise.allSettled` await.
1404
+ //
1405
+ // Why this matters: tab-blur + WS reconnect previously left the user
1406
+ // staring at an unresponsive workflow until init-burst finished.
1407
+ // First click "did nothing" because catalogue / nodeSpec / credentials
1408
+ // queries gate on `isReady` and stayed disabled. The cache (warmed
1409
+ // from localStorage via PersistQueryClientProvider for `nodeSpec` /
1410
+ // `nodeGroups` / `pluginCatalogue` / `skillContent`) carries the
1411
+ // visible state until refreshes land.
1412
+ //
1413
+ // Wave 32 also dropped the legacy hardcoded `probeApiKey` loop over
1414
+ // `['openai', 'anthropic', 'gemini', 'google_maps', 'android_remote']`.
1415
+ // Those probes were redundant — credential state has TWO authoritative
1416
+ // sources already:
1417
+ // 1. The backend's `initial_status` broadcast (handled at line ~638)
1418
+ // pushes the full `api_keys` map on every reconnect.
1419
+ // 2. The catalogue (TanStack Query `useCatalogueQuery`) carries the
1420
+ // `provider.stored` flag for every provider; refetched via the
1421
+ // debounced `invalidateCatalogue(queryClient)` helper that 8+
1422
+ // credential CloudEvent handlers already fire (`api_key_status`,
1423
+ // `credential_catalogue_updated`, `whatsapp_status`,
1424
+ // `twitter_oauth_complete`, `google_oauth_complete`,
1425
+ // `google_status`, `telegram_status`, `initial_status`).
1426
+ // No frontend should hardcode a provider list — adding a new
1427
+ // provider should be a backend-only edit.
1428
+ setIsReady(true);
1266
1429
 
1430
+ // Single-shot catalogue invalidate so any credential mutations that
1431
+ // landed on the server while the socket was disconnected propagate
1432
+ // to the in-memory cache immediately. Debounced (300ms trailing) so
1433
+ // it coalesces with other broadcast-driven invalidations.
1434
+ invalidateCatalogue(queryClient);
1435
+
1436
+ // Page-state restore (background). Fire-and-forget — these writes
1437
+ // hydrate panels that PersistQueryClient doesn't cache (terminal /
1438
+ // chat / console history come straight from the server's per-request
1439
+ // log read, not from a query cache).
1440
+ const sendBurstRequest = <T = any>(payload: object, idPrefix: string): Promise<T> =>
1441
+ new Promise<T>((resolve, reject) => {
1442
+ const requestId = `${idPrefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1443
+ const timeout = setTimeout(() => {
1444
+ ws.removeEventListener('message', handler);
1445
+ reject(new Error('Timeout'));
1446
+ }, 5000);
1267
1447
  const handler = (event: MessageEvent) => {
1268
1448
  try {
1269
1449
  const msg = JSON.parse(event.data);
@@ -1274,124 +1454,77 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1274
1454
  }
1275
1455
  } catch {}
1276
1456
  };
1277
-
1278
1457
  ws.addEventListener('message', handler);
1279
- ws.send(JSON.stringify({ type: 'get_terminal_logs', request_id: requestId }));
1458
+ ws.send(JSON.stringify({ ...payload, request_id: requestId }));
1280
1459
  });
1281
1460
 
1282
- if (terminalResponse.success && terminalResponse.logs) {
1283
- // Map server logs to TerminalLogEntry format (newest first)
1284
- const logs: TerminalLogEntry[] = terminalResponse.logs.map((log: any) => ({
1285
- timestamp: log.timestamp || new Date().toISOString(),
1286
- level: log.level || 'info',
1287
- message: log.message || '',
1288
- source: log.source,
1289
- details: log.details
1290
- })).reverse(); // Server stores oldest first, we want newest first
1291
- setTerminalLogs(logs);
1461
+ void (async () => {
1462
+ try {
1463
+ const terminalResponse = await sendBurstRequest<any>(
1464
+ { type: 'get_terminal_logs' },
1465
+ 'terminal_logs',
1466
+ );
1467
+ if (terminalResponse.success && terminalResponse.logs) {
1468
+ const logs: TerminalLogEntry[] = terminalResponse.logs.map((log: any) => ({
1469
+ timestamp: log.timestamp || new Date().toISOString(),
1470
+ level: log.level || 'info',
1471
+ message: log.message || '',
1472
+ source: log.source,
1473
+ details: log.details,
1474
+ })).reverse();
1475
+ setTerminalLogs(logs);
1476
+ }
1477
+ } catch {
1478
+ // Ignore errors loading terminal logs
1292
1479
  }
1293
- } catch {
1294
- // Ignore errors loading terminal logs
1295
- }
1296
-
1297
- // Load chat message history from database
1298
- try {
1299
- const chatResponse = await new Promise<any>((resolve, reject) => {
1300
- const requestId = `chat_messages_${Date.now()}`;
1301
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1302
-
1303
- const handler = (event: MessageEvent) => {
1304
- try {
1305
- const msg = JSON.parse(event.data);
1306
- if (msg.request_id === requestId) {
1307
- clearTimeout(timeout);
1308
- ws.removeEventListener('message', handler);
1309
- resolve(msg);
1310
- }
1311
- } catch {}
1312
- };
1480
+ })();
1313
1481
 
1314
- ws.addEventListener('message', handler);
1315
- // Scope to the currently-open workflow if any. session_id is
1316
- // re-used as the workflow identifier on the chat side so the
1317
- // panel only shows messages tied to this workflow. Falls
1318
- // back to "default" when no workflow is selected (initial
1319
- // bootstrap before the user opens any workflow).
1482
+ void (async () => {
1483
+ try {
1320
1484
  const workflowId = useAppStore.getState().currentWorkflow?.id || 'default';
1321
- ws.send(JSON.stringify({ type: 'get_chat_messages', session_id: workflowId, request_id: requestId }));
1322
- });
1323
-
1324
- if (chatResponse.success && chatResponse.messages) {
1325
- const messages: ChatMessage[] = chatResponse.messages.map((msg: any) => ({
1326
- role: msg.role as 'user' | 'assistant',
1327
- message: msg.message,
1328
- timestamp: msg.timestamp
1329
- }));
1330
- setChatMessages(messages);
1485
+ const chatResponse = await sendBurstRequest<any>(
1486
+ { type: 'get_chat_messages', session_id: workflowId },
1487
+ 'chat_messages',
1488
+ );
1489
+ if (chatResponse.success && chatResponse.messages) {
1490
+ const messages: ChatMessage[] = chatResponse.messages.map((msg: any) => ({
1491
+ role: msg.role as 'user' | 'assistant',
1492
+ message: msg.message,
1493
+ timestamp: msg.timestamp,
1494
+ }));
1495
+ setChatMessages(messages);
1496
+ }
1497
+ } catch {
1498
+ // Ignore errors loading chat messages
1331
1499
  }
1332
- } catch {
1333
- // Ignore errors loading chat messages
1334
- }
1500
+ })();
1335
1501
 
1336
- // Load console logs from database
1337
- try {
1338
- const consoleRequestId = `console_${Date.now()}`;
1339
- const consoleResponse = await new Promise<any>((resolve, reject) => {
1340
- const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
1341
-
1342
- const handler = (event: MessageEvent) => {
1343
- try {
1344
- const msg = JSON.parse(event.data);
1345
- if (msg.request_id === consoleRequestId) {
1346
- clearTimeout(timeout);
1347
- ws.removeEventListener('message', handler);
1348
- resolve(msg);
1349
- }
1350
- } catch {}
1351
- };
1352
-
1353
- ws.addEventListener('message', handler);
1354
- // Scope to the currently-open workflow so the panel only
1355
- // shows console output from this workflow's runs.
1502
+ void (async () => {
1503
+ try {
1356
1504
  const consoleWorkflowId = useAppStore.getState().currentWorkflow?.id;
1357
- ws.send(JSON.stringify({
1358
- type: 'get_console_logs',
1359
- limit: 100,
1360
- workflow_id: consoleWorkflowId,
1361
- request_id: consoleRequestId,
1362
- }));
1363
- });
1364
-
1365
- if (consoleResponse.success && consoleResponse.logs) {
1366
- const logs: ConsoleLogEntry[] = consoleResponse.logs.map((log: any) => ({
1367
- node_id: log.node_id,
1368
- label: log.label,
1369
- timestamp: log.timestamp,
1370
- data: log.data,
1371
- formatted: log.formatted,
1372
- format: log.format,
1373
- workflow_id: log.workflow_id,
1374
- source_node_id: log.source_node_id,
1375
- source_node_type: log.source_node_type,
1376
- source_node_label: log.source_node_label,
1377
- }));
1378
- setConsoleLogs(logs);
1505
+ const consoleResponse = await sendBurstRequest<any>(
1506
+ { type: 'get_console_logs', limit: 100, workflow_id: consoleWorkflowId },
1507
+ 'console',
1508
+ );
1509
+ if (consoleResponse.success && consoleResponse.logs) {
1510
+ const logs: ConsoleLogEntry[] = consoleResponse.logs.map((log: any) => ({
1511
+ node_id: log.node_id,
1512
+ label: log.label,
1513
+ timestamp: log.timestamp,
1514
+ data: log.data,
1515
+ formatted: log.formatted,
1516
+ format: log.format,
1517
+ workflow_id: log.workflow_id,
1518
+ source_node_id: log.source_node_id,
1519
+ source_node_type: log.source_node_type,
1520
+ source_node_label: log.source_node_label,
1521
+ }));
1522
+ setConsoleLogs(logs);
1523
+ }
1524
+ } catch {
1525
+ // Ignore errors loading console logs
1379
1526
  }
1380
- } catch {
1381
- // Ignore errors loading console logs
1382
- }
1383
-
1384
- // Drain any sends that were queued while the socket was reconnecting.
1385
- // Must run BEFORE setIsReady(true) so isReady-gated callers don't race
1386
- // an empty pendingRequestsRef. Mirrors socket.io-client's offline buffer
1387
- // and Apollo RetryLink semantics.
1388
- drainPendingSends(ws);
1389
-
1390
- // Init burst complete — flip `isReady` so queries that gate on
1391
- // it (catalogue, node specs, node parameters, user settings,
1392
- // credential panels) fire once against a stable socket instead
1393
- // of racing the serial init awaits above.
1394
- setIsReady(true);
1527
+ })();
1395
1528
  };
1396
1529
 
1397
1530
  ws.onmessage = handleMessage;
@@ -1419,9 +1552,10 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1419
1552
  pendingRequestsRef.current.clear();
1420
1553
  }
1421
1554
 
1422
- // Pending-send queue handling: only drop on intentional close (code 1000).
1423
- // Transient closes preserve the queue so the next onopen drain replays them.
1424
- if (event.code === 1000) {
1555
+ // Pending-send queue handling: only drop on intentional close
1556
+ // (RFC 6455 §7.4.1 Normal Closure, code 1000). Transient closes
1557
+ // preserve the queue so the next onopen drain replays them.
1558
+ if (event.code === WS_CLOSE.NORMAL_CLOSURE) {
1425
1559
  if (pendingSendQueueRef.current.length > 0) {
1426
1560
  for (const queued of pendingSendQueueRef.current) {
1427
1561
  try {
@@ -1441,12 +1575,13 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1441
1575
  pingIntervalRef.current = null;
1442
1576
  }
1443
1577
 
1444
- // Reconnect after delay (unless intentional close)
1445
- if (event.code !== 1000) {
1578
+ // PartySocket performs the reconnect itself when
1579
+ // `event.code !== WS_CLOSE.NORMAL_CLOSURE`, honouring the
1580
+ // `WS_RECONNECT` envelope passed at construction time. Surface
1581
+ // "reconnecting" to the UI for transient closes; intentional
1582
+ // closes (logout / unmount) leave the flag false.
1583
+ if (event.code !== WS_CLOSE.NORMAL_CLOSURE) {
1446
1584
  setReconnecting(true);
1447
- reconnectTimeoutRef.current = setTimeout(() => {
1448
- connect();
1449
- }, 3000);
1450
1585
  }
1451
1586
  };
1452
1587
 
@@ -1456,15 +1591,18 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1456
1591
 
1457
1592
  wsRef.current = ws;
1458
1593
  } catch (error) {
1594
+ // Construction-time error (e.g. malformed URL). PartySocket has
1595
+ // not installed its retry loop yet, so just log — there is
1596
+ // nothing to reconnect to. The runtime path (network drops after
1597
+ // successful construction) is covered by PartySocket's internal
1598
+ // jittered backoff.
1459
1599
  console.error('[WebSocket] Failed to create connection:', error);
1460
- setReconnecting(true);
1461
- reconnectTimeoutRef.current = setTimeout(connect, 3000);
1462
1600
  }
1463
1601
  }, [handleMessage, drainPendingSends]);
1464
1602
 
1465
1603
  // Request current status
1466
1604
  const requestStatus = useCallback(() => {
1467
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1605
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1468
1606
  wsRef.current.send(JSON.stringify({ type: 'get_status' }));
1469
1607
  }
1470
1608
  }, []);
@@ -1524,7 +1662,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1524
1662
  // the currently-open workflow so other workflows' history survives.
1525
1663
  const clearConsoleLogs = useCallback(() => {
1526
1664
  setConsoleLogs([]);
1527
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1665
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1528
1666
  const workflowId = useAppStore.getState().currentWorkflow?.id;
1529
1667
  wsRef.current.send(JSON.stringify({
1530
1668
  type: 'clear_console_logs',
@@ -1537,7 +1675,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1537
1675
  const clearTerminalLogs = useCallback(() => {
1538
1676
  setTerminalLogs([]);
1539
1677
  // Also notify server to clear its terminal log history
1540
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1678
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1541
1679
  wsRef.current.send(JSON.stringify({ type: 'clear_terminal_logs' }));
1542
1680
  }
1543
1681
  }, []);
@@ -1548,7 +1686,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1548
1686
  // Uses direct WebSocket send to avoid dependency on sendRequest (which is defined later).
1549
1687
  const clearChatMessages = useCallback(() => {
1550
1688
  setChatMessages([]);
1551
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1689
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1552
1690
  const workflowId = useAppStore.getState().currentWorkflow?.id || 'default';
1553
1691
  wsRef.current.send(JSON.stringify({
1554
1692
  type: 'clear_chat_messages',
@@ -1665,7 +1803,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
1665
1803
  const effectiveTimeout = (timeoutMs === -1 || !useTimeout) ? -1 : actualTimeout;
1666
1804
 
1667
1805
  // FAST PATH: socket open — send immediately.
1668
- if (wsRef.current?.readyState === WebSocket.OPEN) {
1806
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
1669
1807
  const requestId = generateRequestId();
1670
1808
  let timeout: NodeJS.Timeout | null = null;
1671
1809
  if (effectiveTimeout > 0) {
@@ -2113,17 +2251,24 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2113
2251
  provider,
2114
2252
  api_key: apiKey
2115
2253
  });
2254
+ // Backend returns one of:
2255
+ // { success: true, valid: true, models } — key is good
2256
+ // { success: true, valid: false, message } — clean rejection (401/403/timeout/etc)
2257
+ // { success: false, error } — handler bug (uncaught exception)
2258
+ // Surface the message field first; fall back to error so the toast
2259
+ // shows the actual reason instead of a generic "Validation failed".
2116
2260
  const result = {
2117
- valid: response.valid || false,
2118
- message: response.message,
2261
+ valid: response.valid === true,
2262
+ message: response.message ?? response.error,
2119
2263
  models: response.models
2120
2264
  };
2121
2265
 
2122
- // Update apiKeyStatuses on successful validation
2266
+ // Update apiKeyStatuses on successful validation. "is stored"
2267
+ // lives on catalogue.provider.stored — don't duplicate it here.
2123
2268
  if (result.valid) {
2124
2269
  setApiKeyStatuses(prev => ({
2125
2270
  ...prev,
2126
- [provider]: { hasKey: true, valid: true, models: result.models }
2271
+ [provider]: { valid: true, models: result.models }
2127
2272
  }));
2128
2273
  }
2129
2274
 
@@ -2151,12 +2296,13 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2151
2296
  models: response.models,
2152
2297
  };
2153
2298
 
2154
- // Mirror into apiKeyStatuses so consumers reading from context
2155
- // stay in sync without an extra round-trip.
2299
+ // Mirror models into apiKeyStatuses so consumers reading from
2300
+ // context stay in sync without an extra round-trip. "is stored"
2301
+ // lives on the catalogue's provider.stored, not here.
2156
2302
  if (result.hasKey) {
2157
2303
  setApiKeyStatuses(prev => ({
2158
2304
  ...prev,
2159
- [provider]: { hasKey: true, valid: true, models: result.models }
2305
+ [provider]: { valid: true, models: result.models }
2160
2306
  }));
2161
2307
  }
2162
2308
 
@@ -2180,11 +2326,14 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2180
2326
  });
2181
2327
  const success = response.success !== false;
2182
2328
 
2183
- // Update apiKeyStatuses on successful save
2329
+ // Update apiKeyStatuses on successful save. The 'valid: true'
2330
+ // here is optimistic — save_api_key doesn't actually validate
2331
+ // upstream. Catalogue refetch (via the credential.api_key.saved
2332
+ // broadcast) is the truthful source for "is stored".
2184
2333
  if (success) {
2185
2334
  setApiKeyStatuses(prev => ({
2186
2335
  ...prev,
2187
- [provider]: { hasKey: true, valid: true, models }
2336
+ [provider]: { valid: true, models }
2188
2337
  }));
2189
2338
  }
2190
2339
 
@@ -2198,14 +2347,16 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2198
2347
  const deleteApiKeyAsync = useCallback(async (provider: string): Promise<boolean> => {
2199
2348
  try {
2200
2349
  await sendRequest<any>('delete_api_key', { provider });
2201
-
2202
- // Remove from apiKeyStatuses on successful delete
2203
- setApiKeyStatuses(prev => {
2204
- const newStatuses = { ...prev };
2205
- delete newStatuses[provider];
2206
- return newStatuses;
2207
- });
2208
-
2350
+ // Don't optimistically clear apiKeyStatuses[provider] here. The
2351
+ // backend's `api_key_status` broadcast (fired before this reply
2352
+ // lands) already wrote `{valid: false, hasKey: false, message:
2353
+ // 'deleted'}` to every connected client. The catalogue refetch
2354
+ // (debounced 300 ms after `credential_catalogue_updated`) will
2355
+ // flip `provider.stored` to false. A local optimistic clear here
2356
+ // raced with the broadcast and produced a green-flash mid-delete:
2357
+ // broadcast → red, optimistic clear → green (validation undefined
2358
+ // but stored still cached true), catalogue refetch → gray.
2359
+ // Trusting the broadcast pipeline gives a clean red → gray.
2209
2360
  return true;
2210
2361
  } catch (error) {
2211
2362
  console.error('[WebSocket] Failed to delete API key:', error);
@@ -2522,7 +2673,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2522
2673
  }
2523
2674
 
2524
2675
  // Skip if already connected
2525
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2676
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
2526
2677
  return;
2527
2678
  }
2528
2679
 
@@ -2553,7 +2704,7 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2553
2704
  }
2554
2705
  pendingSendQueueRef.current = [];
2555
2706
  }
2556
- wsRef.current.close(1000, 'User logged out');
2707
+ wsRef.current.close(WS_CLOSE.NORMAL_CLOSURE, 'User logged out');
2557
2708
  wsRef.current = null;
2558
2709
  setIsConnected(false);
2559
2710
  setIsReady(false);
@@ -2564,7 +2715,6 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2564
2715
  useEffect(() => {
2565
2716
  return () => {
2566
2717
  isMountedRef.current = false;
2567
- if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
2568
2718
  if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
2569
2719
  // Drain the pending-send queue so any in-flight awaiters fail fast on
2570
2720
  // unmount instead of dangling forever.
@@ -2579,8 +2729,8 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2579
2729
  }
2580
2730
  pendingSendQueueRef.current = [];
2581
2731
  }
2582
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2583
- wsRef.current.close(1000, 'Component unmounted');
2732
+ if (wsRef.current?.readyState === ReconnectingWebSocket.OPEN) {
2733
+ wsRef.current.close(WS_CLOSE.NORMAL_CLOSURE, 'Component unmounted');
2584
2734
  }
2585
2735
  };
2586
2736
  }, []);
@@ -2636,6 +2786,23 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
2636
2786
  // Generic request method
2637
2787
  sendRequest,
2638
2788
 
2789
+ // Generic broadcast subscription
2790
+ addEventListener: (type: string, handler: (data: any) => void) => {
2791
+ let set = eventListenersRef.current.get(type);
2792
+ if (!set) {
2793
+ set = new Set();
2794
+ eventListenersRef.current.set(type, set);
2795
+ }
2796
+ set.add(handler);
2797
+ return () => {
2798
+ const current = eventListenersRef.current.get(type);
2799
+ if (current) {
2800
+ current.delete(handler);
2801
+ if (current.size === 0) eventListenersRef.current.delete(type);
2802
+ }
2803
+ };
2804
+ },
2805
+
2639
2806
  // Node Parameters
2640
2807
  getNodeParameters: getNodeParametersAsync,
2641
2808
  getAllNodeParameters: getAllNodeParametersAsync,