@open-mercato/core 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7195.1.b5efd1d51a

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 (1241) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +4 -3
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +16 -0
  5. package/dist/bootstrap.js +7 -3
  6. package/dist/bootstrap.js.map +2 -2
  7. package/dist/generated/entities/action_log/index.js +2 -0
  8. package/dist/generated/entities/action_log/index.js.map +2 -2
  9. package/dist/generated/entities/user/index.js +2 -0
  10. package/dist/generated/entities/user/index.js.map +2 -2
  11. package/dist/generated/entities/user_task/index.js +14 -0
  12. package/dist/generated/entities/user_task/index.js.map +2 -2
  13. package/dist/generated/entities/workflow_definition/index.js +6 -0
  14. package/dist/generated/entities/workflow_definition/index.js.map +2 -2
  15. package/dist/generated/entities/workflow_definition_draft/index.js +25 -0
  16. package/dist/generated/entities/workflow_definition_draft/index.js.map +7 -0
  17. package/dist/generated/entities/workflow_definition_metric_rollup/index.js +23 -0
  18. package/dist/generated/entities/workflow_definition_metric_rollup/index.js.map +7 -0
  19. package/dist/generated/entities/workflow_instance/index.js +4 -0
  20. package/dist/generated/entities/workflow_instance/index.js.map +2 -2
  21. package/dist/generated/entities.ids.generated.js +4 -1
  22. package/dist/generated/entities.ids.generated.js.map +2 -2
  23. package/dist/generated/entity-fields-registry.js +39 -0
  24. package/dist/generated/entity-fields-registry.js.map +2 -2
  25. package/dist/helpers/integration/workflowsFixtures.js +477 -2
  26. package/dist/helpers/integration/workflowsFixtures.js.map +2 -2
  27. package/dist/helpers/integration/workflowsUi.js +148 -0
  28. package/dist/helpers/integration/workflowsUi.js.map +7 -0
  29. package/dist/modules/attachments/lib/createFromBuffer.js +79 -0
  30. package/dist/modules/attachments/lib/createFromBuffer.js.map +7 -0
  31. package/dist/modules/audit_logs/data/entities.js +5 -0
  32. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  33. package/dist/modules/audit_logs/data/validators.js +1 -0
  34. package/dist/modules/audit_logs/data/validators.js.map +2 -2
  35. package/dist/modules/audit_logs/lib/projections.js +2 -1
  36. package/dist/modules/audit_logs/lib/projections.js.map +2 -2
  37. package/dist/modules/audit_logs/migrations/Migration20260625120000.js +15 -0
  38. package/dist/modules/audit_logs/migrations/Migration20260625120000.js.map +7 -0
  39. package/dist/modules/audit_logs/services/actionLogService.js +3 -0
  40. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  41. package/dist/modules/auth/data/entities.js +6 -0
  42. package/dist/modules/auth/data/entities.js.map +2 -2
  43. package/dist/modules/auth/lib/executionPrincipal.js +140 -0
  44. package/dist/modules/auth/lib/executionPrincipal.js.map +7 -0
  45. package/dist/modules/auth/migrations/Migration20260625050000.js +13 -0
  46. package/dist/modules/auth/migrations/Migration20260625050000.js.map +7 -0
  47. package/dist/modules/business_rules/components/InlineRuleEditor.js +154 -0
  48. package/dist/modules/business_rules/components/InlineRuleEditor.js.map +7 -0
  49. package/dist/modules/business_rules/data/validators.js +6 -0
  50. package/dist/modules/business_rules/data/validators.js.map +2 -2
  51. package/dist/modules/business_rules/lib/rule-engine.js +5 -3
  52. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  53. package/dist/modules/catalog/commands/products.js +5 -0
  54. package/dist/modules/catalog/commands/products.js.map +2 -2
  55. package/dist/modules/catalog/workflows.js +19 -0
  56. package/dist/modules/catalog/workflows.js.map +7 -0
  57. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +3 -1
  58. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  59. package/dist/modules/customer_accounts/cli.js +80 -0
  60. package/dist/modules/customer_accounts/cli.js.map +7 -0
  61. package/dist/modules/customer_accounts/lib/customerRoleAclSync.js +59 -0
  62. package/dist/modules/customer_accounts/lib/customerRoleAclSync.js.map +7 -0
  63. package/dist/modules/customer_accounts/setup.js +4 -2
  64. package/dist/modules/customer_accounts/setup.js.map +2 -2
  65. package/dist/modules/customers/commands/deals.js +4 -0
  66. package/dist/modules/customers/commands/deals.js.map +2 -2
  67. package/dist/modules/customers/components/detail/ChangelogEntryRow.js +1 -1
  68. package/dist/modules/customers/components/detail/ChangelogEntryRow.js.map +2 -2
  69. package/dist/modules/customers/components/detail/utils.js +2 -0
  70. package/dist/modules/customers/components/detail/utils.js.map +2 -2
  71. package/dist/modules/customers/events.js +13 -2
  72. package/dist/modules/customers/events.js.map +2 -2
  73. package/dist/modules/customers/lib/link-workflow-task-handler.js +64 -0
  74. package/dist/modules/customers/lib/link-workflow-task-handler.js.map +7 -0
  75. package/dist/modules/customers/lib/todoCompatibility.js +1 -1
  76. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  77. package/dist/modules/customers/lib/workflowTaskLink.js +9 -0
  78. package/dist/modules/customers/lib/workflowTaskLink.js.map +7 -0
  79. package/dist/modules/customers/subscribers/link-workflow-task.js +12 -0
  80. package/dist/modules/customers/subscribers/link-workflow-task.js.map +7 -0
  81. package/dist/modules/customers/workflows.js +29 -0
  82. package/dist/modules/customers/workflows.js.map +7 -0
  83. package/dist/modules/entities/api/records.js +13 -33
  84. package/dist/modules/entities/api/records.js.map +3 -3
  85. package/dist/modules/entities/lib/entityClassification.js +60 -0
  86. package/dist/modules/entities/lib/entityClassification.js.map +7 -0
  87. package/dist/modules/sales/commands/documents.js +31 -0
  88. package/dist/modules/sales/commands/documents.js.map +2 -2
  89. package/dist/modules/sales/events.js +12 -1
  90. package/dist/modules/sales/events.js.map +2 -2
  91. package/dist/modules/sales/workflows.js +12 -1
  92. package/dist/modules/sales/workflows.js.map +2 -2
  93. package/dist/modules/seeds/cli.js +199 -0
  94. package/dist/modules/seeds/cli.js.map +7 -0
  95. package/dist/modules/seeds/examples/sample-seed.json +26 -0
  96. package/dist/modules/seeds/index.js +12 -0
  97. package/dist/modules/seeds/index.js.map +7 -0
  98. package/dist/modules/workflows/acl.js +96 -0
  99. package/dist/modules/workflows/acl.js.map +2 -2
  100. package/dist/modules/workflows/ai-agents.js +50 -0
  101. package/dist/modules/workflows/ai-agents.js.map +7 -0
  102. package/dist/modules/workflows/ai-tools/authoring-pack.js +582 -0
  103. package/dist/modules/workflows/ai-tools/authoring-pack.js.map +7 -0
  104. package/dist/modules/workflows/ai-tools/types.js +43 -0
  105. package/dist/modules/workflows/ai-tools/types.js.map +7 -0
  106. package/dist/modules/workflows/ai-tools.js +10 -0
  107. package/dist/modules/workflows/ai-tools.js.map +7 -0
  108. package/dist/modules/workflows/api/command-settings/route.js +137 -0
  109. package/dist/modules/workflows/api/command-settings/route.js.map +7 -0
  110. package/dist/modules/workflows/api/commands/route.js +72 -0
  111. package/dist/modules/workflows/api/commands/route.js.map +7 -0
  112. package/dist/modules/workflows/api/definitions/[id]/callers/route.js +76 -0
  113. package/dist/modules/workflows/api/definitions/[id]/callers/route.js.map +7 -0
  114. package/dist/modules/workflows/api/definitions/[id]/context-schema/route.js +134 -0
  115. package/dist/modules/workflows/api/definitions/[id]/context-schema/route.js.map +7 -0
  116. package/dist/modules/workflows/api/definitions/[id]/customize/route.js +5 -1
  117. package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
  118. package/dist/modules/workflows/api/definitions/[id]/draft/route.js +286 -0
  119. package/dist/modules/workflows/api/definitions/[id]/draft/route.js.map +7 -0
  120. package/dist/modules/workflows/api/definitions/[id]/publish/route.js +207 -0
  121. package/dist/modules/workflows/api/definitions/[id]/publish/route.js.map +7 -0
  122. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +5 -0
  123. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
  124. package/dist/modules/workflows/api/definitions/[id]/route.js +93 -9
  125. package/dist/modules/workflows/api/definitions/[id]/route.js.map +3 -3
  126. package/dist/modules/workflows/api/definitions/[id]/test-step/route.js +210 -0
  127. package/dist/modules/workflows/api/definitions/[id]/test-step/route.js.map +7 -0
  128. package/dist/modules/workflows/api/definitions/generate/route.js +233 -0
  129. package/dist/modules/workflows/api/definitions/generate/route.js.map +7 -0
  130. package/dist/modules/workflows/api/definitions/route.js +61 -113
  131. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  132. package/dist/modules/workflows/api/definitions/serialize.js +9 -0
  133. package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
  134. package/dist/modules/workflows/api/endpoints/route.js +69 -0
  135. package/dist/modules/workflows/api/endpoints/route.js.map +7 -0
  136. package/dist/modules/workflows/api/functions/route.js +73 -0
  137. package/dist/modules/workflows/api/functions/route.js.map +7 -0
  138. package/dist/modules/workflows/api/grantable-features/route.js +75 -0
  139. package/dist/modules/workflows/api/grantable-features/route.js.map +7 -0
  140. package/dist/modules/workflows/api/instances/[id]/context/route.js +180 -0
  141. package/dist/modules/workflows/api/instances/[id]/context/route.js.map +7 -0
  142. package/dist/modules/workflows/api/instances/[id]/rerun-step/route.js +184 -0
  143. package/dist/modules/workflows/api/instances/[id]/rerun-step/route.js.map +7 -0
  144. package/dist/modules/workflows/api/instances/[id]/retry/route.js +12 -2
  145. package/dist/modules/workflows/api/instances/[id]/retry/route.js.map +2 -2
  146. package/dist/modules/workflows/api/instances/[id]/step-through/route.js +148 -0
  147. package/dist/modules/workflows/api/instances/[id]/step-through/route.js.map +7 -0
  148. package/dist/modules/workflows/api/instances/[id]/steps/route.js +123 -0
  149. package/dist/modules/workflows/api/instances/[id]/steps/route.js.map +7 -0
  150. package/dist/modules/workflows/api/instances/[id]/would-do/route.js +172 -0
  151. package/dist/modules/workflows/api/instances/[id]/would-do/route.js.map +7 -0
  152. package/dist/modules/workflows/api/instances/bulk-replay/route.js +117 -0
  153. package/dist/modules/workflows/api/instances/bulk-replay/route.js.map +7 -0
  154. package/dist/modules/workflows/api/instances/failure-queue/route.js +165 -0
  155. package/dist/modules/workflows/api/instances/failure-queue/route.js.map +7 -0
  156. package/dist/modules/workflows/api/instances/route.js +94 -3
  157. package/dist/modules/workflows/api/instances/route.js.map +2 -2
  158. package/dist/modules/workflows/api/metrics/definitions/route.js +188 -0
  159. package/dist/modules/workflows/api/metrics/definitions/route.js.map +7 -0
  160. package/dist/modules/workflows/api/openapi.js +415 -4
  161. package/dist/modules/workflows/api/openapi.js.map +2 -2
  162. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +132 -0
  163. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +7 -0
  164. package/dist/modules/workflows/api/portal/tasks/[id]/route.js +97 -0
  165. package/dist/modules/workflows/api/portal/tasks/[id]/route.js.map +7 -0
  166. package/dist/modules/workflows/api/portal/tasks/route.js +126 -0
  167. package/dist/modules/workflows/api/portal/tasks/route.js.map +7 -0
  168. package/dist/modules/workflows/api/rule-usage/route.js +80 -0
  169. package/dist/modules/workflows/api/rule-usage/route.js.map +7 -0
  170. package/dist/modules/workflows/api/task-settings/route.js +107 -0
  171. package/dist/modules/workflows/api/task-settings/route.js.map +7 -0
  172. package/dist/modules/workflows/api/tasks/[id]/claim/route.js +27 -16
  173. package/dist/modules/workflows/api/tasks/[id]/claim/route.js.map +2 -2
  174. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +40 -16
  175. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  176. package/dist/modules/workflows/api/tasks/[id]/reassign/route.js +150 -0
  177. package/dist/modules/workflows/api/tasks/[id]/reassign/route.js.map +7 -0
  178. package/dist/modules/workflows/api/tasks/[id]/route.js +39 -6
  179. package/dist/modules/workflows/api/tasks/[id]/route.js.map +2 -2
  180. package/dist/modules/workflows/api/tasks/[id]/unclaim/route.js +100 -0
  181. package/dist/modules/workflows/api/tasks/[id]/unclaim/route.js.map +7 -0
  182. package/dist/modules/workflows/api/tasks/route.js +37 -6
  183. package/dist/modules/workflows/api/tasks/route.js.map +2 -2
  184. package/dist/modules/workflows/api/tasks/serialize.js +83 -0
  185. package/dist/modules/workflows/api/tasks/serialize.js.map +7 -0
  186. package/dist/modules/workflows/api/templates/route.js +69 -0
  187. package/dist/modules/workflows/api/templates/route.js.map +7 -0
  188. package/dist/modules/workflows/api/work-inbox/next/route.js +129 -0
  189. package/dist/modules/workflows/api/work-inbox/next/route.js.map +7 -0
  190. package/dist/modules/workflows/api/work-inbox/route.js +119 -0
  191. package/dist/modules/workflows/api/work-inbox/route.js.map +7 -0
  192. package/dist/modules/workflows/backend/config/workflows/page.js +10 -0
  193. package/dist/modules/workflows/backend/config/workflows/page.js.map +7 -0
  194. package/dist/modules/workflows/backend/config/workflows/page.meta.js +35 -0
  195. package/dist/modules/workflows/backend/config/workflows/page.meta.js.map +7 -0
  196. package/dist/modules/workflows/backend/definitions/[id]/page.js +6 -271
  197. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +3 -3
  198. package/dist/modules/workflows/backend/definitions/create/page.js +7 -56
  199. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  200. package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
  201. package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
  202. package/dist/modules/workflows/backend/definitions/page.js +131 -51
  203. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  204. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +2606 -677
  205. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +3 -3
  206. package/dist/modules/workflows/backend/instances/[id]/page.js +532 -421
  207. package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
  208. package/dist/modules/workflows/backend/instances/[id]/page.meta.js +1 -1
  209. package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +1 -1
  210. package/dist/modules/workflows/backend/instances/failure-queue/page.js +211 -0
  211. package/dist/modules/workflows/backend/instances/failure-queue/page.js.map +7 -0
  212. package/dist/modules/workflows/backend/instances/failure-queue/page.meta.js +20 -0
  213. package/dist/modules/workflows/backend/instances/failure-queue/page.meta.js.map +7 -0
  214. package/dist/modules/workflows/backend/instances/page.js +159 -22
  215. package/dist/modules/workflows/backend/instances/page.js.map +2 -2
  216. package/dist/modules/workflows/backend/instances/page.meta.js +1 -1
  217. package/dist/modules/workflows/backend/instances/page.meta.js.map +1 -1
  218. package/dist/modules/workflows/backend/tasks/[id]/page.js +553 -392
  219. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +3 -3
  220. package/dist/modules/workflows/backend/tasks/page.js +8 -250
  221. package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
  222. package/dist/modules/workflows/backend/tasks/page.meta.js +1 -0
  223. package/dist/modules/workflows/backend/tasks/page.meta.js.map +2 -2
  224. package/dist/modules/workflows/backend/work-inbox/page.js +399 -0
  225. package/dist/modules/workflows/backend/work-inbox/page.js.map +7 -0
  226. package/dist/modules/workflows/backend/work-inbox/page.meta.js +19 -0
  227. package/dist/modules/workflows/backend/work-inbox/page.meta.js.map +7 -0
  228. package/dist/modules/workflows/cli.js +1 -1
  229. package/dist/modules/workflows/cli.js.map +2 -2
  230. package/dist/modules/workflows/commands/tasks.js +81 -0
  231. package/dist/modules/workflows/commands/tasks.js.map +7 -0
  232. package/dist/modules/workflows/components/ActivitiesEditor.js +281 -287
  233. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  234. package/dist/modules/workflows/components/ActivityEditDialog.js +220 -0
  235. package/dist/modules/workflows/components/ActivityEditDialog.js.map +7 -0
  236. package/dist/modules/workflows/components/AgentSelector.js +115 -0
  237. package/dist/modules/workflows/components/AgentSelector.js.map +7 -0
  238. package/dist/modules/workflows/components/AnnotationEditDialog.js +110 -0
  239. package/dist/modules/workflows/components/AnnotationEditDialog.js.map +7 -0
  240. package/dist/modules/workflows/components/BusinessRuleUsagePanel.js +71 -0
  241. package/dist/modules/workflows/components/BusinessRuleUsagePanel.js.map +7 -0
  242. package/dist/modules/workflows/components/BusinessRulesSelector.js +151 -93
  243. package/dist/modules/workflows/components/BusinessRulesSelector.js.map +2 -2
  244. package/dist/modules/workflows/components/ConfigJsonTextarea.js +85 -0
  245. package/dist/modules/workflows/components/ConfigJsonTextarea.js.map +7 -0
  246. package/dist/modules/workflows/components/ContextSchemaEditor.js +150 -0
  247. package/dist/modules/workflows/components/ContextSchemaEditor.js.map +7 -0
  248. package/dist/modules/workflows/components/DefinitionErrorHandlerField.js +102 -0
  249. package/dist/modules/workflows/components/DefinitionErrorHandlerField.js.map +7 -0
  250. package/dist/modules/workflows/components/DefinitionMetadataDrawer.js +377 -0
  251. package/dist/modules/workflows/components/DefinitionMetadataDrawer.js.map +7 -0
  252. package/dist/modules/workflows/components/DefinitionMetricsCells.js +132 -0
  253. package/dist/modules/workflows/components/DefinitionMetricsCells.js.map +7 -0
  254. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +307 -216
  255. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  256. package/dist/modules/workflows/components/EdgeEditDialog.js +11 -18
  257. package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
  258. package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js +112 -72
  259. package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +2 -2
  260. package/dist/modules/workflows/components/InputDataPanel.js +128 -0
  261. package/dist/modules/workflows/components/InputDataPanel.js.map +7 -0
  262. package/dist/modules/workflows/components/InspectorPanel.js +161 -0
  263. package/dist/modules/workflows/components/InspectorPanel.js.map +7 -0
  264. package/dist/modules/workflows/components/NodeEditDialog.js +363 -41
  265. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  266. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +714 -159
  267. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  268. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  269. package/dist/modules/workflows/components/TemplateGalleryDialog.js +148 -0
  270. package/dist/modules/workflows/components/TemplateGalleryDialog.js.map +7 -0
  271. package/dist/modules/workflows/components/TransitionsEditor.js +80 -74
  272. package/dist/modules/workflows/components/TransitionsEditor.js.map +2 -2
  273. package/dist/modules/workflows/components/TriggersDialog.js +53 -0
  274. package/dist/modules/workflows/components/TriggersDialog.js.map +7 -0
  275. package/dist/modules/workflows/components/TriggersEditor.js +331 -0
  276. package/dist/modules/workflows/components/TriggersEditor.js.map +7 -0
  277. package/dist/modules/workflows/components/WorkflowAiDraftDialog.js +278 -0
  278. package/dist/modules/workflows/components/WorkflowAiDraftDialog.js.map +7 -0
  279. package/dist/modules/workflows/components/WorkflowCodeView.js +173 -0
  280. package/dist/modules/workflows/components/WorkflowCodeView.js.map +7 -0
  281. package/dist/modules/workflows/components/WorkflowCommandPalette.js +62 -0
  282. package/dist/modules/workflows/components/WorkflowCommandPalette.js.map +7 -0
  283. package/dist/modules/workflows/components/WorkflowCompensationGhostEdge.js +70 -0
  284. package/dist/modules/workflows/components/WorkflowCompensationGhostEdge.js.map +7 -0
  285. package/dist/modules/workflows/components/WorkflowDataMappingEdge.js +36 -0
  286. package/dist/modules/workflows/components/WorkflowDataMappingEdge.js.map +7 -0
  287. package/dist/modules/workflows/components/WorkflowGraph.js +4 -2
  288. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  289. package/dist/modules/workflows/components/WorkflowGraphImpl.js +324 -86
  290. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +3 -3
  291. package/dist/modules/workflows/components/WorkflowIconPicker.js +109 -0
  292. package/dist/modules/workflows/components/WorkflowIconPicker.js.map +7 -0
  293. package/dist/modules/workflows/components/WorkflowLegend.js +45 -29
  294. package/dist/modules/workflows/components/WorkflowLegend.js.map +2 -2
  295. package/dist/modules/workflows/components/WorkflowNodeCard.js +221 -22
  296. package/dist/modules/workflows/components/WorkflowNodeCard.js.map +2 -2
  297. package/dist/modules/workflows/components/WorkflowRouteChips.js +111 -0
  298. package/dist/modules/workflows/components/WorkflowRouteChips.js.map +7 -0
  299. package/dist/modules/workflows/components/WorkflowSelector.js +108 -88
  300. package/dist/modules/workflows/components/WorkflowSelector.js.map +2 -2
  301. package/dist/modules/workflows/components/WorkflowTransitionEdge.js +61 -11
  302. package/dist/modules/workflows/components/WorkflowTransitionEdge.js.map +2 -2
  303. package/dist/modules/workflows/components/WorkflowTransitionLabel.js +15 -5
  304. package/dist/modules/workflows/components/WorkflowTransitionLabel.js.map +2 -2
  305. package/dist/modules/workflows/components/fields/ActivityArrayEditor.js +88 -21
  306. package/dist/modules/workflows/components/fields/ActivityArrayEditor.js.map +2 -2
  307. package/dist/modules/workflows/components/fields/ActivityConfigFields.js +615 -0
  308. package/dist/modules/workflows/components/fields/ActivityConfigFields.js.map +7 -0
  309. package/dist/modules/workflows/components/fields/ActivityTestPanel.js +191 -0
  310. package/dist/modules/workflows/components/fields/ActivityTestPanel.js.map +7 -0
  311. package/dist/modules/workflows/components/fields/AgentInvokeConfigField.js +488 -0
  312. package/dist/modules/workflows/components/fields/AgentInvokeConfigField.js.map +7 -0
  313. package/dist/modules/workflows/components/fields/AgentReviewOnBreachField.js +61 -0
  314. package/dist/modules/workflows/components/fields/AgentReviewOnBreachField.js.map +7 -0
  315. package/dist/modules/workflows/components/fields/BranchingRoutesEditor.js +227 -0
  316. package/dist/modules/workflows/components/fields/BranchingRoutesEditor.js.map +7 -0
  317. package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js +37 -4
  318. package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js.map +2 -2
  319. package/dist/modules/workflows/components/fields/CommandPicker.js +111 -0
  320. package/dist/modules/workflows/components/fields/CommandPicker.js.map +7 -0
  321. package/dist/modules/workflows/components/fields/EndpointPicker.js +235 -0
  322. package/dist/modules/workflows/components/fields/EndpointPicker.js.map +7 -0
  323. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -2
  324. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  325. package/dist/modules/workflows/components/fields/FunctionPicker.js +55 -0
  326. package/dist/modules/workflows/components/fields/FunctionPicker.js.map +7 -0
  327. package/dist/modules/workflows/components/fields/MappingArrayEditor.js +38 -15
  328. package/dist/modules/workflows/components/fields/MappingArrayEditor.js.map +2 -2
  329. package/dist/modules/workflows/components/fields/PortFieldArrayEditor.js +206 -0
  330. package/dist/modules/workflows/components/fields/PortFieldArrayEditor.js.map +7 -0
  331. package/dist/modules/workflows/components/fields/RolesMultiSelect.js +53 -0
  332. package/dist/modules/workflows/components/fields/RolesMultiSelect.js.map +7 -0
  333. package/dist/modules/workflows/components/fields/RouteOrderEditor.js +130 -0
  334. package/dist/modules/workflows/components/fields/RouteOrderEditor.js.map +7 -0
  335. package/dist/modules/workflows/components/fields/SchemaBuilderDialog.js +104 -0
  336. package/dist/modules/workflows/components/fields/SchemaBuilderDialog.js.map +7 -0
  337. package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js +2 -2
  338. package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js.map +2 -2
  339. package/dist/modules/workflows/components/fields/TaskAssignmentField.js +210 -0
  340. package/dist/modules/workflows/components/fields/TaskAssignmentField.js.map +7 -0
  341. package/dist/modules/workflows/components/fields/TaskDeadlineFields.js +126 -0
  342. package/dist/modules/workflows/components/fields/TaskDeadlineFields.js.map +7 -0
  343. package/dist/modules/workflows/components/fields/TaskDecisionsField.js +218 -0
  344. package/dist/modules/workflows/components/fields/TaskDecisionsField.js.map +7 -0
  345. package/dist/modules/workflows/components/fields/TaskEntityBindingsField.js +114 -0
  346. package/dist/modules/workflows/components/fields/TaskEntityBindingsField.js.map +7 -0
  347. package/dist/modules/workflows/components/fields/TemplateTextControl.js +64 -0
  348. package/dist/modules/workflows/components/fields/TemplateTextControl.js.map +7 -0
  349. package/dist/modules/workflows/components/fields/VariablePickerButton.js +121 -0
  350. package/dist/modules/workflows/components/fields/VariablePickerButton.js.map +7 -0
  351. package/dist/modules/workflows/components/fields/ledgerDropTarget.js +35 -0
  352. package/dist/modules/workflows/components/fields/ledgerDropTarget.js.map +7 -0
  353. package/dist/modules/workflows/components/fields/useActivityTypeOptions.js +16 -0
  354. package/dist/modules/workflows/components/fields/useActivityTypeOptions.js.map +7 -0
  355. package/dist/modules/workflows/components/formConfig.js +39 -10
  356. package/dist/modules/workflows/components/formConfig.js.map +2 -2
  357. package/dist/modules/workflows/components/mobile/MobileDefinitionDetail.js.map +2 -2
  358. package/dist/modules/workflows/components/mobile/MobileMetadataSheet.js.map +2 -2
  359. package/dist/modules/workflows/components/mobile/MobileTaskForm.js +29 -2
  360. package/dist/modules/workflows/components/mobile/MobileTaskForm.js.map +2 -2
  361. package/dist/modules/workflows/components/mobile/MobileVisualEditor.js +64 -65
  362. package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +2 -2
  363. package/dist/modules/workflows/components/nodes/AnnotationGroupNode.js +64 -0
  364. package/dist/modules/workflows/components/nodes/AnnotationGroupNode.js.map +7 -0
  365. package/dist/modules/workflows/components/nodes/AutomatedNode.js +24 -16
  366. package/dist/modules/workflows/components/nodes/AutomatedNode.js.map +2 -2
  367. package/dist/modules/workflows/components/nodes/EndNode.js +13 -12
  368. package/dist/modules/workflows/components/nodes/EndNode.js.map +2 -2
  369. package/dist/modules/workflows/components/nodes/ErrorOutputHandle.js +29 -0
  370. package/dist/modules/workflows/components/nodes/ErrorOutputHandle.js.map +7 -0
  371. package/dist/modules/workflows/components/nodes/IfElseNode.js +51 -0
  372. package/dist/modules/workflows/components/nodes/IfElseNode.js.map +7 -0
  373. package/dist/modules/workflows/components/nodes/InvokeAgentNode.js +122 -0
  374. package/dist/modules/workflows/components/nodes/InvokeAgentNode.js.map +7 -0
  375. package/dist/modules/workflows/components/nodes/NodeOutcomeRows.js +100 -0
  376. package/dist/modules/workflows/components/nodes/NodeOutcomeRows.js.map +7 -0
  377. package/dist/modules/workflows/components/nodes/ParallelForkNode.js +18 -14
  378. package/dist/modules/workflows/components/nodes/ParallelForkNode.js.map +2 -2
  379. package/dist/modules/workflows/components/nodes/ParallelJoinNode.js +18 -14
  380. package/dist/modules/workflows/components/nodes/ParallelJoinNode.js.map +2 -2
  381. package/dist/modules/workflows/components/nodes/StartNode.js +66 -15
  382. package/dist/modules/workflows/components/nodes/StartNode.js.map +2 -2
  383. package/dist/modules/workflows/components/nodes/StickyNoteNode.js +54 -0
  384. package/dist/modules/workflows/components/nodes/StickyNoteNode.js.map +7 -0
  385. package/dist/modules/workflows/components/nodes/SubWorkflowNode.js +107 -41
  386. package/dist/modules/workflows/components/nodes/SubWorkflowNode.js.map +2 -2
  387. package/dist/modules/workflows/components/nodes/SwitchNode.js +51 -0
  388. package/dist/modules/workflows/components/nodes/SwitchNode.js.map +7 -0
  389. package/dist/modules/workflows/components/nodes/TriggerCap.js +65 -0
  390. package/dist/modules/workflows/components/nodes/TriggerCap.js.map +7 -0
  391. package/dist/modules/workflows/components/nodes/TriggerNode.js +111 -0
  392. package/dist/modules/workflows/components/nodes/TriggerNode.js.map +7 -0
  393. package/dist/modules/workflows/components/nodes/UserTaskNode.js +43 -17
  394. package/dist/modules/workflows/components/nodes/UserTaskNode.js.map +2 -2
  395. package/dist/modules/workflows/components/nodes/WaitForConditionNode.js +53 -0
  396. package/dist/modules/workflows/components/nodes/WaitForConditionNode.js.map +7 -0
  397. package/dist/modules/workflows/components/nodes/WaitForSignalNode.js +21 -15
  398. package/dist/modules/workflows/components/nodes/WaitForSignalNode.js.map +2 -2
  399. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +21 -15
  400. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +2 -2
  401. package/dist/modules/workflows/components/nodes/index.js +14 -0
  402. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  403. package/dist/modules/workflows/components/portal/PortalTaskForm.js +196 -0
  404. package/dist/modules/workflows/components/portal/PortalTaskForm.js.map +7 -0
  405. package/dist/modules/workflows/components/run/AgentActivityPanel.js +22 -0
  406. package/dist/modules/workflows/components/run/AgentActivityPanel.js.map +7 -0
  407. package/dist/modules/workflows/components/run/RerunStepDialog.js +106 -0
  408. package/dist/modules/workflows/components/run/RerunStepDialog.js.map +7 -0
  409. package/dist/modules/workflows/components/run/RunEventBadge.js +30 -0
  410. package/dist/modules/workflows/components/run/RunEventBadge.js.map +7 -0
  411. package/dist/modules/workflows/components/run/RunGantt.js +130 -0
  412. package/dist/modules/workflows/components/run/RunGantt.js.map +7 -0
  413. package/dist/modules/workflows/components/run/RunStatusBadge.js +82 -0
  414. package/dist/modules/workflows/components/run/RunStatusBadge.js.map +7 -0
  415. package/dist/modules/workflows/components/run/RunStepInspector.js +116 -0
  416. package/dist/modules/workflows/components/run/RunStepInspector.js.map +7 -0
  417. package/dist/modules/workflows/components/run/WouldDoReport.js +52 -0
  418. package/dist/modules/workflows/components/run/WouldDoReport.js.map +7 -0
  419. package/dist/modules/workflows/components/run/useAgentLabels.js +28 -0
  420. package/dist/modules/workflows/components/run/useAgentLabels.js.map +7 -0
  421. package/dist/modules/workflows/components/run/useLastRunOverlay.js +89 -0
  422. package/dist/modules/workflows/components/run/useLastRunOverlay.js.map +7 -0
  423. package/dist/modules/workflows/components/run/useLiveAgentActions.js +41 -0
  424. package/dist/modules/workflows/components/run/useLiveAgentActions.js.map +7 -0
  425. package/dist/modules/workflows/components/run/useLiveRunUpdates.js +57 -0
  426. package/dist/modules/workflows/components/run/useLiveRunUpdates.js.map +7 -0
  427. package/dist/modules/workflows/components/settings/CommandSettingsPageClient.js +156 -0
  428. package/dist/modules/workflows/components/settings/CommandSettingsPageClient.js.map +7 -0
  429. package/dist/modules/workflows/components/work-inbox/EntityContextPanel.js +72 -0
  430. package/dist/modules/workflows/components/work-inbox/EntityContextPanel.js.map +7 -0
  431. package/dist/modules/workflows/components/work-inbox/ReassignTaskDialog.js +156 -0
  432. package/dist/modules/workflows/components/work-inbox/ReassignTaskDialog.js.map +7 -0
  433. package/dist/modules/workflows/components/work-inbox/StatusChip.js +46 -0
  434. package/dist/modules/workflows/components/work-inbox/StatusChip.js.map +7 -0
  435. package/dist/modules/workflows/components/work-inbox/TaskActionNotice.js +53 -0
  436. package/dist/modules/workflows/components/work-inbox/TaskActionNotice.js.map +7 -0
  437. package/dist/modules/workflows/components/work-inbox/TaskFormFields.js +261 -0
  438. package/dist/modules/workflows/components/work-inbox/TaskFormFields.js.map +7 -0
  439. package/dist/modules/workflows/components/work-inbox/WorkInboxEmptyState.js +27 -0
  440. package/dist/modules/workflows/components/work-inbox/WorkInboxEmptyState.js.map +7 -0
  441. package/dist/modules/workflows/data/activity-config-schemas.js +191 -0
  442. package/dist/modules/workflows/data/activity-config-schemas.js.map +7 -0
  443. package/dist/modules/workflows/data/activity-config-warnings.js +42 -0
  444. package/dist/modules/workflows/data/activity-config-warnings.js.map +7 -0
  445. package/dist/modules/workflows/data/branching-route-warnings.js +65 -0
  446. package/dist/modules/workflows/data/branching-route-warnings.js.map +7 -0
  447. package/dist/modules/workflows/data/entities.js +144 -4
  448. package/dist/modules/workflows/data/entities.js.map +2 -2
  449. package/dist/modules/workflows/data/metrics-validators.js +35 -0
  450. package/dist/modules/workflows/data/metrics-validators.js.map +7 -0
  451. package/dist/modules/workflows/data/task-primitives.js +14 -0
  452. package/dist/modules/workflows/data/task-primitives.js.map +7 -0
  453. package/dist/modules/workflows/data/validators.js +504 -61
  454. package/dist/modules/workflows/data/validators.js.map +3 -3
  455. package/dist/modules/workflows/di.js +16 -1
  456. package/dist/modules/workflows/di.js.map +2 -2
  457. package/dist/modules/workflows/events.js +66 -8
  458. package/dist/modules/workflows/events.js.map +2 -2
  459. package/dist/modules/workflows/examples/oze-residential-install-60-nodes.json +1545 -0
  460. package/dist/modules/workflows/examples/sales-pipeline-definition.json +35 -35
  461. package/dist/modules/workflows/examples/templates/lead-to-install.json +160 -0
  462. package/dist/modules/workflows/examples/templates/order-approval.json +230 -0
  463. package/dist/modules/workflows/examples/templates/task-escalation.json +110 -0
  464. package/dist/modules/workflows/examples/templates/webhook-integration.json +113 -0
  465. package/dist/modules/workflows/extension-points.js +1 -1
  466. package/dist/modules/workflows/extension-points.js.map +2 -2
  467. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +144 -0
  468. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +7 -0
  469. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.js +12 -0
  470. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.js.map +7 -0
  471. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.js +114 -0
  472. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.js.map +7 -0
  473. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.js +18 -0
  474. package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.js.map +7 -0
  475. package/dist/modules/workflows/lib/activity-executor.js +505 -137
  476. package/dist/modules/workflows/lib/activity-executor.js.map +3 -3
  477. package/dist/modules/workflows/lib/activity-queue-types.js +3 -1
  478. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  479. package/dist/modules/workflows/lib/activity-registry-bootstrap.js +2 -0
  480. package/dist/modules/workflows/lib/activity-registry-bootstrap.js.map +7 -0
  481. package/dist/modules/workflows/lib/activity-registry.js +27 -0
  482. package/dist/modules/workflows/lib/activity-registry.js.map +7 -0
  483. package/dist/modules/workflows/lib/activity-types.js +291 -0
  484. package/dist/modules/workflows/lib/activity-types.js.map +7 -0
  485. package/dist/modules/workflows/lib/activity-worker-handler.js +456 -32
  486. package/dist/modules/workflows/lib/activity-worker-handler.js.map +3 -3
  487. package/dist/modules/workflows/lib/advanced-config.js +31 -0
  488. package/dist/modules/workflows/lib/advanced-config.js.map +7 -0
  489. package/dist/modules/workflows/lib/agent-disposition-task.js +198 -0
  490. package/dist/modules/workflows/lib/agent-disposition-task.js.map +7 -0
  491. package/dist/modules/workflows/lib/agent-outcome-paths.js +82 -0
  492. package/dist/modules/workflows/lib/agent-outcome-paths.js.map +7 -0
  493. package/dist/modules/workflows/lib/agent-result-mapping.js +54 -0
  494. package/dist/modules/workflows/lib/agent-result-mapping.js.map +7 -0
  495. package/dist/modules/workflows/lib/agent-review-inspector.js +61 -0
  496. package/dist/modules/workflows/lib/agent-review-inspector.js.map +7 -0
  497. package/dist/modules/workflows/lib/agent-review.js +66 -0
  498. package/dist/modules/workflows/lib/agent-review.js.map +7 -0
  499. package/dist/modules/workflows/lib/ai-authoring.js +155 -0
  500. package/dist/modules/workflows/lib/ai-authoring.js.map +7 -0
  501. package/dist/modules/workflows/lib/ai-draft-runner.js +43 -0
  502. package/dist/modules/workflows/lib/ai-draft-runner.js.map +7 -0
  503. package/dist/modules/workflows/lib/annotation-events.js +10 -0
  504. package/dist/modules/workflows/lib/annotation-events.js.map +7 -0
  505. package/dist/modules/workflows/lib/branching-routes.js +182 -0
  506. package/dist/modules/workflows/lib/branching-routes.js.map +7 -0
  507. package/dist/modules/workflows/lib/breach-routing.js +50 -0
  508. package/dist/modules/workflows/lib/breach-routing.js.map +7 -0
  509. package/dist/modules/workflows/lib/bulk-replay.js +101 -0
  510. package/dist/modules/workflows/lib/bulk-replay.js.map +7 -0
  511. package/dist/modules/workflows/lib/business-rule-usage.js +91 -0
  512. package/dist/modules/workflows/lib/business-rule-usage.js.map +7 -0
  513. package/dist/modules/workflows/lib/caller-graph.js +53 -0
  514. package/dist/modules/workflows/lib/caller-graph.js.map +7 -0
  515. package/dist/modules/workflows/lib/code-view-apply.js +86 -0
  516. package/dist/modules/workflows/lib/code-view-apply.js.map +7 -0
  517. package/dist/modules/workflows/lib/collect-validation-issues.js +164 -0
  518. package/dist/modules/workflows/lib/collect-validation-issues.js.map +7 -0
  519. package/dist/modules/workflows/lib/compensation-ghosts.js +72 -0
  520. package/dist/modules/workflows/lib/compensation-ghosts.js.map +7 -0
  521. package/dist/modules/workflows/lib/component-guard.js +7 -0
  522. package/dist/modules/workflows/lib/component-guard.js.map +7 -0
  523. package/dist/modules/workflows/lib/condition-handler.js +443 -0
  524. package/dist/modules/workflows/lib/condition-handler.js.map +7 -0
  525. package/dist/modules/workflows/lib/context-ledger.js +448 -0
  526. package/dist/modules/workflows/lib/context-ledger.js.map +7 -0
  527. package/dist/modules/workflows/lib/crud-form-dialogs-flag.js +8 -0
  528. package/dist/modules/workflows/lib/crud-form-dialogs-flag.js.map +7 -0
  529. package/dist/modules/workflows/lib/data-edge-mapping.js +63 -0
  530. package/dist/modules/workflows/lib/data-edge-mapping.js.map +7 -0
  531. package/dist/modules/workflows/lib/definition-context-schema.js +56 -0
  532. package/dist/modules/workflows/lib/definition-context-schema.js.map +7 -0
  533. package/dist/modules/workflows/lib/definition-edit-safety.js +123 -0
  534. package/dist/modules/workflows/lib/definition-edit-safety.js.map +7 -0
  535. package/dist/modules/workflows/lib/definition-error-body.js +33 -0
  536. package/dist/modules/workflows/lib/definition-error-body.js.map +7 -0
  537. package/dist/modules/workflows/lib/definition-evaluation.js +79 -0
  538. package/dist/modules/workflows/lib/definition-evaluation.js.map +7 -0
  539. package/dist/modules/workflows/lib/definition-grant.js +90 -0
  540. package/dist/modules/workflows/lib/definition-grant.js.map +7 -0
  541. package/dist/modules/workflows/lib/definition-json-locations.js +139 -0
  542. package/dist/modules/workflows/lib/definition-json-locations.js.map +7 -0
  543. package/dist/modules/workflows/lib/definition-payload.js +31 -0
  544. package/dist/modules/workflows/lib/definition-payload.js.map +7 -0
  545. package/dist/modules/workflows/lib/draft-restore.js +49 -0
  546. package/dist/modules/workflows/lib/draft-restore.js.map +7 -0
  547. package/dist/modules/workflows/lib/dry-run.js +106 -0
  548. package/dist/modules/workflows/lib/dry-run.js.map +7 -0
  549. package/dist/modules/workflows/lib/duration.js +66 -17
  550. package/dist/modules/workflows/lib/duration.js.map +2 -2
  551. package/dist/modules/workflows/lib/edge-reattachment.js +79 -0
  552. package/dist/modules/workflows/lib/edge-reattachment.js.map +7 -0
  553. package/dist/modules/workflows/lib/edgeFormTransforms.js +7 -11
  554. package/dist/modules/workflows/lib/edgeFormTransforms.js.map +2 -2
  555. package/dist/modules/workflows/lib/editor-annotations.js +195 -0
  556. package/dist/modules/workflows/lib/editor-annotations.js.map +7 -0
  557. package/dist/modules/workflows/lib/editor-commands.js +103 -0
  558. package/dist/modules/workflows/lib/editor-commands.js.map +7 -0
  559. package/dist/modules/workflows/lib/editor-history.js +51 -0
  560. package/dist/modules/workflows/lib/editor-history.js.map +7 -0
  561. package/dist/modules/workflows/lib/endpoint-catalog.js +181 -0
  562. package/dist/modules/workflows/lib/endpoint-catalog.js.map +7 -0
  563. package/dist/modules/workflows/lib/endpoint-path.js +90 -0
  564. package/dist/modules/workflows/lib/endpoint-path.js.map +7 -0
  565. package/dist/modules/workflows/lib/engine-version.js +42 -0
  566. package/dist/modules/workflows/lib/engine-version.js.map +7 -0
  567. package/dist/modules/workflows/lib/error-handler.js +132 -0
  568. package/dist/modules/workflows/lib/error-handler.js.map +7 -0
  569. package/dist/modules/workflows/lib/error-routing.js +104 -0
  570. package/dist/modules/workflows/lib/error-routing.js.map +7 -0
  571. package/dist/modules/workflows/lib/event-logger.js +23 -0
  572. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  573. package/dist/modules/workflows/lib/expression-refs.js +168 -0
  574. package/dist/modules/workflows/lib/expression-refs.js.map +7 -0
  575. package/dist/modules/workflows/lib/failure-grouping.js +62 -0
  576. package/dist/modules/workflows/lib/failure-grouping.js.map +7 -0
  577. package/dist/modules/workflows/lib/find-definition.js +1 -0
  578. package/dist/modules/workflows/lib/find-definition.js.map +2 -2
  579. package/dist/modules/workflows/lib/flow-logic-warnings.js +338 -0
  580. package/dist/modules/workflows/lib/flow-logic-warnings.js.map +7 -0
  581. package/dist/modules/workflows/lib/graph-utils.js +328 -77
  582. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  583. package/dist/modules/workflows/lib/icon-search.js +32 -0
  584. package/dist/modules/workflows/lib/icon-search.js.map +7 -0
  585. package/dist/modules/workflows/lib/insert-at-cursor.js +26 -0
  586. package/dist/modules/workflows/lib/insert-at-cursor.js.map +7 -0
  587. package/dist/modules/workflows/lib/instance-date-filter.js +28 -0
  588. package/dist/modules/workflows/lib/instance-date-filter.js.map +7 -0
  589. package/dist/modules/workflows/lib/interpolation-pipeline.js +271 -0
  590. package/dist/modules/workflows/lib/interpolation-pipeline.js.map +7 -0
  591. package/dist/modules/workflows/lib/ledger-drag.js +28 -0
  592. package/dist/modules/workflows/lib/ledger-drag.js.map +7 -0
  593. package/dist/modules/workflows/lib/ledger-entry-display.js +66 -0
  594. package/dist/modules/workflows/lib/ledger-entry-display.js.map +7 -0
  595. package/dist/modules/workflows/lib/ledger-schema-flatten.js +82 -0
  596. package/dist/modules/workflows/lib/ledger-schema-flatten.js.map +7 -0
  597. package/dist/modules/workflows/lib/metrics/definition-metrics.js +110 -0
  598. package/dist/modules/workflows/lib/metrics/definition-metrics.js.map +7 -0
  599. package/dist/modules/workflows/lib/metrics/metrics-display.js +26 -0
  600. package/dist/modules/workflows/lib/metrics/metrics-display.js.map +7 -0
  601. package/dist/modules/workflows/lib/metrics/rollup-schedule.js +7 -0
  602. package/dist/modules/workflows/lib/metrics/rollup-schedule.js.map +7 -0
  603. package/dist/modules/workflows/lib/metrics/rollup-service.js +160 -0
  604. package/dist/modules/workflows/lib/metrics/rollup-service.js.map +7 -0
  605. package/dist/modules/workflows/lib/node-config-summary.js +128 -0
  606. package/dist/modules/workflows/lib/node-config-summary.js.map +7 -0
  607. package/dist/modules/workflows/lib/node-geometry.js +53 -0
  608. package/dist/modules/workflows/lib/node-geometry.js.map +7 -0
  609. package/dist/modules/workflows/lib/node-nudge.js +44 -0
  610. package/dist/modules/workflows/lib/node-nudge.js.map +7 -0
  611. package/dist/modules/workflows/lib/node-outcome-rows.js +88 -0
  612. package/dist/modules/workflows/lib/node-outcome-rows.js.map +7 -0
  613. package/dist/modules/workflows/lib/node-placement.js +55 -0
  614. package/dist/modules/workflows/lib/node-placement.js.map +7 -0
  615. package/dist/modules/workflows/lib/node-type-icons.js +54 -13
  616. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  617. package/dist/modules/workflows/lib/nodeFormTransforms.js +329 -19
  618. package/dist/modules/workflows/lib/nodeFormTransforms.js.map +2 -2
  619. package/dist/modules/workflows/lib/outcome-routing.js +158 -0
  620. package/dist/modules/workflows/lib/outcome-routing.js.map +7 -0
  621. package/dist/modules/workflows/lib/owned-definition.js +107 -0
  622. package/dist/modules/workflows/lib/owned-definition.js.map +7 -0
  623. package/dist/modules/workflows/lib/owned-node-changes.js +10 -0
  624. package/dist/modules/workflows/lib/owned-node-changes.js.map +7 -0
  625. package/dist/modules/workflows/lib/palette-drag.js +36 -0
  626. package/dist/modules/workflows/lib/palette-drag.js.map +7 -0
  627. package/dist/modules/workflows/lib/palette-drop.js +45 -0
  628. package/dist/modules/workflows/lib/palette-drop.js.map +7 -0
  629. package/dist/modules/workflows/lib/parallel-handler.js +57 -2
  630. package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
  631. package/dist/modules/workflows/lib/port-contract.js +54 -0
  632. package/dist/modules/workflows/lib/port-contract.js.map +7 -0
  633. package/dist/modules/workflows/lib/portal-task-access.js +89 -0
  634. package/dist/modules/workflows/lib/portal-task-access.js.map +7 -0
  635. package/dist/modules/workflows/lib/rerun-from-step.js +65 -0
  636. package/dist/modules/workflows/lib/rerun-from-step.js.map +7 -0
  637. package/dist/modules/workflows/lib/route-chip-events.js +23 -0
  638. package/dist/modules/workflows/lib/route-chip-events.js.map +7 -0
  639. package/dist/modules/workflows/lib/route-chips.js +91 -0
  640. package/dist/modules/workflows/lib/route-chips.js.map +7 -0
  641. package/dist/modules/workflows/lib/route-edge.js +17 -0
  642. package/dist/modules/workflows/lib/route-edge.js.map +7 -0
  643. package/dist/modules/workflows/lib/route-kinds.js +67 -0
  644. package/dist/modules/workflows/lib/route-kinds.js.map +7 -0
  645. package/dist/modules/workflows/lib/route-priority.js +104 -0
  646. package/dist/modules/workflows/lib/route-priority.js.map +7 -0
  647. package/dist/modules/workflows/lib/run-duration.js +44 -0
  648. package/dist/modules/workflows/lib/run-duration.js.map +7 -0
  649. package/dist/modules/workflows/lib/run-event-tone.js +25 -0
  650. package/dist/modules/workflows/lib/run-event-tone.js.map +7 -0
  651. package/dist/modules/workflows/lib/run-execution.js +305 -0
  652. package/dist/modules/workflows/lib/run-execution.js.map +7 -0
  653. package/dist/modules/workflows/lib/run-gantt.js +180 -0
  654. package/dist/modules/workflows/lib/run-gantt.js.map +7 -0
  655. package/dist/modules/workflows/lib/run-outcome-evidence.js +33 -0
  656. package/dist/modules/workflows/lib/run-outcome-evidence.js.map +7 -0
  657. package/dist/modules/workflows/lib/run-outcome.js +48 -0
  658. package/dist/modules/workflows/lib/run-outcome.js.map +7 -0
  659. package/dist/modules/workflows/lib/sample-resolver.js +71 -0
  660. package/dist/modules/workflows/lib/sample-resolver.js.map +7 -0
  661. package/dist/modules/workflows/lib/seeds.js +1 -1
  662. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  663. package/dist/modules/workflows/lib/server-output-contract.js +65 -0
  664. package/dist/modules/workflows/lib/server-output-contract.js.map +7 -0
  665. package/dist/modules/workflows/lib/set-variable.js +47 -0
  666. package/dist/modules/workflows/lib/set-variable.js.map +7 -0
  667. package/dist/modules/workflows/lib/signal-handler.js +63 -4
  668. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  669. package/dist/modules/workflows/lib/start-fixtures.js +42 -0
  670. package/dist/modules/workflows/lib/start-fixtures.js.map +7 -0
  671. package/dist/modules/workflows/lib/status-colors.js +153 -22
  672. package/dist/modules/workflows/lib/status-colors.js.map +2 -2
  673. package/dist/modules/workflows/lib/step-handler.js +529 -53
  674. package/dist/modules/workflows/lib/step-handler.js.map +3 -3
  675. package/dist/modules/workflows/lib/step-presentation.js +147 -0
  676. package/dist/modules/workflows/lib/step-presentation.js.map +7 -0
  677. package/dist/modules/workflows/lib/step-through.js +26 -0
  678. package/dist/modules/workflows/lib/step-through.js.map +7 -0
  679. package/dist/modules/workflows/lib/step-type-conversion.js +167 -0
  680. package/dist/modules/workflows/lib/step-type-conversion.js.map +7 -0
  681. package/dist/modules/workflows/lib/subgraph-clipboard.js +114 -0
  682. package/dist/modules/workflows/lib/subgraph-clipboard.js.map +7 -0
  683. package/dist/modules/workflows/lib/task-decision-context.js +38 -0
  684. package/dist/modules/workflows/lib/task-decision-context.js.map +7 -0
  685. package/dist/modules/workflows/lib/task-decisions.js +49 -0
  686. package/dist/modules/workflows/lib/task-decisions.js.map +7 -0
  687. package/dist/modules/workflows/lib/task-entity-access.js +62 -0
  688. package/dist/modules/workflows/lib/task-entity-access.js.map +7 -0
  689. package/dist/modules/workflows/lib/task-entity-aliases.js +39 -0
  690. package/dist/modules/workflows/lib/task-entity-aliases.js.map +7 -0
  691. package/dist/modules/workflows/lib/task-entity-types.js +36 -0
  692. package/dist/modules/workflows/lib/task-entity-types.js.map +7 -0
  693. package/dist/modules/workflows/lib/task-form-registry.js +33 -0
  694. package/dist/modules/workflows/lib/task-form-registry.js.map +7 -0
  695. package/dist/modules/workflows/lib/task-form-schema.js +75 -0
  696. package/dist/modules/workflows/lib/task-form-schema.js.map +7 -0
  697. package/dist/modules/workflows/lib/task-handler.js +251 -32
  698. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  699. package/dist/modules/workflows/lib/task-inspector-config.js +182 -0
  700. package/dist/modules/workflows/lib/task-inspector-config.js.map +7 -0
  701. package/dist/modules/workflows/lib/task-list-query.js +19 -0
  702. package/dist/modules/workflows/lib/task-list-query.js.map +7 -0
  703. package/dist/modules/workflows/lib/task-notifications.js +119 -0
  704. package/dist/modules/workflows/lib/task-notifications.js.map +7 -0
  705. package/dist/modules/workflows/lib/task-permission-settings.js +43 -0
  706. package/dist/modules/workflows/lib/task-permission-settings.js.map +7 -0
  707. package/dist/modules/workflows/lib/task-presets.js +35 -0
  708. package/dist/modules/workflows/lib/task-presets.js.map +7 -0
  709. package/dist/modules/workflows/lib/task-quick-action.js +45 -0
  710. package/dist/modules/workflows/lib/task-quick-action.js.map +7 -0
  711. package/dist/modules/workflows/lib/task-resolution.js +120 -0
  712. package/dist/modules/workflows/lib/task-resolution.js.map +7 -0
  713. package/dist/modules/workflows/lib/task-sla.js +357 -0
  714. package/dist/modules/workflows/lib/task-sla.js.map +7 -0
  715. package/dist/modules/workflows/lib/task-visibility-request.js +266 -0
  716. package/dist/modules/workflows/lib/task-visibility-request.js.map +7 -0
  717. package/dist/modules/workflows/lib/task-visibility.js +194 -0
  718. package/dist/modules/workflows/lib/task-visibility.js.map +7 -0
  719. package/dist/modules/workflows/lib/timer-handler.js +3 -1
  720. package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
  721. package/dist/modules/workflows/lib/transition-handler.js +148 -20
  722. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  723. package/dist/modules/workflows/lib/trigger-node.js +121 -0
  724. package/dist/modules/workflows/lib/trigger-node.js.map +7 -0
  725. package/dist/modules/workflows/lib/visual-editor-navigation.js +14 -0
  726. package/dist/modules/workflows/lib/visual-editor-navigation.js.map +7 -0
  727. package/dist/modules/workflows/lib/work-inbox/empty-state.js +52 -0
  728. package/dist/modules/workflows/lib/work-inbox/empty-state.js.map +7 -0
  729. package/dist/modules/workflows/lib/work-inbox/entity-links.js +63 -0
  730. package/dist/modules/workflows/lib/work-inbox/entity-links.js.map +7 -0
  731. package/dist/modules/workflows/lib/work-inbox/navigation.js +11 -0
  732. package/dist/modules/workflows/lib/work-inbox/navigation.js.map +7 -0
  733. package/dist/modules/workflows/lib/work-inbox/presentation.js +90 -0
  734. package/dist/modules/workflows/lib/work-inbox/presentation.js.map +7 -0
  735. package/dist/modules/workflows/lib/work-inbox/provider.js +118 -0
  736. package/dist/modules/workflows/lib/work-inbox/provider.js.map +7 -0
  737. package/dist/modules/workflows/lib/work-inbox/query.js +70 -0
  738. package/dist/modules/workflows/lib/work-inbox/query.js.map +7 -0
  739. package/dist/modules/workflows/lib/work-inbox/service.js +56 -0
  740. package/dist/modules/workflows/lib/work-inbox/service.js.map +7 -0
  741. package/dist/modules/workflows/lib/work-inbox/user-task-source.js +149 -0
  742. package/dist/modules/workflows/lib/work-inbox/user-task-source.js.map +7 -0
  743. package/dist/modules/workflows/lib/workflow-command-enablement.js +57 -0
  744. package/dist/modules/workflows/lib/workflow-command-enablement.js.map +7 -0
  745. package/dist/modules/workflows/lib/workflow-command-settings.js +54 -0
  746. package/dist/modules/workflows/lib/workflow-command-settings.js.map +7 -0
  747. package/dist/modules/workflows/lib/workflow-executor.js +658 -6
  748. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  749. package/dist/modules/workflows/lib/workflow-function-registry.js +40 -0
  750. package/dist/modules/workflows/lib/workflow-function-registry.js.map +7 -0
  751. package/dist/modules/workflows/lib/workflow-safe-commands.js +11 -1
  752. package/dist/modules/workflows/lib/workflow-safe-commands.js.map +2 -2
  753. package/dist/modules/workflows/lib/workflow-templates.js +69 -0
  754. package/dist/modules/workflows/lib/workflow-templates.js.map +7 -0
  755. package/dist/modules/workflows/migrations/Migration20260626214855_workflows.js +15 -0
  756. package/dist/modules/workflows/migrations/Migration20260626214855_workflows.js.map +7 -0
  757. package/dist/modules/workflows/migrations/Migration20260626222424_workflows.js +17 -0
  758. package/dist/modules/workflows/migrations/Migration20260626222424_workflows.js.map +7 -0
  759. package/dist/modules/workflows/migrations/Migration20260727074335_workflows.js +15 -0
  760. package/dist/modules/workflows/migrations/Migration20260727074335_workflows.js.map +7 -0
  761. package/dist/modules/workflows/migrations/Migration20260728104038_workflows.js +13 -0
  762. package/dist/modules/workflows/migrations/Migration20260728104038_workflows.js.map +7 -0
  763. package/dist/modules/workflows/migrations/Migration20260728163001_workflows.js +15 -0
  764. package/dist/modules/workflows/migrations/Migration20260728163001_workflows.js.map +7 -0
  765. package/dist/modules/workflows/migrations/Migration20260729061736_workflows.js +13 -0
  766. package/dist/modules/workflows/migrations/Migration20260729061736_workflows.js.map +7 -0
  767. package/dist/modules/workflows/migrations/Migration20260729192652_workflows.js +12 -0
  768. package/dist/modules/workflows/migrations/Migration20260729192652_workflows.js.map +7 -0
  769. package/dist/modules/workflows/migrations/Migration20260730085000_workflows.js +13 -0
  770. package/dist/modules/workflows/migrations/Migration20260730085000_workflows.js.map +7 -0
  771. package/dist/modules/workflows/migrations/Migration20260730092706_workflows.js +13 -0
  772. package/dist/modules/workflows/migrations/Migration20260730092706_workflows.js.map +7 -0
  773. package/dist/modules/workflows/notifications.js +38 -10
  774. package/dist/modules/workflows/notifications.js.map +2 -2
  775. package/dist/modules/workflows/setup.js +81 -1
  776. package/dist/modules/workflows/setup.js.map +2 -2
  777. package/dist/modules/workflows/subscribers/task-assigned-notification.js +19 -27
  778. package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +2 -2
  779. package/dist/modules/workflows/subscribers/task-deadline-breached-notification.js +24 -0
  780. package/dist/modules/workflows/subscribers/task-deadline-breached-notification.js.map +7 -0
  781. package/dist/modules/workflows/subscribers/task-reminder-notification.js +24 -0
  782. package/dist/modules/workflows/subscribers/task-reminder-notification.js.map +7 -0
  783. package/dist/modules/workflows/widgets/injection/pending-work/widget.client.js +73 -0
  784. package/dist/modules/workflows/widgets/injection/pending-work/widget.client.js.map +7 -0
  785. package/dist/modules/workflows/widgets/injection/pending-work/widget.js +17 -0
  786. package/dist/modules/workflows/widgets/injection/pending-work/widget.js.map +7 -0
  787. package/dist/modules/workflows/widgets/injection-table.js +26 -0
  788. package/dist/modules/workflows/widgets/injection-table.js.map +2 -2
  789. package/dist/modules/workflows/workers/workflow-activities.worker.js +61 -39
  790. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  791. package/dist/modules/workflows/workers/workflow-definition-metric-rollup.worker.js +40 -0
  792. package/dist/modules/workflows/workers/workflow-definition-metric-rollup.worker.js.map +7 -0
  793. package/dist/modules/workflows/workers/workflow-instance-bulk-replay.js +43 -0
  794. package/dist/modules/workflows/workers/workflow-instance-bulk-replay.js.map +7 -0
  795. package/dist/modules/workflows/workers/workflow-invoke-agent.worker.js +30 -0
  796. package/dist/modules/workflows/workers/workflow-invoke-agent.worker.js.map +7 -0
  797. package/generated/entities/action_log/index.ts +1 -0
  798. package/generated/entities/user/index.ts +1 -0
  799. package/generated/entities/user_task/index.ts +7 -0
  800. package/generated/entities/workflow_definition/index.ts +3 -0
  801. package/generated/entities/workflow_definition_draft/index.ts +11 -0
  802. package/generated/entities/workflow_definition_metric_rollup/index.ts +10 -0
  803. package/generated/entities/workflow_instance/index.ts +2 -0
  804. package/generated/entities.ids.generated.ts +4 -1
  805. package/generated/entity-fields-registry.ts +39 -0
  806. package/jest.setup.ts +13 -0
  807. package/package.json +8 -7
  808. package/src/bootstrap.ts +13 -3
  809. package/src/helpers/integration/workflowsFixtures.ts +765 -5
  810. package/src/helpers/integration/workflowsUi.ts +318 -0
  811. package/src/modules/attachments/lib/createFromBuffer.ts +120 -0
  812. package/src/modules/audit_logs/data/entities.ts +12 -0
  813. package/src/modules/audit_logs/data/validators.ts +1 -0
  814. package/src/modules/audit_logs/lib/projections.ts +5 -1
  815. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +26 -0
  816. package/src/modules/audit_logs/migrations/Migration20260625120000.ts +15 -0
  817. package/src/modules/audit_logs/services/actionLogService.ts +3 -0
  818. package/src/modules/auth/data/entities.ts +21 -0
  819. package/src/modules/auth/i18n/de.json +31 -0
  820. package/src/modules/auth/i18n/en.json +31 -0
  821. package/src/modules/auth/i18n/es.json +31 -0
  822. package/src/modules/auth/i18n/ko.json +31 -0
  823. package/src/modules/auth/i18n/pl.json +31 -0
  824. package/src/modules/auth/lib/executionPrincipal.ts +235 -0
  825. package/src/modules/auth/migrations/.snapshot-open-mercato.json +16 -0
  826. package/src/modules/auth/migrations/Migration20260625050000.ts +21 -0
  827. package/src/modules/business_rules/components/InlineRuleEditor.tsx +231 -0
  828. package/src/modules/business_rules/data/validators.ts +6 -0
  829. package/src/modules/business_rules/i18n/de.json +4 -0
  830. package/src/modules/business_rules/i18n/en.json +4 -0
  831. package/src/modules/business_rules/i18n/es.json +4 -0
  832. package/src/modules/business_rules/i18n/ko.json +4 -0
  833. package/src/modules/business_rules/i18n/pl.json +4 -0
  834. package/src/modules/business_rules/lib/rule-engine.ts +15 -1
  835. package/src/modules/catalog/commands/products.ts +12 -0
  836. package/src/modules/catalog/i18n/de.json +1 -0
  837. package/src/modules/catalog/i18n/en.json +1 -0
  838. package/src/modules/catalog/i18n/es.json +1 -0
  839. package/src/modules/catalog/i18n/ko.json +1 -0
  840. package/src/modules/catalog/i18n/pl.json +1 -0
  841. package/src/modules/catalog/workflows.ts +35 -0
  842. package/src/modules/customer_accounts/AGENTS.md +11 -0
  843. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +2 -0
  844. package/src/modules/customer_accounts/cli.ts +96 -0
  845. package/src/modules/customer_accounts/i18n/de.json +5 -0
  846. package/src/modules/customer_accounts/i18n/en.json +5 -0
  847. package/src/modules/customer_accounts/i18n/es.json +5 -0
  848. package/src/modules/customer_accounts/i18n/ko.json +5 -0
  849. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  850. package/src/modules/customer_accounts/lib/customerRoleAclSync.ts +92 -0
  851. package/src/modules/customer_accounts/setup.ts +15 -3
  852. package/src/modules/customers/commands/deals.ts +5 -0
  853. package/src/modules/customers/components/detail/ChangelogEntryRow.tsx +4 -2
  854. package/src/modules/customers/components/detail/utils.ts +2 -0
  855. package/src/modules/customers/events.ts +21 -3
  856. package/src/modules/customers/i18n/de.json +4 -0
  857. package/src/modules/customers/i18n/en.json +4 -0
  858. package/src/modules/customers/i18n/es.json +4 -0
  859. package/src/modules/customers/i18n/ko.json +4 -0
  860. package/src/modules/customers/i18n/pl.json +4 -0
  861. package/src/modules/customers/lib/link-workflow-task-handler.ts +136 -0
  862. package/src/modules/customers/lib/todoCompatibility.ts +4 -1
  863. package/src/modules/customers/lib/workflowTaskLink.ts +23 -0
  864. package/src/modules/customers/subscribers/link-workflow-task.ts +19 -0
  865. package/src/modules/customers/workflows.ts +57 -0
  866. package/src/modules/entities/api/records.ts +18 -62
  867. package/src/modules/entities/lib/entityClassification.ts +102 -0
  868. package/src/modules/sales/commands/documents.ts +46 -0
  869. package/src/modules/sales/events.ts +20 -2
  870. package/src/modules/sales/i18n/de.json +2 -1
  871. package/src/modules/sales/i18n/en.json +2 -1
  872. package/src/modules/sales/i18n/es.json +2 -1
  873. package/src/modules/sales/i18n/ko.json +2 -1
  874. package/src/modules/sales/i18n/pl.json +2 -1
  875. package/src/modules/sales/workflows.ts +12 -1
  876. package/src/modules/seeds/README.md +86 -0
  877. package/src/modules/seeds/cli.ts +220 -0
  878. package/src/modules/seeds/examples/sample-seed.json +26 -0
  879. package/src/modules/seeds/index.ts +11 -0
  880. package/src/modules/workflows/AGENTS.md +851 -10
  881. package/src/modules/workflows/acl.ts +96 -0
  882. package/src/modules/workflows/ai-agents.ts +80 -0
  883. package/src/modules/workflows/ai-tools/authoring-pack.ts +798 -0
  884. package/src/modules/workflows/ai-tools/types.ts +117 -0
  885. package/src/modules/workflows/ai-tools.ts +20 -0
  886. package/src/modules/workflows/api/command-settings/route.ts +171 -0
  887. package/src/modules/workflows/api/commands/route.ts +109 -0
  888. package/src/modules/workflows/api/definitions/[id]/callers/route.ts +95 -0
  889. package/src/modules/workflows/api/definitions/[id]/context-schema/route.ts +187 -0
  890. package/src/modules/workflows/api/definitions/[id]/customize/route.ts +6 -1
  891. package/src/modules/workflows/api/definitions/[id]/draft/route.ts +395 -0
  892. package/src/modules/workflows/api/definitions/[id]/publish/route.ts +250 -0
  893. package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +5 -0
  894. package/src/modules/workflows/api/definitions/[id]/route.ts +107 -7
  895. package/src/modules/workflows/api/definitions/[id]/test-step/route.ts +288 -0
  896. package/src/modules/workflows/api/definitions/generate/route.ts +335 -0
  897. package/src/modules/workflows/api/definitions/route.ts +88 -113
  898. package/src/modules/workflows/api/definitions/serialize.ts +9 -0
  899. package/src/modules/workflows/api/endpoints/route.ts +88 -0
  900. package/src/modules/workflows/api/functions/route.ts +91 -0
  901. package/src/modules/workflows/api/grantable-features/route.ts +102 -0
  902. package/src/modules/workflows/api/instances/[id]/context/route.ts +214 -0
  903. package/src/modules/workflows/api/instances/[id]/rerun-step/route.ts +226 -0
  904. package/src/modules/workflows/api/instances/[id]/retry/route.ts +18 -2
  905. package/src/modules/workflows/api/instances/[id]/step-through/route.ts +195 -0
  906. package/src/modules/workflows/api/instances/[id]/steps/route.ts +160 -0
  907. package/src/modules/workflows/api/instances/[id]/would-do/route.ts +216 -0
  908. package/src/modules/workflows/api/instances/bulk-replay/route.ts +141 -0
  909. package/src/modules/workflows/api/instances/failure-queue/route.ts +197 -0
  910. package/src/modules/workflows/api/instances/route.ts +135 -3
  911. package/src/modules/workflows/api/metrics/definitions/route.ts +262 -0
  912. package/src/modules/workflows/api/openapi.ts +549 -2
  913. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +160 -0
  914. package/src/modules/workflows/api/portal/tasks/[id]/route.ts +124 -0
  915. package/src/modules/workflows/api/portal/tasks/route.ts +157 -0
  916. package/src/modules/workflows/api/rule-usage/route.ts +94 -0
  917. package/src/modules/workflows/api/task-settings/route.ts +138 -0
  918. package/src/modules/workflows/api/tasks/[id]/claim/route.ts +42 -17
  919. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +47 -15
  920. package/src/modules/workflows/api/tasks/[id]/reassign/route.ts +197 -0
  921. package/src/modules/workflows/api/tasks/[id]/route.ts +57 -6
  922. package/src/modules/workflows/api/tasks/[id]/unclaim/route.ts +132 -0
  923. package/src/modules/workflows/api/tasks/route.ts +60 -5
  924. package/src/modules/workflows/api/tasks/serialize.ts +119 -0
  925. package/src/modules/workflows/api/templates/route.ts +87 -0
  926. package/src/modules/workflows/api/work-inbox/next/route.ts +170 -0
  927. package/src/modules/workflows/api/work-inbox/route.ts +153 -0
  928. package/src/modules/workflows/backend/config/workflows/page.meta.ts +33 -0
  929. package/src/modules/workflows/backend/config/workflows/page.tsx +12 -0
  930. package/src/modules/workflows/backend/definitions/[id]/page.tsx +19 -343
  931. package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
  932. package/src/modules/workflows/backend/definitions/create/page.tsx +19 -59
  933. package/src/modules/workflows/backend/definitions/page.tsx +156 -76
  934. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +3129 -584
  935. package/src/modules/workflows/backend/instances/[id]/page.meta.ts +1 -1
  936. package/src/modules/workflows/backend/instances/[id]/page.tsx +711 -539
  937. package/src/modules/workflows/backend/instances/failure-queue/page.meta.ts +16 -0
  938. package/src/modules/workflows/backend/instances/failure-queue/page.tsx +311 -0
  939. package/src/modules/workflows/backend/instances/page.meta.ts +1 -1
  940. package/src/modules/workflows/backend/instances/page.tsx +218 -38
  941. package/src/modules/workflows/backend/tasks/[id]/page.tsx +639 -400
  942. package/src/modules/workflows/backend/tasks/page.meta.ts +7 -0
  943. package/src/modules/workflows/backend/tasks/page.tsx +20 -333
  944. package/src/modules/workflows/backend/work-inbox/page.meta.ts +15 -0
  945. package/src/modules/workflows/backend/work-inbox/page.tsx +506 -0
  946. package/src/modules/workflows/cli.ts +1 -1
  947. package/src/modules/workflows/commands/tasks.ts +146 -0
  948. package/src/modules/workflows/components/ActivitiesEditor.tsx +91 -114
  949. package/src/modules/workflows/components/ActivityEditDialog.tsx +244 -0
  950. package/src/modules/workflows/components/AgentSelector.tsx +200 -0
  951. package/src/modules/workflows/components/AnnotationEditDialog.tsx +157 -0
  952. package/src/modules/workflows/components/BusinessRuleUsagePanel.tsx +106 -0
  953. package/src/modules/workflows/components/BusinessRulesSelector.tsx +71 -17
  954. package/src/modules/workflows/components/ConfigJsonTextarea.tsx +111 -0
  955. package/src/modules/workflows/components/ContextSchemaEditor.tsx +165 -0
  956. package/src/modules/workflows/components/DefinitionErrorHandlerField.tsx +133 -0
  957. package/src/modules/workflows/components/DefinitionMetadataDrawer.tsx +456 -0
  958. package/src/modules/workflows/components/DefinitionMetricsCells.tsx +212 -0
  959. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +196 -95
  960. package/src/modules/workflows/components/EdgeEditDialog.tsx +15 -16
  961. package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +130 -80
  962. package/src/modules/workflows/components/InputDataPanel.tsx +177 -0
  963. package/src/modules/workflows/components/InspectorPanel.tsx +250 -0
  964. package/src/modules/workflows/components/NodeEditDialog.tsx +444 -38
  965. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +848 -178
  966. package/src/modules/workflows/components/StepsEditor.tsx +9 -2
  967. package/src/modules/workflows/components/TemplateGalleryDialog.tsx +205 -0
  968. package/src/modules/workflows/components/TransitionsEditor.tsx +98 -89
  969. package/src/modules/workflows/components/TriggersDialog.tsx +83 -0
  970. package/src/modules/workflows/components/TriggersEditor.tsx +363 -0
  971. package/src/modules/workflows/components/WorkflowAiDraftDialog.tsx +383 -0
  972. package/src/modules/workflows/components/WorkflowCodeView.tsx +257 -0
  973. package/src/modules/workflows/components/WorkflowCommandPalette.tsx +82 -0
  974. package/src/modules/workflows/components/WorkflowCompensationGhostEdge.tsx +78 -0
  975. package/src/modules/workflows/components/WorkflowDataMappingEdge.tsx +46 -0
  976. package/src/modules/workflows/components/WorkflowGraph.tsx +54 -1
  977. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +463 -37
  978. package/src/modules/workflows/components/WorkflowIconPicker.tsx +144 -0
  979. package/src/modules/workflows/components/WorkflowLegend.tsx +59 -33
  980. package/src/modules/workflows/components/WorkflowNodeCard.tsx +355 -28
  981. package/src/modules/workflows/components/WorkflowRouteChips.tsx +145 -0
  982. package/src/modules/workflows/components/WorkflowSelector.tsx +33 -14
  983. package/src/modules/workflows/components/WorkflowTransitionEdge.tsx +83 -10
  984. package/src/modules/workflows/components/WorkflowTransitionLabel.tsx +13 -2
  985. package/src/modules/workflows/components/fields/ActivityArrayEditor.tsx +120 -30
  986. package/src/modules/workflows/components/fields/ActivityConfigFields.tsx +675 -0
  987. package/src/modules/workflows/components/fields/ActivityTestPanel.tsx +250 -0
  988. package/src/modules/workflows/components/fields/AgentInvokeConfigField.tsx +580 -0
  989. package/src/modules/workflows/components/fields/AgentReviewOnBreachField.tsx +88 -0
  990. package/src/modules/workflows/components/fields/BranchingRoutesEditor.tsx +323 -0
  991. package/src/modules/workflows/components/fields/BusinessRuleConditionsEditor.tsx +36 -4
  992. package/src/modules/workflows/components/fields/CommandPicker.tsx +158 -0
  993. package/src/modules/workflows/components/fields/EndpointPicker.tsx +330 -0
  994. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -2
  995. package/src/modules/workflows/components/fields/FunctionPicker.tsx +68 -0
  996. package/src/modules/workflows/components/fields/MappingArrayEditor.tsx +46 -12
  997. package/src/modules/workflows/components/fields/PortFieldArrayEditor.tsx +255 -0
  998. package/src/modules/workflows/components/fields/RolesMultiSelect.tsx +68 -0
  999. package/src/modules/workflows/components/fields/RouteOrderEditor.tsx +169 -0
  1000. package/src/modules/workflows/components/fields/SchemaBuilderDialog.tsx +139 -0
  1001. package/src/modules/workflows/components/fields/StartPreConditionsEditor.tsx +2 -2
  1002. package/src/modules/workflows/components/fields/TaskAssignmentField.tsx +355 -0
  1003. package/src/modules/workflows/components/fields/TaskDeadlineFields.tsx +172 -0
  1004. package/src/modules/workflows/components/fields/TaskDecisionsField.tsx +294 -0
  1005. package/src/modules/workflows/components/fields/TaskEntityBindingsField.tsx +156 -0
  1006. package/src/modules/workflows/components/fields/TemplateTextControl.tsx +85 -0
  1007. package/src/modules/workflows/components/fields/VariablePickerButton.tsx +177 -0
  1008. package/src/modules/workflows/components/fields/ledgerDropTarget.ts +55 -0
  1009. package/src/modules/workflows/components/fields/useActivityTypeOptions.ts +19 -0
  1010. package/src/modules/workflows/components/formConfig.tsx +84 -10
  1011. package/src/modules/workflows/components/mobile/MobileDefinitionDetail.tsx +5 -0
  1012. package/src/modules/workflows/components/mobile/MobileMetadataSheet.tsx +10 -0
  1013. package/src/modules/workflows/components/mobile/MobileTaskForm.tsx +55 -19
  1014. package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +49 -40
  1015. package/src/modules/workflows/components/nodes/AnnotationGroupNode.tsx +81 -0
  1016. package/src/modules/workflows/components/nodes/AutomatedNode.tsx +34 -17
  1017. package/src/modules/workflows/components/nodes/EndNode.tsx +15 -14
  1018. package/src/modules/workflows/components/nodes/ErrorOutputHandle.tsx +32 -0
  1019. package/src/modules/workflows/components/nodes/IfElseNode.tsx +64 -0
  1020. package/src/modules/workflows/components/nodes/InvokeAgentNode.tsx +180 -0
  1021. package/src/modules/workflows/components/nodes/NodeOutcomeRows.tsx +143 -0
  1022. package/src/modules/workflows/components/nodes/ParallelForkNode.tsx +28 -15
  1023. package/src/modules/workflows/components/nodes/ParallelJoinNode.tsx +28 -15
  1024. package/src/modules/workflows/components/nodes/StartNode.tsx +78 -18
  1025. package/src/modules/workflows/components/nodes/StickyNoteNode.tsx +65 -0
  1026. package/src/modules/workflows/components/nodes/SubWorkflowNode.tsx +124 -23
  1027. package/src/modules/workflows/components/nodes/SwitchNode.tsx +64 -0
  1028. package/src/modules/workflows/components/nodes/TriggerCap.tsx +103 -0
  1029. package/src/modules/workflows/components/nodes/TriggerNode.tsx +151 -0
  1030. package/src/modules/workflows/components/nodes/UserTaskNode.tsx +67 -22
  1031. package/src/modules/workflows/components/nodes/WaitForConditionNode.tsx +74 -0
  1032. package/src/modules/workflows/components/nodes/WaitForSignalNode.tsx +31 -17
  1033. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +31 -16
  1034. package/src/modules/workflows/components/nodes/index.ts +17 -0
  1035. package/src/modules/workflows/components/portal/PortalTaskForm.tsx +280 -0
  1036. package/src/modules/workflows/components/run/AgentActivityPanel.tsx +48 -0
  1037. package/src/modules/workflows/components/run/RerunStepDialog.tsx +133 -0
  1038. package/src/modules/workflows/components/run/RunEventBadge.tsx +37 -0
  1039. package/src/modules/workflows/components/run/RunGantt.tsx +180 -0
  1040. package/src/modules/workflows/components/run/RunStatusBadge.tsx +112 -0
  1041. package/src/modules/workflows/components/run/RunStepInspector.tsx +176 -0
  1042. package/src/modules/workflows/components/run/WouldDoReport.tsx +93 -0
  1043. package/src/modules/workflows/components/run/useAgentLabels.ts +42 -0
  1044. package/src/modules/workflows/components/run/useLastRunOverlay.ts +150 -0
  1045. package/src/modules/workflows/components/run/useLiveAgentActions.ts +70 -0
  1046. package/src/modules/workflows/components/run/useLiveRunUpdates.ts +89 -0
  1047. package/src/modules/workflows/components/settings/CommandSettingsPageClient.tsx +228 -0
  1048. package/src/modules/workflows/components/work-inbox/EntityContextPanel.tsx +179 -0
  1049. package/src/modules/workflows/components/work-inbox/ReassignTaskDialog.tsx +203 -0
  1050. package/src/modules/workflows/components/work-inbox/StatusChip.tsx +61 -0
  1051. package/src/modules/workflows/components/work-inbox/TaskActionNotice.tsx +77 -0
  1052. package/src/modules/workflows/components/work-inbox/TaskFormFields.tsx +322 -0
  1053. package/src/modules/workflows/components/work-inbox/WorkInboxEmptyState.tsx +43 -0
  1054. package/src/modules/workflows/data/activity-config-schemas.ts +249 -0
  1055. package/src/modules/workflows/data/activity-config-warnings.ts +71 -0
  1056. package/src/modules/workflows/data/branching-route-warnings.ts +129 -0
  1057. package/src/modules/workflows/data/entities.ts +397 -4
  1058. package/src/modules/workflows/data/metrics-validators.ts +51 -0
  1059. package/src/modules/workflows/data/task-primitives.ts +36 -0
  1060. package/src/modules/workflows/data/types.ts +81 -2
  1061. package/src/modules/workflows/data/validators.ts +729 -60
  1062. package/src/modules/workflows/di.ts +22 -0
  1063. package/src/modules/workflows/events.ts +68 -8
  1064. package/src/modules/workflows/examples/oze-residential-install-60-nodes.json +1545 -0
  1065. package/src/modules/workflows/examples/sales-pipeline-definition.json +35 -35
  1066. package/src/modules/workflows/examples/templates/lead-to-install.json +160 -0
  1067. package/src/modules/workflows/examples/templates/order-approval.json +230 -0
  1068. package/src/modules/workflows/examples/templates/task-escalation.json +110 -0
  1069. package/src/modules/workflows/examples/templates/webhook-integration.json +113 -0
  1070. package/src/modules/workflows/extension-points.ts +1 -1
  1071. package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.ts +21 -0
  1072. package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +212 -0
  1073. package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.ts +25 -0
  1074. package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/page.tsx +185 -0
  1075. package/src/modules/workflows/i18n/de.json +1083 -24
  1076. package/src/modules/workflows/i18n/en.json +1081 -22
  1077. package/src/modules/workflows/i18n/es.json +1083 -24
  1078. package/src/modules/workflows/i18n/ko.json +1072 -13
  1079. package/src/modules/workflows/i18n/pl.json +1082 -23
  1080. package/src/modules/workflows/lib/activity-executor.ts +855 -186
  1081. package/src/modules/workflows/lib/activity-queue-types.ts +120 -1
  1082. package/src/modules/workflows/lib/activity-registry-bootstrap.ts +7 -0
  1083. package/src/modules/workflows/lib/activity-registry.ts +89 -0
  1084. package/src/modules/workflows/lib/activity-types.ts +415 -0
  1085. package/src/modules/workflows/lib/activity-worker-handler.ts +744 -35
  1086. package/src/modules/workflows/lib/advanced-config.ts +41 -0
  1087. package/src/modules/workflows/lib/agent-disposition-task.ts +343 -0
  1088. package/src/modules/workflows/lib/agent-outcome-paths.ts +153 -0
  1089. package/src/modules/workflows/lib/agent-result-mapping.ts +88 -0
  1090. package/src/modules/workflows/lib/agent-review-inspector.ts +125 -0
  1091. package/src/modules/workflows/lib/agent-review.ts +171 -0
  1092. package/src/modules/workflows/lib/ai-authoring.ts +402 -0
  1093. package/src/modules/workflows/lib/ai-draft-runner.ts +105 -0
  1094. package/src/modules/workflows/lib/annotation-events.ts +15 -0
  1095. package/src/modules/workflows/lib/branching-routes.ts +280 -0
  1096. package/src/modules/workflows/lib/breach-routing.ts +142 -0
  1097. package/src/modules/workflows/lib/bulk-replay.ts +185 -0
  1098. package/src/modules/workflows/lib/business-rule-usage.ts +154 -0
  1099. package/src/modules/workflows/lib/caller-graph.ts +104 -0
  1100. package/src/modules/workflows/lib/code-view-apply.ts +178 -0
  1101. package/src/modules/workflows/lib/collect-validation-issues.ts +239 -0
  1102. package/src/modules/workflows/lib/compensation-ghosts.ts +134 -0
  1103. package/src/modules/workflows/lib/component-guard.ts +13 -0
  1104. package/src/modules/workflows/lib/condition-handler.ts +658 -0
  1105. package/src/modules/workflows/lib/context-ledger.ts +814 -0
  1106. package/src/modules/workflows/lib/crud-form-dialogs-flag.ts +5 -0
  1107. package/src/modules/workflows/lib/data-edge-mapping.ts +112 -0
  1108. package/src/modules/workflows/lib/definition-context-schema.ts +103 -0
  1109. package/src/modules/workflows/lib/definition-edit-safety.ts +228 -0
  1110. package/src/modules/workflows/lib/definition-error-body.ts +43 -0
  1111. package/src/modules/workflows/lib/definition-evaluation.ts +156 -0
  1112. package/src/modules/workflows/lib/definition-grant.ts +211 -0
  1113. package/src/modules/workflows/lib/definition-json-locations.ts +226 -0
  1114. package/src/modules/workflows/lib/definition-payload.ts +81 -0
  1115. package/src/modules/workflows/lib/draft-restore.ts +72 -0
  1116. package/src/modules/workflows/lib/dry-run.ts +203 -0
  1117. package/src/modules/workflows/lib/duration.ts +138 -24
  1118. package/src/modules/workflows/lib/edge-reattachment.ts +186 -0
  1119. package/src/modules/workflows/lib/edgeFormTransforms.ts +15 -14
  1120. package/src/modules/workflows/lib/editor-annotations.ts +301 -0
  1121. package/src/modules/workflows/lib/editor-commands.ts +205 -0
  1122. package/src/modules/workflows/lib/editor-history.ts +151 -0
  1123. package/src/modules/workflows/lib/endpoint-catalog.ts +283 -0
  1124. package/src/modules/workflows/lib/endpoint-path.ts +134 -0
  1125. package/src/modules/workflows/lib/engine-version.ts +67 -0
  1126. package/src/modules/workflows/lib/error-handler.ts +192 -0
  1127. package/src/modules/workflows/lib/error-routing.ts +236 -0
  1128. package/src/modules/workflows/lib/event-logger.ts +27 -0
  1129. package/src/modules/workflows/lib/expression-refs.ts +291 -0
  1130. package/src/modules/workflows/lib/failure-grouping.ts +134 -0
  1131. package/src/modules/workflows/lib/find-definition.ts +4 -0
  1132. package/src/modules/workflows/lib/flow-logic-warnings.ts +591 -0
  1133. package/src/modules/workflows/lib/graph-utils.ts +620 -110
  1134. package/src/modules/workflows/lib/icon-search.ts +65 -0
  1135. package/src/modules/workflows/lib/insert-at-cursor.ts +53 -0
  1136. package/src/modules/workflows/lib/instance-date-filter.ts +56 -0
  1137. package/src/modules/workflows/lib/interpolation-pipeline.ts +351 -0
  1138. package/src/modules/workflows/lib/ledger-drag.ts +61 -0
  1139. package/src/modules/workflows/lib/ledger-entry-display.ts +89 -0
  1140. package/src/modules/workflows/lib/ledger-schema-flatten.ts +137 -0
  1141. package/src/modules/workflows/lib/metrics/definition-metrics.ts +268 -0
  1142. package/src/modules/workflows/lib/metrics/metrics-display.ts +57 -0
  1143. package/src/modules/workflows/lib/metrics/rollup-schedule.ts +26 -0
  1144. package/src/modules/workflows/lib/metrics/rollup-service.ts +292 -0
  1145. package/src/modules/workflows/lib/node-config-summary.ts +235 -0
  1146. package/src/modules/workflows/lib/node-geometry.ts +168 -0
  1147. package/src/modules/workflows/lib/node-nudge.ts +63 -0
  1148. package/src/modules/workflows/lib/node-outcome-rows.ts +195 -0
  1149. package/src/modules/workflows/lib/node-placement.ts +104 -0
  1150. package/src/modules/workflows/lib/node-type-icons.ts +71 -10
  1151. package/src/modules/workflows/lib/nodeFormTransforms.ts +519 -28
  1152. package/src/modules/workflows/lib/outcome-routing.ts +346 -0
  1153. package/src/modules/workflows/lib/owned-definition.ts +188 -0
  1154. package/src/modules/workflows/lib/owned-node-changes.ts +29 -0
  1155. package/src/modules/workflows/lib/palette-drag.ts +68 -0
  1156. package/src/modules/workflows/lib/palette-drop.ts +114 -0
  1157. package/src/modules/workflows/lib/parallel-handler.ts +79 -2
  1158. package/src/modules/workflows/lib/port-contract.ts +107 -0
  1159. package/src/modules/workflows/lib/portal-task-access.ts +220 -0
  1160. package/src/modules/workflows/lib/rerun-from-step.ts +140 -0
  1161. package/src/modules/workflows/lib/route-chip-events.ts +45 -0
  1162. package/src/modules/workflows/lib/route-chips.ts +177 -0
  1163. package/src/modules/workflows/lib/route-edge.ts +57 -0
  1164. package/src/modules/workflows/lib/route-kinds.ts +166 -0
  1165. package/src/modules/workflows/lib/route-priority.ts +197 -0
  1166. package/src/modules/workflows/lib/run-duration.ts +70 -0
  1167. package/src/modules/workflows/lib/run-event-tone.ts +46 -0
  1168. package/src/modules/workflows/lib/run-execution.ts +598 -0
  1169. package/src/modules/workflows/lib/run-gantt.ts +311 -0
  1170. package/src/modules/workflows/lib/run-outcome-evidence.ts +60 -0
  1171. package/src/modules/workflows/lib/run-outcome.ts +142 -0
  1172. package/src/modules/workflows/lib/sample-resolver.ts +113 -0
  1173. package/src/modules/workflows/lib/seeds.ts +3 -1
  1174. package/src/modules/workflows/lib/server-output-contract.ts +120 -0
  1175. package/src/modules/workflows/lib/set-variable.ts +94 -0
  1176. package/src/modules/workflows/lib/signal-handler.ts +134 -4
  1177. package/src/modules/workflows/lib/start-fixtures.ts +93 -0
  1178. package/src/modules/workflows/lib/status-colors.ts +220 -21
  1179. package/src/modules/workflows/lib/step-handler.ts +744 -68
  1180. package/src/modules/workflows/lib/step-presentation.ts +295 -0
  1181. package/src/modules/workflows/lib/step-through.ts +71 -0
  1182. package/src/modules/workflows/lib/step-type-conversion.ts +263 -0
  1183. package/src/modules/workflows/lib/subgraph-clipboard.ts +214 -0
  1184. package/src/modules/workflows/lib/task-decision-context.ts +76 -0
  1185. package/src/modules/workflows/lib/task-decisions.ts +117 -0
  1186. package/src/modules/workflows/lib/task-entity-access.ts +181 -0
  1187. package/src/modules/workflows/lib/task-entity-aliases.ts +77 -0
  1188. package/src/modules/workflows/lib/task-entity-types.ts +66 -0
  1189. package/src/modules/workflows/lib/task-form-registry.ts +123 -0
  1190. package/src/modules/workflows/lib/task-form-schema.ts +129 -0
  1191. package/src/modules/workflows/lib/task-handler.ts +492 -51
  1192. package/src/modules/workflows/lib/task-inspector-config.ts +348 -0
  1193. package/src/modules/workflows/lib/task-list-query.ts +46 -0
  1194. package/src/modules/workflows/lib/task-notifications.ts +218 -0
  1195. package/src/modules/workflows/lib/task-permission-settings.ts +117 -0
  1196. package/src/modules/workflows/lib/task-presets.ts +95 -0
  1197. package/src/modules/workflows/lib/task-quick-action.ts +111 -0
  1198. package/src/modules/workflows/lib/task-resolution.ts +270 -0
  1199. package/src/modules/workflows/lib/task-sla.ts +628 -0
  1200. package/src/modules/workflows/lib/task-visibility-request.ts +668 -0
  1201. package/src/modules/workflows/lib/task-visibility.ts +627 -0
  1202. package/src/modules/workflows/lib/timer-handler.ts +8 -1
  1203. package/src/modules/workflows/lib/transition-handler.ts +228 -20
  1204. package/src/modules/workflows/lib/trigger-node.ts +272 -0
  1205. package/src/modules/workflows/lib/visual-editor-navigation.ts +29 -0
  1206. package/src/modules/workflows/lib/work-inbox/empty-state.ts +150 -0
  1207. package/src/modules/workflows/lib/work-inbox/entity-links.ts +99 -0
  1208. package/src/modules/workflows/lib/work-inbox/navigation.ts +28 -0
  1209. package/src/modules/workflows/lib/work-inbox/presentation.ts +163 -0
  1210. package/src/modules/workflows/lib/work-inbox/provider.ts +390 -0
  1211. package/src/modules/workflows/lib/work-inbox/query.ts +103 -0
  1212. package/src/modules/workflows/lib/work-inbox/service.ts +122 -0
  1213. package/src/modules/workflows/lib/work-inbox/user-task-source.ts +240 -0
  1214. package/src/modules/workflows/lib/workflow-command-enablement.ts +188 -0
  1215. package/src/modules/workflows/lib/workflow-command-settings.ts +124 -0
  1216. package/src/modules/workflows/lib/workflow-executor.ts +1036 -10
  1217. package/src/modules/workflows/lib/workflow-function-registry.ts +44 -0
  1218. package/src/modules/workflows/lib/workflow-safe-commands.ts +42 -1
  1219. package/src/modules/workflows/lib/workflow-templates.ts +91 -0
  1220. package/src/modules/workflows/migrations/.snapshot-open-mercato.json +2321 -698
  1221. package/src/modules/workflows/migrations/Migration20260626214855_workflows.ts +15 -0
  1222. package/src/modules/workflows/migrations/Migration20260626222424_workflows.ts +17 -0
  1223. package/src/modules/workflows/migrations/Migration20260727074335_workflows.ts +15 -0
  1224. package/src/modules/workflows/migrations/Migration20260728104038_workflows.ts +13 -0
  1225. package/src/modules/workflows/migrations/Migration20260728163001_workflows.ts +36 -0
  1226. package/src/modules/workflows/migrations/Migration20260729061736_workflows.ts +13 -0
  1227. package/src/modules/workflows/migrations/Migration20260729192652_workflows.ts +11 -0
  1228. package/src/modules/workflows/migrations/Migration20260730085000_workflows.ts +13 -0
  1229. package/src/modules/workflows/migrations/Migration20260730092706_workflows.ts +13 -0
  1230. package/src/modules/workflows/notifications.ts +40 -10
  1231. package/src/modules/workflows/setup.ts +128 -0
  1232. package/src/modules/workflows/subscribers/task-assigned-notification.ts +36 -45
  1233. package/src/modules/workflows/subscribers/task-deadline-breached-notification.ts +31 -0
  1234. package/src/modules/workflows/subscribers/task-reminder-notification.ts +31 -0
  1235. package/src/modules/workflows/widgets/injection/pending-work/widget.client.tsx +119 -0
  1236. package/src/modules/workflows/widgets/injection/pending-work/widget.ts +27 -0
  1237. package/src/modules/workflows/widgets/injection-table.ts +52 -2
  1238. package/src/modules/workflows/workers/workflow-activities.worker.ts +95 -42
  1239. package/src/modules/workflows/workers/workflow-definition-metric-rollup.worker.ts +71 -0
  1240. package/src/modules/workflows/workers/workflow-instance-bulk-replay.ts +51 -0
  1241. package/src/modules/workflows/workers/workflow-invoke-agent.worker.ts +72 -0
@@ -1,18 +1,110 @@
1
1
  'use client'
2
2
 
3
3
  import { WorkflowGraph } from '../../../components/WorkflowGraph'
4
+ import { useLastRunOverlay } from '../../../components/run/useLastRunOverlay'
4
5
  // Conditional imports based on feature flag
5
6
  import { NodeEditDialog } from '../../../components/NodeEditDialog'
6
7
  import { EdgeEditDialog } from '../../../components/EdgeEditDialog'
7
8
  import { NodeEditDialogCrudForm } from '../../../components/NodeEditDialogCrudForm'
8
9
  import { EdgeEditDialogCrudForm } from '../../../components/EdgeEditDialogCrudForm'
9
10
  import type { Node, Edge, Connection } from '@xyflow/react'
10
- import { useState, useCallback, useEffect } from 'react'
11
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
11
12
  import { useRouter, useSearchParams, usePathname } from 'next/navigation'
12
- import { graphToDefinition, definitionToGraph, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, ValidationError } from '../../../lib/graph-utils'
13
+ import { graphToDefinition, definitionToGraph, applyAutoLayout, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, getBadgeForNodeType, ValidationError } from '../../../lib/graph-utils'
14
+ import { convertStepType, type ConvertibleStepType } from '../../../lib/step-type-conversion'
15
+ import { collectValidationIssues, countIssuesBySeverity, type WorkflowIssueTranslator, type WorkflowValidationIssue, type ZodIssueLike } from '../../../lib/collect-validation-issues'
16
+ import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
17
+ import type { WorkflowGraphDropEvent, WorkflowGraphFocusTarget, WorkflowGraphNodesChangeMeta } from '../../../components/WorkflowGraph'
18
+ import { resolveNewNodePlacement } from '../../../lib/node-placement'
19
+ import {
20
+ commitEditorHistory,
21
+ createEditorHistory,
22
+ redoEditorHistory,
23
+ undoEditorHistory,
24
+ type EditorHistoryState,
25
+ type WorkflowEditorDocument,
26
+ type WorkflowEditorPanelState,
27
+ } from '../../../lib/editor-history'
28
+ import {
29
+ parseWorkflowSubgraph,
30
+ pasteWorkflowSubgraph,
31
+ serializeWorkflowSubgraph,
32
+ stringifyWorkflowSubgraph,
33
+ type SubgraphClipboardParseFailure,
34
+ type WorkflowSubgraphClipboard,
35
+ } from '../../../lib/subgraph-clipboard'
36
+ import {
37
+ annotationsFromNodes,
38
+ annotationsToNodes,
39
+ createGroupNode,
40
+ createNoteNode,
41
+ isAnnotationNode,
42
+ readEditorAnnotations,
43
+ removeAnnotationNode,
44
+ updateAnnotationNode,
45
+ type WorkflowGroupNodeData,
46
+ type WorkflowNoteNodeData,
47
+ } from '../../../lib/editor-annotations'
48
+ import { WORKFLOW_GROUP_TOGGLE_EVENT } from '../../../lib/annotation-events'
49
+ import { AnnotationEditDialog } from '../../../components/AnnotationEditDialog'
50
+ import { WorkflowCodeView } from '../../../components/WorkflowCodeView'
51
+ import { DefinitionMetadataDrawer, type DefinitionMetadataSection } from '../../../components/DefinitionMetadataDrawer'
52
+ import { TriggersDialog } from '../../../components/TriggersDialog'
53
+ import { describeCodeViewDraft, evaluateCodeViewDraft } from '../../../lib/code-view-apply'
54
+ import {
55
+ WorkflowAiDraftDialog,
56
+ type WorkflowAiDraftApplyRefusal,
57
+ } from '../../../components/WorkflowAiDraftDialog'
58
+ import { buildAiCheckpointLabel } from '../../../lib/ai-authoring'
59
+ import {
60
+ locateDefinitionJsonEntities,
61
+ locateIssues,
62
+ severityByLine,
63
+ } from '../../../lib/definition-json-locations'
64
+ import { WorkflowCommandPalette } from '../../../components/WorkflowCommandPalette'
65
+ import { buildWorkflowEditorCommands, type WorkflowEditorCommand } from '../../../lib/editor-commands'
66
+ import { NUDGE_COMMIT_DELAY_MS, nudgeOffset, nudgeSelectedNodes, resolveNudgeDirection, selectedNodeIds } from '../../../lib/node-nudge'
67
+ import { canRedoEditorHistory, canUndoEditorHistory } from '../../../lib/editor-history'
68
+ import { readPaletteDragItem, writePaletteDragPayload } from '../../../lib/palette-drag'
69
+ import { appendActivityToRoute, insertStepOnRoute, type PaletteDropRejectionCode } from '../../../lib/palette-drop'
70
+ import { useActivityTypeOptions } from '../../../components/fields/useActivityTypeOptions'
71
+ import { resolveActivityIcon } from '../../../components/WorkflowRouteChips'
13
72
  import { performDeleteEdgeFlow, performDeleteNodeFlow } from '../../../lib/visual-editor-delete-flow'
14
- import { humanizeDefinitionIssuePath } from '../../../lib/format-validation-error'
15
- import { workflowDefinitionDataSchema } from '../../../data/validators'
73
+ import { resolveCrudFormDialogsEnabled } from '../../../lib/crud-form-dialogs-flag'
74
+ import { decideDraftRestore, isServerDraftEligible, stableSerializeDefinition } from '../../../lib/draft-restore'
75
+ import { buildDefinitionPayload, buildMetadataPayload } from '../../../lib/definition-payload'
76
+ import { resolveDefinitionInterpolationMode, type WorkflowInterpolationMode } from '../../../lib/interpolation-pipeline'
77
+ import { findRouteKindDescriptorForHandle } from '../../../lib/route-kinds'
78
+ import { buildWorkflowRouteEdge } from '../../../lib/route-edge'
79
+ import { isDecisionSourceHandle, type DecisionRowLike } from '../../../lib/node-outcome-rows'
80
+ import { STRUCTURAL_EDIT_CONFLICT_CODE } from '../../../lib/definition-edit-safety'
81
+ import type { WorkflowErrorHandlerConfig } from '../../../data/validators'
82
+ import { WORKFLOW_NODE_DELETE_EVENT } from '../../../components/WorkflowNodeCard'
83
+ import { WORKFLOW_ROUTE_CHIP_EVENT, WORKFLOW_ROUTE_ACTIVITY_EVENT, type RouteChipEventDetail, type RouteActivityEventDetail } from '../../../lib/route-chip-events'
84
+ import { ActivityEditDialog } from '../../../components/ActivityEditDialog'
85
+ import type { Activity } from '../../../components/fields/ActivityArrayEditor'
86
+ import { applyRouteOrder, canNormalizeRoutePriorities, normalizeRoutePriorities, readRouteOrder, type RouteOrderEntry } from '../../../lib/route-priority'
87
+ import { classifyConnection, applyInputMappingToNodes, buildDataMappingEdge } from '../../../lib/data-edge-mapping'
88
+ import { reattachWorkflowEdge, type EdgeReattachRejection } from '../../../lib/edge-reattachment'
89
+ import { workflowDefinitionDataSchema, type WorkflowIoContract } from '../../../data/validators'
90
+ import { collectActivityConfigWarnings } from '../../../data/activity-config-warnings'
91
+ import {
92
+ applyIfElseRoutes,
93
+ applySwitchRoutes,
94
+ isBranchingNodeType,
95
+ readBranchingRoutes,
96
+ readSwitchField,
97
+ type SwitchRoutesValue,
98
+ } from '../../../lib/branching-routes'
99
+ import {
100
+ buildTriggerPayloadContracts,
101
+ computeContextLedger,
102
+ type LedgerContract,
103
+ type LedgerWorkflowDefinition,
104
+ } from '../../../lib/context-ledger'
105
+ import { useAvailableEvents } from '@open-mercato/ui/backend/inputs/EventSelect'
106
+ import { toUnresolvedContextRefIssues } from '../../../lib/definition-evaluation'
107
+ import type { PinnedSampleEnvelope } from '../../../lib/sample-resolver'
16
108
  import { Page } from '@open-mercato/ui/backend/Page'
17
109
  import { Button } from '@open-mercato/ui/primitives/button'
18
110
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -20,36 +112,91 @@ import { Textarea } from '@open-mercato/ui/primitives/textarea'
20
112
  import { Label } from '@open-mercato/ui/primitives/label'
21
113
  import { Switch } from '@open-mercato/ui/primitives/switch'
22
114
  import {
23
- Dialog,
24
- DialogContent,
25
- DialogDescription,
26
- DialogFooter,
27
- DialogHeader,
28
- DialogTitle,
29
- } from '@open-mercato/ui/primitives/dialog'
30
- import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
115
+ Drawer,
116
+ DrawerBody,
117
+ DrawerContent,
118
+ DrawerDescription,
119
+ DrawerFooter,
120
+ DrawerHeader,
121
+ DrawerTitle,
122
+ } from '@open-mercato/ui/primitives/drawer'
31
123
  import { LoadingMessage } from '@open-mercato/ui/backend/detail'
32
- import { Alert, AlertTitle } from '@open-mercato/ui/primitives/alert'
33
- import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
124
+ import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
125
+ import { ConfirmDialog, useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
34
126
  import { useT } from '@open-mercato/shared/lib/i18n/context'
35
- import { FormHeader } from '@open-mercato/ui/backend/forms'
127
+ import { formatRelativeTime } from '@open-mercato/shared/lib/time'
128
+ import { FormHeader, ActionsDropdown, type ActionMenuEntry } from '@open-mercato/ui/backend/forms'
36
129
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
130
+ import { apiFetch } from '@open-mercato/ui/backend/utils/api'
37
131
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
38
132
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
39
133
  import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
134
+ import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
135
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
136
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
137
+ import {
138
+ listStartFixtures,
139
+ removeStartFixture,
140
+ upsertStartFixture,
141
+ type WorkflowStartFixtures,
142
+ } from '../../../lib/start-fixtures'
40
143
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
41
- import { CircleQuestionMark, PanelTopClose, PanelTopOpen, Play, Save, Trash2 } from 'lucide-react'
144
+ import { ChevronDown, ChevronRight, CircleAlert, CircleQuestionMark, Code, Command, Group, History, Maximize2, Minimize2, Network, PanelLeftClose, PanelLeftOpen, PanelRightOpen, Play, Save, ShieldMinus, Sparkles, StickyNote, Trash2, TriangleAlert, X } from 'lucide-react'
145
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
146
+ import { usePersistedBooleanFlag } from '@open-mercato/ui/backend/crud/usePersistedBooleanFlag'
147
+ import { useSidebarCollapse } from '@open-mercato/ui/backend/AppShell'
42
148
  import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
43
- import { DefinitionTriggersEditor } from '../../../components/DefinitionTriggersEditor'
149
+ import { TemplateGalleryDialog, type WorkflowTemplateGalleryItem } from '../../../components/TemplateGalleryDialog'
44
150
  import { MobileVisualEditor } from '../../../components/mobile/MobileVisualEditor'
45
151
  import { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'
46
- import type { WorkflowDefinitionTrigger } from '../../../data/entities'
152
+ import type { WorkflowContextSchema, WorkflowDefinitionData, WorkflowDefinitionTrigger } from '../../../data/entities'
47
153
  import type { WorkflowMetadataState, WorkflowMetadataHandlers } from '../../../data/types'
48
154
  import * as React from 'react'
49
155
  import { createLogger } from '@open-mercato/shared/lib/logger'
50
156
 
51
157
  const logger = createLogger('workflows')
52
158
 
159
+ type WorkflowDefinitionDraftPayload = {
160
+ definition: Record<string, unknown>
161
+ metadata?: Record<string, unknown> | null
162
+ baseUpdatedAt: string | null
163
+ updatedAt: string | null
164
+ }
165
+
166
+ const DRAFT_AUTOSAVE_DEBOUNCE_MS = 2000
167
+ const DRAFT_SAVED_LABEL_REFRESH_MS = 30000
168
+
169
+ /**
170
+ * Ledger-checked context-reference warnings for the Problems panel (spec
171
+ * section 3.5). The editor validates UNSAVED state, so the ledger is computed
172
+ * CLIENT-side with `resolveOutputContract` pinned to 'unknown' instead of
173
+ * fetching the server ledger from the context-schema API: the client ledger is
174
+ * type-poorer (activity outputs stay single `unknown` nodes rather than typed
175
+ * contract entries) but structurally identical, and since `unknown` entries
176
+ * resolve every sub-path in the checker, the degradation can only suppress
177
+ * warnings, never fabricate them. Warnings merge into the same ZodIssueLike
178
+ * channel as activity-config warnings, so `collectValidationIssues` maps them
179
+ * to nodes/edges and they never block saves.
180
+ */
181
+ function computeClientContextLedger(
182
+ definitionData: WorkflowDefinitionData,
183
+ triggerPayloadContracts?: Record<string, LedgerContract>,
184
+ ) {
185
+ return computeContextLedger(definitionData as unknown as LedgerWorkflowDefinition, {
186
+ resolveOutputContract: () => 'unknown',
187
+ triggerPayloadContracts,
188
+ })
189
+ }
190
+
191
+ function collectContextRefWarnings(
192
+ definitionData: WorkflowDefinitionData,
193
+ translate: ReturnType<typeof useT>,
194
+ triggerPayloadContracts?: Record<string, LedgerContract>,
195
+ ): ZodIssueLike[] {
196
+ const ledger = computeClientContextLedger(definitionData, triggerPayloadContracts)
197
+ return toUnresolvedContextRefIssues(definitionData, ledger, translate)
198
+ }
199
+
53
200
  /**
54
201
  * VisualEditorPage - Visual workflow definition editor
55
202
  *
@@ -62,12 +209,78 @@ const logger = createLogger('workflows')
62
209
  * - Flash Messages: Top-right positioned validation messages
63
210
  * - Edit Dialogs: Modal dialogs for editing steps and transitions
64
211
  */
212
+ /**
213
+ * Resolve the declared IO port contracts of every sub-workflow referenced by a
214
+ * definition, keyed by `subWorkflowId`, so SUB_WORKFLOW nodes can render the
215
+ * child's IN/OUT ports. Fail-open: any lookup error leaves that child without a
216
+ * contract and its node simply renders without ports.
217
+ */
218
+ async function loadSubWorkflowContracts(
219
+ definition: { steps?: Array<{ stepType?: string; config?: { subWorkflowId?: string } }> } | null | undefined,
220
+ ): Promise<Map<string, WorkflowIoContract>> {
221
+ const contracts = new Map<string, WorkflowIoContract>()
222
+ const subWorkflowIds = Array.from(
223
+ new Set(
224
+ (definition?.steps || [])
225
+ .filter((step) => step?.stepType === 'SUB_WORKFLOW' && step?.config?.subWorkflowId)
226
+ .map((step) => step.config!.subWorkflowId as string),
227
+ ),
228
+ )
229
+ await Promise.all(
230
+ subWorkflowIds.map(async (workflowId) => {
231
+ try {
232
+ const res = await apiCall<{ data?: Array<{ definition?: { io?: WorkflowIoContract } }> }>(
233
+ `/api/workflows/definitions?workflowId=${encodeURIComponent(workflowId)}&limit=1`,
234
+ )
235
+ const io = res.ok ? res.result?.data?.[0]?.definition?.io : undefined
236
+ if (io) contracts.set(workflowId, io)
237
+ } catch {
238
+ // fail-open
239
+ }
240
+ }),
241
+ )
242
+ return contracts
243
+ }
244
+
245
+ const PALETTE_NODE_TYPES = ['start', 'userTask', 'automated', 'invokeAgent', 'ifElse', 'switch', 'waitForSignal', 'waitForTimer', 'waitForCondition', 'subWorkflow', 'end'] as const
246
+
247
+ /** Body of a definition update — shared by explicit Save and the quiet autosave. */
248
+ type DefinitionUpdateBody = {
249
+ workflowName: string
250
+ description: string | null
251
+ version: number
252
+ definition: ReturnType<typeof buildDefinitionPayload>
253
+ metadata: ReturnType<typeof buildMetadataPayload>
254
+ enabled: boolean
255
+ effectiveFrom: string | null
256
+ effectiveTo: string | null
257
+ }
258
+
259
+ /**
260
+ * Edit-safety rule (spec 4.1): the definition PUT refuses a topology change
261
+ * while instances are still executing this version. The refusal is a dead end
262
+ * without its remedy, so the rejected payload rides along and is re-applied to
263
+ * the freshly minted version.
264
+ */
265
+ type StructuralEditConflict = {
266
+ activeInstanceCount: number
267
+ payload: DefinitionUpdateBody
268
+ }
269
+
270
+ function readStructuralEditConflictCount(status: number, body: unknown): number | null {
271
+ if (status !== 409) return null
272
+ const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : null
273
+ if (!record || record.code !== STRUCTURAL_EDIT_CONFLICT_CODE) return null
274
+ return typeof record.activeInstanceCount === 'number' ? record.activeInstanceCount : 0
275
+ }
276
+
65
277
  export default function VisualEditorPage() {
66
278
  const t = useT()
67
279
  const router = useRouter()
68
280
  const searchParams = useSearchParams()
69
281
  const pathname = usePathname()
70
282
  const definitionId = searchParams.get('id')
283
+ const templateId = searchParams.get('template')
71
284
  const isMobile = useIsMobile()
72
285
 
73
286
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
@@ -78,17 +291,96 @@ export default function VisualEditorPage() {
78
291
  const [edges, setEdges] = useState<Edge[]>([])
79
292
  const [selectedNode, setSelectedNode] = useState<Node | null>(null)
80
293
  const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null)
81
- const [showMetadata, setShowMetadata] = useState(true)
294
+ const [edgeDialogFocusFieldId, setEdgeDialogFocusFieldId] = useState<string | null>(null)
295
+ // Latest edges, readable from window-event handlers without re-subscribing on
296
+ // every graph change (route chips resolve their edge through this).
297
+ const edgesRef = React.useRef<Edge[]>([])
298
+ edgesRef.current = edges
299
+ const nodesRef = React.useRef<Node[]>([])
300
+ nodesRef.current = nodes
301
+ // Definition metadata lives in a right-side Drawer, so it costs the canvas
302
+ // nothing and never needs hiding. It starts closed — the canvas is what the
303
+ // page is for — and `handleSave` opens it when the required id/name are
304
+ // still missing, which is the moment the author needs it.
305
+ const [metadataOpen, setMetadataOpen] = useState(false)
306
+ // Which section the drawer lands on. Only the canvas trigger pill sets it
307
+ // (fidelity gap #5) — every other way in wants the top of the form — so it is
308
+ // cleared whenever the drawer is opened by anything else.
309
+ const [metadataFocusSection, setMetadataFocusSection] = useState<DefinitionMetadataSection | null>(null)
310
+ const [triggersDialogOpen, setTriggersDialogOpen] = useState(false)
311
+ // The route activity chip's target: which edge + activity the focused
312
+ // single-activity modal is editing (fidelity gap #6 follow-up).
313
+ const [activityEdit, setActivityEdit] = useState<{ edgeId: string; activityId: string } | null>(null)
314
+ // The START node's trigger cap opens a focused Triggers modal (Direction A),
315
+ // NOT the five-section definition drawer — the author clicked "what starts
316
+ // this", so the affordance opens exactly that.
317
+ const handleOpenTriggers = useCallback(() => {
318
+ setTriggersDialogOpen(true)
319
+ }, [])
320
+ // The focus target is one-shot: every other way into the drawer wants the top
321
+ // of the form, so closing it clears the request.
322
+ const handleMetadataOpenChange = useCallback((open: boolean) => {
323
+ if (!open) setMetadataFocusSection(null)
324
+ setMetadataOpen(open)
325
+ }, [])
82
326
  const [isCompactViewport, setIsCompactViewport] = useState(false)
327
+ const [autosaveState, setAutosaveState] = useState<'idle' | 'saving' | 'saved'>('idle')
328
+ const [structuralConflict, setStructuralConflict] = useState<StructuralEditConflict | null>(null)
329
+ const [isCreatingVersion, setIsCreatingVersion] = useState(false)
330
+ // Debounced autosave-on-drag plumbing. `performAutosaveRef` is reassigned each
331
+ // render so the debounced timer always runs the latest closure (no stale nodes).
332
+ const autosaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
333
+ const performAutosaveRef = React.useRef<() => Promise<void>>(async () => {})
334
+ // Last payload the quiet autosave actually persisted, so an unchanged graph
335
+ // never issues a redundant PUT (#4248). Cleared on failure so the next change
336
+ // retries the write.
337
+ const lastAutosavedBodyRef = React.useRef<string | null>(null)
338
+ const { value: paletteCollapsed, toggle: togglePaletteCollapsed, setValue: setPaletteCollapsed } = usePersistedBooleanFlag('om:wf-editor-palette', false)
339
+ const [showPaletteHowTo, setShowPaletteHowTo] = useState(false)
340
+ const { value: focusMode, setValue: setFocusMode, toggle: toggleFocus } = usePersistedBooleanFlag('om:wf-editor-focus', false)
341
+ // Compensation ghosts (spec §4.4) are OFF by default and remembered per
342
+ // author: they are a read-only overlay, never part of the document.
343
+ const { value: showCompensation, toggle: toggleCompensation } = usePersistedBooleanFlag('om:wf-editor-compensation', false)
344
+ // "Show last run" (spec §8.3). OFF by default and remembered per author, and
345
+ // it fetches nothing until it is on — an author editing a definition should
346
+ // not pay for three requests they never asked for.
347
+ const { value: showLastRun, toggle: toggleLastRun } = usePersistedBooleanFlag('om:wf-editor-last-run', false)
348
+ const { requestCollapse, releaseRequest } = useSidebarCollapse()
349
+ // Remember the palette state from before Focus mode took over so we can
350
+ // restore exactly what the author had when they exit.
351
+ const priorPaletteCollapsedRef = React.useRef<boolean | null>(null)
352
+
353
+ // Focus mode orchestrator: collapse the app sidebar + palette when entering
354
+ // and restore the author's prior palette state when leaving. Runs on mount
355
+ // too, so a persisted `focusMode === true` applies the collapses immediately.
356
+ // The metadata Drawer is not touched: it overlays rather than displaces the
357
+ // canvas, so hiding it would buy Focus mode nothing and would only make the
358
+ // palette's "Toggle the workflow details panel" command a silent no-op.
359
+ useEffect(() => {
360
+ if (focusMode) {
361
+ if (priorPaletteCollapsedRef.current === null) priorPaletteCollapsedRef.current = paletteCollapsed
362
+ requestCollapse(true)
363
+ setPaletteCollapsed(true)
364
+ } else {
365
+ releaseRequest()
366
+ if (priorPaletteCollapsedRef.current !== null) {
367
+ setPaletteCollapsed(priorPaletteCollapsedRef.current)
368
+ priorPaletteCollapsedRef.current = null
369
+ }
370
+ }
371
+ // eslint-disable-next-line react-hooks/exhaustive-deps
372
+ }, [focusMode])
373
+
374
+ // Always release the app-sidebar request when the editor unmounts so the
375
+ // user's prior sidebar state is restored when they navigate away.
376
+ useEffect(() => () => releaseRequest(), [releaseRequest])
83
377
 
84
- // Auto-collapse metadata on compact viewports after hydration
378
+ // Track the compact (<1280px) layout after hydration
85
379
  useEffect(() => {
86
380
  if (typeof window === 'undefined') return
87
381
  const mediaQuery = window.matchMedia('(max-width: 1279px)')
88
382
  const applyViewportMode = () => {
89
- const compact = mediaQuery.matches
90
- setIsCompactViewport(compact)
91
- setShowMetadata(!compact)
383
+ setIsCompactViewport(mediaQuery.matches)
92
384
  }
93
385
 
94
386
  applyViewportMode()
@@ -100,10 +392,52 @@ export default function VisualEditorPage() {
100
392
  }, [])
101
393
  const [showNodeDialog, setShowNodeDialog] = useState(false)
102
394
  const [showEdgeDialog, setShowEdgeDialog] = useState(false)
395
+
396
+ const crudFormDialogsEnabled = resolveCrudFormDialogsEnabled(process.env.NEXT_PUBLIC_WORKFLOW_CRUDFORM_ENABLED)
397
+
398
+ // Where the inspectors are MOUNTED. Both of them now open as the wide overlay
399
+ // Drawer (#4708 for the step form, its follow-up for the route form): the
400
+ // 384px rail could not hold either dense form. What survives of the rail
401
+ // decision is the mount point — on a roomy viewport the panels live inside the
402
+ // editor layout row, below it they ride the shared dialog stack — plus the
403
+ // shortcut ownership that keys off it. The legacy dialogs keep their own modal
404
+ // chrome; they are deprecated and are not being redesigned.
405
+ const inspectorsDocked = crudFormDialogsEnabled && !isMobile && !isCompactViewport
406
+
407
+ // Sticky notes and groups (spec 4.5) are canvas nodes with their own tiny
408
+ // inspector — they carry no step configuration, so they never open the step
409
+ // dialog.
410
+ const [selectedAnnotation, setSelectedAnnotation] = useState<Node | null>(null)
411
+ const [showAnnotationDialog, setShowAnnotationDialog] = useState(false)
412
+ // Cmd+K command palette (spec 4.6). Together with the inspector and the
413
+ // Problems panel it is the complete non-pointer authoring path, so every
414
+ // mutating action the toolbar offers is reachable through it.
415
+ const [showCommandPalette, setShowCommandPalette] = useState(false)
416
+ // Code view, stage 1 (spec §2.2): the assembled definition JSON read-only,
417
+ // beside the same structured issue list the Problems panel renders.
418
+ const [showCodeView, setShowCodeView] = useState(false)
103
419
  const [showClearConfirm, setShowClearConfirm] = useState(false)
420
+ const [showTemplateGallery, setShowTemplateGallery] = useState(false)
421
+ const [problems, setProblems] = useState<WorkflowValidationIssue[]>([])
422
+ const [showProblems, setShowProblems] = useState(false)
423
+ const [problemsCollapsed, setProblemsCollapsed] = useState(false)
424
+ const [focusTarget, setFocusTarget] = useState<WorkflowGraphFocusTarget | null>(null)
425
+ const focusRequestRef = React.useRef(0)
426
+
427
+ // Error-severity issue counts per node id — drives the per-node error badges
428
+ // on the canvas; clearing the problems list clears every badge.
429
+ const nodeErrorCounts = React.useMemo(() => {
430
+ const counts: Record<string, number> = {}
431
+ for (const issue of problems) {
432
+ if (issue.severity !== 'error' || !issue.nodeId) continue
433
+ counts[issue.nodeId] = (counts[issue.nodeId] ?? 0) + 1
434
+ }
435
+ return counts
436
+ }, [problems])
104
437
 
105
438
  // Workflow metadata state
106
439
  const [workflowId, setWorkflowId] = useState('')
440
+ const lastRunOverlay = useLastRunOverlay({ workflowId, enabled: showLastRun && !!definitionId })
107
441
  const [workflowName, setWorkflowName] = useState('')
108
442
  const [description, setDescription] = useState('')
109
443
  const [version, setVersion] = useState(1)
@@ -114,16 +448,143 @@ export default function VisualEditorPage() {
114
448
  const [effectiveFrom, setEffectiveFrom] = useState('')
115
449
  const [effectiveTo, setEffectiveTo] = useState('')
116
450
  const [triggers, setTriggers] = useState<WorkflowDefinitionTrigger[]>([])
451
+ const [contextSchema, setContextSchema] = useState<WorkflowContextSchema | undefined>(undefined)
452
+ // The sub-workflow io port contract is not edited on this page, but it must
453
+ // survive the graph → definition rebuild on save/draft, so it is carried as
454
+ // pass-through state exactly like contextSchema.
455
+ const [definitionIo, setDefinitionIo] = useState<WorkflowIoContract | undefined>(undefined)
456
+ // Interpolation mode (spec §3.6): new definitions start strict (matching the
457
+ // POST create default); loaded definitions keep their stored value, and
458
+ // ABSENT stays absent through save round-trips so existing lenient
459
+ // definitions are never flipped by an unrelated edit.
460
+ const [interpolation, setInterpolation] = useState<WorkflowInterpolationMode | undefined>(undefined)
461
+ // Definition-level catch-all error handler (spec section 5.9). Pass-through
462
+ // state like contextSchema/io: absent stays absent through save round-trips.
463
+ const [errorHandler, setErrorHandler] = useState<WorkflowErrorHandlerConfig | undefined>(undefined)
464
+ const [loadedMetadata, setLoadedMetadata] = useState<Record<string, unknown> | null>(null)
117
465
  const [source, setSource] = useState<'code' | 'code_override' | 'user' | null>(null)
118
466
  const [updatedAt, setUpdatedAt] = useState<string | null>(null)
119
467
 
468
+ // Start-instance dialog state (mirrors the non-visual edit page UX)
469
+ const [startOpen, setStartOpen] = useState(false)
470
+ const [startContext, setStartContext] = useState('{}')
471
+ const [starting, setStarting] = useState(false)
472
+ // Spec section 8.2 test-run modes. Independent by design: a real run can be
473
+ // stepped through, and a dry run can be let loose end to end.
474
+ const [startDryRun, setStartDryRun] = useState(false)
475
+ const [startStepThrough, setStartStepThrough] = useState(false)
476
+ const [fixtureName, setFixtureName] = useState('')
477
+
478
+ // Undo/redo (spec §4.5). One stack over the whole editor document, so an
479
+ // inspector save undoes exactly like a deleted route. Snapshots are cheap
480
+ // because every mutating path already replaces the node/edge arrays instead
481
+ // of mutating them, so an entry only holds references.
482
+ const [history, setHistory] = useState<EditorHistoryState<WorkflowEditorDocument>>(createEditorHistory)
483
+ const historyRef = React.useRef(history)
484
+ historyRef.current = history
485
+ const loadedMetadataRef = React.useRef<Record<string, unknown> | null>(null)
486
+ loadedMetadataRef.current = loadedMetadata
487
+
488
+ const historyLabels = useMemo(() => ({
489
+ addStep: t('workflows.visualEditor.history.addStep', 'Add step'),
490
+ editStep: t('workflows.visualEditor.history.editStep', 'Edit step'),
491
+ editRoute: t('workflows.visualEditor.history.editRoute', 'Edit route'),
492
+ deleteStep: t('workflows.visualEditor.history.deleteStep', 'Delete step'),
493
+ deleteRoute: t('workflows.visualEditor.history.deleteRoute', 'Delete route'),
494
+ connect: t('workflows.visualEditor.history.connect', 'Connect steps'),
495
+ reattach: t('workflows.visualEditor.history.reattach', 'Re-target route'),
496
+ move: t('workflows.visualEditor.history.move', 'Move steps'),
497
+ convert: t('workflows.visualEditor.history.convert', 'Change step type'),
498
+ branchRoutes: t('workflows.visualEditor.history.branchRoutes', 'Edit branch routes'),
499
+ routeOrder: t('workflows.visualEditor.history.routeOrder', 'Reorder routes'),
500
+ pinSample: t('workflows.visualEditor.history.pinSample', 'Pin sample'),
501
+ unpinSample: t('workflows.visualEditor.history.unpinSample', 'Unpin sample'),
502
+ tidy: t('workflows.visualEditor.history.tidy', 'Tidy layout'),
503
+ paste: t('workflows.visualEditor.history.paste', 'Paste'),
504
+ duplicate: t('workflows.visualEditor.history.duplicate', 'Duplicate'),
505
+ insertOnRoute: t('workflows.visualEditor.history.insertOnRoute', 'Insert step on route'),
506
+ addActivity: t('workflows.visualEditor.history.addActivity', 'Add action to route'),
507
+ addNote: t('workflows.visualEditor.history.addNote', 'Add note'),
508
+ addGroup: t('workflows.visualEditor.history.addGroup', 'Add group'),
509
+ editAnnotation: t('workflows.visualEditor.history.editAnnotation', 'Edit annotation'),
510
+ deleteAnnotation: t('workflows.visualEditor.history.deleteAnnotation', 'Delete annotation'),
511
+ toggleGroup: t('workflows.visualEditor.history.toggleGroup', 'Collapse group'),
512
+ saveFixture: t('workflows.visualEditor.history.saveFixture', 'Save start fixture'),
513
+ deleteFixture: t('workflows.visualEditor.history.deleteFixture', 'Delete start fixture'),
514
+ applyCode: t('workflows.visualEditor.history.applyCode', 'Apply JSON from Code view'),
515
+ }), [t])
516
+
517
+ // The definition-panel fields ride ALONG with the document so the Code view's
518
+ // Apply — which replaces the whole definition, panel fields included — is one
519
+ // fully reversible action. Every other entry captures them too, which costs
520
+ // five references and makes an undo restore the state it claims to.
521
+ const panelStateRef = React.useRef<WorkflowEditorPanelState>({
522
+ triggers: [],
523
+ contextSchema: undefined,
524
+ io: undefined,
525
+ interpolation: undefined,
526
+ errorHandler: undefined,
527
+ })
528
+ useEffect(() => {
529
+ panelStateRef.current = {
530
+ triggers,
531
+ contextSchema,
532
+ io: definitionIo,
533
+ interpolation,
534
+ errorHandler,
535
+ }
536
+ }, [triggers, contextSchema, definitionIo, interpolation, errorHandler])
537
+
538
+ const captureDocument = useCallback((): WorkflowEditorDocument => ({
539
+ nodes: nodesRef.current,
540
+ edges: edgesRef.current,
541
+ metadata: loadedMetadataRef.current,
542
+ panel: panelStateRef.current,
543
+ }), [])
544
+
545
+ const commitCapturedDocument = useCallback((document: WorkflowEditorDocument, label: string) => {
546
+ setHistory((current) => commitEditorHistory(current, document, label))
547
+ }, [])
548
+
549
+ const commitHistory = useCallback((label: string) => {
550
+ commitCapturedDocument(captureDocument(), label)
551
+ }, [captureDocument, commitCapturedDocument])
552
+
553
+ // A whole-document replacement (draft restore, template load, clear) starts a
554
+ // fresh stack: those paths also rewrite the definition-panel fields, which the
555
+ // document deliberately does not version, so undoing into them would restore
556
+ // half a workflow.
557
+ const resetHistory = useCallback(() => {
558
+ setHistory(createEditorHistory<WorkflowEditorDocument>())
559
+ }, [])
560
+
561
+ const mutationContextId = `workflows.definitions.visual-editor:${definitionId ?? 'unknown'}`
562
+ const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
563
+ contextId: mutationContextId,
564
+ })
565
+
120
566
  const isCodeOnly = source === 'code'
121
567
  const isCodeOverride = source === 'code_override'
122
568
 
569
+ // Per-user draft layer (spec §4.7): drafts persist server-side only for
570
+ // SAVED definitions (uuid ids). Unsaved/new definitions and code-defined
571
+ // workflows keep their state client-side only — no persistence at all.
572
+ // Draft saves deliberately never participate in the definition's optimistic
573
+ // lock; only the explicit Save PUT sends the lock header.
574
+ const draftEligible = isServerDraftEligible(definitionId) && !isCodeOnly
575
+ const [pendingDraft, setPendingDraft] = useState<{ draft: WorkflowDefinitionDraftPayload; baseMismatch: boolean } | null>(null)
576
+ const [draftAutosaveReady, setDraftAutosaveReady] = useState(false)
577
+ const [draftSavedAt, setDraftSavedAt] = useState<string | null>(null)
578
+ const [draftSaveFailed, setDraftSaveFailed] = useState(false)
579
+ const [draftClock, setDraftClock] = useState(0)
580
+ const lastPersistedDraftRef = useRef<string | null>(null)
581
+ const draftSuspendedRef = useRef(false)
582
+
123
583
  // Load existing definition if ID is provided
124
584
  useEffect(() => {
125
585
  const loadDefinition = async () => {
126
586
  if (!definitionId) {
587
+ setInterpolation('strict')
127
588
  setIsLoading(false)
128
589
  return
129
590
  }
@@ -151,19 +612,91 @@ export default function VisualEditorPage() {
151
612
  setEffectiveFrom(definition.effectiveFrom || '')
152
613
  setEffectiveTo(definition.effectiveTo || '')
153
614
 
154
- // Convert definition to graph
155
- const graph = definitionToGraph(definition.definition)
156
- setNodes(graph.nodes)
615
+ // Resolve referenced sub-workflow port contracts so SUB_WORKFLOW nodes
616
+ // render IN/OUT ports without opening the child (fail-open).
617
+ const childContracts = await loadSubWorkflowContracts(definition.definition)
618
+
619
+ // Convert definition to graph. Annotations ride alongside the steps as
620
+ // canvas nodes so a note drags, undoes and autosaves like everything
621
+ // else — they are filtered back out on the way to `definition.steps`.
622
+ const graph = definitionToGraph(definition.definition, { childContracts })
623
+ const loadedMetadataBag = definition.metadata && typeof definition.metadata === 'object'
624
+ ? (definition.metadata as Record<string, unknown>)
625
+ : null
626
+ const annotationNodes = annotationsToNodes(readEditorAnnotations(loadedMetadataBag))
627
+ setNodes([...graph.nodes, ...annotationNodes])
157
628
  setEdges(graph.edges)
158
629
 
159
630
  // Load embedded triggers from definition
160
- setTriggers(definition.definition?.triggers || [])
631
+ const loadedTriggers = definition.definition?.triggers || []
632
+ setTriggers(loadedTriggers)
633
+
634
+ // Carry the declared context schema and the FULL metadata object so
635
+ // save/draft rebuilds cannot silently strip keys the editor does not
636
+ // edit (contextSchema, future metadata.editor.* keys).
637
+ const loadedContextSchema = (definition.definition?.contextSchema ?? undefined) as WorkflowContextSchema | undefined
638
+ setContextSchema(loadedContextSchema)
639
+ const loadedIo = (definition.definition?.io ?? undefined) as WorkflowIoContract | undefined
640
+ setDefinitionIo(loadedIo)
641
+ const loadedInterpolation = resolveDefinitionInterpolationMode(definition.definition)
642
+ setInterpolation(loadedInterpolation)
643
+ const loadedErrorHandler = (definition.definition?.errorHandler ?? undefined) as WorkflowErrorHandlerConfig | undefined
644
+ setErrorHandler(loadedErrorHandler)
645
+ const loadedMetadataObject = loadedMetadataBag ? { ...loadedMetadataBag } : null
646
+ setLoadedMetadata(loadedMetadataObject)
161
647
 
162
648
  // Track source so the editor mirrors the non-visual edit page UX:
163
649
  // code → read-only with Customize button; code_override → editable
164
650
  // with Reset to code; user → editable, no banner.
165
- setSource((definition.source as 'code' | 'code_override' | 'user') ?? null)
166
- setUpdatedAt(typeof definition.updatedAt === 'string' ? definition.updatedAt : null)
651
+ const loadedSource = (definition.source as 'code' | 'code_override' | 'user') ?? null
652
+ setSource(loadedSource)
653
+ const loadedUpdatedAt = typeof definition.updatedAt === 'string' ? definition.updatedAt : null
654
+ setUpdatedAt(loadedUpdatedAt)
655
+
656
+ // Draft layer: compare against the ROUND-TRIPPED definition (graph →
657
+ // definition with the same normalization the autosave uses) so a mere
658
+ // load/serialize drift never looks like an unsaved draft.
659
+ const comparableDefinition = buildDefinitionPayload({
660
+ graphDefinition: graphToDefinition(graph.nodes, graph.edges, { includePositions: true }),
661
+ triggers: loadedTriggers,
662
+ contextSchema: loadedContextSchema,
663
+ io: loadedIo,
664
+ interpolation: loadedInterpolation,
665
+ errorHandler: loadedErrorHandler,
666
+ })
667
+ const loadedDraftMetadata = buildMetadataPayload({
668
+ loadedMetadata: loadedMetadataObject,
669
+ category: definition.metadata?.category || '',
670
+ tags: definition.metadata?.tags || [],
671
+ icon: definition.metadata?.icon || '',
672
+ annotations: annotationsFromNodes(annotationNodes),
673
+ })
674
+ lastPersistedDraftRef.current = stableSerializeDefinition({
675
+ definition: comparableDefinition,
676
+ metadata: loadedDraftMetadata,
677
+ })
678
+
679
+ if (loadedSource !== 'code' && isServerDraftEligible(definitionId)) {
680
+ const draftResult = await apiCall<{ data?: WorkflowDefinitionDraftPayload; error?: string }>(
681
+ `/api/workflows/definitions/${definitionId}/draft`,
682
+ )
683
+ const draft = draftResult.ok ? draftResult.result?.data : undefined
684
+ const decision = draft
685
+ ? decideDraftRestore({
686
+ draftDefinition: draft.definition,
687
+ draftBaseUpdatedAt: draft.baseUpdatedAt,
688
+ loadedDefinition: comparableDefinition,
689
+ definitionUpdatedAt: loadedUpdatedAt,
690
+ })
691
+ : { offerRestore: false as const }
692
+ if (draft && decision.offerRestore) {
693
+ // Hold autosave until the user restores or discards, so editing
694
+ // with the banner open can never silently overwrite the draft.
695
+ setPendingDraft({ draft, baseMismatch: decision.baseMismatch })
696
+ } else {
697
+ setDraftAutosaveReady(true)
698
+ }
699
+ }
167
700
  } catch (error) {
168
701
  logger.error('Error loading workflow definition', { err: error })
169
702
  flash('Failed to load workflow definition', 'error')
@@ -175,13 +708,310 @@ export default function VisualEditorPage() {
175
708
  loadDefinition()
176
709
  }, [definitionId])
177
710
 
711
+ // The canvas owns the annotations, so every persistence path derives them back
712
+ // out of the node array rather than tracking a second copy that could drift.
713
+ const annotations = useMemo(() => annotationsFromNodes(nodes), [nodes])
714
+
715
+ const draftMetadata = useMemo(
716
+ () => buildMetadataPayload({ loadedMetadata, tags, category, icon, annotations }),
717
+ [loadedMetadata, tags, category, icon, annotations],
718
+ )
719
+
720
+ // Debounced autosave-to-draft (~2s): watches the same editor-state dep set
721
+ // the save handler uses and PUTs the per-user draft. Never fires for
722
+ // unsaved/new or code-defined definitions, and failures stay quiet — the
723
+ // small indicator flips to "draft not saved" and the next change retries.
724
+ useEffect(() => {
725
+ if (!draftAutosaveReady || !draftEligible || !definitionId) return
726
+ if (draftSuspendedRef.current) return
727
+ let payload: { definition: ReturnType<typeof buildDefinitionPayload>; metadata: Record<string, unknown> | null; baseUpdatedAt: string | null }
728
+ try {
729
+ payload = {
730
+ definition: buildDefinitionPayload({
731
+ graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
732
+ triggers,
733
+ contextSchema,
734
+ io: definitionIo,
735
+ interpolation,
736
+ errorHandler,
737
+ }),
738
+ metadata: draftMetadata,
739
+ baseUpdatedAt: updatedAt,
740
+ }
741
+ } catch {
742
+ return
743
+ }
744
+ const serialized = stableSerializeDefinition({ definition: payload.definition, metadata: payload.metadata })
745
+ if (serialized === lastPersistedDraftRef.current) return
746
+ const timer = window.setTimeout(async () => {
747
+ if (draftSuspendedRef.current) return
748
+ try {
749
+ const result = await apiCall<{ data?: WorkflowDefinitionDraftPayload; error?: string }>(
750
+ `/api/workflows/definitions/${definitionId}/draft`,
751
+ {
752
+ method: 'PUT',
753
+ headers: { 'Content-Type': 'application/json' },
754
+ body: JSON.stringify(payload),
755
+ },
756
+ )
757
+ if (result.ok) {
758
+ lastPersistedDraftRef.current = serialized
759
+ setDraftSavedAt(new Date().toISOString())
760
+ setDraftSaveFailed(false)
761
+ } else {
762
+ setDraftSaveFailed(true)
763
+ }
764
+ } catch {
765
+ setDraftSaveFailed(true)
766
+ }
767
+ }, DRAFT_AUTOSAVE_DEBOUNCE_MS)
768
+ return () => window.clearTimeout(timer)
769
+ }, [draftAutosaveReady, draftEligible, definitionId, nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, draftMetadata, updatedAt, workflowName, description, version, enabled, effectiveFrom, effectiveTo])
770
+
771
+ // Keep the "Draft saved Xs ago" label fresh without re-rendering per second.
772
+ useEffect(() => {
773
+ if (!draftSavedAt) return
774
+ const interval = window.setInterval(() => setDraftClock((tick) => tick + 1), DRAFT_SAVED_LABEL_REFRESH_MS)
775
+ return () => window.clearInterval(interval)
776
+ }, [draftSavedAt])
777
+
778
+ const draftSavedLabel = useMemo(() => {
779
+ if (!draftSavedAt) return null
780
+ return formatRelativeTime(draftSavedAt, { translate: t })
781
+ // eslint-disable-next-line react-hooks/exhaustive-deps
782
+ }, [draftSavedAt, draftClock, t])
783
+
784
+ const handleRestoreDraft = useCallback(() => {
785
+ if (!pendingDraft) return
786
+ try {
787
+ const draftDefinition = pendingDraft.draft.definition as unknown as Parameters<typeof definitionToGraph>[0]
788
+ const graph = definitionToGraph(draftDefinition)
789
+ setNodes([...graph.nodes, ...annotationsToNodes(readEditorAnnotations(pendingDraft.draft.metadata ?? null))])
790
+ setEdges(graph.edges)
791
+ const draftTriggers = pendingDraft.draft.definition.triggers
792
+ setTriggers(Array.isArray(draftTriggers) ? (draftTriggers as WorkflowDefinitionTrigger[]) : [])
793
+ const draftContextSchema = pendingDraft.draft.definition.contextSchema
794
+ setContextSchema(draftContextSchema ? (draftContextSchema as WorkflowContextSchema) : undefined)
795
+ const draftIo = (pendingDraft.draft.definition as { io?: WorkflowIoContract }).io
796
+ setDefinitionIo(draftIo ?? undefined)
797
+ setInterpolation(resolveDefinitionInterpolationMode(pendingDraft.draft.definition))
798
+ const draftErrorHandler = (pendingDraft.draft.definition as { errorHandler?: WorkflowErrorHandlerConfig }).errorHandler
799
+ setErrorHandler(draftErrorHandler ?? undefined)
800
+ const restoredMetadata = pendingDraft.draft.metadata ?? null
801
+ setLoadedMetadata(restoredMetadata)
802
+ if (restoredMetadata) {
803
+ setCategory(typeof restoredMetadata.category === 'string' ? restoredMetadata.category : '')
804
+ setTags(Array.isArray(restoredMetadata.tags) ? restoredMetadata.tags.filter((tag): tag is string => typeof tag === 'string') : [])
805
+ setIcon(typeof restoredMetadata.icon === 'string' ? restoredMetadata.icon : '')
806
+ }
807
+ lastPersistedDraftRef.current = stableSerializeDefinition({
808
+ definition: pendingDraft.draft.definition,
809
+ metadata: pendingDraft.draft.metadata ?? null,
810
+ })
811
+ resetHistory()
812
+ flash(t('workflows.visualEditor.draft.restored', 'Draft restored'), 'success')
813
+ } catch (error) {
814
+ logger.error('Error restoring workflow definition draft', { err: error })
815
+ flash(t('workflows.visualEditor.draft.restoreFailed', 'Failed to restore draft'), 'error')
816
+ } finally {
817
+ setPendingDraft(null)
818
+ setDraftAutosaveReady(true)
819
+ }
820
+ }, [pendingDraft, resetHistory, t])
821
+
822
+ const handleDiscardDraft = useCallback(async () => {
823
+ setPendingDraft(null)
824
+ setDraftAutosaveReady(true)
825
+ if (!definitionId) return
826
+ try {
827
+ await apiCall(`/api/workflows/definitions/${definitionId}/draft`, { method: 'DELETE' })
828
+ } catch (error) {
829
+ logger.error('Error discarding workflow definition draft', { err: error })
830
+ }
831
+ }, [definitionId])
832
+
833
+ // Schedule a quiet debounced autosave (~900ms). Only for already-saved,
834
+ // editable definitions; new unsaved drafts fall back to an explicit Save.
835
+ const scheduleAutosave = useCallback(() => {
836
+ if (!definitionId || isCodeOnly) return
837
+ if (autosaveTimerRef.current) clearTimeout(autosaveTimerRef.current)
838
+ autosaveTimerRef.current = setTimeout(() => {
839
+ autosaveTimerRef.current = null
840
+ void performAutosaveRef.current()
841
+ }, 900)
842
+ }, [definitionId, isCodeOnly])
843
+
844
+ // Clear any pending autosave timer on unmount.
845
+ useEffect(() => () => {
846
+ if (autosaveTimerRef.current) clearTimeout(autosaveTimerRef.current)
847
+ }, [])
848
+
849
+ const applyHistoryDocument = useCallback((document: WorkflowEditorDocument) => {
850
+ setNodes(document.nodes)
851
+ setEdges(document.edges)
852
+ setLoadedMetadata(document.metadata)
853
+ // Entries captured before the panel fields were versioned carry none; then
854
+ // the panel is left exactly as it is, which is the previous behaviour.
855
+ if (document.panel) {
856
+ setTriggers(document.panel.triggers as WorkflowDefinitionTrigger[])
857
+ setContextSchema(document.panel.contextSchema as WorkflowContextSchema | undefined)
858
+ setDefinitionIo(document.panel.io as WorkflowIoContract | undefined)
859
+ setInterpolation(document.panel.interpolation as WorkflowInterpolationMode | undefined)
860
+ setErrorHandler(document.panel.errorHandler as WorkflowErrorHandlerConfig | undefined)
861
+ }
862
+ scheduleAutosave()
863
+ }, [scheduleAutosave])
864
+
865
+ const handleUndo = useCallback(() => {
866
+ if (isCodeOnly) return
867
+ const step = undoEditorHistory(historyRef.current, captureDocument())
868
+ if (!step) {
869
+ flash(t('workflows.visualEditor.history.nothingToUndo', 'Nothing to undo'), 'info')
870
+ return
871
+ }
872
+ setHistory(step.state)
873
+ applyHistoryDocument(step.document)
874
+ flash(t('workflows.visualEditor.history.undone', 'Undone: {label}', { label: step.label }), 'success')
875
+ }, [isCodeOnly, captureDocument, applyHistoryDocument, t])
876
+
877
+ const handleRedo = useCallback(() => {
878
+ if (isCodeOnly) return
879
+ const step = redoEditorHistory(historyRef.current, captureDocument())
880
+ if (!step) {
881
+ flash(t('workflows.visualEditor.history.nothingToRedo', 'Nothing to redo'), 'info')
882
+ return
883
+ }
884
+ setHistory(step.state)
885
+ applyHistoryDocument(step.document)
886
+ flash(t('workflows.visualEditor.history.redone', 'Redone: {label}', { label: step.label }), 'success')
887
+ }, [isCodeOnly, captureDocument, applyHistoryDocument, t])
888
+
889
+ // Copy / paste / duplicate of a selected subgraph (spec §4.5). The payload on
890
+ // the system clipboard is portable JSON in the DEFINITION vocabulary, so it
891
+ // travels to another workflow and to the Code view. When the browser denies
892
+ // clipboard access (insecure context, no permission) the editor falls back to
893
+ // an in-page buffer and says so, rather than failing silently.
894
+ const localClipboardRef = React.useRef<string | null>(null)
895
+
896
+ const selectedSubgraph = useCallback(() => {
897
+ const selectedIds = nodesRef.current.filter((node) => node.selected).map((node) => node.id)
898
+ return serializeWorkflowSubgraph(nodesRef.current, edgesRef.current, selectedIds)
899
+ }, [])
900
+
901
+ const handleCopySelection = useCallback(async () => {
902
+ const payload = selectedSubgraph()
903
+ if (!payload) {
904
+ flash(t('workflows.visualEditor.clipboard.nothingSelected', 'Select one or more steps first'), 'info')
905
+ return
906
+ }
907
+ const serialized = stringifyWorkflowSubgraph(payload)
908
+ localClipboardRef.current = serialized
909
+ try {
910
+ await navigator.clipboard.writeText(serialized)
911
+ flash(
912
+ t('workflows.visualEditor.clipboard.copied', 'Copied {count} step(s)', { count: String(payload.steps.length) }),
913
+ 'success',
914
+ )
915
+ } catch (error) {
916
+ logger.warn('Workflow subgraph copy fell back to the in-page buffer', { err: error })
917
+ flash(
918
+ t(
919
+ 'workflows.visualEditor.clipboard.copyUnavailable',
920
+ 'The browser blocked clipboard access, so the selection was kept in this editor only — pasting into another tab will not work.',
921
+ ),
922
+ 'warning',
923
+ )
924
+ }
925
+ }, [selectedSubgraph, t])
926
+
927
+ const describeClipboardFailure = useCallback((code: SubgraphClipboardParseFailure['code']) => {
928
+ switch (code) {
929
+ case 'invalidJson':
930
+ return t('workflows.visualEditor.clipboard.invalidJson', 'The clipboard does not contain valid JSON.')
931
+ case 'empty':
932
+ return t('workflows.visualEditor.clipboard.empty', 'The copied selection contains no steps.')
933
+ default:
934
+ return t('workflows.visualEditor.clipboard.unsupportedFormat', 'The clipboard does not contain a copied workflow selection.')
935
+ }
936
+ }, [t])
937
+
938
+ const spliceSubgraph = useCallback((payload: WorkflowSubgraphClipboard, label: string) => {
939
+ const result = pasteWorkflowSubgraph(nodesRef.current, edgesRef.current, payload)
940
+ commitHistory(label)
941
+ setNodes(result.nodes)
942
+ setEdges(result.edges)
943
+ scheduleAutosave()
944
+ flash(
945
+ t('workflows.visualEditor.clipboard.pasted', 'Pasted {count} step(s)', { count: String(result.pastedNodeIds.length) }),
946
+ 'success',
947
+ )
948
+ }, [commitHistory, scheduleAutosave, t])
949
+
950
+ const handlePaste = useCallback(async () => {
951
+ if (isCodeOnly) return
952
+ let text: string | null = null
953
+ try {
954
+ text = await navigator.clipboard.readText()
955
+ } catch (error) {
956
+ logger.warn('Workflow subgraph paste fell back to the in-page buffer', { err: error })
957
+ text = localClipboardRef.current
958
+ if (!text) {
959
+ flash(
960
+ t(
961
+ 'workflows.visualEditor.clipboard.pasteUnavailable',
962
+ 'The browser blocked clipboard access, so there is nothing to paste. Copy a selection in this editor first.',
963
+ ),
964
+ 'error',
965
+ )
966
+ return
967
+ }
968
+ }
969
+ const parsed = parseWorkflowSubgraph(text ?? '')
970
+ if (!parsed.ok) {
971
+ flash(describeClipboardFailure(parsed.code), 'error')
972
+ return
973
+ }
974
+ spliceSubgraph(parsed.payload, historyLabels.paste)
975
+ }, [isCodeOnly, describeClipboardFailure, spliceSubgraph, historyLabels.paste, t])
976
+
977
+ const handleDuplicateSelection = useCallback(() => {
978
+ if (isCodeOnly) return
979
+ const payload = selectedSubgraph()
980
+ if (!payload) {
981
+ flash(t('workflows.visualEditor.clipboard.nothingSelected', 'Select one or more steps first'), 'info')
982
+ return
983
+ }
984
+ spliceSubgraph(payload, historyLabels.duplicate)
985
+ }, [isCodeOnly, selectedSubgraph, spliceSubgraph, historyLabels.duplicate, t])
986
+
178
987
  // Handle node changes from ReactFlow. The lazy graph applies React Flow's
179
- // change reducers internally (#3169) and hands back the resolved nodes, so
180
- // this page never imports the @xyflow/react runtime.
181
- const handleNodesChange = useCallback((nextNodes: Node[]) => {
988
+ // change reducers internally (#3169) and hands back the resolved nodes plus a
989
+ // note on what the batch meant, so this page never imports the @xyflow/react
990
+ // runtime. A drag persists once, on drag end (#4248); selecting or measuring
991
+ // a node changes nothing worth saving and must not touch the stored row.
992
+ // A drag reports one change batch per frame, so the undoable "before" state is
993
+ // the arrangement captured when the drag STARTED — not the previous frame.
994
+ const dragBaselineRef = React.useRef<WorkflowEditorDocument | null>(null)
995
+
996
+ const handleNodesChange = useCallback((nextNodes: Node[], meta?: WorkflowGraphNodesChangeMeta) => {
182
997
  if (isCodeOnly) return
998
+ if ((meta?.dragging || meta?.resizing) && !dragBaselineRef.current) dragBaselineRef.current = captureDocument()
999
+ const baseline = dragBaselineRef.current ?? captureDocument()
183
1000
  setNodes(nextNodes)
184
- }, [isCodeOnly])
1001
+ if (meta && !meta.persistable) return
1002
+ dragBaselineRef.current = null
1003
+ commitCapturedDocument(baseline, historyLabels.move)
1004
+ scheduleAutosave()
1005
+ }, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.move, scheduleAutosave])
1006
+
1007
+ // Auto-arrange / Tidy: the single intentional full re-layout. Re-runs dagre
1008
+ // (LR) over the current graph, overwrites positions, and persists via autosave.
1009
+ const handleAutoArrange = useCallback(() => {
1010
+ if (isCodeOnly) return
1011
+ commitHistory(historyLabels.tidy)
1012
+ setNodes((nds) => applyAutoLayout(nds, edges))
1013
+ scheduleAutosave()
1014
+ }, [isCodeOnly, edges, commitHistory, historyLabels.tidy, scheduleAutosave])
185
1015
 
186
1016
  // Handle edge changes from ReactFlow (resolved edges from the lazy graph).
187
1017
  const handleEdgesChange = useCallback((nextEdges: Edge[]) => {
@@ -189,33 +1019,147 @@ export default function VisualEditorPage() {
189
1019
  setEdges(nextEdges)
190
1020
  }, [isCodeOnly])
191
1021
 
192
- // Handle adding new node from palette
193
- const handleAddNode = useCallback((nodeType: string) => {
1022
+ const buildPaletteNode = useCallback((nodeType: string, dropPosition?: { x: number; y: number } | null): Node => ({
1023
+ id: generateStepId(nodeType),
1024
+ type: nodeType,
1025
+ position: resolveNewNodePlacement(nodesRef.current, { dropPosition }),
1026
+ data: {
1027
+ label: getDefaultLabel(nodeType),
1028
+ description: '',
1029
+ badge: getDefaultBadge(nodeType),
1030
+ status: 'pending',
1031
+ },
1032
+ }), [])
1033
+
1034
+ // Handle adding new node from palette. A drop position (drag-from-palette)
1035
+ // places the card under the cursor; the click-append path lands after the
1036
+ // right-most card. Both are nudged clear of existing cards (#4248) and both
1037
+ // count as a manual arrangement, so the placement is persisted like a drag.
1038
+ const handleAddNode = useCallback((nodeType: string, dropPosition?: { x: number; y: number } | null) => {
194
1039
  if (isCodeOnly) return
195
- const newNode: Node = {
196
- id: generateStepId(nodeType),
197
- type: nodeType,
198
- position: {
199
- x: 250 + nodes.length * 50,
200
- y: 100 + nodes.length * 150,
201
- },
202
- data: {
203
- label: getDefaultLabel(nodeType),
204
- description: '',
205
- badge: getDefaultBadge(nodeType),
206
- status: 'pending',
207
- },
1040
+ const newNode = buildPaletteNode(nodeType, dropPosition)
1041
+ commitHistory(historyLabels.addStep)
1042
+ setNodes((nds) => [...nds, newNode])
1043
+ scheduleAutosave()
1044
+ }, [isCodeOnly, buildPaletteNode, commitHistory, historyLabels.addStep, scheduleAutosave])
1045
+
1046
+ // Notes and groups are added from the palette exactly like steps: a click
1047
+ // appends one at a free spot and opens its inspector, so the keyboard path is
1048
+ // the same path.
1049
+ const handleAddAnnotation = useCallback((kind: 'note' | 'group') => {
1050
+ if (isCodeOnly) return
1051
+ const position = resolveNewNodePlacement(nodesRef.current, {})
1052
+ const node = kind === 'note'
1053
+ ? createNoteNode(position, '')
1054
+ : createGroupNode(position, t('workflows.annotations.group.untitled', 'Untitled group'))
1055
+ commitHistory(kind === 'note' ? historyLabels.addNote : historyLabels.addGroup)
1056
+ setNodes((nds) => [...nds, node])
1057
+ setSelectedAnnotation(node)
1058
+ setShowAnnotationDialog(true)
1059
+ scheduleAutosave()
1060
+ }, [isCodeOnly, commitHistory, historyLabels.addNote, historyLabels.addGroup, scheduleAutosave, t])
1061
+
1062
+ const activityTypeOptions = useActivityTypeOptions()
1063
+
1064
+ const describePaletteDropRejection = useCallback((code: PaletteDropRejectionCode) => {
1065
+ switch (code) {
1066
+ case 'dataMappingRoute':
1067
+ return t('workflows.visualEditor.palette.rejected.dataMappingRoute', 'Data mapping links are edited in the step configuration, not on the canvas.')
1068
+ default:
1069
+ return t('workflows.visualEditor.palette.rejected.unknownRoute', 'That route no longer exists.')
208
1070
  }
1071
+ }, [t])
1072
+
1073
+ // Drag-from-palette (spec §4.2). Dropping on empty canvas places the step at
1074
+ // the cursor; dropping on a route splices it between that route's endpoints;
1075
+ // dropping an action on a route appends it to the route's activities. The
1076
+ // graph resolved the flow-space position and the route under the cursor — the
1077
+ // effect itself is pure (`lib/palette-drop.ts`).
1078
+ const handleCanvasDrop = useCallback((event: WorkflowGraphDropEvent) => {
1079
+ if (isCodeOnly) return
1080
+ const item = readPaletteDragItem(event.dataTransfer)
1081
+ if (!item) return
209
1082
 
210
- setNodes((nds) => [...nds, newNode])
211
- }, [nodes.length, isCodeOnly])
1083
+ if (item.kind === 'step') {
1084
+ if (!event.edgeId) {
1085
+ handleAddNode(item.nodeType, event.position)
1086
+ return
1087
+ }
1088
+ const result = insertStepOnRoute(
1089
+ nodesRef.current,
1090
+ edgesRef.current,
1091
+ event.edgeId,
1092
+ buildPaletteNode(item.nodeType, event.position),
1093
+ )
1094
+ if (!result.ok) {
1095
+ flash(describePaletteDropRejection(result.code), 'error')
1096
+ return
1097
+ }
1098
+ commitHistory(historyLabels.insertOnRoute)
1099
+ setNodes(result.nodes)
1100
+ setEdges(result.edges)
1101
+ scheduleAutosave()
1102
+ flash(t('workflows.visualEditor.palette.insertedOnRoute', 'Step inserted on the route'), 'success')
1103
+ return
1104
+ }
1105
+
1106
+ if (!event.edgeId) {
1107
+ flash(t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there'), 'info')
1108
+ return
1109
+ }
1110
+ const activityLabel = activityTypeOptions.find((option) => option.value === item.activityType)?.label ?? item.activityType
1111
+ const result = appendActivityToRoute(edgesRef.current, event.edgeId, {
1112
+ activityId: `activity_${Date.now()}`,
1113
+ activityName: activityLabel,
1114
+ activityType: item.activityType,
1115
+ config: {},
1116
+ })
1117
+ if (!result.ok) {
1118
+ flash(describePaletteDropRejection(result.code), 'error')
1119
+ return
1120
+ }
1121
+ commitHistory(historyLabels.addActivity)
1122
+ setEdges(result.edges)
1123
+ scheduleAutosave()
1124
+ flash(t('workflows.visualEditor.palette.activityAdded', '{activity} added to the route', { activity: activityLabel }), 'success')
1125
+ }, [
1126
+ isCodeOnly,
1127
+ handleAddNode,
1128
+ buildPaletteNode,
1129
+ activityTypeOptions,
1130
+ describePaletteDropRejection,
1131
+ commitHistory,
1132
+ historyLabels.insertOnRoute,
1133
+ historyLabels.addActivity,
1134
+ scheduleAutosave,
1135
+ t,
1136
+ ])
1137
+
1138
+ const handlePaletteStepDragStart = useCallback((event: React.DragEvent<HTMLButtonElement>, nodeType: string) => {
1139
+ writePaletteDragPayload(event.dataTransfer, { kind: 'step', nodeType }, NODE_TYPE_LABELS[nodeType as keyof typeof NODE_TYPE_LABELS]?.title ?? nodeType)
1140
+ }, [])
1141
+
1142
+ const handlePaletteActivityDragStart = useCallback((event: React.DragEvent<HTMLButtonElement>, activityType: string, label: string) => {
1143
+ writePaletteDragPayload(event.dataTransfer, { kind: 'activity', activityType }, label)
1144
+ }, [])
212
1145
 
213
1146
  // Handle node selection - open edit dialog (suppressed in read-only mode
214
1147
  // so users can't open the node editor on a code-defined workflow).
215
1148
  const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
216
1149
  if (isCodeOnly) return
1150
+ if (isAnnotationNode(node)) {
1151
+ setSelectedAnnotation(node)
1152
+ setSelectedNode(null)
1153
+ setSelectedEdge(null)
1154
+ setShowAnnotationDialog(true)
1155
+ return
1156
+ }
217
1157
  setSelectedNode(node)
218
1158
  setSelectedEdge(null)
1159
+ // With a docked rail the canvas stays clickable, so a step click can arrive
1160
+ // while the ROUTE inspector is open. Closing the other one keeps a single
1161
+ // rail; the modal variant could never reach this state.
1162
+ setShowEdgeDialog(false)
219
1163
  setShowNodeDialog(true)
220
1164
  }, [isCodeOnly])
221
1165
 
@@ -224,11 +1168,53 @@ export default function VisualEditorPage() {
224
1168
  if (isCodeOnly) return
225
1169
  setSelectedEdge(edge)
226
1170
  setSelectedNode(null)
1171
+ setEdgeDialogFocusFieldId(null)
1172
+ setShowNodeDialog(false)
1173
+ setShowEdgeDialog(true)
1174
+ }, [isCodeOnly])
1175
+
1176
+ // Route chips (#4244) open the edge dialog on the section behind the chip.
1177
+ // Chips render inside React Flow's label portal, so they announce on `window`
1178
+ // instead of bubbling to `onEdgeClick` — same bridge as the node delete button.
1179
+ const handleRouteChipOpen = useCallback((edgeId: string, section: RouteChipEventDetail['section']) => {
1180
+ if (isCodeOnly) return
1181
+ const edge = edgesRef.current.find((candidate) => candidate.id === edgeId)
1182
+ if (!edge) return
1183
+ setSelectedEdge(edge)
1184
+ setSelectedNode(null)
1185
+ setEdgeDialogFocusFieldId(section === 'condition' ? 'condition' : 'activities')
227
1186
  setShowEdgeDialog(true)
228
1187
  }, [isCodeOnly])
229
1188
 
230
1189
  // Save node updates
231
1190
  const handleSaveNode = useCallback((nodeId: string, updates: Partial<Node['data']>) => {
1191
+ commitHistory(historyLabels.editStep)
1192
+ if (Object.prototype.hasOwnProperty.call(updates, 'decisions')) {
1193
+ const previousNode = nodesRef.current.find((node) => node.id === nodeId)
1194
+ const previousDecisions = previousNode?.data.decisions as DecisionRowLike[] | undefined
1195
+ const nextDecisions = updates.decisions as DecisionRowLike[] | undefined
1196
+ const previousIds = new Set(
1197
+ (previousDecisions ?? []).flatMap((decision) =>
1198
+ typeof decision.transitionId === 'string' ? [decision.transitionId] : []
1199
+ ),
1200
+ )
1201
+ const nextIds = new Set(
1202
+ (nextDecisions ?? []).flatMap((decision) =>
1203
+ typeof decision.transitionId === 'string' ? [decision.transitionId] : []
1204
+ ),
1205
+ )
1206
+ setEdges((currentEdges) =>
1207
+ currentEdges.map((edge) => {
1208
+ if (edge.source !== nodeId) return edge
1209
+ if (nextIds.has(edge.id)) return { ...edge, sourceHandle: edge.id }
1210
+ if (previousIds.has(edge.id) && edge.sourceHandle === edge.id) {
1211
+ const { sourceHandle: _removed, ...remainingEdge } = edge
1212
+ return remainingEdge
1213
+ }
1214
+ return edge
1215
+ }),
1216
+ )
1217
+ }
232
1218
  setNodes((nds) =>
233
1219
  nds.map((node) =>
234
1220
  node.id === nodeId
@@ -237,10 +1223,59 @@ export default function VisualEditorPage() {
237
1223
  )
238
1224
  )
239
1225
  flash('Node updated successfully', 'success')
240
- }, [])
1226
+ }, [commitHistory, historyLabels.editStep])
1227
+
1228
+ // Branching (IF_ELSE / SWITCH) routes are read from and written back to the
1229
+ // node's outgoing edges — the inspector never introduces a bespoke shape.
1230
+ const branchingRoutesValue = useMemo<SwitchRoutesValue | undefined>(() => {
1231
+ if (!selectedNode || !isBranchingNodeType(selectedNode.type)) return undefined
1232
+ return {
1233
+ field: readSwitchField(edges, selectedNode.id),
1234
+ routes: readBranchingRoutes(edges, selectedNode.id),
1235
+ }
1236
+ }, [selectedNode, edges])
1237
+
1238
+ const handleSaveBranchingRoutes = useCallback((nodeId: string, value: SwitchRoutesValue) => {
1239
+ const nodeType = nodes.find((node) => node.id === nodeId)?.type
1240
+ commitHistory(historyLabels.branchRoutes)
1241
+ setEdges((eds) => (nodeType === 'switch' ? applySwitchRoutes(eds, nodeId, value) : applyIfElseRoutes(eds, nodeId, value.routes)))
1242
+ scheduleAutosave()
1243
+ }, [nodes, commitHistory, historyLabels.branchRoutes, scheduleAutosave])
1244
+
1245
+ // Route order (spec 4.4): a non-branching step's outgoing routes are ordered
1246
+ // in the inspector and the priority number is derived from that order.
1247
+ const routeOrderValue = useMemo<RouteOrderEntry[] | undefined>(() => {
1248
+ if (!selectedNode || isBranchingNodeType(selectedNode.type)) return undefined
1249
+ return readRouteOrder(edges, selectedNode.id)
1250
+ }, [selectedNode, edges])
1251
+
1252
+ // Nodes whose legacy priorities have already been rewritten this session. The
1253
+ // normalization pass runs on the FIRST edit of a node's routes and is
1254
+ // idempotent afterwards.
1255
+ const normalizedRouteNodesRef = React.useRef<Set<string>>(new Set())
1256
+
1257
+ // A `source: 'code'` definition is read-only in the editor, so normalization
1258
+ // simply cannot run for it. Customize mints an editable override (source
1259
+ // becomes 'code_override') and the pass runs there on the first route edit —
1260
+ // which is exactly the spec's "only on Customize".
1261
+ const normalizeRoutesOnFirstEdit = useCallback((nodeId: string) => {
1262
+ if (!canNormalizeRoutePriorities(source)) return
1263
+ if (normalizedRouteNodesRef.current.has(nodeId)) return
1264
+ normalizedRouteNodesRef.current.add(nodeId)
1265
+ setEdges((eds) => normalizeRoutePriorities(eds, nodeId))
1266
+ }, [source])
1267
+
1268
+ const handleSaveRouteOrder = useCallback((nodeId: string, entries: RouteOrderEntry[]) => {
1269
+ if (isCodeOnly) return
1270
+ commitHistory(historyLabels.routeOrder)
1271
+ normalizeRoutesOnFirstEdit(nodeId)
1272
+ setEdges((eds) => applyRouteOrder(eds, nodeId, entries))
1273
+ scheduleAutosave()
1274
+ }, [isCodeOnly, normalizeRoutesOnFirstEdit, commitHistory, historyLabels.routeOrder, scheduleAutosave])
241
1275
 
242
1276
  // Save edge updates
243
1277
  const handleSaveEdge = useCallback((edgeId: string, updates: Partial<Edge['data']>) => {
1278
+ commitHistory(historyLabels.editRoute)
244
1279
  setEdges((eds) =>
245
1280
  eds.map((edge) =>
246
1281
  edge.id === edgeId
@@ -249,11 +1284,14 @@ export default function VisualEditorPage() {
249
1284
  )
250
1285
  )
251
1286
  flash('Transition updated successfully', 'success')
252
- }, [])
1287
+ }, [commitHistory, historyLabels.editRoute])
253
1288
 
254
- // Delete edge
1289
+ // Delete edge. The undo snapshot is taken before the confirmation and only
1290
+ // committed once the delete actually lands, so a cancelled confirm leaves no
1291
+ // entry on the stack.
255
1292
  const handleDeleteEdge = useCallback(async (edgeId: string) => {
256
- await performDeleteEdgeFlow(edgeId, {
1293
+ const before = captureDocument()
1294
+ const deleted = await performDeleteEdgeFlow(edgeId, {
257
1295
  confirm,
258
1296
  t,
259
1297
  setShowEdgeDialog,
@@ -261,11 +1299,62 @@ export default function VisualEditorPage() {
261
1299
  setEdges,
262
1300
  notifyDeleted: () => flash('Transition deleted successfully', 'success'),
263
1301
  })
264
- }, [confirm, t])
1302
+ if (deleted) commitCapturedDocument(before, historyLabels.deleteRoute)
1303
+ }, [confirm, t, captureDocument, commitCapturedDocument, historyLabels.deleteRoute])
1304
+
1305
+ // Annotation inspector (spec 4.5). Notes and groups edit a single field each
1306
+ // and commit one undo entry, exactly like a step inspector save.
1307
+ const handleSaveAnnotation = useCallback((nodeId: string, updates: Partial<WorkflowNoteNodeData & WorkflowGroupNodeData>) => {
1308
+ if (isCodeOnly) return
1309
+ commitHistory(historyLabels.editAnnotation)
1310
+ setNodes((nds) => updateAnnotationNode(nds, nodeId, updates))
1311
+ scheduleAutosave()
1312
+ }, [isCodeOnly, commitHistory, historyLabels.editAnnotation, scheduleAutosave])
1313
+
1314
+ const handleDeleteAnnotation = useCallback(async (nodeId: string) => {
1315
+ if (isCodeOnly) return
1316
+ const before = captureDocument()
1317
+ setShowAnnotationDialog(false)
1318
+ setSelectedAnnotation(null)
1319
+ const confirmed = await confirm({
1320
+ title: t('workflows.annotations.confirmDeleteTitle', 'Delete annotation?'),
1321
+ text: t('workflows.annotations.confirmDeleteText', 'The note or group is removed from the canvas. Nothing the workflow executes changes.'),
1322
+ variant: 'destructive',
1323
+ })
1324
+ if (!confirmed) return
1325
+ commitCapturedDocument(before, historyLabels.deleteAnnotation)
1326
+ setNodes((nds) => removeAnnotationNode(nds, nodeId))
1327
+ scheduleAutosave()
1328
+ }, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.deleteAnnotation, confirm, scheduleAutosave, t])
1329
+
1330
+ const handleToggleGroupCollapsed = useCallback((nodeId: string) => {
1331
+ if (isCodeOnly) return
1332
+ const node = nodesRef.current.find((candidate) => candidate.id === nodeId)
1333
+ if (!node) return
1334
+ const collapsed = (node.data as Partial<WorkflowGroupNodeData> | undefined)?.collapsed === true
1335
+ commitHistory(historyLabels.toggleGroup)
1336
+ setNodes((nds) => updateAnnotationNode(nds, nodeId, { collapsed: !collapsed }))
1337
+ scheduleAutosave()
1338
+ }, [isCodeOnly, commitHistory, historyLabels.toggleGroup, scheduleAutosave])
1339
+
1340
+ useEffect(() => {
1341
+ const onToggle = (event: Event) => {
1342
+ const nodeId = (event as CustomEvent<{ nodeId?: string }>).detail?.nodeId
1343
+ if (typeof nodeId === 'string') handleToggleGroupCollapsed(nodeId)
1344
+ }
1345
+ window.addEventListener(WORKFLOW_GROUP_TOGGLE_EVENT, onToggle)
1346
+ return () => window.removeEventListener(WORKFLOW_GROUP_TOGGLE_EVENT, onToggle)
1347
+ }, [handleToggleGroupCollapsed])
265
1348
 
266
1349
  // Delete node
267
1350
  const handleDeleteNode = useCallback(async (nodeId: string) => {
268
- await performDeleteNodeFlow(nodeId, {
1351
+ const annotation = nodesRef.current.find((candidate) => candidate.id === nodeId && isAnnotationNode(candidate))
1352
+ if (annotation) {
1353
+ await handleDeleteAnnotation(nodeId)
1354
+ return
1355
+ }
1356
+ const before = captureDocument()
1357
+ const deleted = await performDeleteNodeFlow(nodeId, {
269
1358
  nodes,
270
1359
  confirm,
271
1360
  t,
@@ -275,102 +1364,746 @@ export default function VisualEditorPage() {
275
1364
  setEdges,
276
1365
  notifyDeleted: () => flash('Step deleted successfully', 'success'),
277
1366
  })
278
- }, [confirm, nodes, t])
1367
+ if (deleted) commitCapturedDocument(before, historyLabels.deleteStep)
1368
+ }, [confirm, nodes, t, captureDocument, commitCapturedDocument, historyLabels.deleteStep, handleDeleteAnnotation])
1369
+
1370
+ // In-place step type conversion (spec 4.5, #4237). The step keeps its id,
1371
+ // name, position and every incoming/outgoing route; config the new type
1372
+ // cannot execute is quarantined by `convertStepType` and stays visible in the
1373
+ // inspector. The dialog closes before the confirm so only one modal is on
1374
+ // screen (same reason as the delete flow).
1375
+ const handleConvertNodeType = useCallback(async (nodeId: string, targetType: ConvertibleStepType) => {
1376
+ if (isCodeOnly) return
1377
+ const node = nodesRef.current.find((candidate) => candidate.id === nodeId)
1378
+ if (!node) return
1379
+
1380
+ const before = captureDocument()
1381
+ const result = convertStepType({ type: node.type, data: node.data as Record<string, unknown> }, targetType)
1382
+ if (!result.ok) {
1383
+ flash(t('workflows.stepConversion.unavailable', 'This step type cannot be changed.'), 'error')
1384
+ return
1385
+ }
279
1386
 
280
- // Handle new connections
1387
+ setShowNodeDialog(false)
1388
+ setSelectedNode(null)
1389
+ const confirmed = await confirm({
1390
+ title: t('workflows.stepConversion.confirmTitle', 'Change step type?'),
1391
+ text: result.quarantined.length > 0
1392
+ ? t(
1393
+ 'workflows.stepConversion.confirmWithQuarantine',
1394
+ 'The step keeps its name, position and routes. {count} setting(s) the new type cannot execute move to Unmapped configuration: {keys}',
1395
+ { count: String(result.quarantined.length), keys: result.quarantined.join(', ') },
1396
+ )
1397
+ : t('workflows.stepConversion.confirm', 'The step keeps its name, position and routes.'),
1398
+ })
1399
+ if (!confirmed) return
1400
+
1401
+ commitCapturedDocument(before, historyLabels.convert)
1402
+ setNodes((nds) =>
1403
+ nds.map((candidate) =>
1404
+ candidate.id === nodeId
1405
+ ? { ...candidate, type: result.type, data: { ...result.data, badge: getBadgeForNodeType(result.type) } }
1406
+ : candidate,
1407
+ ),
1408
+ )
1409
+ scheduleAutosave()
1410
+ flash(
1411
+ result.quarantined.length > 0
1412
+ ? t('workflows.stepConversion.convertedWithQuarantine', 'Step type changed; {count} setting(s) parked as unmapped configuration', { count: String(result.quarantined.length) })
1413
+ : t('workflows.stepConversion.converted', 'Step type changed'),
1414
+ result.quarantined.length > 0 ? 'warning' : 'success',
1415
+ )
1416
+ }, [isCodeOnly, confirm, captureDocument, commitCapturedDocument, historyLabels.convert, scheduleAutosave, t])
1417
+
1418
+ // Inline node delete: a node's trash button dispatches WORKFLOW_NODE_DELETE_EVENT
1419
+ // (decoupled from the node component); route it through the same confirm +
1420
+ // edge-cleanup flow as the edit dialog's delete. No-op in read-only mode.
1421
+ useEffect(() => {
1422
+ if (isCodeOnly) return
1423
+ const onNodeDelete = (event: Event) => {
1424
+ const nodeId = (event as CustomEvent<{ nodeId?: string }>).detail?.nodeId
1425
+ if (typeof nodeId === 'string') void handleDeleteNode(nodeId)
1426
+ }
1427
+ window.addEventListener(WORKFLOW_NODE_DELETE_EVENT, onNodeDelete)
1428
+ return () => window.removeEventListener(WORKFLOW_NODE_DELETE_EVENT, onNodeDelete)
1429
+ }, [isCodeOnly, handleDeleteNode])
1430
+
1431
+ useEffect(() => {
1432
+ const onChipOpen = (event: Event) => {
1433
+ const detail = (event as CustomEvent<RouteChipEventDetail>).detail
1434
+ if (detail?.edgeId) handleRouteChipOpen(detail.edgeId, detail.section)
1435
+ }
1436
+ window.addEventListener(WORKFLOW_ROUTE_CHIP_EVENT, onChipOpen)
1437
+ return () => window.removeEventListener(WORKFLOW_ROUTE_CHIP_EVENT, onChipOpen)
1438
+ }, [handleRouteChipOpen])
1439
+
1440
+ useEffect(() => {
1441
+ const onActivityOpen = (event: Event) => {
1442
+ if (isCodeOnly) return
1443
+ const detail = (event as CustomEvent<RouteActivityEventDetail>).detail
1444
+ if (detail?.edgeId && detail?.activityId) {
1445
+ setActivityEdit({ edgeId: detail.edgeId, activityId: detail.activityId })
1446
+ }
1447
+ }
1448
+ window.addEventListener(WORKFLOW_ROUTE_ACTIVITY_EVENT, onActivityOpen)
1449
+ return () => window.removeEventListener(WORKFLOW_ROUTE_ACTIVITY_EVENT, onActivityOpen)
1450
+ }, [isCodeOnly])
1451
+
1452
+ // Handle new connections. A drop onto a sub-workflow IN port authors a field
1453
+ // mapping (written to the target step's config.inputMapping + a distinct data
1454
+ // edge); a plain handle-to-handle connection stays a control-flow transition.
281
1455
  const handleConnect = useCallback((connection: Connection) => {
282
- const newEdge: Edge = {
283
- id: generateTransitionId(connection.source!, connection.target!),
1456
+ const classification = classifyConnection(connection)
1457
+
1458
+ if (classification.kind === 'data-ignored') {
1459
+ return
1460
+ }
1461
+
1462
+ if (classification.kind === 'data-mapping') {
1463
+ const { targetNodeId, childPortKey, parentPath } = classification
1464
+ commitHistory(historyLabels.connect)
1465
+ setNodes((nds) => applyInputMappingToNodes(nds, targetNodeId, childPortKey, parentPath))
1466
+ const dataEdge = buildDataMappingEdge(connection, childPortKey)
1467
+ setEdges((eds) => appendWorkflowEdge(eds.filter((e) => e.id !== dataEdge.id), dataEdge))
1468
+ return
1469
+ }
1470
+
1471
+ // A connection drawn from one of a node's kinded output handles authors that
1472
+ // kind of route (spec 5.9 error routes, SLA-breach routes, …): normal
1473
+ // routing never selects one, the engine follows it only down its own path.
1474
+ const routeKind = findRouteKindDescriptorForHandle(connection.sourceHandle)
1475
+ const sourceNode = nodesRef.current.find((node) => node.id === connection.source)
1476
+ const decisions = sourceNode?.data.decisions as DecisionRowLike[] | undefined
1477
+ const decisionTransitionId = isDecisionSourceHandle(decisions, connection.sourceHandle)
1478
+ ? connection.sourceHandle
1479
+ : null
1480
+ if (
1481
+ decisionTransitionId &&
1482
+ edgesRef.current.some(
1483
+ (edge) =>
1484
+ edge.source === connection.source &&
1485
+ (edge.id === decisionTransitionId || edge.sourceHandle === decisionTransitionId),
1486
+ )
1487
+ ) {
1488
+ return
1489
+ }
1490
+
1491
+ const routeData = routeKind
1492
+ ? {
1493
+ kind: routeKind.kind,
1494
+ ...routeKind.discriminatorFields({
1495
+ sourceHandle: connection.sourceHandle,
1496
+ }),
1497
+ }
1498
+ : {}
1499
+
1500
+ // A drawn route is the same object a saved one loads as, so it takes the
1501
+ // workflow edge renderer (a curve) immediately rather than on the next
1502
+ // reload — see `lib/route-edge.ts`.
1503
+ const newEdge: Edge = buildWorkflowRouteEdge({
1504
+ id: decisionTransitionId ?? generateTransitionId(),
284
1505
  source: connection.source!,
285
1506
  target: connection.target!,
286
- type: 'smoothstep',
1507
+ sourceHandle: routeKind ? connection.sourceHandle : decisionTransitionId,
287
1508
  data: {
288
1509
  trigger: 'auto',
289
1510
  preConditions: [],
290
1511
  postConditions: [],
291
1512
  activities: [],
292
1513
  label: '',
1514
+ ...routeData,
293
1515
  },
294
- }
1516
+ })
295
1517
 
1518
+ commitHistory(historyLabels.connect)
296
1519
  setEdges((eds) => appendWorkflowEdge(eds, newEdge))
297
- }, [])
1520
+ }, [commitHistory, historyLabels.connect])
1521
+
1522
+ // Route reattachment (#4233): dropping an existing route endpoint on another
1523
+ // node re-targets it. The route keeps its durable transitionId, so its label,
1524
+ // condition, activities and priority travel with it. A refused target leaves
1525
+ // the edge list untouched, which is what snaps the endpoint back — the reason
1526
+ // is surfaced instead of silently reverting.
1527
+ const describeReattachRejection = useCallback((rejection: EdgeReattachRejection) => {
1528
+ switch (rejection.code) {
1529
+ case 'selfLoop':
1530
+ return t('workflows.reattach.rejected.selfLoop', 'A route cannot start and end on the same step.')
1531
+ case 'duplicateRoute':
1532
+ return t('workflows.reattach.rejected.duplicateRoute', 'A route already connects those two steps.')
1533
+ case 'dataMappingRoute':
1534
+ return t('workflows.reattach.rejected.dataMappingRoute', 'Data mapping links are edited in the step configuration, not on the canvas.')
1535
+ case 'errorHandleUnsupported':
1536
+ return t('workflows.reattach.rejected.errorHandleUnsupported', 'An error route can only start at a step that can fail.')
1537
+ case 'kindHandleUnsupported':
1538
+ return t('workflows.reattach.rejected.kindHandleUnsupported', 'This route cannot start at that type of step.')
1539
+ case 'graphInvalid':
1540
+ return t('workflows.reattach.rejected.graphInvalid', 'That target would break the workflow: {reason}', { reason: rejection.detail ?? '' })
1541
+ case 'forkJoinInvalid':
1542
+ return t('workflows.reattach.rejected.forkJoinInvalid', 'That target would break the parallel branch structure: {reason}', { reason: rejection.detail ?? '' })
1543
+ default:
1544
+ return t('workflows.reattach.rejected.unsupported', 'That route cannot be re-targeted.')
1545
+ }
1546
+ }, [t])
1547
+
1548
+ const handleReconnect = useCallback((oldEdge: Edge, connection: Connection) => {
1549
+ if (isCodeOnly) return
1550
+ const result = reattachWorkflowEdge(nodesRef.current, edgesRef.current, oldEdge.id, connection)
1551
+ if (!result.ok) {
1552
+ flash(describeReattachRejection(result), 'error')
1553
+ return
1554
+ }
1555
+ commitHistory(historyLabels.reattach)
1556
+ setEdges(result.edges)
1557
+ scheduleAutosave()
1558
+ flash(t('workflows.reattach.retargeted', 'Route re-targeted'), 'success')
1559
+ }, [isCodeOnly, describeReattachRejection, commitHistory, historyLabels.reattach, scheduleAutosave, t])
1560
+
1561
+ // Typed trigger contextMapping targets (spec section 3.1, step 1.9): the
1562
+ // ledger stays pure, so trigger event payload contracts are pre-resolved
1563
+ // here from the already-fetched declared-events list (which carries
1564
+ // payloadSchema since step 1.7) and passed in as plain data. Schema-less or
1565
+ // wildcard triggers get no contract and their mapping targets stay unknown.
1566
+ // Handler-step candidates for the definition-level error handler: every step
1567
+ // that can actually receive the run (START/END are not recovery targets).
1568
+ const errorHandlerStepOptions = useMemo(
1569
+ () =>
1570
+ nodes
1571
+ .filter((node) => node.type !== 'start' && node.type !== 'end')
1572
+ .map((node) => ({
1573
+ stepId: node.id,
1574
+ label: typeof node.data?.label === 'string' && node.data.label ? node.data.label : node.id,
1575
+ })),
1576
+ [nodes],
1577
+ )
1578
+
1579
+ const { events: availableEvents } = useAvailableEvents()
1580
+ const triggerPayloadContracts = useMemo(
1581
+ () => buildTriggerPayloadContracts(triggers, availableEvents),
1582
+ [triggers, availableEvents],
1583
+ )
1584
+
1585
+ // Ledger entries for the variable picker in the open edit dialog (spec
1586
+ // section 3.5, step 3.2). Computed lazily — only while a dialog is open —
1587
+ // with the same client-side 'unknown'-contract ledger the Problems warnings
1588
+ // use, so the picker never offers a path the ref checker would then flag.
1589
+ // Node dialogs get the edited step's incoming view; edge dialogs get the
1590
+ // TARGET step's incoming view, matching the transition scope rule in
1591
+ // lib/expression-refs.ts.
1592
+ const dialogLedger = useMemo(() => {
1593
+ if (!showNodeDialog && !showEdgeDialog) return null
1594
+ try {
1595
+ const definitionData = buildDefinitionPayload({
1596
+ graphDefinition: graphToDefinition(nodes, edges),
1597
+ triggers,
1598
+ contextSchema,
1599
+ io: definitionIo,
1600
+ })
1601
+ return computeClientContextLedger(definitionData, triggerPayloadContracts)
1602
+ } catch {
1603
+ return null
1604
+ }
1605
+ }, [showNodeDialog, showEdgeDialog, nodes, edges, triggers, contextSchema, definitionIo, triggerPayloadContracts])
1606
+
1607
+ // A step nobody has wired yet has no incoming route, so the fixpoint leaves
1608
+ // its ledger empty and the Input data panel opened on nothing at all (issue
1609
+ // #5988). What such a step will receive once it is wired is exactly what the
1610
+ // run starts with — the START step's ledger IS that initial context, since it
1611
+ // has no incoming transitions either — so an unwired step borrows it rather
1612
+ // than showing the author an empty picker.
1613
+ const nodeDialogLedgerEntries = useMemo(() => {
1614
+ if (!dialogLedger || !selectedNode) return undefined
1615
+ const stepEntries = dialogLedger.steps[selectedNode.id]?.entries
1616
+ if (stepEntries && stepEntries.length > 0) return stepEntries
1617
+ const startNode = nodes.find((candidate) => candidate.type === 'start')
1618
+ const startEntries = startNode ? dialogLedger.steps[startNode.id]?.entries : undefined
1619
+ return startEntries && startEntries.length > 0 ? startEntries : stepEntries
1620
+ }, [dialogLedger, nodes, selectedNode])
1621
+
1622
+ // Pinned per-step samples carried inside metadata.editor.samples (spec
1623
+ // section 3.6, step 4.4). The page owns the metadata object, so pin/unpin
1624
+ // write through setLoadedMetadata and the explicit Save (and draft
1625
+ // autosave) persist them via buildMetadataPayload's spread.
1626
+ const editorSamples = useMemo(() => {
1627
+ const editorValue = loadedMetadata?.editor
1628
+ if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return undefined
1629
+ const samplesValue = (editorValue as Record<string, unknown>).samples
1630
+ if (!samplesValue || typeof samplesValue !== 'object' || Array.isArray(samplesValue)) return undefined
1631
+ return samplesValue as Record<string, PinnedSampleEnvelope>
1632
+ }, [loadedMetadata])
1633
+
1634
+ const handlePinSample = useCallback((stepId: string, data: unknown) => {
1635
+ commitHistory(historyLabels.pinSample)
1636
+ setLoadedMetadata((previous) => {
1637
+ const base: Record<string, unknown> = { ...(previous ?? {}) }
1638
+ const editorValue = base.editor
1639
+ const editor: Record<string, unknown> =
1640
+ editorValue && typeof editorValue === 'object' && !Array.isArray(editorValue)
1641
+ ? { ...(editorValue as Record<string, unknown>) }
1642
+ : {}
1643
+ const samplesValue = editor.samples
1644
+ const samples: Record<string, unknown> =
1645
+ samplesValue && typeof samplesValue === 'object' && !Array.isArray(samplesValue)
1646
+ ? { ...(samplesValue as Record<string, unknown>) }
1647
+ : {}
1648
+ samples[stepId] = { pinnedAt: new Date().toISOString(), source: 'test', data }
1649
+ editor.samples = samples
1650
+ base.editor = editor
1651
+ return base
1652
+ })
1653
+ flash(t('workflows.testStep.pinnedFlash', 'Sample pinned — it is stored with the definition on save'), 'success')
1654
+ }, [commitHistory, historyLabels.pinSample, t])
1655
+
1656
+ const handleUnpinSample = useCallback((stepId: string) => {
1657
+ commitHistory(historyLabels.unpinSample)
1658
+ setLoadedMetadata((previous) => {
1659
+ if (!previous) return previous
1660
+ const editorValue = previous.editor
1661
+ if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return previous
1662
+ const editor = { ...(editorValue as Record<string, unknown>) }
1663
+ const samplesValue = editor.samples
1664
+ if (!samplesValue || typeof samplesValue !== 'object' || Array.isArray(samplesValue)) return previous
1665
+ const samples = { ...(samplesValue as Record<string, unknown>) }
1666
+ if (!(stepId in samples)) return previous
1667
+ delete samples[stepId]
1668
+ if (Object.keys(samples).length > 0) editor.samples = samples
1669
+ else delete editor.samples
1670
+ const base = { ...previous }
1671
+ if (Object.keys(editor).length > 0) base.editor = editor
1672
+ else delete base.editor
1673
+ return Object.keys(base).length > 0 ? base : null
1674
+ })
1675
+ }, [commitHistory, historyLabels.unpinSample])
1676
+
1677
+ // Named START contexts (spec section 8.1). They live beside samples in
1678
+ // metadata.editor, but they are a DIFFERENT thing — a whole start context,
1679
+ // not per-step data — so they get their own key and their own cap.
1680
+ const startFixtures = useMemo(() => {
1681
+ const editorValue = loadedMetadata?.editor
1682
+ if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return undefined
1683
+ const fixturesValue = (editorValue as Record<string, unknown>).fixtures
1684
+ if (!fixturesValue || typeof fixturesValue !== 'object' || Array.isArray(fixturesValue)) return undefined
1685
+ return fixturesValue as WorkflowStartFixtures
1686
+ }, [loadedMetadata])
1687
+
1688
+ const startFixtureList = useMemo(() => listStartFixtures(startFixtures), [startFixtures])
1689
+
1690
+ const handleSaveFixture = useCallback(() => {
1691
+ let context: Record<string, unknown>
1692
+ try {
1693
+ const parsed = startContext.trim() ? JSON.parse(startContext) : {}
1694
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1695
+ throw new Error('[internal] context must be a JSON object')
1696
+ }
1697
+ context = parsed as Record<string, unknown>
1698
+ } catch {
1699
+ flash(t('workflows.startInstance.invalidJson'), 'error')
1700
+ return
1701
+ }
298
1702
 
299
- // Validate workflow
300
- const handleValidate = useCallback(() => {
1703
+ const result = upsertStartFixture(startFixtures, fixtureName, context, new Date().toISOString())
1704
+ if (!result.ok) {
1705
+ flash(t(`workflows.fixtures.errors.${result.reason}`), 'error')
1706
+ return
1707
+ }
1708
+
1709
+ commitHistory(historyLabels.saveFixture)
1710
+ setLoadedMetadata((previous) => {
1711
+ const base: Record<string, unknown> = { ...(previous ?? {}) }
1712
+ const editorValue = base.editor
1713
+ const editor: Record<string, unknown> =
1714
+ editorValue && typeof editorValue === 'object' && !Array.isArray(editorValue)
1715
+ ? { ...(editorValue as Record<string, unknown>) }
1716
+ : {}
1717
+ editor.fixtures = result.fixtures
1718
+ base.editor = editor
1719
+ return base
1720
+ })
1721
+ setFixtureName('')
1722
+ flash(t('workflows.fixtures.saved'), 'success')
1723
+ }, [startContext, startFixtures, fixtureName, commitHistory, historyLabels.saveFixture, t])
1724
+
1725
+ const handleApplyFixture = useCallback((name: string) => {
1726
+ const fixture = startFixtures?.[name]
1727
+ if (!fixture) return
1728
+ setStartContext(JSON.stringify(fixture.context, null, 2))
1729
+ setFixtureName(fixture.name)
1730
+ }, [startFixtures])
1731
+
1732
+ const handleDeleteFixture = useCallback((name: string) => {
1733
+ commitHistory(historyLabels.deleteFixture)
1734
+ setLoadedMetadata((previous) => {
1735
+ if (!previous) return previous
1736
+ const editorValue = previous.editor
1737
+ if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return previous
1738
+ const editor = { ...(editorValue as Record<string, unknown>) }
1739
+ const next = removeStartFixture(editor.fixtures as WorkflowStartFixtures | undefined, name)
1740
+ if (Object.keys(next).length > 0) editor.fixtures = next
1741
+ else delete editor.fixtures
1742
+ const base = { ...previous }
1743
+ if (Object.keys(editor).length > 0) base.editor = editor
1744
+ else delete base.editor
1745
+ return Object.keys(base).length > 0 ? base : null
1746
+ })
1747
+ }, [commitHistory, historyLabels.deleteFixture])
1748
+
1749
+ const edgeDialogLedgerEntries = useMemo(
1750
+ () => (dialogLedger && selectedEdge ? dialogLedger.steps[selectedEdge.target]?.entries : undefined),
1751
+ [dialogLedger, selectedEdge],
1752
+ )
1753
+
1754
+ // Bridge the pure issue collector to the page's i18n. `t` already accepts a
1755
+ // fallback plus params, so the flow-logic messages stay translatable without
1756
+ // the collector depending on React.
1757
+ const translateIssue = useCallback<WorkflowIssueTranslator>(
1758
+ (key, fallback, params) => t(key, fallback, params),
1759
+ [t],
1760
+ )
1761
+
1762
+ // Collect every graph and schema issue for the current document. Shared by the
1763
+ // explicit Validate action and the Code view's JSON-schema validation display
1764
+ // (spec §2.2), so both surfaces can never disagree about what is wrong.
1765
+ const evaluateWorkflowIssues = useCallback((): WorkflowValidationIssue[] => {
301
1766
  const graphErrors = validateWorkflowGraph(nodes, edges)
302
- const allErrors: ValidationError[] = [...graphErrors]
1767
+ let zodIssues: ZodIssueLike[] = []
1768
+ let configWarnings: ZodIssueLike[] = []
1769
+ let schemaFailureMessage: string | null = null
1770
+ let flowLogicDefinition: WorkflowDefinitionData | null = null
1771
+ let flowLogicLedger: ReturnType<typeof computeClientContextLedger> | undefined
303
1772
 
304
- // Run Zod schema validation
305
1773
  try {
306
1774
  const definitionData = graphToDefinition(nodes, edges, { includePositions: true })
307
1775
  const result = workflowDefinitionDataSchema.safeParse(definitionData)
308
-
309
1776
  if (!result.success) {
310
- // Convert Zod errors to validation errors
311
- result.error.issues.forEach((issue) => {
312
- allErrors.push({
313
- type: 'error',
314
- message: `Schema validation: ${issue.path.join('.')} - ${issue.message}`,
315
- })
316
- })
1777
+ zodIssues = result.error.issues
317
1778
  }
1779
+ const ledgerDefinition = buildDefinitionPayload({ graphDefinition: definitionData, triggers, contextSchema, io: definitionIo, interpolation, errorHandler })
1780
+ flowLogicDefinition = ledgerDefinition
1781
+ flowLogicLedger = computeClientContextLedger(ledgerDefinition, triggerPayloadContracts)
1782
+ configWarnings = [
1783
+ ...collectActivityConfigWarnings(definitionData),
1784
+ ...collectContextRefWarnings(ledgerDefinition, t, triggerPayloadContracts),
1785
+ ]
318
1786
  } catch (error) {
319
- allErrors.push({
320
- type: 'error',
321
- message: `Schema validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
322
- })
1787
+ schemaFailureMessage = error instanceof Error ? error.message : String(error)
323
1788
  }
324
1789
 
325
- if (allErrors.length === 0) {
326
- flash('Validation passed! Your workflow is valid and ready to save.', 'success')
1790
+ const issues = collectValidationIssues({
1791
+ graphErrors,
1792
+ zodIssues,
1793
+ configWarnings,
1794
+ nodes,
1795
+ edges,
1796
+ definition: flowLogicDefinition,
1797
+ ledger: flowLogicLedger,
1798
+ translate: translateIssue,
1799
+ })
1800
+ if (schemaFailureMessage) {
1801
+ issues.unshift({
1802
+ id: 'schema-exception',
1803
+ severity: 'error',
1804
+ message: t('workflows.visualEditor.problems.schemaValidationFailed', 'Schema validation failed: {message}', { message: schemaFailureMessage }),
1805
+ })
1806
+ }
1807
+ return issues
1808
+ }, [nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, triggerPayloadContracts, translateIssue, t])
1809
+
1810
+ // Validate workflow — collect every graph and schema issue into the problems
1811
+ // panel. Shared by the explicit Validate action and by the "Problems pass
1812
+ // already run" an applied AI draft owes the author (spec §9).
1813
+ const runProblemsPass = useCallback(() => {
1814
+ const issues = evaluateWorkflowIssues()
1815
+ setProblems(issues)
1816
+
1817
+ if (issues.length === 0) {
1818
+ setShowProblems(false)
1819
+ flash(t('workflows.visualEditor.problems.validationPassed', 'Validation passed! Your workflow is valid and ready to save.'), 'success')
327
1820
  } else {
328
- // Show first error/warning message
329
- const firstError = allErrors[0]
330
- const errorCount = allErrors.length
331
- const message = errorCount > 1
332
- ? `${firstError.message} (and ${errorCount - 1} more ${errorCount === 2 ? 'issue' : 'issues'})`
333
- : firstError.message
334
- flash(message, firstError.type === 'error' ? 'error' : 'warning')
1821
+ setShowProblems(true)
1822
+ setProblemsCollapsed(false)
1823
+ const { errors, warnings } = countIssuesBySeverity(issues)
1824
+ flash(
1825
+ t('workflows.visualEditor.problems.summary', 'Validation found {errors} error(s) and {warnings} warning(s).', { errors, warnings }),
1826
+ errors > 0 ? 'error' : 'warning',
1827
+ )
1828
+ }
1829
+ }, [evaluateWorkflowIssues, t])
1830
+
1831
+ const handleValidate = runProblemsPass
1832
+
1833
+ // Code view, stage 1 (spec §2.2). The JSON is assembled through the same
1834
+ // builders Save uses, so what an author reads is exactly what is persisted;
1835
+ // it is only computed while the panel is open.
1836
+ const codeViewJson = useMemo(() => {
1837
+ if (!showCodeView) return ''
1838
+ try {
1839
+ const definitionData = buildDefinitionPayload({
1840
+ graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
1841
+ triggers,
1842
+ contextSchema,
1843
+ io: definitionIo,
1844
+ interpolation,
1845
+ errorHandler,
1846
+ })
1847
+ return JSON.stringify(definitionData, null, 2)
1848
+ } catch (error) {
1849
+ return t(
1850
+ 'workflows.visualEditor.codeView.assemblyFailed',
1851
+ 'The definition could not be assembled: {message}',
1852
+ { message: error instanceof Error ? error.message : String(error) },
1853
+ )
335
1854
  }
336
- }, [nodes, edges])
1855
+ }, [showCodeView, nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, t])
1856
+
1857
+ const codeViewIssues = useMemo(
1858
+ () => (showCodeView ? evaluateWorkflowIssues() : []),
1859
+ [showCodeView, evaluateWorkflowIssues],
1860
+ )
1861
+
1862
+ // Code view stage 2 (spec §2.2). Safety model, stated once:
1863
+ // canvas → code is LIVE (the panel re-renders from the canvas whenever the
1864
+ // author has not started editing); code → canvas needs an explicit Apply.
1865
+ // Parsing, validation and the gutter markers stay live as you type, so the
1866
+ // feedback loop is immediate even though the commit is not — mutating the
1867
+ // canvas per keystroke would delete every node the moment "steps" is
1868
+ // mid-rename, and push one undo entry per character.
1869
+ const [codeDraft, setCodeDraft] = useState<string | null>(null)
1870
+ const codeDraftText = codeDraft ?? codeViewJson
1871
+
1872
+ // The ONE gate a whole definition passes before it may replace the document.
1873
+ // Extracted so the Code view's Apply and the AI draft's Apply (spec §9) share
1874
+ // it verbatim — an AI-authored graph has earned exactly as much trust as a
1875
+ // pasted one, which is to say none.
1876
+ const definitionApplyGate = useMemo(() => ({
1877
+ parseDefinition: (parsed: unknown) => {
1878
+ const result = workflowDefinitionDataSchema.safeParse(parsed)
1879
+ if (result.success) return { ok: true as const, definition: result.data as WorkflowDefinitionData }
1880
+ return {
1881
+ ok: false as const,
1882
+ messages: result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`),
1883
+ }
1884
+ },
1885
+ // Warnings never block, exactly as they never block a Save; only graph
1886
+ // ERRORS do, because a canvas holding a graph the engine rejects is worse
1887
+ // than no apply — the author has lost the text they typed.
1888
+ // `autoLayout: false` on purpose: whether a definition is VALID cannot
1889
+ // depend on running a layout engine over it, and skipping dagre keeps this
1890
+ // memo cheap enough to run on every keystroke.
1891
+ validateGraph: (definition: WorkflowDefinitionData) => {
1892
+ const graph = definitionToGraph(definition, { autoLayout: false })
1893
+ return validateWorkflowGraph(graph.nodes, graph.edges)
1894
+ .filter((issue) => issue.type === 'error')
1895
+ .map((issue) => issue.message)
1896
+ },
1897
+ }), [])
1898
+
1899
+ const codeApplyDecision = useMemo(
1900
+ () => evaluateCodeViewDraft<WorkflowDefinitionData>({
1901
+ draftText: codeDraftText,
1902
+ canvasText: codeViewJson,
1903
+ ...definitionApplyGate,
1904
+ }),
1905
+ [codeDraftText, codeViewJson, definitionApplyGate],
1906
+ )
1907
+
1908
+ const codeDraftStatus = useMemo(() => describeCodeViewDraft(codeApplyDecision), [codeApplyDecision])
1909
+
1910
+ const codeIssueLocations = useMemo(() => {
1911
+ if (!showCodeView) return { severityByLine: new Map<number, 'error' | 'warning'>(), lineByIssueId: new Map<string, number>() }
1912
+ let parsed: unknown
1913
+ try {
1914
+ parsed = JSON.parse(codeDraftText)
1915
+ } catch {
1916
+ // Unparseable text has no entity positions to point at; the parse-error
1917
+ // line is marked on its own by `codeDraftStatus`.
1918
+ return { severityByLine: new Map<number, 'error' | 'warning'>(), lineByIssueId: new Map<string, number>() }
1919
+ }
1920
+ const locations = locateDefinitionJsonEntities(codeDraftText, parsed)
1921
+ const located = locateIssues(codeViewIssues, locations)
1922
+ return {
1923
+ severityByLine: severityByLine(located),
1924
+ lineByIssueId: new Map(located.map((issue) => [issue.issueId, issue.line])),
1925
+ }
1926
+ }, [showCodeView, codeDraftText, codeViewIssues])
1927
+
1928
+ // Replace the whole editor document from a validated definition, as ONE undo
1929
+ // entry named `label`. Shared by the Code view's Apply and the AI draft's
1930
+ // Apply so a twenty-node generation is exactly as reversible as a paste.
1931
+ const applyDefinitionDocument = useCallback((definition: WorkflowDefinitionData, label: string) => {
1932
+ // Committed BEFORE the replacement, so undo restores the graph AND the
1933
+ // panel fields the applied definition also carried.
1934
+ commitHistory(label)
1935
+ const graph = definitionToGraph(definition, { autoLayout: true })
1936
+ setNodes(graph.nodes)
1937
+ setEdges(graph.edges)
1938
+ setTriggers(definition.triggers ?? [])
1939
+ // The zod-parsed shapes leave the optional field flags optional; the editor
1940
+ // state types have them resolved. The same narrowing the draft-restore path
1941
+ // uses applies here.
1942
+ setContextSchema(definition.contextSchema as WorkflowContextSchema | undefined)
1943
+ setDefinitionIo(definition.io as WorkflowIoContract | undefined)
1944
+ setInterpolation(definition.interpolation)
1945
+ setErrorHandler(definition.errorHandler)
1946
+ scheduleAutosave()
1947
+ }, [commitHistory, scheduleAutosave])
1948
+
1949
+ const handleApplyCodeDraft = useCallback(() => {
1950
+ if (!codeApplyDecision.ok) return
1951
+ applyDefinitionDocument(codeApplyDecision.definition, historyLabels.applyCode)
1952
+ setCodeDraft(null)
1953
+ flash(t('workflows.visualEditor.codeView.applied'), 'success')
1954
+ }, [codeApplyDecision, applyDefinitionDocument, historyLabels.applyCode, t])
1955
+
1956
+ // ---------------------------------------------------------------------
1957
+ // Prompt-to-draft (spec §9)
1958
+ //
1959
+ // The generated definition NEVER reaches the canvas by a path of its own: it
1960
+ // goes through `evaluateCodeViewDraft`, the same gate a pasted definition
1961
+ // passes, and lands through `applyDefinitionDocument`, the same replacement
1962
+ // the Code view's Apply performs. So a refused draft leaves the canvas
1963
+ // byte-identical, and an applied one — however many nodes it minted — is ONE
1964
+ // named entry on the existing undo stack.
1965
+ //
1966
+ // Nothing here writes to the server. The route that generated the draft
1967
+ // persisted nothing either; Save and Enable stay exactly where they were.
1968
+ // ---------------------------------------------------------------------
1969
+ const [showAiDraft, setShowAiDraft] = useState(false)
1970
+ const [problemsPassToken, setProblemsPassToken] = useState(0)
1971
+
1972
+ // The pass has to observe the APPLIED graph, and `evaluateWorkflowIssues`
1973
+ // closes over the node/edge state, so it runs on the commit after the apply
1974
+ // rather than inside it. The ref keeps it the page's ONE evaluator.
1975
+ const runProblemsPassRef = React.useRef(runProblemsPass)
1976
+ runProblemsPassRef.current = runProblemsPass
1977
+ useEffect(() => {
1978
+ if (problemsPassToken === 0) return
1979
+ runProblemsPassRef.current()
1980
+ }, [problemsPassToken])
1981
+
1982
+ const handleApplyAiDraft = useCallback((input: {
1983
+ definition: Record<string, unknown>
1984
+ prompt: string
1985
+ }): WorkflowAiDraftApplyRefusal | null => {
1986
+ const decision = evaluateCodeViewDraft<WorkflowDefinitionData>({
1987
+ // Serialized so the AI path and the Code view path are literally the same
1988
+ // call. It also proves the draft survives the JSON round trip the Code
1989
+ // view and every save will put it through.
1990
+ draftText: JSON.stringify(input.definition, null, 2),
1991
+ ...definitionApplyGate,
1992
+ })
1993
+
1994
+ if (!decision.ok) {
1995
+ switch (decision.reason) {
1996
+ case 'parseError':
1997
+ return { reason: 'parseError', messages: [decision.message] }
1998
+ case 'schemaError':
1999
+ case 'graphError':
2000
+ return { reason: decision.reason, messages: decision.messages }
2001
+ default:
2002
+ // `unchanged` cannot occur — no `canvasText` is supplied — but a
2003
+ // refusal must never fall through into a silent apply.
2004
+ return {
2005
+ reason: 'schemaError',
2006
+ messages: [t('workflows.aiDraft.refusedUnknown', 'The generated definition was refused.')],
2007
+ }
2008
+ }
2009
+ }
2010
+
2011
+ applyDefinitionDocument(
2012
+ decision.definition,
2013
+ buildAiCheckpointLabel(input.prompt, {
2014
+ prefix: t('workflows.visualEditor.history.aiDraft', 'AI draft'),
2015
+ }),
2016
+ )
2017
+ setProblemsPassToken((token) => token + 1)
2018
+ flash(
2019
+ t('workflows.aiDraft.applied', 'AI draft applied. Review the problems, then save when you are happy with it.'),
2020
+ 'success',
2021
+ )
2022
+ return null
2023
+ }, [definitionApplyGate, applyDefinitionDocument, t])
2024
+
2025
+ const handleRevertCodeDraft = useCallback(() => {
2026
+ setCodeDraft(null)
2027
+ }, [])
2028
+
2029
+ // Closing the panel discards an unapplied draft: keeping it would let the
2030
+ // author reopen the view onto text that no longer describes the canvas, with
2031
+ // no signal that the two had diverged.
2032
+ const handleCloseCodeView = useCallback(() => {
2033
+ setCodeDraft(null)
2034
+ setShowCodeView(false)
2035
+ }, [])
2036
+
2037
+ const handleCopyDefinitionJson = useCallback(async () => {
2038
+ try {
2039
+ await navigator.clipboard.writeText(codeViewJson)
2040
+ flash(t('workflows.visualEditor.codeView.copied', 'Definition JSON copied'), 'success')
2041
+ } catch (error) {
2042
+ logger.warn('Workflow definition JSON copy was blocked by the browser', { err: error })
2043
+ flash(
2044
+ t(
2045
+ 'workflows.visualEditor.codeView.copyUnavailable',
2046
+ 'The browser blocked clipboard access, so nothing was copied — select the JSON and copy it manually.',
2047
+ ),
2048
+ 'warning',
2049
+ )
2050
+ }
2051
+ }, [codeViewJson, t])
2052
+
2053
+ // Focus the offending node or edge on the canvas when a problem row is clicked
2054
+ const handleProblemClick = useCallback((issue: WorkflowValidationIssue) => {
2055
+ if (!issue.nodeId && !issue.edgeId) return
2056
+ focusRequestRef.current += 1
2057
+ setFocusTarget({
2058
+ ...(issue.nodeId ? { nodeId: issue.nodeId } : { edgeId: issue.edgeId }),
2059
+ requestId: focusRequestRef.current,
2060
+ })
2061
+ }, [])
337
2062
 
338
2063
  // Save workflow definition
339
2064
  const handleSave = useCallback(async () => {
340
- // Validate required fields
2065
+ // Validate required fields. They live in the details drawer, so open it —
2066
+ // an error toast pointing at fields the author cannot see is a dead end.
341
2067
  if (!workflowId || !workflowName) {
342
- flash('Workflow ID and Name are required fields', 'error')
2068
+ setMetadataOpen(true)
2069
+ flash(t('workflows.visualEditor.metadata.requiredFields', 'Workflow ID and name are required.'), 'error')
343
2070
  return
344
2071
  }
345
2072
 
346
- // Validate workflow structure
347
- const errors = validateWorkflowGraph(nodes, edges)
348
- const criticalErrors = errors.filter(e => e.type === 'error')
349
- if (criticalErrors.length > 0) {
350
- flash(`Cannot save: ${criticalErrors.length} validation error(s) found. Please fix them first.`, 'error')
351
- return
352
- }
2073
+ // Validate workflow structure and schema, surfacing every issue in the problems panel
2074
+ const graphErrors = validateWorkflowGraph(nodes, edges)
353
2075
 
354
- // Generate definition data and include triggers
355
- const graphDefinition = graphToDefinition(nodes, edges, { includePositions: true })
356
- const definitionData = {
357
- ...graphDefinition,
358
- triggers: triggers.length > 0 ? triggers : undefined,
359
- }
2076
+ // Generate definition data and re-attach triggers, contextSchema, io, and
2077
+ // the interpolation mode
2078
+ const definitionData = buildDefinitionPayload({
2079
+ graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
2080
+ triggers,
2081
+ contextSchema,
2082
+ io: definitionIo,
2083
+ interpolation,
2084
+ errorHandler,
2085
+ })
360
2086
 
361
- // Run Zod schema validation before saving. Report every issue, not just the
362
- // first: a save rejected for one missing activity field used to look like
363
- // "nothing happened" once the operator fixed that field and hit a second
364
- // one (#4232). Paths are humanized (steps.2.activities.0.config.endpoint →
365
- // step 3 › activity 1 › endpoint) so the message points at the node to open.
366
2087
  const schemaResult = workflowDefinitionDataSchema.safeParse(definitionData)
367
- if (!schemaResult.success) {
368
- const issues = schemaResult.error.issues
369
- const described = issues
370
- .slice(0, 3)
371
- .map((issue) => `${humanizeDefinitionIssuePath(issue.path)}: ${issue.message}`)
372
- const suffix = issues.length > described.length ? ` (+${issues.length - described.length} more)` : ''
373
- flash(`Cannot save — ${described.join('; ')}${suffix}`, 'error')
2088
+ const issues = collectValidationIssues({
2089
+ graphErrors,
2090
+ zodIssues: schemaResult.success ? [] : schemaResult.error.issues,
2091
+ configWarnings: [
2092
+ ...collectActivityConfigWarnings(definitionData),
2093
+ ...collectContextRefWarnings(definitionData, t, triggerPayloadContracts),
2094
+ ],
2095
+ nodes,
2096
+ edges,
2097
+ definition: definitionData,
2098
+ ledger: computeClientContextLedger(definitionData, triggerPayloadContracts),
2099
+ translate: translateIssue,
2100
+ })
2101
+ setProblems(issues)
2102
+ const { errors } = countIssuesBySeverity(issues)
2103
+ if (errors > 0) {
2104
+ setShowProblems(true)
2105
+ setProblemsCollapsed(false)
2106
+ flash(t('workflows.visualEditor.problems.saveBlocked', 'Cannot save: {count} validation error(s) found. Please fix them first.', { count: errors }), 'error')
374
2107
  return
375
2108
  }
376
2109
 
@@ -378,35 +2111,34 @@ export default function VisualEditorPage() {
378
2111
 
379
2112
  try {
380
2113
 
381
- const metadata: any = {}
382
- if (category) metadata.category = category
383
- if (tags.length > 0) metadata.tags = tags
384
- if (icon) metadata.icon = icon
2114
+ const metadataPayload = buildMetadataPayload({ loadedMetadata, tags, category, icon, definition: definitionData, annotations })
385
2115
 
386
2116
  // Determine if creating new or updating existing
387
2117
  const isUpdate = !!definitionId
388
2118
 
389
2119
  let result
2120
+ let updateBody: DefinitionUpdateBody | null = null
390
2121
  if (isUpdate) {
391
2122
  // Update existing definition — send the full editable payload so metadata
392
2123
  // edits (name, description, version, category, tags, icon, effective
393
2124
  // dates) actually persist. Previously only `definition` + `enabled`
394
2125
  // were sent, silently dropping every other field.
2126
+ updateBody = {
2127
+ workflowName,
2128
+ description: description || null,
2129
+ version,
2130
+ definition: definitionData,
2131
+ metadata: metadataPayload,
2132
+ enabled,
2133
+ effectiveFrom: effectiveFrom || null,
2134
+ effectiveTo: effectiveTo || null,
2135
+ }
395
2136
  result = await withScopedApiRequestHeaders(
396
2137
  buildOptimisticLockHeader(updatedAt),
397
2138
  () => apiCall<{ data: any; error?: string }>(`/api/workflows/definitions/${definitionId}`, {
398
2139
  method: 'PUT',
399
2140
  headers: { 'Content-Type': 'application/json' },
400
- body: JSON.stringify({
401
- workflowName,
402
- description: description || null,
403
- version,
404
- definition: definitionData,
405
- metadata: Object.keys(metadata).length > 0 ? metadata : null,
406
- enabled,
407
- effectiveFrom: effectiveFrom || null,
408
- effectiveTo: effectiveTo || null,
409
- }),
2141
+ body: JSON.stringify(updateBody),
410
2142
  }),
411
2143
  )
412
2144
  } else {
@@ -420,7 +2152,7 @@ export default function VisualEditorPage() {
420
2152
  description: description || null,
421
2153
  version,
422
2154
  definition: definitionData,
423
- metadata: Object.keys(metadata).length > 0 ? metadata : null,
2155
+ metadata: metadataPayload,
424
2156
  enabled,
425
2157
  effectiveFrom: effectiveFrom || null,
426
2158
  effectiveTo: effectiveTo || null,
@@ -429,32 +2161,475 @@ export default function VisualEditorPage() {
429
2161
  }
430
2162
 
431
2163
  if (!result.ok) {
432
- const conflictError = Object.assign(new Error(t('workflows.messages.saveFailed', 'Failed to save')), {
2164
+ const activeInstanceCount = readStructuralEditConflictCount(result.status, result.result)
2165
+ if (activeInstanceCount !== null && updateBody) {
2166
+ setStructuralConflict({ activeInstanceCount, payload: updateBody })
2167
+ return
2168
+ }
2169
+ const conflictError = Object.assign(new Error(t('workflows.messages.saveFailed', 'Failed to save')), {
2170
+ status: result.status,
2171
+ ...(result.result && typeof result.result === 'object' ? result.result : {}),
2172
+ })
2173
+ if (!surfaceRecordConflict(conflictError, t)) {
2174
+ flash(formatWorkflowValidationError(result.result, t('workflows.messages.saveFailed', 'Failed to save')), 'error')
2175
+ }
2176
+ return
2177
+ }
2178
+
2179
+ setStructuralConflict(null)
2180
+ if (updateBody) lastAutosavedBodyRef.current = stableSerializeDefinition(updateBody)
2181
+
2182
+ const savedDefinition = result.result?.data
2183
+
2184
+ // Explicit Save promoted the working copy: drop the per-user draft
2185
+ // (fire-and-forget) and suspend autosave so a pending debounce can't
2186
+ // recreate it before the redirect.
2187
+ draftSuspendedRef.current = true
2188
+ setPendingDraft(null)
2189
+ setDraftSavedAt(null)
2190
+ setDraftSaveFailed(false)
2191
+ if (isUpdate && isServerDraftEligible(definitionId)) {
2192
+ void apiCall(`/api/workflows/definitions/${definitionId}/draft`, { method: 'DELETE' }).catch(() => undefined)
2193
+ }
2194
+
2195
+ flash(
2196
+ isUpdate
2197
+ ? t('workflows.messages.workflowUpdated', 'Workflow updated successfully')
2198
+ : t('workflows.messages.workflowCreated', 'Workflow created successfully'),
2199
+ 'success',
2200
+ )
2201
+
2202
+ // Stay on the visual editor after saving. On update, refresh the local
2203
+ // optimistic-lock token so the next save keeps working. On create, switch
2204
+ // the editor into edit mode by pointing the URL at the new id — the load
2205
+ // effect then re-syncs from the persisted definition without leaving the
2206
+ // canvas.
2207
+ if (isUpdate) {
2208
+ if (typeof savedDefinition?.updatedAt === 'string') {
2209
+ setUpdatedAt(savedDefinition.updatedAt)
2210
+ }
2211
+ } else if (savedDefinition?.id) {
2212
+ router.replace(`/backend/definitions/visual-editor?id=${encodeURIComponent(savedDefinition.id)}`)
2213
+ }
2214
+
2215
+ } catch (error) {
2216
+ logger.error('Error saving workflow definition', { err: error })
2217
+ flash(t('workflows.messages.saveFailed', 'Failed to save'), 'error')
2218
+ } finally {
2219
+ setIsSaving(false)
2220
+ }
2221
+ }, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, icon, effectiveFrom, effectiveTo, triggers, contextSchema, definitionIo, interpolation, errorHandler, loadedMetadata, annotations, definitionId, updatedAt, router, t])
2222
+
2223
+ // ── Non-pointer authoring path (spec §4.6) ────────────────────────────────
2224
+ // Every canvas operation is reachable from the keyboard: the command palette
2225
+ // reaches all of them, and the direct bindings below cover the ones an author
2226
+ // performs constantly (open, delete, nudge).
2227
+
2228
+ const focusNode = useCallback((nodeId: string) => {
2229
+ focusRequestRef.current += 1
2230
+ setFocusTarget({ nodeId, requestId: focusRequestRef.current })
2231
+ }, [])
2232
+
2233
+ const openSelectedInspector = useCallback(() => {
2234
+ const selected = nodesRef.current.find((node) => node.selected)
2235
+ if (!selected) {
2236
+ const selectedEdgeNode = edgesRef.current.find((edge) => edge.selected)
2237
+ if (!selectedEdgeNode || isCodeOnly) return
2238
+ setSelectedEdge(selectedEdgeNode)
2239
+ setSelectedNode(null)
2240
+ setEdgeDialogFocusFieldId(null)
2241
+ setShowEdgeDialog(true)
2242
+ return
2243
+ }
2244
+ handleNodeClick({} as React.MouseEvent, selected)
2245
+ }, [isCodeOnly, handleNodeClick])
2246
+
2247
+ // Del removes whatever is selected. Each removal runs its own confirm +
2248
+ // cleanup flow, so a multi-selection deletes exactly as clicking each trash
2249
+ // button would — and each one is undoable.
2250
+ const handleDeleteSelection = useCallback(async () => {
2251
+ if (isCodeOnly) return
2252
+ const nodeIds = selectedNodeIds(nodesRef.current)
2253
+ const edgeIds = edgesRef.current.filter((edge) => edge.selected).map((edge) => edge.id)
2254
+ if (nodeIds.length === 0 && edgeIds.length === 0) {
2255
+ flash(t('workflows.visualEditor.keyboard.nothingSelected', 'Select a step or a route first'), 'info')
2256
+ return
2257
+ }
2258
+ for (const edgeId of edgeIds) await handleDeleteEdge(edgeId)
2259
+ for (const nodeId of nodeIds) await handleDeleteNode(nodeId)
2260
+ }, [isCodeOnly, handleDeleteEdge, handleDeleteNode, t])
2261
+
2262
+ // Arrow-key nudging follows the drag rule (#4248): a burst of keystrokes is
2263
+ // ONE arrangement, so the baseline is captured on the first press and the undo
2264
+ // entry plus the autosave land once the burst settles.
2265
+ const nudgeBaselineRef = React.useRef<WorkflowEditorDocument | null>(null)
2266
+ const nudgeCommitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
2267
+
2268
+ const handleNudge = useCallback((key: string, coarse: boolean) => {
2269
+ if (isCodeOnly) return false
2270
+ const direction = resolveNudgeDirection(key)
2271
+ if (!direction) return false
2272
+ if (selectedNodeIds(nodesRef.current).length === 0) return false
2273
+ if (!nudgeBaselineRef.current) nudgeBaselineRef.current = captureDocument()
2274
+ setNodes((nds) => nudgeSelectedNodes(nds, nudgeOffset(direction, coarse)))
2275
+ if (nudgeCommitTimerRef.current) clearTimeout(nudgeCommitTimerRef.current)
2276
+ nudgeCommitTimerRef.current = setTimeout(() => {
2277
+ nudgeCommitTimerRef.current = null
2278
+ const baseline = nudgeBaselineRef.current
2279
+ nudgeBaselineRef.current = null
2280
+ if (baseline) commitCapturedDocument(baseline, historyLabels.move)
2281
+ scheduleAutosave()
2282
+ }, NUDGE_COMMIT_DELAY_MS)
2283
+ return true
2284
+ }, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.move, scheduleAutosave])
2285
+
2286
+ useEffect(() => () => {
2287
+ if (nudgeCommitTimerRef.current) clearTimeout(nudgeCommitTimerRef.current)
2288
+ }, [])
2289
+
2290
+ const commandPaletteCommands = useMemo<WorkflowEditorCommand[]>(() => buildWorkflowEditorCommands({
2291
+ readOnly: isCodeOnly,
2292
+ canUndo: canUndoEditorHistory(history),
2293
+ canRedo: canRedoEditorHistory(history),
2294
+ hasNodes: nodes.length > 0,
2295
+ hasSelection: nodes.some((node) => node.selected) || edges.some((edge) => edge.selected),
2296
+ canRunTest: !!definitionId,
2297
+ stepTypes: PALETTE_NODE_TYPES.map((nodeType) => ({
2298
+ nodeType,
2299
+ typeLabel: NODE_TYPE_LABELS[nodeType]?.title ?? nodeType,
2300
+ })),
2301
+ steps: nodes
2302
+ .filter((node) => !isAnnotationNode(node))
2303
+ .map((node) => ({
2304
+ id: node.id,
2305
+ label: typeof node.data?.label === 'string' && node.data.label ? node.data.label : node.id,
2306
+ typeLabel: NODE_TYPE_LABELS[(node.type ?? '') as keyof typeof NODE_TYPE_LABELS]?.title ?? node.type ?? '',
2307
+ })),
2308
+ labels: {
2309
+ undo: t('workflows.commandPalette.undo', 'Undo'),
2310
+ redo: t('workflows.commandPalette.redo', 'Redo'),
2311
+ deleteSelection: t('workflows.commandPalette.delete', 'Delete selection'),
2312
+ copy: t('workflows.commandPalette.copy', 'Copy selection'),
2313
+ paste: t('workflows.commandPalette.paste', 'Paste'),
2314
+ duplicate: t('workflows.commandPalette.duplicate', 'Duplicate selection'),
2315
+ addStep: (typeLabel: string) => t('workflows.commandPalette.addStep', 'Add step: {type}', { type: typeLabel }),
2316
+ addNote: t('workflows.commandPalette.addNote', 'Add note'),
2317
+ addGroup: t('workflows.commandPalette.addGroup', 'Add group'),
2318
+ goToStep: t('workflows.commandPalette.goToStep', 'Go to step'),
2319
+ tidy: t('workflows.visualEditor.autoArrange'),
2320
+ togglePalette: t('workflows.commandPalette.togglePalette', 'Toggle the step palette'),
2321
+ toggleMetadata: t('workflows.commandPalette.toggleMetadata', 'Toggle the workflow details panel'),
2322
+ toggleFocus: t('workflows.commandPalette.toggleFocus', 'Toggle focus mode'),
2323
+ toggleProblems: t('workflows.commandPalette.toggleProblems', 'Toggle the Problems panel'),
2324
+ toggleCodeView: t('workflows.commandPalette.toggleCodeView', 'Toggle the Code view'),
2325
+ toggleCompensation: t('workflows.commandPalette.toggleCompensation', 'Toggle compensation paths'),
2326
+ validate: t('workflows.visualEditor.validate'),
2327
+ runTest: t('workflows.actions.startInstance'),
2328
+ save: t('workflows.common.save'),
2329
+ aiDraft: t('workflows.aiDraft.open', 'Draft this workflow with AI'),
2330
+ },
2331
+ actions: {
2332
+ undo: handleUndo,
2333
+ redo: handleRedo,
2334
+ deleteSelection: () => { void handleDeleteSelection() },
2335
+ copy: () => { void handleCopySelection() },
2336
+ paste: () => { void handlePaste() },
2337
+ duplicate: handleDuplicateSelection,
2338
+ addStep: (nodeType: string) => handleAddNode(nodeType),
2339
+ addNote: () => handleAddAnnotation('note'),
2340
+ addGroup: () => handleAddAnnotation('group'),
2341
+ goToStep: focusNode,
2342
+ tidy: handleAutoArrange,
2343
+ togglePalette: togglePaletteCollapsed,
2344
+ toggleMetadata: () => setMetadataOpen((open) => !open),
2345
+ toggleFocus,
2346
+ toggleProblems: () => setShowProblems((visible) => !visible),
2347
+ toggleCodeView: () => setShowCodeView((visible) => !visible),
2348
+ toggleCompensation,
2349
+ validate: handleValidate,
2350
+ runTest: () => setStartOpen(true),
2351
+ save: () => { void handleSave() },
2352
+ aiDraft: () => setShowAiDraft(true),
2353
+ },
2354
+ }), [
2355
+ isCodeOnly, history, nodes, edges, definitionId, t,
2356
+ handleUndo, handleRedo, handleDeleteSelection, handleCopySelection, handlePaste, handleDuplicateSelection,
2357
+ handleAddNode, handleAddAnnotation, focusNode, handleAutoArrange, togglePaletteCollapsed, toggleFocus,
2358
+ toggleCompensation, handleValidate, handleSave,
2359
+ ])
2360
+
2361
+ // Keyboard shortcuts: Cmd/Ctrl+K opens the command palette, Cmd/Ctrl+Z undoes,
2362
+ // Cmd/Ctrl+Shift+Z redoes, Cmd/Ctrl+C, +V and +D copy, paste and duplicate the
2363
+ // selected subgraph, Enter opens the inspector for the selection, Del removes
2364
+ // it, the arrows nudge it, `F` toggles Focus mode and `Esc` exits it.
2365
+ //
2366
+ // Everything except the palette is suppressed while the user is typing in a
2367
+ // field (a text input keeps its own native undo, copy/paste and caret keys) or
2368
+ // while a dialog is open, so a shortcut never hijacks form input or the
2369
+ // dialog's own Escape-to-close. Cmd+K is deliberately exempt from the typing
2370
+ // guard: it is the way out of any focus, which is the whole point of a palette.
2371
+ useEffect(() => {
2372
+ if (isMobile) return
2373
+ const onKeyDown = (event: KeyboardEvent) => {
2374
+ const active = document.activeElement as HTMLElement | null
2375
+ const tag = (active?.tagName || '').toLowerCase()
2376
+ const isEditing = tag === 'input' || tag === 'textarea' || tag === 'select' || !!active?.isContentEditable
2377
+ // Docking is a SPATIAL change, not a keyboard one: an open inspector
2378
+ // still owns the shortcuts, because it holds a form with unsaved values
2379
+ // and every canvas binding here either mutates the document under it
2380
+ // (undo, delete, paste) or would save a graph that omits the edit sitting
2381
+ // in the rail. What docking buys is that the canvas stays visible and
2382
+ // clickable — re-targeting is a click, and Escape closes the rail.
2383
+ const isInspectorOpen = showNodeDialog || showEdgeDialog
2384
+ const isDialogOpen = isInspectorOpen || showAnnotationDialog || showClearConfirm || startOpen || showCodeView
2385
+ // The details drawer is a modal overlay, so the canvas bindings must not
2386
+ // fire behind it. Cmd+S is the deliberate exception: the drawer has no
2387
+ // submit of its own to protect — saving the workflow IS its primary
2388
+ // action — so the shortcut keeps working while it is open.
2389
+ const isOverlayOpen = isDialogOpen || metadataOpen || showAiDraft
2390
+ const isCommandKey = (event.metaKey || event.ctrlKey) && !event.altKey
2391
+
2392
+ if (isCommandKey && (event.key === 'k' || event.key === 'K')) {
2393
+ if (isOverlayOpen) return
2394
+ event.preventDefault()
2395
+ setShowCommandPalette((open) => !open)
2396
+ return
2397
+ }
2398
+ if (isCommandKey && !event.shiftKey && (event.key === 's' || event.key === 'S')) {
2399
+ if (isDialogOpen || isCodeOnly) return
2400
+ event.preventDefault()
2401
+ void handleSave()
2402
+ return
2403
+ }
2404
+ if (isEditing || showCommandPalette) return
2405
+
2406
+ if (isCommandKey && (event.key === 'z' || event.key === 'Z')) {
2407
+ if (isOverlayOpen) return
2408
+ event.preventDefault()
2409
+ if (event.shiftKey) handleRedo()
2410
+ else handleUndo()
2411
+ return
2412
+ }
2413
+ if (isCommandKey && !event.shiftKey && !isOverlayOpen) {
2414
+ if (event.key === 'c' || event.key === 'C') {
2415
+ event.preventDefault()
2416
+ void handleCopySelection()
2417
+ return
2418
+ }
2419
+ if (event.key === 'v' || event.key === 'V') {
2420
+ event.preventDefault()
2421
+ void handlePaste()
2422
+ return
2423
+ }
2424
+ if (event.key === 'd' || event.key === 'D') {
2425
+ event.preventDefault()
2426
+ handleDuplicateSelection()
2427
+ return
2428
+ }
2429
+ }
2430
+ if (event.metaKey || event.ctrlKey || event.altKey) return
2431
+ if (event.key === 'Escape') {
2432
+ // Escape raised inside the rail is claimed by the rail itself. Reaching
2433
+ // here means focus is on the canvas, where Escape should still close the
2434
+ // inspector before it falls through to leaving Focus mode.
2435
+ if (inspectorsDocked && isInspectorOpen) {
2436
+ event.preventDefault()
2437
+ setShowNodeDialog(false)
2438
+ setShowEdgeDialog(false)
2439
+ return
2440
+ }
2441
+ if (focusMode && !isOverlayOpen) {
2442
+ event.preventDefault()
2443
+ setFocusMode(false)
2444
+ }
2445
+ return
2446
+ }
2447
+ if (isOverlayOpen) return
2448
+ if (event.key === 'Enter') {
2449
+ event.preventDefault()
2450
+ openSelectedInspector()
2451
+ return
2452
+ }
2453
+ if (event.key === 'Delete' || event.key === 'Backspace') {
2454
+ event.preventDefault()
2455
+ void handleDeleteSelection()
2456
+ return
2457
+ }
2458
+ if (resolveNudgeDirection(event.key)) {
2459
+ if (handleNudge(event.key, event.shiftKey)) event.preventDefault()
2460
+ return
2461
+ }
2462
+ if (event.key === 'f' || event.key === 'F') {
2463
+ event.preventDefault()
2464
+ toggleFocus()
2465
+ }
2466
+ }
2467
+ window.addEventListener('keydown', onKeyDown)
2468
+ return () => window.removeEventListener('keydown', onKeyDown)
2469
+ }, [
2470
+ isMobile, focusMode, inspectorsDocked, showNodeDialog, showEdgeDialog, showAnnotationDialog, showClearConfirm, startOpen,
2471
+ showCodeView, metadataOpen, showAiDraft, showCommandPalette, toggleFocus, setFocusMode, handleUndo, handleRedo, handleCopySelection, handlePaste,
2472
+ handleDuplicateSelection, openSelectedInspector, handleDeleteSelection, handleNudge,
2473
+ handleSave, isCodeOnly,
2474
+ ])
2475
+
2476
+ // Quiet autosave routine (no redirect, no success flash). Mirrors the update
2477
+ // branch of `handleSave` exactly — same payload and the same optimistic-lock
2478
+ // header — so dragged positions persist without an explicit Save. Reassigned
2479
+ // every render and invoked through `performAutosaveRef` by the debounced timer
2480
+ // so it always sees the latest nodes/edges/metadata.
2481
+ performAutosaveRef.current = async () => {
2482
+ if (!definitionId || isCodeOnly) return
2483
+ if (!workflowId || !workflowName) return
2484
+
2485
+ const criticalErrors = validateWorkflowGraph(nodes, edges).filter((e) => e.type === 'error')
2486
+ if (criticalErrors.length > 0) return
2487
+
2488
+ // Same payload builders as the explicit Save: the quiet autosave must not
2489
+ // strip contextSchema, io, interpolation, or unedited metadata keys
2490
+ // (editor samples).
2491
+ const definitionData = buildDefinitionPayload({
2492
+ graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
2493
+ triggers,
2494
+ contextSchema,
2495
+ io: definitionIo,
2496
+ interpolation,
2497
+ errorHandler,
2498
+ })
2499
+ if (!workflowDefinitionDataSchema.safeParse(definitionData).success) return
2500
+
2501
+ const metadataPayload = buildMetadataPayload({ loadedMetadata, tags, category, icon, definition: definitionData, annotations })
2502
+
2503
+ const updateBody: DefinitionUpdateBody = {
2504
+ workflowName,
2505
+ description: description || null,
2506
+ version,
2507
+ definition: definitionData,
2508
+ metadata: metadataPayload,
2509
+ enabled,
2510
+ effectiveFrom: effectiveFrom || null,
2511
+ effectiveTo: effectiveTo || null,
2512
+ }
2513
+
2514
+ // Autosave only when the payload actually differs from what the row already
2515
+ // holds. Without this a selection or a re-render would PUT an identical body
2516
+ // and bump the optimistic-lock token for nothing.
2517
+ const serializedBody = stableSerializeDefinition(updateBody)
2518
+ if (serializedBody === lastAutosavedBodyRef.current) return
2519
+ lastAutosavedBodyRef.current = serializedBody
2520
+
2521
+ setAutosaveState('saving')
2522
+ try {
2523
+ const result = await withScopedApiRequestHeaders(
2524
+ buildOptimisticLockHeader(updatedAt),
2525
+ () => apiCall<{ data: any; error?: string }>(`/api/workflows/definitions/${definitionId}`, {
2526
+ method: 'PUT',
2527
+ headers: { 'Content-Type': 'application/json' },
2528
+ body: JSON.stringify(updateBody),
2529
+ }),
2530
+ )
2531
+
2532
+ if (!result.ok) {
2533
+ setAutosaveState('idle')
2534
+ lastAutosavedBodyRef.current = null
2535
+ // A quiet autosave carrying a structural change hits the same edit-safety
2536
+ // rule as Save. Surfacing the banner here keeps the author from watching
2537
+ // "Saved" never appear with no explanation.
2538
+ const activeInstanceCount = readStructuralEditConflictCount(result.status, result.result)
2539
+ if (activeInstanceCount !== null) {
2540
+ setStructuralConflict({ activeInstanceCount, payload: updateBody })
2541
+ return
2542
+ }
2543
+ const conflictError = Object.assign(new Error('[internal] workflow autosave failed'), {
433
2544
  status: result.status,
434
2545
  ...(result.result && typeof result.result === 'object' ? result.result : {}),
435
2546
  })
436
- if (!surfaceRecordConflict(conflictError, t)) {
437
- flash(`Failed to save: ${result.result?.error || 'Unknown error'}`, 'error')
438
- }
2547
+ surfaceRecordConflict(conflictError, t)
439
2548
  return
440
2549
  }
441
2550
 
2551
+ setStructuralConflict(null)
2552
+
442
2553
  const savedDefinition = result.result?.data
2554
+ if (typeof savedDefinition?.updatedAt === 'string') {
2555
+ setUpdatedAt(savedDefinition.updatedAt)
2556
+ }
2557
+ setAutosaveState('saved')
2558
+ } catch (error) {
2559
+ logger.error('workflow autosave failed', {
2560
+ error: error instanceof Error ? error.message : String(error),
2561
+ })
2562
+ lastAutosavedBodyRef.current = null
2563
+ setAutosaveState('idle')
2564
+ }
2565
+ }
443
2566
 
444
- flash(`Workflow ${isUpdate ? 'updated' : 'created'} successfully!`, 'success')
2567
+ // Fade the "Saved" affordance back to idle shortly after a successful autosave.
2568
+ useEffect(() => {
2569
+ if (autosaveState !== 'saved') return
2570
+ const timer = setTimeout(() => setAutosaveState('idle'), 2000)
2571
+ return () => clearTimeout(timer)
2572
+ }, [autosaveState])
2573
+
2574
+ // Remedy for the edit-safety rule: mint the next version from the stored
2575
+ // definition (the same machinery Publish uses), apply the rejected edit to
2576
+ // that fresh row, and continue editing it. Instances that pinned the previous
2577
+ // row keep executing the definition they started on.
2578
+ const handleCreateVersion = useCallback(async () => {
2579
+ if (!definitionId || !structuralConflict) return
2580
+ setIsCreatingVersion(true)
2581
+ try {
2582
+ const publishResult = await apiCall<{ data?: { id?: string; version?: number; updatedAt?: string }; error?: string }>(
2583
+ `/api/workflows/definitions/${definitionId}/publish`,
2584
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) },
2585
+ )
2586
+ const mintedVersion = publishResult.result?.data
2587
+ if (!publishResult.ok || !mintedVersion?.id) {
2588
+ flash(
2589
+ publishResult.result?.error
2590
+ || t('workflows.visualEditor.editSafety.createVersionFailed', 'Could not create a new version.'),
2591
+ 'error',
2592
+ )
2593
+ return
2594
+ }
445
2595
 
446
- // Redirect to definition detail page after short delay
447
- setTimeout(() => {
448
- router.push(`/backend/definitions/${savedDefinition.id}`)
449
- }, 1500)
2596
+ const nextVersion = mintedVersion.version ?? version
2597
+ const saveResult = await withScopedApiRequestHeaders(
2598
+ buildOptimisticLockHeader(mintedVersion.updatedAt ?? null),
2599
+ () => apiCall<{ data?: { updatedAt?: string }; error?: string }>(
2600
+ `/api/workflows/definitions/${mintedVersion.id}`,
2601
+ {
2602
+ method: 'PUT',
2603
+ headers: { 'Content-Type': 'application/json' },
2604
+ body: JSON.stringify({ ...structuralConflict.payload, version: nextVersion }),
2605
+ },
2606
+ ),
2607
+ )
2608
+ if (!saveResult.ok) {
2609
+ flash(
2610
+ formatWorkflowValidationError(
2611
+ saveResult.result,
2612
+ t('workflows.visualEditor.editSafety.createVersionFailed', 'Could not create a new version.'),
2613
+ ),
2614
+ 'error',
2615
+ )
2616
+ return
2617
+ }
450
2618
 
451
- } catch (error) {
452
- logger.error('Error saving workflow definition', { err: error })
453
- flash('Failed to save workflow definition. Please try again.', 'error')
2619
+ setStructuralConflict(null)
2620
+ setVersion(nextVersion)
2621
+ setUpdatedAt(saveResult.result?.data?.updatedAt ?? null)
2622
+ flash(
2623
+ t('workflows.visualEditor.editSafety.createVersionSucceeded', 'Version {version} created. Your changes were saved there.', {
2624
+ version: String(nextVersion),
2625
+ }),
2626
+ 'success',
2627
+ )
2628
+ router.replace(`/backend/definitions/visual-editor?id=${encodeURIComponent(mintedVersion.id)}`)
454
2629
  } finally {
455
- setIsSaving(false)
2630
+ setIsCreatingVersion(false)
456
2631
  }
457
- }, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, icon, effectiveFrom, effectiveTo, triggers, definitionId, updatedAt, router])
2632
+ }, [definitionId, structuralConflict, version, router, t])
458
2633
 
459
2634
  // Customize a code-defined workflow → creates an override and reloads the
460
2635
  // editor pointed at the new UUID. Mirrors the non-visual edit page button.
@@ -510,101 +2685,121 @@ export default function VisualEditorPage() {
510
2685
  }
511
2686
  }, [definitionId, workflowId, router, confirm, t])
512
2687
 
513
- // Test workflow
514
- const handleTest = useCallback(() => {
515
- // First validate
516
- const errors = validateWorkflowGraph(nodes, edges)
517
- const criticalErrors = errors.filter((e) => e.type === 'error')
518
- if (criticalErrors.length > 0) {
519
- flash(`Cannot test: ${criticalErrors.length} validation error(s) found. Please fix them first.`, 'error')
2688
+ // Start a workflow instance with an initial JSON context, mirroring the
2689
+ // non-visual edit page. Requires a persisted definition (the executor
2690
+ // resolves the definition by workflowId + version).
2691
+ const handleStartInstance = useCallback(async () => {
2692
+ let initialContext: Record<string, unknown>
2693
+ try {
2694
+ const parsed = startContext.trim() ? JSON.parse(startContext) : {}
2695
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
2696
+ throw new Error('[internal] context must be a JSON object')
2697
+ }
2698
+ initialContext = parsed as Record<string, unknown>
2699
+ } catch {
2700
+ flash(t('workflows.startInstance.invalidJson'), 'error')
520
2701
  return
521
2702
  }
522
-
523
- // TODO: Implement test logic (create instance, run first step)
524
- flash('Test functionality will be implemented next', 'info')
525
- }, [nodes, edges])
526
-
527
- // Load example workflow
528
- const handleLoadExample = useCallback(() => {
529
- // Set example metadata
530
- setWorkflowId('approval_workflow')
531
- setWorkflowName('Simple Approval Workflow')
532
- setDescription('A basic approval workflow for reviewing and approving requests')
533
- setVersion(1)
534
- setEnabled(true)
535
- setCategory('Approvals')
536
- setTags(['approval', 'review'])
537
-
538
- const exampleNodes: Node[] = [
539
- {
540
- id: 'start',
541
- type: 'start',
542
- position: { x: 250, y: 50 },
543
- data: {
544
- label: 'Start',
545
- description: 'Workflow begins',
546
- status: 'pending',
547
- badge: 'Start',
548
- },
549
- },
550
- {
551
- id: 'step1',
552
- type: 'userTask',
553
- position: { x: 250, y: 250 },
554
- data: {
555
- label: 'Review Request',
556
- description: 'User reviews the incoming request',
557
- status: 'pending',
558
- stepNumber: 1,
559
- badge: 'User Task',
560
- assignedToRoles: ['Reviewer'],
561
- },
562
- },
563
- {
564
- id: 'end',
565
- type: 'end',
566
- position: { x: 250, y: 450 },
567
- data: {
568
- label: 'Complete',
569
- description: 'Workflow ends',
570
- status: 'pending',
571
- badge: 'End',
572
- },
573
- },
574
- ]
575
-
576
- const exampleEdges: Edge[] = [
577
- {
578
- id: 'e-start-step1',
579
- source: 'start',
580
- target: 'step1',
581
- type: 'smoothstep',
582
- data: {
583
- trigger: 'auto',
584
- preConditions: [],
585
- postConditions: [],
586
- activities: [],
2703
+ setStarting(true)
2704
+ try {
2705
+ const result = await runMutation({
2706
+ operation: async () => {
2707
+ const response = await apiFetch('/api/workflows/instances', {
2708
+ method: 'POST',
2709
+ headers: { 'Content-Type': 'application/json' },
2710
+ body: JSON.stringify({
2711
+ workflowId,
2712
+ version,
2713
+ initialContext,
2714
+ ...(startDryRun ? { dryRun: true } : {}),
2715
+ ...(startStepThrough ? { stepThrough: true } : {}),
2716
+ }),
2717
+ })
2718
+ if (!response.ok) {
2719
+ const errorBody = await readJsonSafe<{ error?: string }>(response, null)
2720
+ throw new Error(errorBody?.error || t('workflows.startInstance.failed'))
2721
+ }
2722
+ return readJsonSafe<{ data?: { instance?: { id?: string } } }>(response, null)
587
2723
  },
588
- },
589
- {
590
- id: 'e-step1-end',
591
- source: 'step1',
592
- target: 'end',
593
- type: 'smoothstep',
594
- data: {
595
- trigger: 'auto',
596
- preConditions: [],
597
- postConditions: [],
598
- activities: [],
2724
+ mutationPayload: { resourceId: definitionId, operation: 'start-instance' },
2725
+ context: {
2726
+ formId: mutationContextId,
2727
+ resourceKind: 'workflows.instance',
2728
+ resourceId: definitionId,
2729
+ operation: 'start-instance',
2730
+ retryLastMutation,
599
2731
  },
600
- },
601
- ]
602
-
603
- setNodes(exampleNodes)
604
- setEdges(exampleEdges)
605
- flash('Example workflow loaded', 'success')
2732
+ })
2733
+ flash(t('workflows.startInstance.started'), 'success')
2734
+ setStartOpen(false)
2735
+ const instanceId = result?.data?.instance?.id
2736
+ if (instanceId) {
2737
+ router.push(`/backend/instances/${instanceId}`)
2738
+ router.refresh()
2739
+ }
2740
+ } catch (error) {
2741
+ flash(error instanceof Error ? error.message : t('workflows.startInstance.failed'), 'error')
2742
+ } finally {
2743
+ setStarting(false)
2744
+ }
2745
+ }, [startContext, startDryRun, startStepThrough, workflowId, version, definitionId, mutationContextId, retryLastMutation, router, t])
2746
+
2747
+ // Apply a gallery template to the canvas: populate metadata from the
2748
+ // template's i18n keys and convert its definition into graph nodes/edges.
2749
+ const applyTemplate = useCallback((template: WorkflowTemplateGalleryItem) => {
2750
+ setWorkflowId(template.id.replace(/-/g, '_'))
2751
+ setWorkflowName(t(template.nameKey))
2752
+ setDescription(t(template.descriptionKey))
2753
+ setVersion(1)
2754
+ setEnabled(true)
2755
+ setCategory(template.category)
2756
+ setTags([])
2757
+ setIcon(template.icon)
2758
+
2759
+ const graph = definitionToGraph(template.definition)
2760
+ setNodes(graph.nodes)
2761
+ setEdges(graph.edges)
2762
+ setTriggers(template.definition.triggers || [])
2763
+ setContextSchema(template.definition.contextSchema ?? undefined)
2764
+ setDefinitionIo((template.definition.io ?? undefined) as WorkflowIoContract | undefined)
2765
+ setInterpolation(resolveDefinitionInterpolationMode(template.definition) ?? 'strict')
2766
+ setErrorHandler((template.definition as { errorHandler?: WorkflowErrorHandlerConfig }).errorHandler ?? undefined)
2767
+ setLoadedMetadata(null)
2768
+ resetHistory()
2769
+ flash(t('workflows.visualEditor.templateLoaded', 'Template loaded'), 'success')
2770
+ }, [resetHistory, t])
2771
+
2772
+ // Open the template gallery (replaces the old hardcoded inline example).
2773
+ const handleOpenTemplateGallery = useCallback(() => {
2774
+ setShowTemplateGallery(true)
606
2775
  }, [])
607
2776
 
2777
+ const handleTemplateSelect = useCallback((template: WorkflowTemplateGalleryItem | null) => {
2778
+ if (template) applyTemplate(template)
2779
+ }, [applyTemplate])
2780
+
2781
+ // Populate the canvas from ?template=<id> when creating a new workflow.
2782
+ useEffect(() => {
2783
+ if (definitionId || !templateId) return
2784
+ let cancelled = false
2785
+ const loadTemplate = async () => {
2786
+ const result = await apiCall<{ items?: WorkflowTemplateGalleryItem[]; error?: string }>('/api/workflows/templates')
2787
+ if (cancelled) return
2788
+ const template = result.ok
2789
+ ? (result.result?.items || []).find((item) => item.id === templateId)
2790
+ : undefined
2791
+ if (template) {
2792
+ applyTemplate(template)
2793
+ } else {
2794
+ flash(t('workflows.templates.gallery.notFound', 'Template not found'), 'error')
2795
+ }
2796
+ }
2797
+ void loadTemplate()
2798
+ return () => {
2799
+ cancelled = true
2800
+ }
2801
+ }, [definitionId, templateId, applyTemplate, t])
2802
+
608
2803
  // Clear canvas
609
2804
  const handleClear = useCallback(() => {
610
2805
  if (nodes.length > 0 || edges.length > 0 || workflowId || workflowName) {
@@ -627,9 +2822,13 @@ export default function VisualEditorPage() {
627
2822
  setEffectiveFrom('')
628
2823
  setEffectiveTo('')
629
2824
  setTriggers([])
2825
+ setContextSchema(undefined)
2826
+ setDefinitionIo(undefined)
2827
+ setLoadedMetadata(null)
630
2828
  setShowClearConfirm(false)
2829
+ resetHistory()
631
2830
  flash('Canvas cleared', 'success')
632
- }, [])
2831
+ }, [resetHistory])
633
2832
 
634
2833
  // Publish page-load record context to the AppShell-owned `backend:record:current`
635
2834
  // mount so the enterprise record_locks widget resolves `workflows.definition` + id
@@ -660,44 +2859,385 @@ export default function VisualEditorPage() {
660
2859
  workflowId, workflowName, description, version,
661
2860
  enabled, category, tags, icon,
662
2861
  effectiveFrom, effectiveTo, triggers,
2862
+ contextSchema, interpolation, errorHandler,
663
2863
  }
664
2864
 
665
2865
  const metadataHandlers: WorkflowMetadataHandlers = {
666
2866
  setWorkflowId, setWorkflowName, setDescription, setVersion,
667
2867
  setEnabled, setCategory, setTags, setIcon,
668
2868
  setEffectiveFrom, setEffectiveTo, setTriggers,
2869
+ setContextSchema,
2870
+ setInterpolation,
2871
+ setErrorHandler: (next) => setErrorHandler(next ?? undefined),
669
2872
  }
670
2873
 
671
- const sharedDialogs = (
2874
+ // The required identity fields now live behind the details drawer, so the
2875
+ // toolbar has to say when they are still blank.
2876
+ const metadataIncomplete = !isCodeOnly && (!workflowId || !workflowName)
2877
+
2878
+ const inspectorPanels = (
672
2879
  <>
673
- {process.env.NEXT_PUBLIC_WORKFLOW_CRUDFORM_ENABLED === 'true' ? (
674
- <NodeEditDialogCrudForm node={selectedNode} isOpen={showNodeDialog} onClose={() => setShowNodeDialog(false)} onSave={handleSaveNode} onDelete={handleDeleteNode} />
2880
+ {crudFormDialogsEnabled ? (
2881
+ // Both forms are dense (Invoke-Agent, timeout, sub-editors on the step;
2882
+ // condition, mapping and activities on the route) and the 384px docked
2883
+ // rail made them unusable, so each opens as the wide overlay Drawer that
2884
+ // mirrors the definition metadata drawer, with its ledger in a sticky
2885
+ // side column.
2886
+ <NodeEditDialogCrudForm node={selectedNode} isOpen={showNodeDialog} onClose={() => setShowNodeDialog(false)} onSave={handleSaveNode} onDelete={handleDeleteNode} ledgerEntries={nodeDialogLedgerEntries} definitionId={definitionId} samples={editorSamples} onPinSample={handlePinSample} onUnpinSample={handleUnpinSample} branchingRoutes={branchingRoutesValue} onSaveBranchingRoutes={handleSaveBranchingRoutes} routeOrder={routeOrderValue} onSaveRouteOrder={handleSaveRouteOrder} onConvertType={handleConvertNodeType} variant="overlay" wide />
675
2887
  ) : (
676
2888
  <NodeEditDialog node={selectedNode} isOpen={showNodeDialog} onClose={() => setShowNodeDialog(false)} onSave={handleSaveNode} onDelete={handleDeleteNode} />
677
2889
  )}
678
- {process.env.NEXT_PUBLIC_WORKFLOW_CRUDFORM_ENABLED === 'true' ? (
679
- <EdgeEditDialogCrudForm edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} />
2890
+ {crudFormDialogsEnabled ? (
2891
+ <EdgeEditDialogCrudForm edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} ledgerEntries={edgeDialogLedgerEntries} focusFieldId={edgeDialogFocusFieldId} variant="overlay" wide />
680
2892
  ) : (
681
2893
  <EdgeEditDialog edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} />
682
2894
  )}
683
- <Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}>
684
- <DialogContent className="sm:max-w-md">
685
- <DialogHeader>
686
- <DialogTitle>{t('workflows.visualEditor.clearTitle')}</DialogTitle>
687
- <DialogDescription>{t('workflows.visualEditor.clearDescription')}</DialogDescription>
688
- </DialogHeader>
689
- <DialogFooter>
690
- <Button variant="outline" onClick={() => setShowClearConfirm(false)}>{t('common.cancel', 'Cancel')}</Button>
691
- <Button variant="destructive-solid" onClick={confirmClear}>{t('common.clear', 'Clear')}</Button>
692
- </DialogFooter>
693
- </DialogContent>
694
- </Dialog>
695
2895
  </>
696
2896
  )
697
2897
 
2898
+ const sharedDialogs = (
2899
+ <>
2900
+ {/* Docked inspectors render inside the editor's layout row instead, so
2901
+ they shrink the canvas rather than covering it. */}
2902
+ {inspectorsDocked ? null : inspectorPanels}
2903
+ <WorkflowCommandPalette
2904
+ open={showCommandPalette}
2905
+ onOpenChange={setShowCommandPalette}
2906
+ commands={commandPaletteCommands}
2907
+ />
2908
+ <DefinitionMetadataDrawer
2909
+ open={metadataOpen}
2910
+ onOpenChange={handleMetadataOpenChange}
2911
+ focusSection={metadataFocusSection}
2912
+ definitionId={definitionId}
2913
+ readOnly={isCodeOnly}
2914
+ metadata={metadata}
2915
+ handlers={metadataHandlers}
2916
+ errorHandlerStepOptions={errorHandlerStepOptions}
2917
+ onSave={() => { void handleSave() }}
2918
+ isSaving={isSaving}
2919
+ />
2920
+ <TriggersDialog
2921
+ open={triggersDialogOpen}
2922
+ onOpenChange={setTriggersDialogOpen}
2923
+ triggers={triggers}
2924
+ onSave={setTriggers}
2925
+ />
2926
+ {(() => {
2927
+ const edge = activityEdit ? edges.find((candidate) => candidate.id === activityEdit.edgeId) ?? null : null
2928
+ const activities = (edge?.data?.activities as Activity[] | undefined) ?? []
2929
+ const activity = activityEdit ? activities.find((item) => item.activityId === activityEdit.activityId) ?? null : null
2930
+ const labelFor = (nodeId: string) =>
2931
+ (nodes.find((node) => node.id === nodeId)?.data?.label as string | undefined) ?? nodeId
2932
+ const routeLabel = edge ? `${labelFor(edge.source)} → ${labelFor(edge.target)}` : undefined
2933
+ return (
2934
+ <ActivityEditDialog
2935
+ open={!!activity}
2936
+ onOpenChange={(open) => { if (!open) setActivityEdit(null) }}
2937
+ activity={activity}
2938
+ routeLabel={routeLabel}
2939
+ onSave={(updated) => {
2940
+ if (!activityEdit) return
2941
+ handleSaveEdge(activityEdit.edgeId, {
2942
+ activities: activities.map((item) => (item.activityId === updated.activityId ? updated : item)),
2943
+ })
2944
+ }}
2945
+ onRemove={() => {
2946
+ if (!activityEdit) return
2947
+ handleSaveEdge(activityEdit.edgeId, {
2948
+ activities: activities.filter((item) => item.activityId !== activityEdit.activityId),
2949
+ })
2950
+ }}
2951
+ onOpenFullTransition={() => {
2952
+ if (activityEdit) handleRouteChipOpen(activityEdit.edgeId, 'activities')
2953
+ }}
2954
+ />
2955
+ )
2956
+ })()}
2957
+ <WorkflowCodeView
2958
+ isOpen={showCodeView}
2959
+ onClose={handleCloseCodeView}
2960
+ definitionJson={codeViewJson}
2961
+ issues={codeViewIssues}
2962
+ onCopy={() => { void handleCopyDefinitionJson() }}
2963
+ onPasteSubgraph={() => { void handlePaste() }}
2964
+ canPaste={!isCodeOnly}
2965
+ onIssueClick={handleProblemClick}
2966
+ draftText={codeDraftText}
2967
+ onDraftChange={setCodeDraft}
2968
+ onApply={handleApplyCodeDraft}
2969
+ onRevert={handleRevertCodeDraft}
2970
+ draftStatus={codeDraftStatus}
2971
+ severityByLine={codeIssueLocations.severityByLine}
2972
+ lineByIssueId={codeIssueLocations.lineByIssueId}
2973
+ />
2974
+ <WorkflowAiDraftDialog
2975
+ open={showAiDraft}
2976
+ onOpenChange={setShowAiDraft}
2977
+ onApply={handleApplyAiDraft}
2978
+ canvasHasContent={nodes.length > 0}
2979
+ />
2980
+ <AnnotationEditDialog
2981
+ node={selectedAnnotation}
2982
+ isOpen={showAnnotationDialog}
2983
+ onClose={() => setShowAnnotationDialog(false)}
2984
+ onSave={handleSaveAnnotation}
2985
+ onDelete={handleDeleteAnnotation}
2986
+ />
2987
+ <TemplateGalleryDialog
2988
+ open={showTemplateGallery}
2989
+ onOpenChange={setShowTemplateGallery}
2990
+ onSelect={handleTemplateSelect}
2991
+ />
2992
+ {/* Clearing the canvas is a yes/no interruption that must block — a rail
2993
+ would make an irreversible action easy to miss. */}
2994
+ <ConfirmDialog
2995
+ open={showClearConfirm}
2996
+ onOpenChange={setShowClearConfirm}
2997
+ title={t('workflows.visualEditor.clearTitle')}
2998
+ text={t('workflows.visualEditor.clearDescription')}
2999
+ confirmText={t('common.clear', 'Clear')}
3000
+ cancelText={t('common.cancel', 'Cancel')}
3001
+ variant="destructive"
3002
+ onConfirm={confirmClear}
3003
+ />
3004
+ {/* Starting a run is a FORM — a context payload, saved fixtures and two
3005
+ run-mode switches — authored against the canvas it will execute, so it
3006
+ is a rail rather than a modal that hides the graph. */}
3007
+ <Drawer open={startOpen} onOpenChange={setStartOpen}>
3008
+ <DrawerContent
3009
+ data-testid="workflow-start-instance-drawer"
3010
+ closeAriaLabel={t('workflows.startInstance.close')}
3011
+ onKeyDown={(event) => {
3012
+ if (!(event.metaKey || event.ctrlKey) || event.key !== 'Enter') return
3013
+ event.preventDefault()
3014
+ event.stopPropagation()
3015
+ if (starting) return
3016
+ void handleStartInstance()
3017
+ }}
3018
+ >
3019
+ <DrawerHeader>
3020
+ <DrawerTitle>{t('workflows.startInstance.title')}</DrawerTitle>
3021
+ <DrawerDescription>{t('workflows.startInstance.description')}</DrawerDescription>
3022
+ </DrawerHeader>
3023
+ <DrawerBody className="space-y-3 py-2">
3024
+
3025
+ {startFixtureList.length > 0 ? (
3026
+ <div className="space-y-1">
3027
+ <span className="text-overline text-muted-foreground">{t('workflows.fixtures.label')}</span>
3028
+ <div className="flex flex-wrap gap-1">
3029
+ {startFixtureList.map((fixture) => (
3030
+ <span key={fixture.name} className="inline-flex items-center gap-1 rounded-md border border-border bg-muted px-2 py-1">
3031
+ <button
3032
+ type="button"
3033
+ className="text-xs font-medium hover:underline"
3034
+ onClick={() => handleApplyFixture(fixture.name)}
3035
+ >
3036
+ {fixture.name}
3037
+ </button>
3038
+ <button
3039
+ type="button"
3040
+ className="text-xs text-muted-foreground hover:text-foreground"
3041
+ aria-label={t('workflows.fixtures.delete', { name: fixture.name })}
3042
+ onClick={() => handleDeleteFixture(fixture.name)}
3043
+ >
3044
+ &times;
3045
+ </button>
3046
+ </span>
3047
+ ))}
3048
+ </div>
3049
+ </div>
3050
+ ) : null}
3051
+
3052
+ <label className="text-sm font-medium" htmlFor="workflow-start-context">
3053
+ {t('workflows.startInstance.contextLabel')}
3054
+ </label>
3055
+ <Textarea
3056
+ id="workflow-start-context"
3057
+ value={startContext}
3058
+ onChange={(e) => setStartContext(e.target.value)}
3059
+ rows={9}
3060
+ spellCheck={false}
3061
+ className="font-mono text-sm"
3062
+ />
3063
+
3064
+ <div className="flex items-end gap-2">
3065
+ <div className="flex-1 space-y-1">
3066
+ <Label htmlFor="workflow-fixture-name" className="text-xs">
3067
+ {t('workflows.fixtures.nameLabel')}
3068
+ </Label>
3069
+ <Input
3070
+ id="workflow-fixture-name"
3071
+ value={fixtureName}
3072
+ onChange={(e) => setFixtureName(e.target.value)}
3073
+ placeholder={t('workflows.fixtures.namePlaceholder')}
3074
+ />
3075
+ </div>
3076
+ <Button variant="outline" onClick={handleSaveFixture} disabled={!fixtureName.trim()}>
3077
+ {t('workflows.fixtures.save')}
3078
+ </Button>
3079
+ </div>
3080
+ <p className="text-xs text-muted-foreground">{t('workflows.fixtures.storageWarning')}</p>
3081
+
3082
+ <div className="space-y-2 rounded-md border border-border p-2">
3083
+ <div className="flex items-center justify-between gap-2">
3084
+ <Label htmlFor="workflow-start-dry-run" className="text-sm font-medium">
3085
+ {t('workflows.startInstance.dryRunLabel')}
3086
+ </Label>
3087
+ <Switch
3088
+ id="workflow-start-dry-run"
3089
+ checked={startDryRun}
3090
+ onCheckedChange={(checked) => setStartDryRun(checked === true)}
3091
+ />
3092
+ </div>
3093
+ <p className="text-xs text-muted-foreground">{t('workflows.startInstance.dryRunHint')}</p>
3094
+ <div className="flex items-center justify-between gap-2">
3095
+ <Label htmlFor="workflow-start-step-through" className="text-sm font-medium">
3096
+ {t('workflows.startInstance.stepThroughLabel')}
3097
+ </Label>
3098
+ <Switch
3099
+ id="workflow-start-step-through"
3100
+ checked={startStepThrough}
3101
+ onCheckedChange={(checked) => setStartStepThrough(checked === true)}
3102
+ />
3103
+ </div>
3104
+ <p className="text-xs text-muted-foreground">{t('workflows.startInstance.stepThroughHint')}</p>
3105
+ </div>
3106
+ </DrawerBody>
3107
+ <DrawerFooter>
3108
+ <Button variant="outline" onClick={() => setStartOpen(false)} disabled={starting}>
3109
+ {t('workflows.startInstance.cancel')}
3110
+ </Button>
3111
+ <Button onClick={() => void handleStartInstance()} disabled={starting}>
3112
+ {starting ? <Spinner className="h-4 w-4" /> : t('workflows.startInstance.start')}
3113
+ </Button>
3114
+ </DrawerFooter>
3115
+ </DrawerContent>
3116
+ </Drawer>
3117
+ </>
3118
+ )
3119
+
3120
+ const structuralConflictBanner = structuralConflict ? (
3121
+ <div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
3122
+ <Alert variant="warning">
3123
+ <AlertTitle>
3124
+ {t('workflows.visualEditor.editSafety.bannerTitle', 'This change needs a new version')}
3125
+ </AlertTitle>
3126
+ <AlertDescription>
3127
+ {t(
3128
+ 'workflows.visualEditor.editSafety.bannerDescription',
3129
+ '{count} instance(s) are still running on this version. Changing the steps or routes would re-point them mid-flight, so the change is saved to a new version instead.',
3130
+ { count: String(structuralConflict.activeInstanceCount) },
3131
+ )}
3132
+ </AlertDescription>
3133
+ <div className="mt-2 flex flex-wrap gap-2">
3134
+ <Button size="sm" onClick={handleCreateVersion} disabled={isCreatingVersion} className="h-8 text-xs">
3135
+ {t('workflows.visualEditor.editSafety.createVersion', 'Create version')}
3136
+ </Button>
3137
+ <Button
3138
+ size="sm"
3139
+ variant="outline"
3140
+ onClick={() => setStructuralConflict(null)}
3141
+ disabled={isCreatingVersion}
3142
+ className="h-8 text-xs"
3143
+ >
3144
+ {t('workflows.visualEditor.editSafety.keepEditing', 'Keep editing')}
3145
+ </Button>
3146
+ </div>
3147
+ </Alert>
3148
+ </div>
3149
+ ) : null
3150
+
3151
+ const draftRestoreBanner = pendingDraft ? (
3152
+ <div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
3153
+ <Alert variant="info">
3154
+ <AlertTitle>
3155
+ {t('workflows.visualEditor.draft.bannerTitle', 'You have an unsaved draft from {time}', {
3156
+ time: formatRelativeTime(pendingDraft.draft.updatedAt, { translate: t }) ?? '',
3157
+ })}
3158
+ </AlertTitle>
3159
+ {pendingDraft.baseMismatch && (
3160
+ <AlertDescription>
3161
+ {t('workflows.visualEditor.draft.bannerConflict', 'The workflow definition has changed since this draft was made. Restoring will apply your draft over the newer version.')}
3162
+ </AlertDescription>
3163
+ )}
3164
+ <div className="mt-2 flex flex-wrap gap-2">
3165
+ <Button size="sm" onClick={handleRestoreDraft} className="h-8 text-xs">
3166
+ {t('workflows.visualEditor.draft.restore', 'Restore draft')}
3167
+ </Button>
3168
+ <Button size="sm" variant="outline" onClick={handleDiscardDraft} className="h-8 text-xs">
3169
+ {t('workflows.visualEditor.draft.discard', 'Discard')}
3170
+ </Button>
3171
+ </div>
3172
+ </Alert>
3173
+ </div>
3174
+ ) : null
3175
+
3176
+ const { errors: problemErrorCount, warnings: problemWarningCount } = countIssuesBySeverity(problems)
3177
+
3178
+ const problemsPanel = showProblems && problems.length > 0 ? (
3179
+ <div className="shrink-0 border-t border-border bg-background px-3 py-2 md:px-6 md:py-3">
3180
+ <div className={`rounded-lg border bg-card ${problemErrorCount > 0 ? 'border-status-error-border' : 'border-status-warning-border'}`}>
3181
+ <div className="flex items-center justify-between gap-2 px-3 py-2">
3182
+ <Button
3183
+ variant="ghost"
3184
+ size="sm"
3185
+ onClick={() => setProblemsCollapsed((collapsed) => !collapsed)}
3186
+ aria-expanded={!problemsCollapsed}
3187
+ className="h-auto gap-2 p-0 text-sm font-semibold text-foreground hover:bg-transparent"
3188
+ >
3189
+ {problemsCollapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronDown className="h-4 w-4" aria-hidden="true" />}
3190
+ {t('workflows.visualEditor.problems.title', 'Problems')}
3191
+ <span className="text-xs font-normal text-muted-foreground">
3192
+ {t('workflows.visualEditor.problems.counts', '{errors} error(s) · {warnings} warning(s)', { errors: problemErrorCount, warnings: problemWarningCount })}
3193
+ </span>
3194
+ </Button>
3195
+ <Button
3196
+ variant="ghost"
3197
+ size="sm"
3198
+ onClick={() => setShowProblems(false)}
3199
+ aria-label={t('workflows.visualEditor.problems.dismiss', 'Dismiss problems')}
3200
+ className="h-7 w-7 p-0"
3201
+ >
3202
+ <X className="h-4 w-4" aria-hidden="true" />
3203
+ </Button>
3204
+ </div>
3205
+ {!problemsCollapsed && (
3206
+ <ul className="max-h-56 overflow-y-auto border-t border-border">
3207
+ {problems.map((issue) => {
3208
+ const isNavigable = Boolean(issue.nodeId || issue.edgeId)
3209
+ return (
3210
+ <li key={issue.id}>
3211
+ <Button
3212
+ variant="ghost"
3213
+ onClick={() => handleProblemClick(issue)}
3214
+ disabled={!isNavigable}
3215
+ className={`flex h-auto w-full items-start justify-start gap-2 rounded-none px-3 py-1.5 text-left text-sm font-normal ${isNavigable ? 'hover:bg-muted' : 'cursor-default hover:bg-transparent'}`}
3216
+ >
3217
+ {issue.severity === 'error' ? (
3218
+ <CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-error-text" aria-hidden="true" />
3219
+ ) : (
3220
+ <TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-warning-text" aria-hidden="true" />
3221
+ )}
3222
+ <span className="min-w-0 flex-1 text-foreground">{issue.message}</span>
3223
+ {issue.nodeLabel && (
3224
+ <span className="shrink-0 text-xs text-muted-foreground">{issue.nodeLabel}</span>
3225
+ )}
3226
+ </Button>
3227
+ </li>
3228
+ )
3229
+ })}
3230
+ </ul>
3231
+ )}
3232
+ </div>
3233
+ </div>
3234
+ ) : null
3235
+
698
3236
  if (isMobile) {
699
3237
  return (
700
3238
  <Page className="flex h-[100svh] flex-col space-y-0 overflow-hidden">
3239
+ {structuralConflictBanner}
3240
+ {draftRestoreBanner}
701
3241
  <MobileVisualEditor
702
3242
  definitionId={definitionId}
703
3243
  isSaving={isSaving}
@@ -708,14 +3248,15 @@ export default function VisualEditorPage() {
708
3248
  onNodeClick={handleNodeClick}
709
3249
  onEdgeClick={handleEdgeClick}
710
3250
  onConnect={handleConnect}
3251
+ onReconnect={handleReconnect}
711
3252
  onAddNode={handleAddNode}
712
3253
  onSave={handleSave}
713
3254
  onValidate={handleValidate}
714
- onTest={handleTest}
715
- onLoadExample={handleLoadExample}
3255
+ onStartInstance={() => setStartOpen(true)}
3256
+ onLoadExample={handleOpenTemplateGallery}
716
3257
  onClear={handleClear}
717
3258
  metadata={metadata}
718
- metadataHandlers={metadataHandlers}
3259
+ onMetadataOpenChange={handleMetadataOpenChange}
719
3260
  />
720
3261
  {sharedDialogs}
721
3262
  {ConfirmDialogElement}
@@ -723,89 +3264,168 @@ export default function VisualEditorPage() {
723
3264
  )
724
3265
  }
725
3266
 
3267
+ // Break the editor out of `main`'s centered `max-w-screen-2xl` so the canvas
3268
+ // uses the full content column on wide screens (the side gutters the author
3269
+ // saw). Width = the content column (viewport minus the sidebar offset exposed
3270
+ // by AppShell); the negative margins cancel `main`'s auto-centering
3271
+ // (`max(0, (col-1536)/2)`) and its `lg:p-6` padding (24px). Desktop-only — the
3272
+ // compact (<1280px) layout keeps the normal centered container.
3273
+ const fullBleedSideMargin =
3274
+ 'calc(-1 * (max(0px, (100vw - var(--app-content-offset, 0px) - 1536px) / 2) + 24px))'
3275
+ const fullBleedStyle: React.CSSProperties = !isCompactViewport
3276
+ ? {
3277
+ width: 'calc(100vw - var(--app-content-offset, 0px))',
3278
+ marginLeft: fullBleedSideMargin,
3279
+ marginRight: fullBleedSideMargin,
3280
+ }
3281
+ : {}
3282
+
726
3283
  return (
727
- <Page className="space-y-0 overflow-x-hidden">
3284
+ <Page
3285
+ className="flex min-h-[calc(100svh-7rem)] flex-col space-y-0 overflow-x-hidden"
3286
+ style={fullBleedStyle}
3287
+ >
728
3288
  {/* Page Header */}
729
- <div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
3289
+ <div className={`shrink-0 border-b border-border bg-background ${focusMode ? 'px-3 py-1.5 md:px-4' : 'px-3 py-2 md:px-6 md:py-3'}`}>
730
3290
  <FormHeader
731
3291
  mode="detail"
732
3292
  backHref="/backend/definitions"
733
3293
  backLabel={t('workflows.definitions.backToList', 'Back to definitions')}
734
3294
  title={definitionId ? (workflowName || t('workflows.definitions.singular')) : t('workflows.backend.definitions.visual_editor.title')}
735
- subtitle={definitionId
736
- ? t('workflows.definitions.detail.summary', 'Editing workflow definition')
737
- : t('workflows.definitions.create.summary', 'Create and edit workflow definitions visually with a drag-and-drop interface')
3295
+ subtitle={focusMode
3296
+ ? undefined
3297
+ : definitionId
3298
+ ? t('workflows.definitions.detail.summary', 'Editing workflow definition')
3299
+ : t('workflows.definitions.create.summary', 'Create and edit workflow definitions visually with a drag-and-drop interface')
738
3300
  }
739
3301
  actionsContent={
740
3302
  <div className="flex flex-wrap items-center justify-end gap-1 md:gap-2">
741
- <Button
742
- variant="outline"
743
- size="sm"
744
- onClick={() => setShowMetadata(!showMetadata)}
745
- disabled={isSaving}
746
- className="h-8 px-2 text-xs"
747
- aria-label={showMetadata ? t('workflows.visualEditor.hideMetadata') : t('workflows.visualEditor.showMetadata')}
748
- >
749
- {showMetadata ? <PanelTopClose className="mr-1.5 h-4 w-4" /> : <PanelTopOpen className="mr-1.5 h-4 w-4" />}
750
- {showMetadata ? t('workflows.visualEditor.hideMetadata') : t('workflows.visualEditor.showMetadata')}
751
- </Button>
752
- {!isCodeOnly && (
753
- <Button
754
- variant="outline"
755
- size="sm"
756
- onClick={handleLoadExample}
757
- disabled={isSaving}
758
- className="h-8 text-xs"
759
- >
760
- {t('workflows.visualEditor.loadExample')}
761
- </Button>
3303
+ {draftEligible && (draftSaveFailed || draftSavedLabel) && (
3304
+ <span role="status" className="text-xs text-muted-foreground">
3305
+ {draftSaveFailed
3306
+ ? t('workflows.visualEditor.draft.saveFailed', 'Draft not saved')
3307
+ : t('workflows.visualEditor.draft.saved', 'Draft saved {time}', { time: draftSavedLabel ?? '' })}
3308
+ </span>
762
3309
  )}
763
- {!isCodeOnly && (
764
- <Button
765
- variant="destructive"
766
- size="sm"
767
- onClick={handleClear}
768
- disabled={isSaving}
769
- className="h-8 px-2 text-xs"
770
- aria-label={t('workflows.visualEditor.clear')}
771
- >
772
- <Trash2 className="mr-1.5 h-4 w-4" />
773
- {t('workflows.visualEditor.clear')}
774
- </Button>
3310
+ {!isCodeOnly && definitionId && autosaveState !== 'idle' && (
3311
+ <span className="mr-1 text-xs text-muted-foreground" aria-live="polite">
3312
+ {autosaveState === 'saving' ? t('workflows.visualEditor.autosaving') : t('workflows.visualEditor.autosaved')}
3313
+ </span>
775
3314
  )}
776
- <Button
777
- variant="outline"
778
- size="sm"
779
- onClick={handleValidate}
780
- disabled={isSaving}
781
- className="h-8 px-2 text-xs"
782
- aria-label={t('workflows.visualEditor.validate')}
783
- >
784
- <CircleQuestionMark className="mr-1.5 h-4 w-4" />
785
- {t('workflows.visualEditor.validate')}
786
- </Button>
787
- {!isCodeOnly && (
3315
+ {focusMode ? (
3316
+ // Focus mode collapses the toolbar to the essentials; the toggle
3317
+ // stays reachable so the author can leave, and Cmd+K still opens
3318
+ // every command.
788
3319
  <Button
789
3320
  variant="outline"
790
3321
  size="sm"
791
- onClick={handleTest}
3322
+ onClick={toggleFocus}
792
3323
  disabled={isSaving}
793
- className="h-8 text-xs"
3324
+ className="h-8 px-2 text-xs"
3325
+ aria-label={t('workflows.visualEditor.exitFocusMode')}
794
3326
  >
795
- <Play className="mr-1.5 h-4 w-4" />
796
- {t('workflows.visualEditor.runTest')}
3327
+ <Minimize2 className="mr-1.5 h-4 w-4" />
3328
+ {t('workflows.visualEditor.exitFocusMode')}
797
3329
  </Button>
798
- )}
799
- {isCodeOverride && (
3330
+ ) : (
3331
+ <>
3332
+ {/* Validate — surfaces the last Problems pass's error count so the
3333
+ author sees "3 errors" without opening the panel; icon + count,
3334
+ never colour alone (spec §4.6). */}
800
3335
  <Button
801
3336
  variant="outline"
802
3337
  size="sm"
803
- onClick={handleResetToCode}
3338
+ onClick={handleValidate}
804
3339
  disabled={isSaving}
805
- className="h-8 text-xs"
3340
+ className="h-8 px-2 text-xs"
3341
+ aria-label={problemErrorCount > 0
3342
+ ? t('workflows.visualEditor.validateWithErrors', '{count} validation errors', { count: problemErrorCount })
3343
+ : t('workflows.visualEditor.validate')}
806
3344
  >
807
- {t('workflows.actions.resetToCode')}
3345
+ {problemErrorCount > 0 ? (
3346
+ <CircleAlert className="mr-1.5 h-4 w-4 text-destructive" aria-hidden="true" />
3347
+ ) : (
3348
+ <CircleQuestionMark className="mr-1.5 h-4 w-4" aria-hidden="true" />
3349
+ )}
3350
+ {t('workflows.visualEditor.validate')}
3351
+ {problemErrorCount > 0 && (
3352
+ <span className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-white">
3353
+ {problemErrorCount}
3354
+ </span>
3355
+ )}
808
3356
  </Button>
3357
+ {/* One grouped overflow keeps only the essentials — Validate,
3358
+ Start instance, Update — in the row. The metadata-incomplete
3359
+ warning rides the trigger (attention dot) and the Settings
3360
+ item, so hiding Settings here never buries it. */}
3361
+ {(() => {
3362
+ const moreActions: ActionMenuEntry[] = []
3363
+ moreActions.push({ id: 'group-tools', header: t('workflows.visualEditor.menuGroups.tools', 'Tools') })
3364
+ moreActions.push({ id: 'commands', label: t('workflows.commandPalette.buttonLabel', 'Commands'), icon: Command, onSelect: () => setShowCommandPalette(true) })
3365
+ if (!isCodeOnly) moreActions.push({ id: 'ai-draft', label: t('workflows.aiDraft.toggleShort', 'AI draft'), icon: Sparkles, onSelect: () => setShowAiDraft(true) })
3366
+ moreActions.push({ id: 'group-view', header: t('workflows.visualEditor.menuGroups.view', 'View') })
3367
+ moreActions.push({ id: 'focus', label: t('workflows.visualEditor.enterFocusMode'), icon: Maximize2, onSelect: toggleFocus, disabled: isSaving })
3368
+ moreActions.push({ id: 'settings', label: t('workflows.visualEditor.metadata.buttonLabel', 'Settings'), icon: metadataIncomplete ? TriangleAlert : PanelRightOpen, onSelect: () => setMetadataOpen(true), disabled: isSaving })
3369
+ moreActions.push({ id: 'code', label: t('workflows.visualEditor.codeView.title', 'Code'), icon: Code, onSelect: () => setShowCodeView(true) })
3370
+ moreActions.push({ id: 'group-canvas', header: t('workflows.visualEditor.menuGroups.canvas', 'Canvas') })
3371
+ if (!isCodeOnly) moreActions.push({ id: 'load-example', label: t('workflows.visualEditor.loadExample'), onSelect: handleOpenTemplateGallery, disabled: isSaving })
3372
+ if (!isCodeOnly) moreActions.push({ id: 'auto-arrange', label: t('workflows.visualEditor.autoArrange'), icon: Network, onSelect: handleAutoArrange, disabled: isSaving || nodes.length === 0 })
3373
+ moreActions.push({ id: 'compensation', label: t('workflows.compensation.toggleShort', 'Compensation'), icon: ShieldMinus, onSelect: toggleCompensation })
3374
+ if (isCodeOverride) moreActions.push({ id: 'reset-to-code', label: t('workflows.actions.resetToCode'), onSelect: handleResetToCode, disabled: isSaving })
3375
+ if (!isCodeOnly) {
3376
+ moreActions.push({ separator: true, id: 'sep-destructive' })
3377
+ moreActions.push({ id: 'clear', label: t('workflows.visualEditor.clear'), icon: Trash2, onSelect: handleClear, disabled: isSaving, destructive: true })
3378
+ }
3379
+ return (
3380
+ <ActionsDropdown
3381
+ items={moreActions}
3382
+ label={t('workflows.visualEditor.more', 'More')}
3383
+ ariaLabel={t('workflows.visualEditor.more', 'More')}
3384
+ triggerClassName="h-8 px-2 text-xs"
3385
+ triggerIcon={false}
3386
+ attention={metadataIncomplete}
3387
+ />
3388
+ )
3389
+ })()}
3390
+ {/* Execution overlay (spec §8.3): "Show last run" paints node
3391
+ states with DS status tokens and the taken path. It stays a
3392
+ toolbar button rather than joining the More menu because it
3393
+ is a STATEFUL toggle with something to report: a menu entry
3394
+ can neither announce `aria-pressed` nor keep the "(never
3395
+ run)" caption visible once the menu closes, and §8.3 requires
3396
+ a definition that has never run to say so. Read-only —
3397
+ derived at render time, never part of the document. */}
3398
+ {definitionId && (
3399
+ <Button
3400
+ variant="outline"
3401
+ size="sm"
3402
+ onClick={toggleLastRun}
3403
+ aria-pressed={showLastRun}
3404
+ className="h-8 px-2 text-xs"
3405
+ aria-label={t('workflows.lastRun.toggle', 'Show the last run on the canvas')}
3406
+ >
3407
+ <History className="mr-1.5 h-4 w-4" aria-hidden="true" />
3408
+ {t('workflows.lastRun.toggleShort', 'Last run')}
3409
+ {showLastRun && lastRunOverlay.isUnavailable ? (
3410
+ <span className="ml-1.5 text-muted-foreground">
3411
+ {t('workflows.lastRun.never', '(never run)')}
3412
+ </span>
3413
+ ) : null}
3414
+ </Button>
3415
+ )}
3416
+ {definitionId && (
3417
+ <Button
3418
+ variant="outline"
3419
+ size="sm"
3420
+ onClick={() => setStartOpen(true)}
3421
+ disabled={isSaving}
3422
+ className="h-8 text-xs"
3423
+ >
3424
+ <Play className="mr-1.5 h-4 w-4" />
3425
+ {t('workflows.actions.startInstance')}
3426
+ </Button>
3427
+ )}
3428
+ </>
809
3429
  )}
810
3430
  {isCodeOnly ? (
811
3431
  <Button
@@ -849,150 +3469,11 @@ export default function VisualEditorPage() {
849
3469
  </div>
850
3470
  )}
851
3471
 
852
- {/* Workflow Metadata Form */}
853
- {showMetadata && (
854
- <div className={isCompactViewport
855
- ? 'shrink-0 border-b border-border bg-background px-3 py-2 max-h-[60svh] overflow-y-auto overscroll-contain md:px-6 md:py-3'
856
- : 'shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3'
857
- }>
858
- <fieldset disabled={isCodeOnly} className="rounded-lg border bg-card p-3 disabled:opacity-70 md:p-4">
859
- <h2 className="mb-3 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.workflowMetadata')}</h2>
860
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 md:gap-4">
861
- {/* Workflow ID */}
862
- <div className="min-w-0 space-y-1">
863
- <Label htmlFor="workflowId" className="text-xs">{t('workflows.form.workflowId')} *</Label>
864
- <Input
865
- id="workflowId"
866
- value={workflowId}
867
- onChange={(e) => setWorkflowId(e.target.value)}
868
- placeholder="checkout_workflow"
869
- disabled={!!definitionId}
870
- className="h-8 text-sm"
871
- />
872
- {definitionId && <p className="text-overline text-muted-foreground">{t('workflows.visualEditor.readOnly')}</p>}
873
- </div>
874
-
875
- {/* Workflow Name */}
876
- <div className="min-w-0 space-y-1">
877
- <Label htmlFor="workflowName" className="text-xs">{t('workflows.form.workflowName')} *</Label>
878
- <Input
879
- id="workflowName"
880
- value={workflowName}
881
- onChange={(e) => setWorkflowName(e.target.value)}
882
- placeholder="Checkout Process"
883
- className="h-8 text-sm"
884
- />
885
- </div>
886
-
887
- {/* Category */}
888
- <div className="min-w-0 space-y-1">
889
- <Label htmlFor="category" className="text-xs">{t('workflows.form.category')}</Label>
890
- <Input
891
- id="category"
892
- value={category}
893
- onChange={(e) => setCategory(e.target.value)}
894
- placeholder="E-Commerce"
895
- className="h-8 text-sm"
896
- />
897
- </div>
898
-
899
- {/* Description */}
900
- <div className="min-w-0 space-y-1 sm:col-span-2 lg:col-span-3">
901
- <Label htmlFor="description" className="text-xs">{t('workflows.form.description')}</Label>
902
- <Textarea
903
- id="description"
904
- value={description}
905
- onChange={(e) => setDescription(e.target.value)}
906
- placeholder={t('workflows.form.placeholders.description')}
907
- rows={2}
908
- className="min-h-[60px] text-sm"
909
- />
910
- </div>
911
-
912
- {/* Version */}
913
- <div className="min-w-0 space-y-1">
914
- <Label htmlFor="version" className="text-xs">{t('workflows.form.version')} *</Label>
915
- <Input
916
- id="version"
917
- type="number"
918
- value={version}
919
- onChange={(e) => setVersion(parseInt(e.target.value) || 1)}
920
- min={1}
921
- disabled={!!definitionId}
922
- className="h-8 text-sm"
923
- />
924
- </div>
925
-
926
- {/* Enabled */}
927
- <div className="min-w-0 space-y-1">
928
- <Label className="text-xs">{t('common.enabled', 'Enabled')}</Label>
929
- <div className="flex h-8 items-center gap-2">
930
- <Switch
931
- id="enabled"
932
- checked={enabled}
933
- onCheckedChange={setEnabled}
934
- />
935
- <Label htmlFor="enabled" className="cursor-pointer text-xs font-normal">
936
- {enabled ? t('common.on', 'On') : t('common.off', 'Off')}
937
- </Label>
938
- </div>
939
- </div>
940
-
941
- {/* Tags */}
942
- <div className="min-w-0 space-y-1">
943
- <Label className="text-xs">{t('workflows.form.tags')}</Label>
944
- <TagsInput
945
- value={tags}
946
- onChange={setTags}
947
- placeholder={t('workflows.form.placeholders.tags')}
948
- />
949
- </div>
950
-
951
- {/* Icon */}
952
- <div className="min-w-0 space-y-1">
953
- <Label htmlFor="icon" className="text-xs">{t('workflows.form.icon')}</Label>
954
- <Input
955
- id="icon"
956
- value={icon}
957
- onChange={(e) => setIcon(e.target.value)}
958
- placeholder="ShoppingCart"
959
- className="h-8 text-sm"
960
- />
961
- </div>
962
-
963
- <div className="min-w-0 space-y-1">
964
- <Label htmlFor="effectiveFrom" className="text-xs">{t('workflows.form.effectiveFrom')}</Label>
965
- <Input
966
- id="effectiveFrom"
967
- type="date"
968
- value={effectiveFrom}
969
- onChange={(e) => setEffectiveFrom(e.target.value)}
970
- className="h-8 text-sm"
971
- />
972
- </div>
973
-
974
- <div className="min-w-0 space-y-1">
975
- <Label htmlFor="effectiveTo" className="text-xs">{t('workflows.form.effectiveTo')}</Label>
976
- <Input
977
- id="effectiveTo"
978
- type="date"
979
- value={effectiveTo}
980
- onChange={(e) => setEffectiveTo(e.target.value)}
981
- className="h-8 text-sm"
982
- />
983
- </div>
984
- </div>
985
- </fieldset>
3472
+ {/* Edit-safety rule: structural change refused while instances run (spec §4.1) */}
3473
+ {structuralConflictBanner}
986
3474
 
987
- {/* Event Triggers — also locked when the workflow is code-defined */}
988
- <fieldset disabled={isCodeOnly} className="mt-3 disabled:opacity-70">
989
- <DefinitionTriggersEditor
990
- value={triggers}
991
- onChange={setTriggers}
992
- />
993
- </fieldset>
994
- </div>
995
- )}
3475
+ {/* Per-user draft restore banner (spec §4.7) */}
3476
+ {draftRestoreBanner}
996
3477
 
997
3478
  {/* Main Content */}
998
3479
  {isCompactViewport ? (
@@ -1007,8 +3488,18 @@ export default function VisualEditorPage() {
1007
3488
  onNodeClick={handleNodeClick}
1008
3489
  onEdgeClick={handleEdgeClick}
1009
3490
  onConnect={handleConnect}
3491
+ onReconnect={handleReconnect}
3492
+ onCanvasDrop={handleCanvasDrop}
1010
3493
  editable={!isCodeOnly}
1011
3494
  height="100%"
3495
+ focusTarget={focusTarget}
3496
+ nodeErrorCounts={nodeErrorCounts}
3497
+ showCompensation={showCompensation}
3498
+ runOverlay={lastRunOverlay.execution}
3499
+ runAgentLabels={lastRunOverlay.agentLabels}
3500
+ triggers={triggers}
3501
+ definitionEnabled={enabled}
3502
+ onOpenTriggers={handleOpenTriggers}
1012
3503
  />
1013
3504
  </div>
1014
3505
 
@@ -1018,7 +3509,7 @@ export default function VisualEditorPage() {
1018
3509
  <h2 className="mb-2 text-lg font-semibold text-foreground">{t('workflows.visualEditor.startBuilding')}</h2>
1019
3510
  <p className="mb-4 text-sm text-muted-foreground">{t('workflows.visualEditor.tapToAddBelow')}</p>
1020
3511
  <button
1021
- onClick={handleLoadExample}
3512
+ onClick={handleOpenTemplateGallery}
1022
3513
  className="pointer-events-auto text-sm text-primary hover:underline"
1023
3514
  >
1024
3515
  {t('workflows.visualEditor.loadExampleWorkflow')}
@@ -1034,11 +3525,13 @@ export default function VisualEditorPage() {
1034
3525
  <p className="mb-3 text-xs text-muted-foreground">{t('workflows.visualEditor.tapToAdd')}</p>
1035
3526
 
1036
3527
  <div className="flex gap-2 overflow-x-auto pb-1">
1037
- {(['start', 'userTask', 'automated', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
3528
+ {(['start', 'userTask', 'automated', 'invokeAgent', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
1038
3529
  const Icon = NODE_TYPE_ICONS[nodeType]
1039
3530
  return (
1040
3531
  <button
1041
3532
  key={nodeType}
3533
+ draggable
3534
+ onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
1042
3535
  onClick={() => handleAddNode(nodeType)}
1043
3536
  className="flex shrink-0 items-center gap-1 rounded-md border bg-background px-2 py-1 text-xs hover:bg-muted active:bg-muted/50"
1044
3537
  >
@@ -1052,143 +3545,172 @@ export default function VisualEditorPage() {
1052
3545
  )}
1053
3546
  </div>
1054
3547
  ) : (
1055
- <div className="flex min-h-[72svh] min-w-0 flex-1 border-t border-border">
1056
- {/* Left Sidebar - Step Palette (hidden in read-only mode) */}
3548
+ <div data-testid="workflow-editor-row" className="flex min-h-0 min-w-0 flex-1 border-t border-border pl-3 md:pl-6">
3549
+ {/* Left Sidebar - Step Palette rail (hidden in read-only mode) */}
1057
3550
  {!isCodeOnly && (
1058
- <div className="w-[24rem] shrink-0 overflow-y-auto border-r border-border bg-background p-6">
1059
- <div className="rounded-lg border bg-card p-4">
1060
- <h2 className="mb-2 text-sm font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.stepPalette')}</h2>
1061
- <p className="mb-4 text-xs text-muted-foreground">
1062
- {t('workflows.visualEditor.clickToAdd')}
1063
- </p>
1064
-
1065
- <div className="space-y-3">
1066
- {/* START Step */}
1067
- <button
1068
- onClick={() => handleAddNode('start')}
1069
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1070
- >
1071
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.start} opacity-60 transition-opacity group-hover:opacity-100`}>
1072
- {(() => {
1073
- const Icon = NODE_TYPE_ICONS.start
1074
- return <Icon className="h-4 w-4" />
1075
- })()}
1076
- </div>
1077
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.start.title}</div>
1078
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.start.description}</div>
1079
- </button>
1080
-
1081
- {/* USER_TASK Step */}
1082
- <button
1083
- onClick={() => handleAddNode('userTask')}
1084
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1085
- >
1086
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.userTask} opacity-60 transition-opacity group-hover:opacity-100`}>
1087
- {(() => {
1088
- const Icon = NODE_TYPE_ICONS.userTask
1089
- return <Icon className="h-4 w-4" />
1090
- })()}
1091
- </div>
1092
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.userTask.title}</div>
1093
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.userTask.description}</div>
1094
- </button>
3551
+ <div className={`${paletteCollapsed ? 'w-14' : 'w-48'} shrink-0 overflow-y-auto border-r border-border bg-background p-2`}>
3552
+ <div className={`mb-2 flex items-center ${paletteCollapsed ? 'justify-center' : 'justify-between'}`}>
3553
+ {!paletteCollapsed && (
3554
+ <h2 className="px-1 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.stepPalette')}</h2>
3555
+ )}
3556
+ <IconButton
3557
+ type="button"
3558
+ variant="ghost"
3559
+ size="sm"
3560
+ onClick={togglePaletteCollapsed}
3561
+ title={paletteCollapsed ? t('workflows.visualEditor.expandPalette') : t('workflows.visualEditor.collapsePalette')}
3562
+ aria-label={paletteCollapsed ? t('workflows.visualEditor.expandPalette') : t('workflows.visualEditor.collapsePalette')}
3563
+ >
3564
+ {paletteCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
3565
+ </IconButton>
3566
+ </div>
1095
3567
 
1096
- {/* AUTOMATED Step */}
1097
- <button
1098
- onClick={() => handleAddNode('automated')}
1099
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1100
- >
1101
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.automated} opacity-60 transition-opacity group-hover:opacity-100`}>
1102
- {(() => {
1103
- const Icon = NODE_TYPE_ICONS.automated
1104
- return <Icon className="h-4 w-4" />
1105
- })()}
1106
- </div>
1107
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.automated.title}</div>
1108
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.automated.description}</div>
1109
- </button>
3568
+ {!paletteCollapsed && (
3569
+ <p className="mb-2 px-1 text-xs text-muted-foreground">{t('workflows.visualEditor.clickToAdd')}</p>
3570
+ )}
1110
3571
 
1111
- {/* WAIT_FOR_SIGNAL Step */}
1112
- <button
1113
- onClick={() => handleAddNode('waitForSignal')}
1114
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1115
- >
1116
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.waitForSignal} opacity-60 transition-opacity group-hover:opacity-100`}>
1117
- {(() => {
1118
- const Icon = NODE_TYPE_ICONS.waitForSignal
1119
- return <Icon className="h-4 w-4" />
1120
- })()}
1121
- </div>
1122
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.waitForSignal.title}</div>
1123
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForSignal.description}</div>
1124
- </button>
3572
+ <div className={`flex flex-col gap-1 ${paletteCollapsed ? 'items-center' : ''}`}>
3573
+ {PALETTE_NODE_TYPES.map((nodeType) => {
3574
+ const Icon = NODE_TYPE_ICONS[nodeType]
3575
+ const label = NODE_TYPE_LABELS[nodeType]
3576
+ const tooltip = `${label.title} — ${label.description}`
3577
+ if (paletteCollapsed) {
3578
+ return (
3579
+ <button
3580
+ key={nodeType}
3581
+ type="button"
3582
+ draggable
3583
+ onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
3584
+ onClick={() => handleAddNode(nodeType)}
3585
+ title={tooltip}
3586
+ aria-label={tooltip}
3587
+ className="flex h-9 w-9 items-center justify-center rounded-md border hover:bg-muted"
3588
+ >
3589
+ <Icon className={`h-4 w-4 ${NODE_TYPE_COLORS[nodeType]}`} />
3590
+ </button>
3591
+ )
3592
+ }
3593
+ return (
3594
+ <button
3595
+ key={nodeType}
3596
+ type="button"
3597
+ draggable
3598
+ onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
3599
+ onClick={() => handleAddNode(nodeType)}
3600
+ title={label.description}
3601
+ className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
3602
+ >
3603
+ <Icon className={`h-4 w-4 shrink-0 ${NODE_TYPE_COLORS[nodeType]}`} />
3604
+ <span className="truncate font-medium text-foreground">{label.title}</span>
3605
+ </button>
3606
+ )
3607
+ })}
3608
+ </div>
1125
3609
 
1126
- {/* WAIT_FOR_TIMER Step */}
1127
- <button
1128
- onClick={() => handleAddNode('waitForTimer')}
1129
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1130
- >
1131
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.waitForTimer} opacity-60 transition-opacity group-hover:opacity-100`}>
1132
- {(() => {
1133
- const Icon = NODE_TYPE_ICONS.waitForTimer
1134
- return <Icon className="h-4 w-4" />
1135
- })()}
1136
- </div>
1137
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.waitForTimer.title}</div>
1138
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForTimer.description}</div>
1139
- </button>
3610
+ {/* Actions (spec §4.4): drag one onto a route to append it to that
3611
+ route's activities — the chips (#4244) then render it. Clicking
3612
+ explains where the keyboard path is, so the entry is never dead. */}
3613
+ {!paletteCollapsed && (
3614
+ <div className="mt-4">
3615
+ <h2 className="mb-1 px-1 text-xs font-semibold uppercase text-muted-foreground">
3616
+ {t('workflows.visualEditor.palette.actionsTitle', 'Actions')}
3617
+ </h2>
3618
+ <p className="mb-2 px-1 text-xs text-muted-foreground">
3619
+ {t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there')}
3620
+ </p>
3621
+ <div className="flex flex-col gap-1">
3622
+ {activityTypeOptions.map((option) => {
3623
+ const ActionIcon = resolveActivityIcon(option.value)
3624
+ return (
3625
+ <button
3626
+ key={option.value}
3627
+ type="button"
3628
+ draggable
3629
+ onDragStart={(event) => handlePaletteActivityDragStart(event, option.value, option.label)}
3630
+ onClick={() => flash(t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there'), 'info')}
3631
+ title={t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there')}
3632
+ className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
3633
+ >
3634
+ <ActionIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
3635
+ <span className="truncate font-medium text-foreground">{option.label}</span>
3636
+ </button>
3637
+ )
3638
+ })}
3639
+ </div>
3640
+ </div>
3641
+ )}
1140
3642
 
1141
- {/* SUB_WORKFLOW Step */}
1142
- <button
1143
- onClick={() => handleAddNode('subWorkflow')}
1144
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
1145
- >
1146
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.subWorkflow} opacity-60 transition-opacity group-hover:opacity-100`}>
1147
- {(() => {
1148
- const Icon = NODE_TYPE_ICONS.subWorkflow
1149
- return <Icon className="h-4 w-4" />
1150
- })()}
1151
- </div>
1152
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.subWorkflow.title}</div>
1153
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.subWorkflow.description}</div>
1154
- </button>
3643
+ {/* Annotations (spec §4.5): documentation only — filtered out of the
3644
+ saved definition, so they can never change what the engine runs. */}
3645
+ {!paletteCollapsed && (
3646
+ <div className="mt-4">
3647
+ <h2 className="mb-1 px-1 text-xs font-semibold uppercase text-muted-foreground">
3648
+ {t('workflows.annotations.paletteTitle', 'Annotations')}
3649
+ </h2>
3650
+ <p className="mb-2 px-1 text-xs text-muted-foreground">
3651
+ {t('workflows.annotations.paletteHint', 'Documentation only — never executed')}
3652
+ </p>
3653
+ <div className="flex flex-col gap-1">
3654
+ <button
3655
+ type="button"
3656
+ onClick={() => handleAddAnnotation('note')}
3657
+ title={t('workflows.annotations.note.addHint', 'Add a markdown sticky note to the canvas')}
3658
+ className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
3659
+ >
3660
+ <StickyNote className="h-4 w-4 shrink-0 text-status-warning-text" aria-hidden="true" />
3661
+ <span className="truncate font-medium text-foreground">{t('workflows.annotations.note.add', 'Note')}</span>
3662
+ </button>
3663
+ <button
3664
+ type="button"
3665
+ onClick={() => handleAddAnnotation('group')}
3666
+ title={t('workflows.annotations.group.addHint', 'Add a named region around part of the canvas')}
3667
+ className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
3668
+ >
3669
+ <Group className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
3670
+ <span className="truncate font-medium text-foreground">{t('workflows.annotations.group.add', 'Group')}</span>
3671
+ </button>
3672
+ </div>
3673
+ </div>
3674
+ )}
1155
3675
 
1156
- {/* END Step */}
3676
+ {!paletteCollapsed && (
3677
+ <div className="mt-3">
1157
3678
  <button
1158
- onClick={() => handleAddNode('end')}
1159
- className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
3679
+ type="button"
3680
+ onClick={() => setShowPaletteHowTo((prev) => !prev)}
3681
+ aria-expanded={showPaletteHowTo}
3682
+ className="flex w-full items-center gap-1.5 rounded-md px-1 py-1 text-xs text-muted-foreground hover:text-foreground"
1160
3683
  >
1161
- <div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.end} opacity-60 transition-opacity group-hover:opacity-100`}>
1162
- {(() => {
1163
- const Icon = NODE_TYPE_ICONS.end
1164
- return <Icon className="h-4 w-4" />
1165
- })()}
1166
- </div>
1167
- <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.end.title}</div>
1168
- <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.end.description}</div>
3684
+ <CircleQuestionMark className="h-3.5 w-3.5" />
3685
+ <span>{t('workflows.visualEditor.howToUse', 'How to use:')}</span>
1169
3686
  </button>
3687
+ {showPaletteHowTo && (
3688
+ <Alert variant="info" className="mt-2">
3689
+ <AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
3690
+ <div className="mt-2">
3691
+ <ul className="list-inside list-disc space-y-1 text-xs">
3692
+ <li>{t('workflows.visualEditor.hint.addSteps', 'Click step types to add them')}</li>
3693
+ <li>{t('workflows.visualEditor.hint.dragFromPalette', 'Drag a step onto the canvas, or onto a route to insert it there')}</li>
3694
+ <li>{t('workflows.visualEditor.hint.dragSteps', 'Drag steps to position them')}</li>
3695
+ <li>{t('workflows.visualEditor.hint.connectSteps', 'Connect steps by dragging from handles')}</li>
3696
+ <li>{t('workflows.visualEditor.hint.editSteps', 'Click steps and transitions to edit them')}</li>
3697
+ <li>{t('workflows.visualEditor.hint.copyPaste', 'Select steps and copy, paste or duplicate them with Cmd/Ctrl+C, +V and +D')}</li>
3698
+ <li>{t('workflows.visualEditor.hint.commandPalette', 'Press Cmd/Ctrl+K for every command without the mouse')}</li>
3699
+ <li>{t('workflows.visualEditor.hint.keyboardCanvas', 'With a step selected: Enter opens it, Del removes it, arrows nudge it (hold Shift for bigger steps)')}</li>
3700
+ <li>{t('workflows.visualEditor.hint.validate', 'Validate before saving')}</li>
3701
+ </ul>
3702
+ </div>
3703
+ </Alert>
3704
+ )}
1170
3705
  </div>
1171
-
1172
- {/* Instructions */}
1173
- <Alert status="information" className="mt-6">
1174
- <AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
1175
- <div className="mt-2">
1176
- <ul className="list-inside list-disc space-y-1 text-xs">
1177
- <li>{t('workflows.visualEditor.hint.addSteps', 'Click step types to add them')}</li>
1178
- <li>{t('workflows.visualEditor.hint.dragSteps', 'Drag steps to position them')}</li>
1179
- <li>{t('workflows.visualEditor.hint.connectSteps', 'Connect steps by dragging from handles')}</li>
1180
- <li>{t('workflows.visualEditor.hint.editSteps', 'Click steps and transitions to edit them')}</li>
1181
- <li>{t('workflows.visualEditor.hint.validate', 'Validate before saving')}</li>
1182
- </ul>
1183
- </div>
1184
- </Alert>
1185
- </div>
3706
+ )}
1186
3707
  </div>
1187
3708
  )}
1188
3709
 
1189
- {/* Main Canvas */}
3710
+ {/* Main Canvas — the header already carries the Focus toggle, so no
3711
+ separate in-canvas Exit-focus button (avoids two identical controls). */}
1190
3712
  <div className="min-w-0 flex-1 p-6">
1191
- <div className="relative h-[72svh] min-h-[640px]">
3713
+ <div className="relative h-full min-h-[480px]">
1192
3714
  <div className="h-full rounded-lg border bg-card">
1193
3715
  <WorkflowGraph
1194
3716
  initialNodes={nodes}
@@ -1198,8 +3720,18 @@ export default function VisualEditorPage() {
1198
3720
  onNodeClick={handleNodeClick}
1199
3721
  onEdgeClick={handleEdgeClick}
1200
3722
  onConnect={handleConnect}
3723
+ onReconnect={handleReconnect}
3724
+ onCanvasDrop={handleCanvasDrop}
1201
3725
  editable={!isCodeOnly}
1202
3726
  height="100%"
3727
+ focusTarget={focusTarget}
3728
+ nodeErrorCounts={nodeErrorCounts}
3729
+ showCompensation={showCompensation}
3730
+ runOverlay={lastRunOverlay.execution}
3731
+ runAgentLabels={lastRunOverlay.agentLabels}
3732
+ triggers={triggers}
3733
+ definitionEnabled={enabled}
3734
+ onOpenTriggers={handleOpenTriggers}
1203
3735
  />
1204
3736
  </div>
1205
3737
 
@@ -1214,7 +3746,7 @@ export default function VisualEditorPage() {
1214
3746
  {t('workflows.visualEditor.clickToAddFromPalette')}
1215
3747
  </p>
1216
3748
  <button
1217
- onClick={handleLoadExample}
3749
+ onClick={handleOpenTemplateGallery}
1218
3750
  className="pointer-events-auto text-sm text-primary hover:underline"
1219
3751
  >
1220
3752
  {t('workflows.visualEditor.loadExampleWorkflow')}
@@ -1224,8 +3756,14 @@ export default function VisualEditorPage() {
1224
3756
  )}
1225
3757
  </div>
1226
3758
  </div>
3759
+
3760
+ {/* Docked step / route inspector (spec §4.1). It is a sibling of the
3761
+ canvas, not an overlay on it, so opening it narrows the graph and
3762
+ leaves it visible and clickable. */}
3763
+ {inspectorsDocked ? inspectorPanels : null}
1227
3764
  </div>
1228
3765
  )}
3766
+ {problemsPanel}
1229
3767
  {sharedDialogs}
1230
3768
  {ConfirmDialogElement}
1231
3769
  </Page>
@@ -1242,6 +3780,10 @@ function getDefaultLabel(nodeType: string): string {
1242
3780
  decision: 'Decision Point',
1243
3781
  waitForSignal: 'Wait for Signal',
1244
3782
  waitForTimer: 'Wait for Timer',
3783
+ waitForCondition: 'Wait for Condition',
3784
+ invokeAgent: 'Invoke Agent',
3785
+ ifElse: 'If / Else',
3786
+ switch: 'Switch',
1245
3787
  }
1246
3788
  return labels[nodeType] || 'New Step'
1247
3789
  }
@@ -1255,7 +3797,10 @@ function getDefaultBadge(nodeType: string): string {
1255
3797
  decision: 'Decision',
1256
3798
  waitForSignal: 'Wait for Signal',
1257
3799
  waitForTimer: 'Wait for Timer',
3800
+ waitForCondition: 'Wait for Condition',
3801
+ invokeAgent: 'Invoke Agent',
3802
+ ifElse: 'If / Else',
3803
+ switch: 'Switch',
1258
3804
  }
1259
3805
  return badges[nodeType] || 'Task'
1260
3806
  }
1261
-