@xdelivered/emberflow 0.2.0

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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,1182 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { ChevronRightIcon, CodeIcon, PinIcon, PlayIcon } from 'lucide-react';
3
+ import { BranchRulesEditor } from './BranchRulesEditor';
4
+ import { ExecutionPager } from './ExecutionPager';
5
+ import { NodeRunModal } from './NodeRunModal';
6
+ import { Json } from './Json';
7
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
8
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
9
+ import { Badge } from '@/components/ui/badge';
10
+ import { Button } from '@/components/ui/button';
11
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
12
+ import { Combobox } from '@/components/ui/combobox';
13
+ import type { ComboboxOption } from '@/components/ui/combobox';
14
+ import { Input } from '@/components/ui/input';
15
+ import { useBuilderStore } from '../store/builderStore';
16
+ import { isMountablePath } from '../../server/pathGuard';
17
+ import type { AuthPolicy, FieldDefinition, HttpTrigger, LogLine, WorkflowNode } from '../engine';
18
+ import { keyValueRows, simpleNodeDescription } from '@/lib/registerLens';
19
+ import { cn } from '@/lib/utils';
20
+
21
+ // Trace-kind presentation, mirrored from the runbook badge hover card so the
22
+ // Inspector speaks the same language (SQL/HTTP/LLM · query/endpoint/model).
23
+ const TRACE_KIND_LABEL: Record<string, string> = { db: 'SQL', http: 'HTTP', llm: 'LLM', compute: 'FN' };
24
+ const TRACE_KIND_COLOR: Record<string, string> = { db: '#8fb8d8', http: '#c9a6e8', llm: '#d8c88f', compute: '#9aa0a6' };
25
+ const TRACE_KIND_DETAIL_LABEL: Record<string, string> = { db: 'query', http: 'endpoint', llm: 'model', compute: 'detail' };
26
+
27
+ function statusText(status: string) {
28
+ if (status === 'succeeded') return 'text-success';
29
+ if (status === 'failed') return 'text-destructive';
30
+ if (status === 'running') return 'text-highlight';
31
+ return 'text-muted-foreground';
32
+ }
33
+
34
+ function statusDotClass(status: string) {
35
+ if (status === 'succeeded') return 'bg-success';
36
+ if (status === 'failed') return 'bg-destructive';
37
+ if (status === 'running') return 'bg-highlight animate-pulse';
38
+ return 'bg-muted-foreground';
39
+ }
40
+
41
+ /**
42
+ * Simple outcome line: the last info-level log for this node in the
43
+ * current run — same rule the runbook projection uses for step outcomes
44
+ * (see lastInfoOutcome in runbookProjection.ts). Kept local here since that
45
+ * helper isn't exported; duplicating a four-line loop beats reaching across
46
+ * modules for it.
47
+ */
48
+ function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
49
+ for (let i = logs.length - 1; i >= 0; i--) {
50
+ const line = logs[i];
51
+ if (line.nodeId === nodeId && line.level === 'info') return line.message;
52
+ }
53
+ return '';
54
+ }
55
+
56
+ /**
57
+ * Simple register key-value grid for a node's input/output payload —
58
+ * two-column rows (mono key · plain value), with complex (object/array)
59
+ * values collapsed behind a toggle button that expands an inline <Json>
60
+ * block. The destructive error pane is unchanged and never folds.
61
+ */
62
+ function KeyValueGrid({
63
+ value,
64
+ error,
65
+ emptyLabel,
66
+ }: {
67
+ value: Record<string, unknown> | undefined;
68
+ error?: string;
69
+ emptyLabel: string;
70
+ }) {
71
+ const rows = keyValueRows(value);
72
+ const [open, setOpen] = useState<Record<string, boolean>>({});
73
+
74
+ useEffect(() => {
75
+ setOpen({});
76
+ }, [value]);
77
+
78
+ return (
79
+ <div>
80
+ {rows.length === 0 && (
81
+ <div className="text-[12px] text-muted-foreground/60">{emptyLabel}</div>
82
+ )}
83
+ {rows.map((row) => (
84
+ <div className="mb-1.5" key={row.key}>
85
+ <div className="flex items-baseline justify-between gap-3">
86
+ <span className="shrink-0 font-mono text-[10.5px] text-muted-foreground">{row.key}</span>
87
+ {row.complex ? (
88
+ <button
89
+ type="button"
90
+ onClick={() => setOpen((o) => ({ ...o, [row.key]: !o[row.key] }))}
91
+ aria-expanded={!!open[row.key]}
92
+ className="min-w-0 truncate text-[12px] text-foreground underline decoration-dotted underline-offset-2 hover:text-highlight"
93
+ >
94
+ {row.display}
95
+ </button>
96
+ ) : (
97
+ <span className="min-w-0 truncate text-[12px] text-foreground" title={row.display}>
98
+ {row.display}
99
+ </span>
100
+ )}
101
+ </div>
102
+ {row.complex && open[row.key] && (
103
+ <div className="mt-1">
104
+ <Json value={(value as Record<string, unknown>)[row.key]} maxHeight={220} />
105
+ </div>
106
+ )}
107
+ </div>
108
+ ))}
109
+ {error && (
110
+ <pre className="mt-2 overflow-x-auto rounded-md border border-destructive/50 bg-background p-2 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
111
+ {error}
112
+ </pre>
113
+ )}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ /**
119
+ * The declared shape of a node's input or output — field names (with types) it
120
+ * will carry, before any run. Regular nodes read their schema; dynamic nodes
121
+ * (Input) declare no schema and instead carry their emitted field names in
122
+ * config.fields, which are the node's *output* shape. Returns [] when there is
123
+ * genuinely nothing to describe, so the caller can drop the section entirely
124
+ * rather than print a "nothing yet" placeholder.
125
+ */
126
+ function shapeFields(
127
+ node: WorkflowNode,
128
+ definition: { inputSchema?: { fields: FieldDefinition[] }; outputSchema?: { fields: FieldDefinition[] } } | undefined,
129
+ kind: 'input' | 'output',
130
+ ): { name: string; type?: string }[] {
131
+ const declared = (kind === 'input' ? definition?.inputSchema : definition?.outputSchema)?.fields ?? [];
132
+ if (declared.length) return declared.map((f) => ({ name: f.name, type: f.type }));
133
+ if (kind === 'output' && Array.isArray(node.config?.fields)) {
134
+ return (node.config.fields as Array<{ name?: unknown; type?: unknown }>)
135
+ .filter((f) => typeof f?.name === 'string' && f.name)
136
+ .map((f) => ({ name: String(f.name), type: typeof f.type === 'string' ? f.type : undefined }));
137
+ }
138
+ return [];
139
+ }
140
+
141
+ /** The expected shape as a quiet field-name·type list — shown before a run in
142
+ * place of a "nothing yet" placeholder, so the user sees what will flow. */
143
+ function ShapeList({ fields }: { fields: { name: string; type?: string }[] }) {
144
+ return (
145
+ <div>
146
+ {fields.map((f) => (
147
+ <div className="mb-1 flex items-baseline justify-between gap-3" key={f.name}>
148
+ <span className="font-mono text-[10.5px] text-muted-foreground">{f.name}</span>
149
+ {f.type && <span className="shrink-0 text-[10px] text-muted-foreground/50">{f.type}</span>}
150
+ </div>
151
+ ))}
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function FieldTag({ field }: { field: FieldDefinition }) {
157
+ return (
158
+ <span className="font-mono text-[10px] text-muted-foreground/70">
159
+ {field.type}
160
+ {field.type === 'enum' && field.enumValues ? `: ${field.enumValues.join(' | ')}` : ''}
161
+ </span>
162
+ );
163
+ }
164
+
165
+ function FieldName({ field }: { field: FieldDefinition }) {
166
+ return (
167
+ <span className="font-mono text-[11.5px] text-foreground/90">
168
+ {field.name}
169
+ {field.required && <span className="ml-0.5 text-destructive">*</span>}
170
+ </span>
171
+ );
172
+ }
173
+
174
+ function ValuePreview({ value }: { value: unknown }) {
175
+ if (value === undefined) return <span className="text-[11px] text-muted-foreground/60">—</span>;
176
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
177
+ return (
178
+ <code
179
+ className="inline-block max-w-full truncate rounded-sm bg-secondary/60 px-1.5 font-mono text-[11px] text-muted-foreground"
180
+ title={text}
181
+ >
182
+ {text}
183
+ </code>
184
+ );
185
+ }
186
+
187
+ function RunValue({ label, value }: { label: string; value: unknown }) {
188
+ return (
189
+ <div className="mt-1 flex items-baseline gap-2">
190
+ <span className="shrink-0 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/60">
191
+ {label}
192
+ </span>
193
+ <ValuePreview value={value} />
194
+ </div>
195
+ );
196
+ }
197
+
198
+ /** Declared output fields for a node, with values from a given output payload (last run or a selected iteration record). */
199
+ function OutputFieldsList({
200
+ fields,
201
+ outputValue,
202
+ error,
203
+ }: {
204
+ fields: FieldDefinition[];
205
+ outputValue: Record<string, unknown> | undefined;
206
+ error?: string;
207
+ }) {
208
+ return (
209
+ <>
210
+ {fields.map((f) => (
211
+ <div className="mb-3" key={f.name}>
212
+ <div className="mb-1 flex items-baseline justify-between">
213
+ <FieldName field={f} />
214
+ <FieldTag field={f} />
215
+ </div>
216
+ {outputValue !== undefined && <RunValue label="last run" value={outputValue?.[f.name]} />}
217
+ </div>
218
+ ))}
219
+ {fields.length === 0 && (
220
+ <div className="text-[12px] text-muted-foreground/60">No declared outputs.</div>
221
+ )}
222
+ {error && (
223
+ <pre className="mt-2 overflow-x-auto rounded-md border border-destructive/50 bg-background p-2 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
224
+ {error}
225
+ </pre>
226
+ )}
227
+ </>
228
+ );
229
+ }
230
+
231
+ function SectionTitle({ children }: { children: React.ReactNode }) {
232
+ return (
233
+ <h3 className="mb-2.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
234
+ {children}
235
+ </h3>
236
+ );
237
+ }
238
+
239
+ function isSecretRef(value: unknown): value is { $secret: string } {
240
+ return (
241
+ value !== null &&
242
+ typeof value === 'object' &&
243
+ !Array.isArray(value) &&
244
+ typeof (value as { $secret?: unknown }).$secret === 'string'
245
+ );
246
+ }
247
+
248
+ /** True when a config value should be edited as JSON rather than a plain string. */
249
+ function isJsonConfigValue(value: unknown, field: FieldDefinition): boolean {
250
+ if (typeof value === 'object' && value !== null) return true;
251
+ return value === undefined && (field.type === 'object' || field.type === 'array');
252
+ }
253
+
254
+ /**
255
+ * Mono JSON editor for object/array config values — replaces the string
256
+ * Input, which otherwise coerces objects to "[object Object]". Edits are
257
+ * kept in local draft state and only committed to the store on blur, once
258
+ * they parse as valid JSON; invalid JSON is left in the draft with an
259
+ * inline error rather than clobbering the stored value.
260
+ */
261
+ function JsonConfigField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
262
+ const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
263
+ const value = node.config[field.name];
264
+ const [draft, setDraft] = useState(() => JSON.stringify(value ?? {}, null, 2));
265
+ const [error, setError] = useState<string | null>(null);
266
+
267
+ // Reset the draft whenever the node, field, or stored value changes from
268
+ // outside this field (node switch, pin, mapping, undo, etc).
269
+ useEffect(() => {
270
+ setDraft(JSON.stringify(value ?? {}, null, 2));
271
+ setError(null);
272
+ }, [node.id, field.name, value]);
273
+
274
+ const rows = Math.min(10, Math.max(3, draft.split('\n').length));
275
+
276
+ const handleBlur = () => {
277
+ try {
278
+ const parsed = JSON.parse(draft);
279
+ setError(null);
280
+ updateNodeConfig(node.id, field.name, parsed);
281
+ } catch (err) {
282
+ setError(err instanceof Error ? err.message : 'Invalid JSON');
283
+ }
284
+ };
285
+
286
+ return (
287
+ <div className="mb-2">
288
+ <div className="mb-1 flex items-baseline justify-between">
289
+ <FieldName field={field} />
290
+ <FieldTag field={field} />
291
+ </div>
292
+ <textarea
293
+ value={draft}
294
+ onChange={(e) => setDraft(e.target.value)}
295
+ onBlur={handleBlur}
296
+ spellCheck={false}
297
+ rows={rows}
298
+ placeholder="{}"
299
+ className="w-full resize-y rounded-md border border-border bg-background p-3 font-mono text-[12px] leading-relaxed outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
300
+ />
301
+ {error && <div className="mt-1 text-[11px] text-destructive">{error}</div>}
302
+ </div>
303
+ );
304
+ }
305
+
306
+ /** A Subflow node's workflowId, chosen from the workflows in the workspace. */
307
+ function WorkflowSelectField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
308
+ const workflows = useBuilderStore((s) => s.workflows);
309
+ const flowId = useBuilderStore((s) => s.flow.id);
310
+ const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
311
+ const value = node.config[field.name];
312
+ // A flow can't call itself (validation error), so leave self out of the list.
313
+ const options: ComboboxOption[] = workflows
314
+ .filter((w) => w.id !== flowId)
315
+ .map((w) => ({ value: w.id, label: w.folder ? `${w.folder} / ${w.name}` : w.name }));
316
+
317
+ return (
318
+ <div className="mb-2 flex items-center justify-between gap-2.5">
319
+ <FieldName field={field} />
320
+ <Combobox
321
+ className="max-w-44"
322
+ options={options}
323
+ value={String(value ?? '')}
324
+ onChange={(v) => updateNodeConfig(node.id, field.name, v)}
325
+ placeholder="select a workflow"
326
+ searchPlaceholder="Search workflows…"
327
+ clearLabel="none"
328
+ />
329
+ </div>
330
+ );
331
+ }
332
+
333
+ function ConfigField({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
334
+ const updateNodeConfig = useBuilderStore((s) => s.updateNodeConfig);
335
+ const value = node.config[field.name];
336
+
337
+ if (node.type === 'Subflow' && field.name === 'workflowId') {
338
+ return <WorkflowSelectField node={node} field={field} />;
339
+ }
340
+
341
+ if (isSecretRef(value)) {
342
+ return (
343
+ <div className="mb-2 flex items-center justify-between gap-2.5">
344
+ <FieldName field={field} />
345
+ <Badge variant="mono" title="Resolved from runner secrets at execution time">
346
+ secret · {value.$secret}
347
+ </Badge>
348
+ </div>
349
+ );
350
+ }
351
+
352
+ if (isJsonConfigValue(value, field)) {
353
+ return <JsonConfigField key={`${node.id}:${field.name}`} node={node} field={field} />;
354
+ }
355
+
356
+ if (field.type === 'boolean') {
357
+ return (
358
+ <label className="mb-2 flex items-center justify-between gap-2.5">
359
+ <FieldName field={field} />
360
+ <input
361
+ type="checkbox"
362
+ className="accent-(--highlight)"
363
+ checked={Boolean(value)}
364
+ onChange={(e) => updateNodeConfig(node.id, field.name, e.target.checked)}
365
+ />
366
+ </label>
367
+ );
368
+ }
369
+
370
+ if (field.type === 'enum') {
371
+ return (
372
+ <div className="mb-2 flex items-center justify-between gap-2.5">
373
+ <FieldName field={field} />
374
+ <Combobox
375
+ className="max-w-40"
376
+ options={(field.enumValues ?? []).map((v) => ({ value: v, label: v }))}
377
+ value={String(value ?? '')}
378
+ onChange={(v) => updateNodeConfig(node.id, field.name, v)}
379
+ clearLabel="none"
380
+ />
381
+ </div>
382
+ );
383
+ }
384
+
385
+ return (
386
+ <label className="mb-2 flex items-center justify-between gap-2.5">
387
+ <FieldName field={field} />
388
+ <Input
389
+ className="max-w-40"
390
+ type={field.type === 'number' ? 'number' : 'text'}
391
+ value={String(value ?? '')}
392
+ spellCheck={false}
393
+ onChange={(e) =>
394
+ updateNodeConfig(
395
+ node.id,
396
+ field.name,
397
+ field.type === 'number' ? Number(e.target.value) : e.target.value,
398
+ )
399
+ }
400
+ />
401
+ </label>
402
+ );
403
+ }
404
+
405
+
406
+ /**
407
+ * Read-only input wiring: where each declared input comes from, in plain
408
+ * language. Mappings are agent-authored — the user reads them, never edits
409
+ * them here — so this is a static statement, not a picker: `field ← Source ·
410
+ * out`, or a config default, or an unmapped note.
411
+ */
412
+ function InputWiringRow({ node, field }: { node: WorkflowNode; field: FieldDefinition }) {
413
+ const flow = useBuilderStore((s) => s.flow);
414
+ const mapping = node.inputMap?.[field.name];
415
+ const configValue = node.config[field.name];
416
+
417
+ let source: React.ReactNode;
418
+ if (mapping) {
419
+ const src = flow.nodes.find((n) => n.id === mapping.sourceNodeId);
420
+ const srcLabel = src?.label ?? mapping.sourceNodeId;
421
+ source = (
422
+ <span className="min-w-0 truncate text-[12px] text-foreground/90">
423
+ <span className="text-highlight">{srcLabel}</span>
424
+ {mapping.sourceField !== '$' && (
425
+ <span className="font-mono text-[11px] text-muted-foreground"> · {mapping.sourceField}</span>
426
+ )}
427
+ </span>
428
+ );
429
+ } else if (configValue !== undefined) {
430
+ const text = typeof configValue === 'string' ? configValue : JSON.stringify(configValue);
431
+ source = (
432
+ <code
433
+ className="min-w-0 truncate rounded-sm bg-secondary/60 px-1.5 font-mono text-[11px] text-muted-foreground"
434
+ title={text}
435
+ >
436
+ {text}
437
+ </code>
438
+ );
439
+ } else {
440
+ source = <span className="text-[11.5px] text-muted-foreground/50 italic">not wired</span>;
441
+ }
442
+
443
+ return (
444
+ <div className="flex items-baseline gap-2 py-1">
445
+ <span className="shrink-0 font-mono text-[11.5px] text-foreground/90">
446
+ {field.name}
447
+ {field.required && <span className="ml-0.5 text-destructive">*</span>}
448
+ </span>
449
+ <span className="shrink-0 text-muted-foreground/50">←</span>
450
+ {source}
451
+ </div>
452
+ );
453
+ }
454
+
455
+ /**
456
+ * The per-node action bar — one consistent row shared by both registers so the
457
+ * controls look and sit the same everywhere: run this node in isolation, view
458
+ * its code, and pin/unpin its last output. `Pin` only appears once there's an
459
+ * output to pin; when pinned it becomes an in-place toggle in the accent colour.
460
+ */
461
+ function NodeActions({
462
+ onRun,
463
+ onCode,
464
+ hasOutput,
465
+ pinned,
466
+ onPin,
467
+ onUnpin,
468
+ }: {
469
+ onRun: () => void;
470
+ onCode: () => void;
471
+ hasOutput: boolean;
472
+ pinned: boolean;
473
+ onPin: () => void;
474
+ onUnpin: () => void;
475
+ }) {
476
+ const btn = 'gap-1 text-muted-foreground hover:text-foreground';
477
+ return (
478
+ <div className="flex items-center gap-0.5">
479
+ <Button variant="ghost" size="xs" className={btn} onClick={onRun} title="Run this node in isolation">
480
+ <PlayIcon /> Run
481
+ </Button>
482
+ <Button variant="ghost" size="xs" className={btn} onClick={onCode} title="View this node's implementation">
483
+ <CodeIcon /> Code
484
+ </Button>
485
+ {hasOutput &&
486
+ (pinned ? (
487
+ <Button
488
+ variant="ghost"
489
+ size="xs"
490
+ className="gap-1 text-highlight hover:text-highlight"
491
+ onClick={onUnpin}
492
+ title="Pinned — runs reuse this output without executing. Click to unpin."
493
+ >
494
+ <PinIcon className="fill-current" /> Pinned
495
+ </Button>
496
+ ) : (
497
+ <Button variant="ghost" size="xs" className={btn} onClick={onPin} title="Pin last output — runs reuse it without executing">
498
+ <PinIcon /> Pin
499
+ </Button>
500
+ ))}
501
+ </div>
502
+ );
503
+ }
504
+
505
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
506
+ type AuthKind = 'inherit' | 'none' | 'bearer' | 'apiKey';
507
+
508
+ function authKindOf(auth: HttpTrigger['auth']): AuthKind {
509
+ if (auth === undefined || auth === 'inherit') return 'inherit';
510
+ if (auth === 'none') return 'none';
511
+ return auth.scheme;
512
+ }
513
+
514
+ const baseInputClass =
515
+ 'w-full rounded-md border border-border bg-background px-2 py-1.5 text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]';
516
+
517
+ // Slug of the flow's name (falling back to the last path segment of its id)
518
+ // used to seed a sensible non-root default path when HTTP is first enabled.
519
+ function defaultHttpPath(flowId: string, flowName: string): string {
520
+ const source = flowName?.trim() || flowId.split('/').pop() || 'operation';
521
+ const slug = source
522
+ .toLowerCase()
523
+ .replace(/[^a-z0-9]+/g, '-')
524
+ .replace(/^-+|-+$/g, '');
525
+ return `/${slug || 'operation'}`;
526
+ }
527
+
528
+ /**
529
+ * Flow-level HTTP endpoint editor: method/path/inputSchema/auth for the
530
+ * active flow's `http` trigger. This is deliberately independent of node
531
+ * selection — any flow can become an operation by adding `http`, so it
532
+ * renders regardless of which (if any) node is selected. Every write goes
533
+ * through `setFlowHttp`, which flows through the same touched()+autosave
534
+ * path (see the store's flow-change subscription) as every other flow edit,
535
+ * so this reuses the existing PUT /workflows/:id persistence — no new path.
536
+ */
537
+ function HttpSection() {
538
+ const flowId = useBuilderStore((s) => s.flow.id);
539
+ const flowName = useBuilderStore((s) => s.flow.name);
540
+ const http = useBuilderStore((s) => s.flow.http);
541
+ const setFlowHttp = useBuilderStore((s) => s.setFlowHttp);
542
+ const [advancedOpen, setAdvancedOpen] = useState(false);
543
+ const [pathHint, setPathHint] = useState<string | null>(null);
544
+ // Holds the raw text of an in-progress, not-yet-valid path edit (e.g. the
545
+ // user cleared the field or typed '/') so the input reflects what they
546
+ // typed instead of snapping back to the last-persisted path. Cleared
547
+ // (falls back to `http.path`) once the text becomes a valid sub-path.
548
+ const [pathDraft, setPathDraft] = useState<string | null>(null);
549
+
550
+ const [schemaDraft, setSchemaDraft] = useState(() => JSON.stringify(http?.inputSchema ?? {}, null, 2));
551
+ const [schemaError, setSchemaError] = useState<string | null>(null);
552
+
553
+ // Reset the schema draft only when the active flow changes — not on every
554
+ // http edit, which would otherwise reformat the textarea under the user's
555
+ // fingers as they type (setFlowHttp round-trips through JSON.stringify).
556
+ useEffect(() => {
557
+ setSchemaDraft(JSON.stringify(http?.inputSchema ?? {}, null, 2));
558
+ setSchemaError(null);
559
+ setPathHint(null);
560
+ setPathDraft(null);
561
+ // eslint-disable-next-line react-hooks/exhaustive-deps
562
+ }, [flowId]);
563
+
564
+ const enabled = !!http;
565
+ const kind = authKindOf(http?.auth);
566
+ const authObj: AuthPolicy | undefined = http && typeof http.auth === 'object' ? http.auth : undefined;
567
+
568
+ const toggleEnabled = (checked: boolean) => {
569
+ setPathHint(null);
570
+ setPathDraft(null);
571
+ setFlowHttp(checked ? (http ?? { method: 'GET', path: defaultHttpPath(flowId, flowName) }) : undefined);
572
+ };
573
+
574
+ const handlePathChange = (raw: string) => {
575
+ if (!http) return;
576
+ if (!isMountablePath(raw)) {
577
+ setPathDraft(raw);
578
+ setPathHint('Path must be a sub-path like /things (not /)');
579
+ return;
580
+ }
581
+ setPathDraft(null);
582
+ setPathHint(null);
583
+ setFlowHttp({ ...http, path: raw });
584
+ };
585
+
586
+ const setAuthKind = (next: AuthKind) => {
587
+ if (!http) return;
588
+ if (next === 'inherit') {
589
+ const { auth: _drop, ...rest } = http;
590
+ setFlowHttp(rest);
591
+ } else if (next === 'none') {
592
+ setFlowHttp({ ...http, auth: 'none' });
593
+ } else {
594
+ setFlowHttp({
595
+ ...http,
596
+ auth: { scheme: next, secretRef: authObj?.secretRef ?? '', ...(authObj?.header ? { header: authObj.header } : {}) },
597
+ });
598
+ }
599
+ };
600
+
601
+ const handleSchemaChange = (text: string) => {
602
+ setSchemaDraft(text);
603
+ if (!http) return;
604
+ try {
605
+ const parsed = JSON.parse(text);
606
+ setSchemaError(null);
607
+ setFlowHttp({ ...http, inputSchema: parsed });
608
+ } catch (err) {
609
+ setSchemaError(err instanceof Error ? err.message : 'Invalid JSON');
610
+ }
611
+ };
612
+
613
+ return (
614
+ <section className="mt-4 border-t border-border pt-3">
615
+ <button
616
+ type="button"
617
+ onClick={() => setAdvancedOpen((o) => !o)}
618
+ aria-expanded={advancedOpen}
619
+ className="flex w-full cursor-pointer items-center gap-1.5 text-left text-[10px] font-medium uppercase tracking-widest text-muted-foreground transition-colors hover:text-foreground"
620
+ >
621
+ <ChevronRightIcon
622
+ className={cn('size-3 shrink-0 transition-transform', advancedOpen && 'rotate-90')}
623
+ />
624
+ Advanced · HTTP &amp; auth
625
+ </button>
626
+
627
+ {advancedOpen && (
628
+ <div className="mt-3">
629
+ <div className="mb-2.5 flex items-center justify-between">
630
+ <SectionTitle>HTTP</SectionTitle>
631
+ <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
632
+ HTTP endpoint
633
+ <input
634
+ type="checkbox"
635
+ className="accent-(--highlight)"
636
+ checked={enabled}
637
+ onChange={(e) => toggleEnabled(e.target.checked)}
638
+ aria-label="HTTP endpoint"
639
+ />
640
+ </label>
641
+ </div>
642
+
643
+ {enabled && http && (
644
+ <>
645
+ <div className="mb-2 flex items-center gap-2">
646
+ <select
647
+ className={cn(baseInputClass, 'w-24 shrink-0')}
648
+ value={http.method}
649
+ aria-label="HTTP method"
650
+ onChange={(e) => setFlowHttp({ ...http, method: e.target.value })}
651
+ >
652
+ {HTTP_METHODS.map((m) => (
653
+ <option key={m} value={m}>
654
+ {m}
655
+ </option>
656
+ ))}
657
+ </select>
658
+ <input
659
+ className={cn(baseInputClass, 'font-mono')}
660
+ value={pathDraft ?? http.path}
661
+ placeholder="/path/:id"
662
+ spellCheck={false}
663
+ aria-label="HTTP path"
664
+ onChange={(e) => handlePathChange(e.target.value)}
665
+ />
666
+ </div>
667
+ {pathHint && <div className="mb-2 -mt-1 text-[11px] text-destructive">{pathHint}</div>}
668
+
669
+ <div className="mb-2">
670
+ <div className="mb-1 text-[10.5px] text-muted-foreground">Input schema</div>
671
+ <textarea
672
+ value={schemaDraft}
673
+ onChange={(e) => handleSchemaChange(e.target.value)}
674
+ spellCheck={false}
675
+ rows={Math.min(10, Math.max(3, schemaDraft.split('\n').length))}
676
+ placeholder="{}"
677
+ className="w-full resize-y rounded-md border border-border bg-background p-3 font-mono text-[12px] leading-relaxed outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
678
+ />
679
+ {schemaError && <div className="mt-1 text-[11px] text-destructive">invalid JSON: {schemaError}</div>}
680
+ </div>
681
+
682
+ <div className="mb-2">
683
+ <div className="mb-1 text-[10.5px] text-muted-foreground">Auth</div>
684
+ <select
685
+ className={baseInputClass}
686
+ value={kind}
687
+ aria-label="Auth policy"
688
+ onChange={(e) => setAuthKind(e.target.value as AuthKind)}
689
+ >
690
+ <option value="inherit">Inherit</option>
691
+ <option value="none">None (public)</option>
692
+ <option value="bearer">Bearer token</option>
693
+ <option value="apiKey">API key</option>
694
+ </select>
695
+ </div>
696
+
697
+ {(kind === 'bearer' || kind === 'apiKey') && (
698
+ <div className="mb-2 space-y-2">
699
+ <label className="block">
700
+ <div className="mb-1 text-[10.5px] text-muted-foreground">Secret ref</div>
701
+ <input
702
+ className={cn(baseInputClass, 'font-mono')}
703
+ value={authObj?.secretRef ?? ''}
704
+ spellCheck={false}
705
+ placeholder="e.g. API_TOKEN"
706
+ onChange={(e) =>
707
+ setFlowHttp({
708
+ ...http,
709
+ auth: { scheme: kind, secretRef: e.target.value, ...(authObj?.header ? { header: authObj.header } : {}) },
710
+ })
711
+ }
712
+ />
713
+ </label>
714
+ {kind === 'apiKey' && (
715
+ <label className="block">
716
+ <div className="mb-1 text-[10.5px] text-muted-foreground">Header (default x-api-key)</div>
717
+ <input
718
+ className={cn(baseInputClass, 'font-mono')}
719
+ value={authObj?.header ?? ''}
720
+ spellCheck={false}
721
+ placeholder="x-api-key"
722
+ onChange={(e) =>
723
+ setFlowHttp({
724
+ ...http,
725
+ auth: { scheme: 'apiKey', secretRef: authObj?.secretRef ?? '', header: e.target.value },
726
+ })
727
+ }
728
+ />
729
+ </label>
730
+ )}
731
+ </div>
732
+ )}
733
+ </>
734
+ )}
735
+ </div>
736
+ )}
737
+ </section>
738
+ );
739
+ }
740
+
741
+ export function Inspector() {
742
+ const flow = useBuilderStore((s) => s.flow);
743
+ const registry = useBuilderStore((s) => s.registry);
744
+ const run = useBuilderStore((s) => s.run);
745
+ const logs = useBuilderStore((s) => s.logs);
746
+ const register = useBuilderStore((s) => s.viewRegister);
747
+ const selectedNodeId = useBuilderStore((s) => s.selectedNodeId);
748
+ const renameNode = useBuilderStore((s) => s.renameNode);
749
+ const pinNodeOutput = useBuilderStore((s) => s.pinNodeOutput);
750
+ const unpinNode = useBuilderStore((s) => s.unpinNode);
751
+ const setNodeRetry = useBuilderStore((s) => s.setNodeRetry);
752
+ const [codeOpen, setCodeOpen] = useState(false);
753
+ const [runOpen, setRunOpen] = useState(false);
754
+ const [schemaOpen, setSchemaOpen] = useState(false);
755
+ const [detailsOpen, setDetailsOpen] = useState(false);
756
+
757
+ // Mechanism (config/mappings/pin/run/code) folds shut whenever the
758
+ // selection changes, so switching nodes never leaks an open fold from one
759
+ // node onto another.
760
+ useEffect(() => {
761
+ setDetailsOpen(false);
762
+ }, [selectedNodeId]);
763
+
764
+ const node = flow.nodes.find((n) => n.id === selectedNodeId);
765
+
766
+ if (!node) {
767
+ return (
768
+ <div className="p-4 pt-6 text-[13px] text-muted-foreground">
769
+ <div className="mb-1.5 font-medium text-foreground/80">Nothing selected</div>
770
+ <p className="leading-relaxed">
771
+ Select a node to inspect its configuration, inputs, outputs, and the data from the
772
+ last run.
773
+ </p>
774
+ <HttpSection />
775
+ </div>
776
+ );
777
+ }
778
+
779
+ const definition = registry.has(node.type) ? registry.get(node.type).definition : undefined;
780
+ const state = run?.nodeStates[node.id];
781
+ const outputValue = state?.output as Record<string, unknown> | undefined;
782
+ const executions = state?.executions;
783
+ const source = registry.has(node.type) ? (registry.getSource(node.type) ?? '') : '';
784
+
785
+ const codeDialog = (
786
+ <Dialog open={codeOpen} onOpenChange={setCodeOpen}>
787
+ <DialogContent className="max-w-4xl">
788
+ <DialogTitle className="flex items-center gap-2">
789
+ {node.label}
790
+ <Badge variant="mono">{node.type}</Badge>
791
+ </DialogTitle>
792
+ <DialogDescription>{definition?.description ?? 'Node implementation'}</DialogDescription>
793
+ <div className="min-h-0 flex-1 overflow-auto rounded-md border border-border bg-background">
794
+ <SyntaxHighlighter
795
+ language="javascript"
796
+ style={vscDarkPlus}
797
+ showLineNumbers
798
+ customStyle={{
799
+ margin: 0,
800
+ background: 'transparent',
801
+ fontSize: '12.5px',
802
+ lineHeight: 1.6,
803
+ padding: '12px 2px',
804
+ }}
805
+ codeTagProps={{ style: { background: 'transparent', textShadow: 'none' } }}
806
+ lineNumberStyle={{
807
+ color: 'var(--muted-foreground)',
808
+ opacity: 0.35,
809
+ background: 'transparent',
810
+ minWidth: '2.75em',
811
+ paddingRight: '1em',
812
+ }}
813
+ >
814
+ {source}
815
+ </SyntaxHighlighter>
816
+ </div>
817
+ </DialogContent>
818
+ </Dialog>
819
+ );
820
+
821
+ if (register === 'simple') {
822
+ // Failed nodes lead with their error, matching the runbook projection's
823
+ // outcome rule — a stale info line in the outcome slot would misreport.
824
+ const outcome =
825
+ state?.status === 'failed'
826
+ ? state.error || lastInfoOutcome(logs, node.id)
827
+ : lastInfoOutcome(logs, node.id);
828
+ const pinned = !!(node.metadata && 'pinnedOutput' in node.metadata);
829
+ const plainDescription = simpleNodeDescription(definition);
830
+
831
+ return (
832
+ <div className="p-4">
833
+ <div className="mb-2">
834
+ <input
835
+ className="-ml-1.5 w-full rounded-sm border border-transparent bg-transparent px-1.5 py-1 text-[15px] font-semibold tracking-tight outline-none transition-colors hover:border-border focus:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
836
+ value={node.label}
837
+ onChange={(e) => renameNode(node.id, e.target.value)}
838
+ aria-label="Node label"
839
+ spellCheck={false}
840
+ />
841
+ <div className="my-1 flex items-center gap-2 text-[11px] text-muted-foreground">
842
+ <span className="font-mono">{node.type}</span>
843
+ {state && (
844
+ <>
845
+ <span className={cn('size-1.5 rounded-full', statusDotClass(state.status))} />
846
+ <span className={statusText(state.status)}>{state.status}</span>
847
+ </>
848
+ )}
849
+ {definition?.effects === 'mutation' && (
850
+ <span className="text-[11px] text-warn" title="Mutation: has side effects">
851
+
852
+ </span>
853
+ )}
854
+ </div>
855
+ <div className="-ml-1.5 mt-1.5">
856
+ <NodeActions
857
+ onRun={() => setRunOpen(true)}
858
+ onCode={() => setCodeOpen(true)}
859
+ hasOutput={state?.output !== undefined}
860
+ pinned={pinned}
861
+ onPin={() => state?.output !== undefined && pinNodeOutput(node.id, state.output)}
862
+ onUnpin={() => unpinNode(node.id)}
863
+ />
864
+ </div>
865
+ </div>
866
+
867
+ {plainDescription && (
868
+ <p className="mb-3 text-[12.5px] leading-relaxed text-muted-foreground">
869
+ {plainDescription}
870
+ </p>
871
+ )}
872
+
873
+ {outcome && (
874
+ <p className="mb-3 text-[12px] leading-relaxed text-muted-foreground/80 italic">{outcome}</p>
875
+ )}
876
+
877
+ {(() => {
878
+ const inputVal = state?.input as Record<string, unknown> | undefined;
879
+ const hasInputVal = !!inputVal && Object.keys(inputVal).length > 0;
880
+ const inputShape = shapeFields(node, definition, 'input');
881
+ // Nothing to say — no run value and no declared shape — so no section.
882
+ if (!hasInputVal && inputShape.length === 0) return null;
883
+ return (
884
+ <section className="mt-3 border-t border-border pt-3">
885
+ <SectionTitle>Inputs</SectionTitle>
886
+ {hasInputVal ? <KeyValueGrid value={inputVal} emptyLabel="" /> : <ShapeList fields={inputShape} />}
887
+ </section>
888
+ );
889
+ })()}
890
+
891
+ {(() => {
892
+ const hasExecOutputs = !!executions && executions.length > 1;
893
+ const hasOutputVal = !!outputValue && Object.keys(outputValue).length > 0;
894
+ const outputShape = shapeFields(node, definition, 'output');
895
+ if (!hasExecOutputs && !hasOutputVal && !state?.error && outputShape.length === 0) return null;
896
+ return (
897
+ <section className="mt-4 border-t border-border pt-3">
898
+ <SectionTitle>Outputs</SectionTitle>
899
+ {hasExecOutputs ? (
900
+ <ExecutionPager executions={executions!}>
901
+ {(record) => (
902
+ <KeyValueGrid
903
+ value={record.output as Record<string, unknown> | undefined}
904
+ error={record.error}
905
+ emptyLabel=""
906
+ />
907
+ )}
908
+ </ExecutionPager>
909
+ ) : hasOutputVal || state?.error ? (
910
+ <KeyValueGrid value={outputValue} error={state?.error} emptyLabel="" />
911
+ ) : (
912
+ <ShapeList fields={outputShape} />
913
+ )}
914
+ </section>
915
+ );
916
+ })()}
917
+
918
+ <section className="mt-4 border-t border-border pt-3">
919
+ <button
920
+ type="button"
921
+ onClick={() => setDetailsOpen((o) => !o)}
922
+ aria-expanded={detailsOpen}
923
+ className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground transition-colors hover:text-foreground"
924
+ >
925
+ <ChevronRightIcon
926
+ className={cn('size-3 shrink-0 transition-transform', detailsOpen && 'rotate-90')}
927
+ />
928
+ Details
929
+ </button>
930
+
931
+ {detailsOpen && (
932
+ <div className="mt-3">
933
+ {(definition?.configSchema?.fields.length ?? 0) > 0 && (
934
+ <section className="mb-4">
935
+ <SectionTitle>Config</SectionTitle>
936
+ {node.type === 'Conditional' ? (
937
+ <BranchRulesEditor node={node} />
938
+ ) : (
939
+ definition!.configSchema!.fields.map((f) => (
940
+ <ConfigField key={f.name} node={node} field={f} />
941
+ ))
942
+ )}
943
+ </section>
944
+ )}
945
+
946
+ {(definition?.inputSchema?.fields.length ?? 0) > 0 && (
947
+ <section className="mb-4">
948
+ <SectionTitle>Input mappings</SectionTitle>
949
+ {definition!.inputSchema!.fields.map((f) => (
950
+ <InputWiringRow key={f.name} node={node} field={f} />
951
+ ))}
952
+ </section>
953
+ )}
954
+ </div>
955
+ )}
956
+ </section>
957
+
958
+ <HttpSection />
959
+
960
+ <NodeRunModal node={node} open={runOpen} onOpenChange={setRunOpen} />
961
+ {codeDialog}
962
+ </div>
963
+ );
964
+ }
965
+
966
+ return (
967
+ <div className="p-4">
968
+ <div className="mb-2">
969
+ <input
970
+ className="-ml-1.5 w-full rounded-sm border border-transparent bg-transparent px-1.5 py-1 text-[15px] font-semibold tracking-tight outline-none transition-colors hover:border-border focus:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
971
+ value={node.label}
972
+ onChange={(e) => renameNode(node.id, e.target.value)}
973
+ aria-label="Node label"
974
+ spellCheck={false}
975
+ />
976
+ <div className="my-1 flex items-center gap-2 text-[11px] text-muted-foreground">
977
+ <span className="font-mono">{node.type}</span>
978
+ {state && (
979
+ <>
980
+ <span className="text-border">·</span>
981
+ <span className={statusText(state.status)}>{state.status}</span>
982
+ </>
983
+ )}
984
+ </div>
985
+ <div className="-ml-1.5 mt-1.5">
986
+ <NodeActions
987
+ onRun={() => setRunOpen(true)}
988
+ onCode={() => setCodeOpen(true)}
989
+ hasOutput={state?.output !== undefined}
990
+ pinned={!!(node.metadata && 'pinnedOutput' in node.metadata)}
991
+ onPin={() => state?.output !== undefined && pinNodeOutput(node.id, state.output)}
992
+ onUnpin={() => unpinNode(node.id)}
993
+ />
994
+ </div>
995
+ <NodeRunModal node={node} open={runOpen} onOpenChange={setRunOpen} />
996
+ {codeDialog}
997
+ {definition?.description && (
998
+ <p className="mt-1.5 text-[12.5px] leading-relaxed text-muted-foreground">
999
+ {definition.description}
1000
+ </p>
1001
+ )}
1002
+ </div>
1003
+
1004
+ {/* The real call this node makes — same kind + endpoint/query/model the
1005
+ runbook badge reveals on hover, surfaced here so it's readable without
1006
+ hunting for the badge. */}
1007
+ {definition?.traceKind && definition.traceKind !== 'compute' && (
1008
+ <section className="mt-4 border-t border-border pt-3">
1009
+ <div className="mb-2 flex items-center gap-2">
1010
+ <span
1011
+ className="rounded px-1.5 py-0.5 font-mono text-[9px] font-semibold tracking-wide"
1012
+ style={{
1013
+ color: TRACE_KIND_COLOR[definition.traceKind],
1014
+ backgroundColor: `${TRACE_KIND_COLOR[definition.traceKind]}1f`,
1015
+ }}
1016
+ >
1017
+ {TRACE_KIND_LABEL[definition.traceKind]}
1018
+ </span>
1019
+ <span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
1020
+ {TRACE_KIND_DETAIL_LABEL[definition.traceKind]}
1021
+ </span>
1022
+ {definition.effects === 'mutation' && (
1023
+ <span className="text-[11px] text-warn" title="Mutation: has side effects">⚡</span>
1024
+ )}
1025
+ </div>
1026
+ {definition.traceDetail ? (
1027
+ <p className="font-mono text-[11.5px] leading-relaxed break-words text-foreground/85">
1028
+ {definition.traceDetail}
1029
+ </p>
1030
+ ) : (
1031
+ <p className="text-[11.5px] text-muted-foreground/60">No trace detail recorded.</p>
1032
+ )}
1033
+ </section>
1034
+ )}
1035
+
1036
+ {(definition?.configSchema?.fields.length ?? 0) > 0 && (
1037
+ <section className="mt-4 border-t border-border pt-3">
1038
+ <SectionTitle>Config</SectionTitle>
1039
+ {node.type === 'Conditional' ? (
1040
+ <BranchRulesEditor node={node} />
1041
+ ) : (
1042
+ definition!.configSchema!.fields.map((f) => (
1043
+ <ConfigField key={f.name} node={node} field={f} />
1044
+ ))
1045
+ )}
1046
+ </section>
1047
+ )}
1048
+
1049
+ {node.type !== 'Input' && node.type !== 'Response' && node.type !== 'Result' && (
1050
+ <section className="mt-4 border-t border-border pt-3">
1051
+ <SectionTitle>Retry</SectionTitle>
1052
+ <div className="mt-1.5 flex items-center gap-2 text-[11.5px] text-muted-foreground">
1053
+ <input
1054
+ type="number"
1055
+ min={1}
1056
+ value={node.retry?.maxTries ?? ''}
1057
+ placeholder="1"
1058
+ onChange={(e) => {
1059
+ const maxTries = Number(e.target.value);
1060
+ setNodeRetry(
1061
+ node.id,
1062
+ Number.isInteger(maxTries) && maxTries > 1
1063
+ ? { maxTries, ...(node.retry?.waitMs ? { waitMs: node.retry.waitMs } : {}) }
1064
+ : undefined,
1065
+ );
1066
+ }}
1067
+ className="w-14 rounded-md border border-border bg-background px-2 py-1 font-mono text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
1068
+ aria-label="Total attempts"
1069
+ title="Total attempts including the first — 1 or empty disables retry"
1070
+ />
1071
+ <span>tries</span>
1072
+ <input
1073
+ type="number"
1074
+ min={0}
1075
+ step={100}
1076
+ value={node.retry?.waitMs ?? ''}
1077
+ placeholder="0"
1078
+ disabled={!node.retry}
1079
+ onChange={(e) => {
1080
+ if (!node.retry) return;
1081
+ const waitMs = Number(e.target.value);
1082
+ setNodeRetry(node.id, {
1083
+ maxTries: node.retry.maxTries,
1084
+ ...(Number.isFinite(waitMs) && waitMs > 0 ? { waitMs } : {}),
1085
+ });
1086
+ }}
1087
+ className="w-20 rounded-md border border-border bg-background px-2 py-1 font-mono text-[12px] outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:opacity-40"
1088
+ aria-label="Wait between attempts (ms)"
1089
+ title="Fixed wait between attempts, in milliseconds"
1090
+ />
1091
+ <span>ms between</span>
1092
+ </div>
1093
+ </section>
1094
+ )}
1095
+
1096
+ {/* Run I/O leads in technical: when the node has actually executed, the
1097
+ run's real output (then input) as formatted JSON is what a developer
1098
+ came to see — declarations and mappings are agent territory and fold
1099
+ below. */}
1100
+ {state && (state.output !== undefined || state.input !== undefined || state.error) && (
1101
+ <section className="mt-4 border-t border-border pt-3">
1102
+ <SectionTitle>Run I/O</SectionTitle>
1103
+ {executions && executions.length > 1 ? (
1104
+ <ExecutionPager executions={executions}>
1105
+ {(record) => (
1106
+ <RunIoPanes input={record.input} output={record.output} error={record.error} />
1107
+ )}
1108
+ </ExecutionPager>
1109
+ ) : (
1110
+ <RunIoPanes input={state.input} output={state.output} error={state.error} />
1111
+ )}
1112
+ </section>
1113
+ )}
1114
+
1115
+ <section className="mt-4 border-t border-border pt-3">
1116
+ <button
1117
+ type="button"
1118
+ onClick={() => setSchemaOpen((v) => !v)}
1119
+ aria-expanded={schemaOpen}
1120
+ className="flex w-full cursor-pointer items-center gap-1.5 text-left"
1121
+ >
1122
+ <ChevronRightIcon
1123
+ className={cn('size-3 text-muted-foreground transition-transform', schemaOpen && 'rotate-90')}
1124
+ />
1125
+ <span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
1126
+ Wiring &amp; schema
1127
+ </span>
1128
+ </button>
1129
+ {schemaOpen && (
1130
+ <div className="mt-3">
1131
+ {(definition?.inputSchema?.fields.length ?? 0) > 0 && (
1132
+ <div className="mb-4">
1133
+ <SectionTitle>Input wiring</SectionTitle>
1134
+ {definition!.inputSchema!.fields.map((f) => (
1135
+ <InputWiringRow key={f.name} node={node} field={f} />
1136
+ ))}
1137
+ </div>
1138
+ )}
1139
+ <SectionTitle>Outputs</SectionTitle>
1140
+ <OutputFieldsList
1141
+ fields={definition?.outputSchema?.fields ?? []}
1142
+ outputValue={outputValue}
1143
+ error={state?.error}
1144
+ />
1145
+ </div>
1146
+ )}
1147
+ </section>
1148
+
1149
+ <HttpSection />
1150
+ </div>
1151
+ );
1152
+ }
1153
+
1154
+ /** Technical Run I/O: the run's actual payloads as formatted JSON — output
1155
+ * first (what the node produced is the headline), then input, error verbatim. */
1156
+ function RunIoPanes({ input, output, error }: { input: unknown; output: unknown; error?: string }) {
1157
+ return (
1158
+ <div className="space-y-3">
1159
+ {error && (
1160
+ <pre className="overflow-x-auto rounded-md border border-destructive/50 bg-background p-2.5 font-mono text-[11.5px] whitespace-pre-wrap text-destructive-foreground">
1161
+ {error}
1162
+ </pre>
1163
+ )}
1164
+ {output !== undefined && (
1165
+ <div>
1166
+ <div className="mb-1 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/70">
1167
+ Output
1168
+ </div>
1169
+ <Json value={output} maxHeight={280} />
1170
+ </div>
1171
+ )}
1172
+ {input !== undefined && (
1173
+ <div>
1174
+ <div className="mb-1 text-[9.5px] font-medium uppercase tracking-widest text-muted-foreground/70">
1175
+ Input
1176
+ </div>
1177
+ <Json value={input} maxHeight={200} />
1178
+ </div>
1179
+ )}
1180
+ </div>
1181
+ );
1182
+ }