@jiggai/kitchen 0.1.8 → 0.1.10-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -0
  3. package/.next/build-manifest.json +6 -6
  4. package/.next/prerender-manifest.json +3 -27
  5. package/.next/required-server-files.js +4 -4
  6. package/.next/required-server-files.json +4 -4
  7. package/.next/routes-manifest.json +30 -0
  8. package/.next/server/app/_global-error/page/build-manifest.json +4 -4
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_global-error.html +2 -2
  13. package/.next/server/app/_global-error.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/_not-found/page/build-manifest.json +4 -4
  20. package/.next/server/app/_not-found/page.js +10 -9
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.rsc +4 -4
  25. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
  26. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  28. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/.next/server/app/agents/[agentId]/page/build-manifest.json +4 -4
  32. package/.next/server/app/agents/[agentId]/page.js +7 -7
  33. package/.next/server/app/agents/[agentId]/page.js.nft.json +1 -1
  34. package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/agents/add/route.js +1 -1
  36. package/.next/server/app/api/agents/add/route.js.nft.json +1 -1
  37. package/.next/server/app/api/agents/files/route.js.nft.json +1 -1
  38. package/.next/server/app/api/agents/identity/route.js +1 -1
  39. package/.next/server/app/api/agents/identity/route.js.nft.json +1 -1
  40. package/.next/server/app/api/agents/route.js +1 -1
  41. package/.next/server/app/api/agents/route.js.nft.json +1 -1
  42. package/.next/server/app/api/gateway/restart/route.js +1 -1
  43. package/.next/server/app/api/gateway/restart/route.js.nft.json +1 -1
  44. package/.next/server/app/api/goals/[id]/promote/route.js.nft.json +1 -1
  45. package/.next/server/app/api/goals/[id]/route.js.nft.json +1 -1
  46. package/.next/server/app/api/goals/route.js.nft.json +1 -1
  47. package/.next/server/app/api/recipes/[id]/route.js.nft.json +1 -1
  48. package/.next/server/app/api/recipes/clone/route.js.nft.json +1 -1
  49. package/.next/server/app/api/recipes/delete/route.js.nft.json +1 -1
  50. package/.next/server/app/api/recipes/route.js +1 -1
  51. package/.next/server/app/api/recipes/route.js.nft.json +1 -1
  52. package/.next/server/app/api/recipes/team-agents/route.js.nft.json +1 -1
  53. package/.next/server/app/api/scaffold/route.js.nft.json +1 -1
  54. package/.next/server/app/api/swarms/start/route/app-paths-manifest.json +3 -0
  55. package/.next/server/app/api/swarms/start/route/build-manifest.json +11 -0
  56. package/.next/server/app/api/swarms/start/route/server-reference-manifest.json +4 -0
  57. package/.next/server/app/api/swarms/start/route.js +6 -0
  58. package/.next/server/app/api/swarms/start/route.js.map +5 -0
  59. package/.next/server/app/api/swarms/start/route.js.nft.json +1 -0
  60. package/.next/server/app/api/swarms/start/route_client-reference-manifest.js +2 -0
  61. package/.next/server/app/api/swarms/status/route/app-paths-manifest.json +3 -0
  62. package/.next/server/app/api/swarms/status/route/build-manifest.json +11 -0
  63. package/.next/server/app/api/swarms/status/route/server-reference-manifest.json +4 -0
  64. package/.next/server/app/api/swarms/status/route.js +6 -0
  65. package/.next/server/app/api/swarms/status/route.js.map +5 -0
  66. package/.next/server/app/api/swarms/status/route.js.nft.json +1 -0
  67. package/.next/server/app/api/swarms/status/route_client-reference-manifest.js +2 -0
  68. package/.next/server/app/api/teams/orchestrator/route/app-paths-manifest.json +3 -0
  69. package/.next/server/app/api/teams/orchestrator/route/build-manifest.json +11 -0
  70. package/.next/server/app/api/teams/orchestrator/route/server-reference-manifest.json +4 -0
  71. package/.next/server/app/api/teams/orchestrator/route.js +6 -0
  72. package/.next/server/app/api/teams/orchestrator/route.js.map +5 -0
  73. package/.next/server/app/api/teams/orchestrator/route.js.nft.json +1 -0
  74. package/.next/server/app/api/teams/orchestrator/route_client-reference-manifest.js +2 -0
  75. package/.next/server/app/api/teams/remove-team/route.js +1 -1
  76. package/.next/server/app/api/teams/remove-team/route.js.nft.json +1 -1
  77. package/.next/server/app/api/teams/skills/install/route.js +1 -1
  78. package/.next/server/app/api/teams/skills/install/route.js.nft.json +1 -1
  79. package/.next/server/app/api/teams/workflow-runs/route/app-paths-manifest.json +3 -0
  80. package/.next/server/app/api/teams/workflow-runs/route/build-manifest.json +11 -0
  81. package/.next/server/app/api/teams/workflow-runs/route/server-reference-manifest.json +4 -0
  82. package/.next/server/app/api/teams/workflow-runs/route.js +7 -0
  83. package/.next/server/app/api/teams/workflow-runs/route.js.map +5 -0
  84. package/.next/server/app/api/teams/workflow-runs/route.js.nft.json +1 -0
  85. package/.next/server/app/api/teams/workflow-runs/route_client-reference-manifest.js +2 -0
  86. package/.next/server/app/api/teams/workflows/route/app-paths-manifest.json +3 -0
  87. package/.next/server/app/api/teams/workflows/route/build-manifest.json +11 -0
  88. package/.next/server/app/api/teams/workflows/route/server-reference-manifest.json +4 -0
  89. package/.next/server/app/api/teams/workflows/route.js +6 -0
  90. package/.next/server/app/api/teams/workflows/route.js.map +5 -0
  91. package/.next/server/app/api/teams/workflows/route.js.nft.json +1 -0
  92. package/.next/server/app/api/teams/workflows/route_client-reference-manifest.js +2 -0
  93. package/.next/server/app/api/tickets/move/route.js +1 -1
  94. package/.next/server/app/api/tickets/move/route.js.nft.json +1 -1
  95. package/.next/server/app/channels/page/build-manifest.json +4 -4
  96. package/.next/server/app/channels/page.js +7 -7
  97. package/.next/server/app/channels/page.js.nft.json +1 -1
  98. package/.next/server/app/channels/page_client-reference-manifest.js +1 -1
  99. package/.next/server/app/channels.html +2 -2
  100. package/.next/server/app/channels.rsc +5 -5
  101. package/.next/server/app/channels.segments/_full.segment.rsc +5 -5
  102. package/.next/server/app/channels.segments/_head.segment.rsc +1 -1
  103. package/.next/server/app/channels.segments/_index.segment.rsc +3 -3
  104. package/.next/server/app/channels.segments/_tree.segment.rsc +2 -2
  105. package/.next/server/app/channels.segments/channels/__PAGE__.segment.rsc +2 -2
  106. package/.next/server/app/channels.segments/channels.segment.rsc +1 -1
  107. package/.next/server/app/cron-jobs/page/build-manifest.json +4 -4
  108. package/.next/server/app/cron-jobs/page.js +7 -7
  109. package/.next/server/app/cron-jobs/page.js.nft.json +1 -1
  110. package/.next/server/app/cron-jobs/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/cron-jobs.html +1 -1
  112. package/.next/server/app/cron-jobs.rsc +6 -6
  113. package/.next/server/app/cron-jobs.segments/_full.segment.rsc +6 -6
  114. package/.next/server/app/cron-jobs.segments/_head.segment.rsc +1 -1
  115. package/.next/server/app/cron-jobs.segments/_index.segment.rsc +3 -3
  116. package/.next/server/app/cron-jobs.segments/_tree.segment.rsc +2 -2
  117. package/.next/server/app/cron-jobs.segments/cron-jobs/__PAGE__.segment.rsc +3 -3
  118. package/.next/server/app/cron-jobs.segments/cron-jobs.segment.rsc +1 -1
  119. package/.next/server/app/goals/[id]/page/build-manifest.json +4 -4
  120. package/.next/server/app/goals/[id]/page.js +7 -7
  121. package/.next/server/app/goals/[id]/page.js.nft.json +1 -1
  122. package/.next/server/app/goals/[id]/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/goals/new/page/build-manifest.json +4 -4
  124. package/.next/server/app/goals/new/page.js +7 -7
  125. package/.next/server/app/goals/new/page.js.nft.json +1 -1
  126. package/.next/server/app/goals/new/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/goals/new.html +2 -2
  128. package/.next/server/app/goals/new.rsc +5 -5
  129. package/.next/server/app/goals/new.segments/_full.segment.rsc +5 -5
  130. package/.next/server/app/goals/new.segments/_head.segment.rsc +1 -1
  131. package/.next/server/app/goals/new.segments/_index.segment.rsc +3 -3
  132. package/.next/server/app/goals/new.segments/_tree.segment.rsc +2 -2
  133. package/.next/server/app/goals/new.segments/goals/new/__PAGE__.segment.rsc +2 -2
  134. package/.next/server/app/goals/new.segments/goals/new.segment.rsc +1 -1
  135. package/.next/server/app/goals/new.segments/goals.segment.rsc +1 -1
  136. package/.next/server/app/goals/page/build-manifest.json +4 -4
  137. package/.next/server/app/goals/page.js +7 -7
  138. package/.next/server/app/goals/page.js.nft.json +1 -1
  139. package/.next/server/app/goals/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/goals.html +1 -1
  141. package/.next/server/app/goals.rsc +5 -5
  142. package/.next/server/app/goals.segments/_full.segment.rsc +5 -5
  143. package/.next/server/app/goals.segments/_head.segment.rsc +1 -1
  144. package/.next/server/app/goals.segments/_index.segment.rsc +3 -3
  145. package/.next/server/app/goals.segments/_tree.segment.rsc +2 -2
  146. package/.next/server/app/goals.segments/goals/__PAGE__.segment.rsc +2 -2
  147. package/.next/server/app/goals.segments/goals.segment.rsc +1 -1
  148. package/.next/server/app/page/build-manifest.json +4 -4
  149. package/.next/server/app/page.js +7 -7
  150. package/.next/server/app/page.js.nft.json +1 -1
  151. package/.next/server/app/page_client-reference-manifest.js +1 -1
  152. package/.next/server/app/recipes/[id]/page/build-manifest.json +4 -4
  153. package/.next/server/app/recipes/[id]/page.js +8 -8
  154. package/.next/server/app/recipes/[id]/page.js.nft.json +1 -1
  155. package/.next/server/app/recipes/[id]/page_client-reference-manifest.js +1 -1
  156. package/.next/server/app/recipes/page/build-manifest.json +4 -4
  157. package/.next/server/app/recipes/page.js +8 -8
  158. package/.next/server/app/recipes/page.js.nft.json +1 -1
  159. package/.next/server/app/recipes/page_client-reference-manifest.js +1 -1
  160. package/.next/server/app/settings/page/build-manifest.json +4 -4
  161. package/.next/server/app/settings/page.js +7 -7
  162. package/.next/server/app/settings/page.js.nft.json +1 -1
  163. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  164. package/.next/server/app/settings.html +1 -1
  165. package/.next/server/app/settings.rsc +5 -5
  166. package/.next/server/app/settings.segments/_full.segment.rsc +5 -5
  167. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  168. package/.next/server/app/settings.segments/_index.segment.rsc +3 -3
  169. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  170. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  171. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  172. package/.next/server/app/teams/[teamId]/page/build-manifest.json +4 -4
  173. package/.next/server/app/teams/[teamId]/page.js +8 -8
  174. package/.next/server/app/teams/[teamId]/page.js.nft.json +1 -1
  175. package/.next/server/app/teams/[teamId]/page_client-reference-manifest.js +1 -1
  176. package/.next/server/app/tickets/[ticket]/page/build-manifest.json +4 -4
  177. package/.next/server/app/tickets/[ticket]/page.js +7 -7
  178. package/.next/server/app/tickets/[ticket]/page.js.nft.json +1 -1
  179. package/.next/server/app/tickets/[ticket]/page_client-reference-manifest.js +1 -1
  180. package/.next/server/app/tickets/page/build-manifest.json +4 -4
  181. package/.next/server/app/tickets/page.js +7 -7
  182. package/.next/server/app/tickets/page.js.nft.json +1 -1
  183. package/.next/server/app/tickets/page_client-reference-manifest.js +1 -1
  184. package/.next/server/app-paths-manifest.json +5 -0
  185. package/.next/server/chunks/[root-of-the-server]__065a27a4._.js +3 -0
  186. package/.next/server/chunks/[root-of-the-server]__065a27a4._.js.map +1 -0
  187. package/.next/server/chunks/{[root-of-the-server]__94d68aa3._.js → [root-of-the-server]__1989d64f._.js} +2 -2
  188. package/.next/server/chunks/{[root-of-the-server]__94d68aa3._.js.map → [root-of-the-server]__1989d64f._.js.map} +1 -1
  189. package/.next/server/chunks/{[root-of-the-server]__c4ffbb03._.js → [root-of-the-server]__21b72903._.js} +2 -2
  190. package/.next/server/chunks/{[root-of-the-server]__c4ffbb03._.js.map → [root-of-the-server]__21b72903._.js.map} +1 -1
  191. package/.next/server/chunks/{[root-of-the-server]__0c01c5c7._.js → [root-of-the-server]__262d8a6e._.js} +2 -2
  192. package/.next/server/chunks/{[root-of-the-server]__0c01c5c7._.js.map → [root-of-the-server]__262d8a6e._.js.map} +1 -1
  193. package/.next/server/chunks/{[root-of-the-server]__b11231a9._.js → [root-of-the-server]__2f2c799d._.js} +2 -2
  194. package/.next/server/chunks/{[root-of-the-server]__b11231a9._.js.map → [root-of-the-server]__2f2c799d._.js.map} +1 -1
  195. package/.next/server/chunks/[root-of-the-server]__6f85770b._.js +3 -0
  196. package/.next/server/chunks/[root-of-the-server]__6f85770b._.js.map +1 -0
  197. package/.next/server/chunks/[root-of-the-server]__7cf49621._.js +3 -0
  198. package/.next/server/chunks/[root-of-the-server]__7cf49621._.js.map +1 -0
  199. package/.next/server/chunks/[root-of-the-server]__a27f8405._.js +1 -1
  200. package/.next/server/chunks/[root-of-the-server]__a27f8405._.js.map +1 -1
  201. package/.next/server/chunks/[root-of-the-server]__a5226713._.js +3 -0
  202. package/.next/server/chunks/[root-of-the-server]__a5226713._.js.map +1 -0
  203. package/.next/server/chunks/[root-of-the-server]__c7f92fce._.js +3 -0
  204. package/.next/server/chunks/[root-of-the-server]__c7f92fce._.js.map +1 -0
  205. package/.next/server/chunks/{[root-of-the-server]__d737ca42._.js → [root-of-the-server]__e8bfeaba._.js} +2 -2
  206. package/.next/server/chunks/{[root-of-the-server]__d737ca42._.js.map → [root-of-the-server]__e8bfeaba._.js.map} +1 -1
  207. package/.next/server/chunks/{[root-of-the-server]__4eda99a9._.js → [root-of-the-server]__ebaa0038._.js} +2 -2
  208. package/.next/server/chunks/{[root-of-the-server]__4eda99a9._.js.map → [root-of-the-server]__ebaa0038._.js.map} +1 -1
  209. package/.next/server/chunks/{[root-of-the-server]__b59b3cdd._.js → [root-of-the-server]__f5546a39._.js} +2 -2
  210. package/.next/server/chunks/{[root-of-the-server]__b59b3cdd._.js.map → [root-of-the-server]__f5546a39._.js.map} +1 -1
  211. package/.next/server/chunks/{[root-of-the-server]__e6184ba3._.js → [root-of-the-server]__fdda9176._.js} +2 -2
  212. package/.next/server/chunks/{[root-of-the-server]__e6184ba3._.js.map → [root-of-the-server]__fdda9176._.js.map} +1 -1
  213. package/.next/server/chunks/_next-internal_server_app_api_swarms_start_route_actions_b79e9029.js +3 -0
  214. package/.next/server/chunks/_next-internal_server_app_api_swarms_start_route_actions_b79e9029.js.map +1 -0
  215. package/.next/server/chunks/_next-internal_server_app_api_swarms_status_route_actions_54826df0.js +3 -0
  216. package/.next/server/chunks/_next-internal_server_app_api_swarms_status_route_actions_54826df0.js.map +1 -0
  217. package/.next/server/chunks/_next-internal_server_app_api_teams_orchestrator_route_actions_1949c463.js +3 -0
  218. package/.next/server/chunks/_next-internal_server_app_api_teams_orchestrator_route_actions_1949c463.js.map +1 -0
  219. package/.next/server/chunks/_next-internal_server_app_api_teams_workflow-runs_route_actions_8327ac7d.js +3 -0
  220. package/.next/server/chunks/_next-internal_server_app_api_teams_workflow-runs_route_actions_8327ac7d.js.map +1 -0
  221. package/.next/server/chunks/_next-internal_server_app_api_teams_workflows_route_actions_43dd6e07.js +3 -0
  222. package/.next/server/chunks/_next-internal_server_app_api_teams_workflows_route_actions_43dd6e07.js.map +1 -0
  223. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_1fe98a49.js +14 -0
  224. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_1fe98a49.js.map +1 -0
  225. package/.next/server/chunks/ssr/[root-of-the-server]__0d4cb2ba._.js +3 -0
  226. package/.next/server/chunks/ssr/[root-of-the-server]__0fd02d84._.js +3 -0
  227. package/.next/server/chunks/ssr/{[root-of-the-server]__4393e897._.js.map → [root-of-the-server]__0fd02d84._.js.map} +1 -1
  228. package/.next/server/chunks/ssr/[root-of-the-server]__285ae6b7._.js +3 -0
  229. package/.next/server/chunks/ssr/{[root-of-the-server]__46e08d44._.js.map → [root-of-the-server]__285ae6b7._.js.map} +1 -1
  230. package/.next/server/chunks/ssr/[root-of-the-server]__2bef6884._.js +3 -0
  231. package/.next/server/chunks/ssr/{[root-of-the-server]__15600e29._.js.map → [root-of-the-server]__2bef6884._.js.map} +1 -1
  232. package/.next/server/chunks/ssr/[root-of-the-server]__3efd25c4._.js +3 -0
  233. package/.next/server/chunks/ssr/{[root-of-the-server]__2a6f1e3e._.js.map → [root-of-the-server]__3efd25c4._.js.map} +1 -1
  234. package/.next/server/chunks/ssr/[root-of-the-server]__491e06fa._.js +1 -1
  235. package/.next/server/chunks/ssr/[root-of-the-server]__491e06fa._.js.map +1 -1
  236. package/.next/server/chunks/ssr/[root-of-the-server]__550d78f8._.js +3 -0
  237. package/.next/server/chunks/ssr/{[root-of-the-server]__bc3b27b0._.js.map → [root-of-the-server]__550d78f8._.js.map} +1 -1
  238. package/.next/server/chunks/ssr/{[root-of-the-server]__e2e52c6e._.js → [root-of-the-server]__78cdd31e._.js} +2 -2
  239. package/.next/server/chunks/ssr/[root-of-the-server]__8ecc89e4._.js +3 -0
  240. package/.next/server/chunks/ssr/{[root-of-the-server]__507d2fc9._.js.map → [root-of-the-server]__8ecc89e4._.js.map} +1 -1
  241. package/.next/server/chunks/ssr/[root-of-the-server]__92524828._.js +3 -0
  242. package/.next/server/chunks/ssr/{[root-of-the-server]__247198dc._.js.map → [root-of-the-server]__92524828._.js.map} +1 -1
  243. package/.next/server/chunks/ssr/[root-of-the-server]__9e1ab064._.js +3 -0
  244. package/.next/server/chunks/ssr/[root-of-the-server]__9e1ab064._.js.map +1 -0
  245. package/.next/server/chunks/ssr/[root-of-the-server]__a36a058d._.js +3 -0
  246. package/.next/server/chunks/ssr/{[root-of-the-server]__8d24c9c3._.js.map → [root-of-the-server]__a36a058d._.js.map} +1 -1
  247. package/.next/server/chunks/ssr/[root-of-the-server]__a8eca9e1._.js +3 -0
  248. package/.next/server/chunks/ssr/{[root-of-the-server]__82ce3aee._.js.map → [root-of-the-server]__a8eca9e1._.js.map} +1 -1
  249. package/.next/server/chunks/ssr/[root-of-the-server]__aa8e2649._.js +3 -0
  250. package/.next/server/chunks/ssr/{[root-of-the-server]__f75a61bf._.js.map → [root-of-the-server]__aa8e2649._.js.map} +1 -1
  251. package/.next/server/chunks/ssr/[root-of-the-server]__b2617fbf._.js +3 -0
  252. package/.next/server/chunks/ssr/{[root-of-the-server]__fd669584._.js.map → [root-of-the-server]__b2617fbf._.js.map} +1 -1
  253. package/.next/server/chunks/ssr/[root-of-the-server]__b5f65083._.js +3 -0
  254. package/.next/server/chunks/ssr/[root-of-the-server]__b5f65083._.js.map +1 -0
  255. package/.next/server/chunks/ssr/[root-of-the-server]__e5d416d5._.js +3 -0
  256. package/.next/server/chunks/ssr/{[root-of-the-server]__3ad3e5b1._.js.map → [root-of-the-server]__e5d416d5._.js.map} +1 -1
  257. package/.next/server/chunks/ssr/{[root-of-the-server]__b9356576._.js → [root-of-the-server]__f62d412e._.js} +2 -2
  258. package/.next/server/chunks/ssr/{[root-of-the-server]__b9356576._.js.map → [root-of-the-server]__f62d412e._.js.map} +1 -1
  259. package/.next/server/chunks/ssr/[root-of-the-server]__fbe5ff69._.js +3 -0
  260. package/.next/server/chunks/ssr/{[root-of-the-server]__3575e6da._.js.map → [root-of-the-server]__fbe5ff69._.js.map} +1 -1
  261. package/.next/server/chunks/ssr/{_8c45edba._.js → _0bdae23a._.js} +3 -3
  262. package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_68c68167.js.map → _0bdae23a._.js.map} +1 -1
  263. package/.next/server/chunks/ssr/{_0808c2b9._.js → _347e2d56._.js} +3 -3
  264. package/.next/server/chunks/ssr/_347e2d56._.js.map +1 -0
  265. package/.next/server/chunks/ssr/{_68793c1f._.js → _49fb12c2._.js} +3 -3
  266. package/.next/server/chunks/ssr/_49fb12c2._.js.map +1 -0
  267. package/.next/server/chunks/ssr/{_3c8b2df6._.js → _536983a4._.js} +3 -3
  268. package/.next/server/chunks/ssr/_536983a4._.js.map +1 -0
  269. package/.next/server/chunks/ssr/{_cd23c5af._.js → _5b282394._.js} +3 -3
  270. package/.next/server/chunks/ssr/_5b282394._.js.map +1 -0
  271. package/.next/server/chunks/ssr/{_6e70b5a4._.js → _5cc24343._.js} +3 -3
  272. package/.next/server/chunks/ssr/_5cc24343._.js.map +1 -0
  273. package/.next/server/chunks/ssr/{_92140ca3._.js → _7eac37fb._.js} +3 -3
  274. package/.next/server/chunks/ssr/_7eac37fb._.js.map +1 -0
  275. package/.next/server/chunks/ssr/{_8c8207c1._.js → _8062e992._.js} +3 -3
  276. package/.next/server/chunks/ssr/_8062e992._.js.map +1 -0
  277. package/.next/server/chunks/ssr/{_25e6aab8._.js → _99994c05._.js} +3 -3
  278. package/.next/server/chunks/ssr/_99994c05._.js.map +1 -0
  279. package/.next/server/chunks/ssr/{_b5e9afcc._.js → _aafc99aa._.js} +3 -3
  280. package/.next/server/chunks/ssr/_aafc99aa._.js.map +1 -0
  281. package/.next/server/chunks/ssr/{_39e6e769._.js → _b7f3caf7._.js} +3 -3
  282. package/.next/server/chunks/ssr/_b7f3caf7._.js.map +1 -0
  283. package/.next/server/chunks/ssr/_c2041f88._.js +4 -0
  284. package/.next/server/chunks/ssr/_c2041f88._.js.map +1 -0
  285. package/.next/server/chunks/ssr/{_3158c108._.js → _f4d8af3b._.js} +3 -3
  286. package/.next/server/chunks/ssr/_f4d8af3b._.js.map +1 -0
  287. package/.next/server/chunks/ssr/{node_modules_next_dist_12287b3d._.js → node_modules_next_dist_8a37abed._.js} +2 -2
  288. package/.next/server/chunks/ssr/{node_modules_next_dist_12287b3d._.js.map → node_modules_next_dist_8a37abed._.js.map} +1 -1
  289. package/.next/server/chunks/ssr/{node_modules_next_dist_client_components_9774470f._.js → node_modules_next_dist_client_components_2fffaa3a._.js} +2 -2
  290. package/.next/server/chunks/ssr/{node_modules_next_dist_client_components_9774470f._.js.map → node_modules_next_dist_client_components_2fffaa3a._.js.map} +1 -1
  291. package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_68c68167.js → node_modules_next_dist_esm_build_templates_app-page_b5ef32b4.js} +3 -3
  292. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_b5ef32b4.js.map +1 -0
  293. package/.next/server/chunks/ssr/src_app_HomeClient_tsx_f9f7568d._.js +1 -1
  294. package/.next/server/chunks/ssr/src_app_HomeClient_tsx_f9f7568d._.js.map +1 -1
  295. package/.next/server/chunks/ssr/src_app_global-error_tsx_aa661ba4._.js +3 -0
  296. package/.next/server/chunks/ssr/src_app_global-error_tsx_aa661ba4._.js.map +1 -0
  297. package/.next/server/chunks/ssr/src_app_not-found_tsx_3f23d179._.js +3 -0
  298. package/.next/server/chunks/ssr/src_app_not-found_tsx_3f23d179._.js.map +1 -0
  299. package/.next/server/chunks/ssr/src_app_teams_[teamId]_team-editor_tsx_2900b91d._.js +2 -2
  300. package/.next/server/chunks/ssr/src_app_teams_[teamId]_team-editor_tsx_2900b91d._.js.map +1 -1
  301. package/.next/server/middleware-build-manifest.js +4 -4
  302. package/.next/server/pages/404.html +1 -1
  303. package/.next/server/pages/500.html +2 -2
  304. package/.next/server/server-reference-manifest.js +1 -1
  305. package/.next/server/server-reference-manifest.json +1 -1
  306. package/.next/static/chunks/{40f7fde35327d214.js → 423ca3a7f09c1470.js} +1 -1
  307. package/.next/static/chunks/{82abf2d65f5428ae.js → 68a088aa49e6124a.js} +3 -3
  308. package/.next/static/chunks/96b0d0ef92ef0eca.css +3 -0
  309. package/.next/static/chunks/a92e9d0c91b71b55.js +1 -0
  310. package/.next/static/chunks/bd3fcd39d918f9b4.js +10 -0
  311. package/.next/static/chunks/c84580993f22c17a.js +1 -0
  312. package/.next/static/chunks/f3e3461eda4a8d62.js +1 -0
  313. package/.next/static/chunks/{turbopack-ae1340e658f67df8.js → turbopack-6c658f991a22d28a.js} +1 -1
  314. package/package.json +2 -2
  315. package/public/android-chrome-192x192.png +0 -0
  316. package/public/android-chrome-512x512.png +0 -0
  317. package/public/apple-touch-icon.png +0 -0
  318. package/public/chef.jpg +0 -0
  319. package/public/favicon-16x16.png +0 -0
  320. package/public/favicon-32x32.png +0 -0
  321. package/public/favicon.ico +0 -0
  322. package/src/app/HomeClient.tsx +1 -1
  323. package/src/app/api/cron/jobs/route.ts +12 -4
  324. package/src/app/api/swarms/start/route.ts +118 -0
  325. package/src/app/api/swarms/status/route.ts +50 -0
  326. package/src/app/api/teams/orchestrator/route.ts +218 -0
  327. package/src/app/api/teams/workflow-runs/route.ts +431 -0
  328. package/src/app/api/teams/workflows/route.ts +88 -0
  329. package/src/app/cron-jobs/page.tsx +1 -1
  330. package/src/app/global-error.tsx +50 -0
  331. package/src/app/not-found.tsx +8 -0
  332. package/src/app/recipes/page.tsx +1 -1
  333. package/src/app/settings/page.tsx +1 -1
  334. package/src/app/teams/[teamId]/OrchestratorPanel.tsx +228 -0
  335. package/src/app/teams/[teamId]/page.tsx +25 -29
  336. package/src/app/teams/[teamId]/team-editor.tsx +1656 -8
  337. package/src/app/tickets/[ticket]/page.tsx +3 -0
  338. package/src/app/tickets/page.tsx +3 -0
  339. package/src/components/AppShell.tsx +245 -31
  340. package/src/lib/workflows/README.md +11 -0
  341. package/src/lib/workflows/runs-storage.ts +71 -0
  342. package/src/lib/workflows/runs-types.ts +32 -0
  343. package/src/lib/workflows/storage.ts +82 -0
  344. package/src/lib/workflows/types.ts +50 -0
  345. package/src/lib/workflows/validate.ts +70 -0
  346. package/.next/server/app/tickets.html +0 -1
  347. package/.next/server/app/tickets.meta +0 -15
  348. package/.next/server/app/tickets.rsc +0 -19
  349. package/.next/server/app/tickets.segments/_full.segment.rsc +0 -19
  350. package/.next/server/app/tickets.segments/_head.segment.rsc +0 -6
  351. package/.next/server/app/tickets.segments/_index.segment.rsc +0 -6
  352. package/.next/server/app/tickets.segments/_tree.segment.rsc +0 -4
  353. package/.next/server/app/tickets.segments/tickets/__PAGE__.segment.rsc +0 -8
  354. package/.next/server/app/tickets.segments/tickets.segment.rsc +0 -4
  355. package/.next/server/chunks/ssr/[root-of-the-server]__15600e29._.js +0 -3
  356. package/.next/server/chunks/ssr/[root-of-the-server]__247198dc._.js +0 -3
  357. package/.next/server/chunks/ssr/[root-of-the-server]__2a6f1e3e._.js +0 -3
  358. package/.next/server/chunks/ssr/[root-of-the-server]__346f79e5._.js +0 -3
  359. package/.next/server/chunks/ssr/[root-of-the-server]__346f79e5._.js.map +0 -1
  360. package/.next/server/chunks/ssr/[root-of-the-server]__3575e6da._.js +0 -3
  361. package/.next/server/chunks/ssr/[root-of-the-server]__3ad3e5b1._.js +0 -3
  362. package/.next/server/chunks/ssr/[root-of-the-server]__3bc7ad0a._.js +0 -3
  363. package/.next/server/chunks/ssr/[root-of-the-server]__4393e897._.js +0 -3
  364. package/.next/server/chunks/ssr/[root-of-the-server]__46e08d44._.js +0 -3
  365. package/.next/server/chunks/ssr/[root-of-the-server]__507d2fc9._.js +0 -3
  366. package/.next/server/chunks/ssr/[root-of-the-server]__82ce3aee._.js +0 -3
  367. package/.next/server/chunks/ssr/[root-of-the-server]__8d24c9c3._.js +0 -3
  368. package/.next/server/chunks/ssr/[root-of-the-server]__a457c799._.js +0 -3
  369. package/.next/server/chunks/ssr/[root-of-the-server]__a457c799._.js.map +0 -1
  370. package/.next/server/chunks/ssr/[root-of-the-server]__bc3b27b0._.js +0 -3
  371. package/.next/server/chunks/ssr/[root-of-the-server]__f75a61bf._.js +0 -3
  372. package/.next/server/chunks/ssr/[root-of-the-server]__fd669584._.js +0 -3
  373. package/.next/server/chunks/ssr/_0808c2b9._.js.map +0 -1
  374. package/.next/server/chunks/ssr/_25e6aab8._.js.map +0 -1
  375. package/.next/server/chunks/ssr/_3158c108._.js.map +0 -1
  376. package/.next/server/chunks/ssr/_39e6e769._.js.map +0 -1
  377. package/.next/server/chunks/ssr/_3c8b2df6._.js.map +0 -1
  378. package/.next/server/chunks/ssr/_68793c1f._.js.map +0 -1
  379. package/.next/server/chunks/ssr/_6e70b5a4._.js.map +0 -1
  380. package/.next/server/chunks/ssr/_8c45edba._.js.map +0 -1
  381. package/.next/server/chunks/ssr/_8c8207c1._.js.map +0 -1
  382. package/.next/server/chunks/ssr/_92140ca3._.js.map +0 -1
  383. package/.next/server/chunks/ssr/_b5e9afcc._.js.map +0 -1
  384. package/.next/server/chunks/ssr/_cd23c5af._.js.map +0 -1
  385. package/.next/server/chunks/ssr/_d27483a1._.js +0 -4
  386. package/.next/server/chunks/ssr/_d27483a1._.js.map +0 -1
  387. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_45780354.js +0 -3
  388. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_45780354.js.map +0 -1
  389. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_ece394eb.js +0 -3
  390. package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_ece394eb.js.map +0 -1
  391. package/.next/static/chunks/8484c54dc9a377e8.js +0 -10
  392. package/.next/static/chunks/8662fcc3cdff66f3.js +0 -1
  393. package/.next/static/chunks/a4e69b85b74277a7.css +0 -3
  394. package/.next/static/chunks/a9ed074e89b16a5e.js +0 -1
  395. /package/.next/server/chunks/ssr/{[root-of-the-server]__3bc7ad0a._.js.map → [root-of-the-server]__0d4cb2ba._.js.map} +0 -0
  396. /package/.next/server/chunks/ssr/{[root-of-the-server]__e2e52c6e._.js.map → [root-of-the-server]__78cdd31e._.js.map} +0 -0
  397. /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_buildManifest.js +0 -0
  398. /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_clientMiddlewareManifest.json +0 -0
  399. /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_ssgManifest.js +0 -0
@@ -1,11 +1,14 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useMemo, useState } from "react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  import { useRouter } from "next/navigation";
6
6
  import { DeleteTeamModal } from "./DeleteTeamModal";
7
7
  import { PublishChangesModal } from "./PublishChangesModal";
8
8
  import { useToast } from "@/components/ToastProvider";
9
+ import { OrchestratorPanel } from "./OrchestratorPanel";
10
+ import type { WorkflowFileV1 } from "@/lib/workflows/types";
11
+ import { validateWorkflowFileV1 } from "@/lib/workflows/validate";
9
12
 
10
13
  type RecipeListItem = {
11
14
  id: string;
@@ -96,7 +99,7 @@ function forceFrontmatterTeamTeamId(md: string, teamId: string) {
96
99
  return `---\n${next.join("\n")}\n---\n${body}`;
97
100
  }
98
101
 
99
- export default function TeamEditor({ teamId }: { teamId: string }) {
102
+ export default function TeamEditor({ teamId, initialTab }: { teamId: string; initialTab?: string }) {
100
103
  const router = useRouter();
101
104
  const [recipes, setRecipes] = useState<RecipeListItem[]>([]);
102
105
  const [fromId, setFromId] = useState<string>("");
@@ -107,7 +110,15 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
107
110
  const [toName, setToName] = useState<string>(teamId);
108
111
  const [content, setContent] = useState<string>("");
109
112
  const [loadedRecipeHash, setLoadedRecipeHash] = useState<string | null>(null);
110
- const [activeTab, setActiveTab] = useState<"recipe" | "agents" | "skills" | "cron" | "files">("recipe");
113
+ const [activeTab, setActiveTab] = useState<"recipe" | "agents" | "skills" | "cron" | "workflows" | "files" | "orchestrator">("recipe");
114
+
115
+ useEffect(() => {
116
+ const allowed = ["recipe", "agents", "skills", "cron", "workflows", "files", "orchestrator"] as const;
117
+ if (initialTab && (allowed as readonly string[]).includes(initialTab)) {
118
+ setActiveTab(initialTab as (typeof allowed)[number]);
119
+ }
120
+ }, [initialTab]);
121
+
111
122
  const [loading, setLoading] = useState(true);
112
123
  const [saving, setSaving] = useState(false);
113
124
  const [publishing, setPublishing] = useState(false);
@@ -137,9 +148,147 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
137
148
  const [cronJobs, setCronJobs] = useState<unknown[]>([]);
138
149
  const [cronLoading, setCronLoading] = useState(false);
139
150
 
151
+ const [workflowFiles, setWorkflowFiles] = useState<string[]>([]);
152
+ const [workflowFilesLoading, setWorkflowFilesLoading] = useState(false);
153
+ const [workflowFilesError, setWorkflowFilesError] = useState<string>("");
154
+ const [selectedWorkflowFile, setSelectedWorkflowFile] = useState<string>("");
155
+ const [workflowJsonText, setWorkflowJsonText] = useState<string>("");
156
+ const [workflowSaving, setWorkflowSaving] = useState(false);
157
+ const [workflowView, setWorkflowView] = useState<"canvas" | "json">("canvas");
158
+ const [workflowSelectedNodeId, setWorkflowSelectedNodeId] = useState<string>("");
159
+ const [workflowDragging, setWorkflowDragging] = useState<null | { nodeId: string; dx: number; dy: number; containerLeft: number; containerTop: number }>(null);
160
+
161
+ // Canvas editor helpers (minimal MVP): add/remove nodes + edges via inspector forms.
162
+ const [workflowNewNodeId, setWorkflowNewNodeId] = useState<string>("");
163
+ const [workflowNewNodeName, setWorkflowNewNodeName] = useState<string>("");
164
+ const [workflowNewNodeType, setWorkflowNewNodeType] = useState<WorkflowFileV1["nodes"][number]["type"]>("llm");
165
+ const [workflowNewEdgeFrom, setWorkflowNewEdgeFrom] = useState<string>("");
166
+ const [workflowNewEdgeTo, setWorkflowNewEdgeTo] = useState<string>("");
167
+ const [workflowNewEdgeLabel, setWorkflowNewEdgeLabel] = useState<string>("");
168
+
169
+ const [workflowEditorOpen, setWorkflowEditorOpen] = useState(false);
170
+
171
+ const [workflowCreateOpen, setWorkflowCreateOpen] = useState(false);
172
+ const [workflowCreateId, setWorkflowCreateId] = useState("new-workflow");
173
+ const [workflowCreateName, setWorkflowCreateName] = useState("New workflow");
174
+
175
+ const [workflowRuns, setWorkflowRuns] = useState<string[]>([]);
176
+ const [workflowRunsLoading, setWorkflowRunsLoading] = useState(false);
177
+ const [workflowRunsError, setWorkflowRunsError] = useState<string>("");
178
+ const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string>("");
179
+ const [selectedWorkflowRun, setSelectedWorkflowRun] = useState<unknown | null>(null);
180
+
140
181
  const [teamAgents, setTeamAgents] = useState<Array<{ id: string; identityName?: string }>>([]);
141
182
  const [teamAgentsLoading, setTeamAgentsLoading] = useState(false);
142
183
 
184
+ const workflowCanvasRef = useRef<HTMLDivElement | null>(null);
185
+ const workflowImportInputRef = useRef<HTMLInputElement | null>(null);
186
+
187
+ const workflowParsed = useMemo(() => {
188
+ const raw = String(workflowJsonText || "").trim();
189
+ if (!raw) return null;
190
+ try {
191
+ return JSON.parse(raw) as WorkflowFileV1;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }, [workflowJsonText]);
196
+
197
+ const workflowParseError = useMemo(() => {
198
+ const raw = String(workflowJsonText || "").trim();
199
+ if (!raw) return "";
200
+ try {
201
+ JSON.parse(raw);
202
+ return "";
203
+ } catch (e: unknown) {
204
+ return e instanceof Error ? e.message : "Invalid JSON";
205
+ }
206
+ }, [workflowJsonText]);
207
+
208
+ const workflowValidation = useMemo(() => {
209
+ if (!workflowParsed) return { errors: [], warnings: [] };
210
+ try {
211
+ return validateWorkflowFileV1(workflowParsed);
212
+ } catch (e: unknown) {
213
+ return { errors: [e instanceof Error ? e.message : String(e)], warnings: [] };
214
+ }
215
+ }, [workflowParsed]);
216
+
217
+ useEffect(() => {
218
+ const wfId = String(workflowParsed?.id ?? "").trim();
219
+ if (!wfId) {
220
+ setWorkflowRuns([]);
221
+ setSelectedWorkflowRunId("");
222
+ setSelectedWorkflowRun(null);
223
+ setWorkflowRunsError("");
224
+ return;
225
+ }
226
+
227
+ let canceled = false;
228
+ (async () => {
229
+ setWorkflowRunsLoading(true);
230
+ setWorkflowRunsError("");
231
+ try {
232
+ const res = await fetch(
233
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}`,
234
+ { cache: "no-store" }
235
+ );
236
+ const json = await res.json();
237
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to list runs");
238
+ const files = Array.isArray(json.files) ? json.files : [];
239
+ const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
240
+ if (canceled) return;
241
+ setWorkflowRuns(list);
242
+ if (selectedWorkflowRunId && !list.some((f: string) => f === `${selectedWorkflowRunId}.run.json`)) {
243
+ setSelectedWorkflowRunId("");
244
+ setSelectedWorkflowRun(null);
245
+ }
246
+ } catch (e: unknown) {
247
+ if (canceled) return;
248
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
249
+ } finally {
250
+ if (!canceled) setWorkflowRunsLoading(false);
251
+ }
252
+ })();
253
+
254
+ return () => {
255
+ canceled = true;
256
+ };
257
+ }, [teamId, workflowParsed?.id, selectedWorkflowRunId]);
258
+
259
+ useEffect(() => {
260
+ if (!workflowDragging) return;
261
+
262
+ function onMove(e: PointerEvent) {
263
+ if (!workflowDragging) return;
264
+ const wf = workflowParsed;
265
+ if (!wf) return;
266
+
267
+ const node = wf.nodes.find((n) => n.id === workflowDragging.nodeId);
268
+ if (!node) return;
269
+
270
+ const x = Math.max(0, Math.round(e.clientX - workflowDragging.containerLeft - workflowDragging.dx));
271
+ const y = Math.max(0, Math.round(e.clientY - workflowDragging.containerTop - workflowDragging.dy));
272
+
273
+ const next: WorkflowFileV1 = {
274
+ ...wf,
275
+ nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, x, y } : n)),
276
+ };
277
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
278
+ }
279
+
280
+ function onUp() {
281
+ setWorkflowDragging(null);
282
+ }
283
+
284
+ window.addEventListener("pointermove", onMove);
285
+ window.addEventListener("pointerup", onUp);
286
+ return () => {
287
+ window.removeEventListener("pointermove", onMove);
288
+ window.removeEventListener("pointerup", onUp);
289
+ };
290
+ }, [workflowDragging, workflowParsed]);
291
+
143
292
  const recipeAgents = useMemo(() => {
144
293
  const md = String(content ?? "");
145
294
  if (!md.startsWith("---\n")) return [] as Array<{ role: string; name?: string }>;
@@ -262,16 +411,18 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
262
411
  void (async () => {
263
412
  setTeamFilesLoading(true);
264
413
  setCronLoading(true);
414
+ setWorkflowFilesLoading(true);
265
415
  setTeamAgentsLoading(true);
266
416
  setSkillsLoading(true);
267
417
 
268
418
  try {
269
- const [filesRes, cronRes, agentsRes, skillsRes, availableSkillsRes] = await Promise.all([
419
+ const [filesRes, cronRes, agentsRes, skillsRes, availableSkillsRes, workflowsRes] = await Promise.all([
270
420
  fetch(`/api/teams/files?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
271
421
  fetch(`/api/cron/jobs?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
272
422
  fetch("/api/agents", { cache: "no-store" }),
273
423
  fetch(`/api/teams/skills?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
274
424
  fetch("/api/skills/available", { cache: "no-store" }),
425
+ fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
275
426
  ]);
276
427
 
277
428
  try {
@@ -304,6 +455,22 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
304
455
  // ignore
305
456
  }
306
457
 
458
+ try {
459
+ const workflowsJson = (await workflowsRes.json()) as { ok?: boolean; files?: unknown[]; dir?: unknown };
460
+ if (workflowsRes.ok && workflowsJson.ok) {
461
+ const files = Array.isArray(workflowsJson.files) ? workflowsJson.files : [];
462
+ const list = files.map((f) => String(f ?? '').trim()).filter((f) => Boolean(f));
463
+ setWorkflowFiles(list);
464
+ setSelectedWorkflowFile((prev) => {
465
+ const p = String(prev ?? '').trim();
466
+ if (p && list.includes(p)) return p;
467
+ return list[0] ?? '';
468
+ });
469
+ }
470
+ } catch {
471
+ // ignore
472
+ }
473
+
307
474
  try {
308
475
  const agentsJson = (await agentsRes.json()) as { agents?: unknown[] };
309
476
  if (agentsRes.ok) {
@@ -347,6 +514,7 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
347
514
  } finally {
348
515
  setTeamFilesLoading(false);
349
516
  setCronLoading(false);
517
+ setWorkflowFilesLoading(false);
350
518
  setTeamAgentsLoading(false);
351
519
  setSkillsLoading(false);
352
520
  }
@@ -498,15 +666,16 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
498
666
  }
499
667
 
500
668
  // Initial load only gates the minimal state (recipes + team meta). Everything else streams in.
501
- if (loading) return <div className="ck-glass mx-auto max-w-4xl p-6">Loading…</div>;
669
+ if (loading) return <div className="ck-glass w-full p-6">Loading…</div>;
502
670
 
503
671
  return (
504
- <div className="ck-glass mx-auto max-w-6xl p-6 sm:p-8">
672
+ <div className="ck-glass w-full p-6 sm:p-8">
505
673
  <h1 className="text-2xl font-semibold tracking-tight">Team editor</h1>
506
674
  <p className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">
507
675
  Bootstrap a <strong>custom team recipe</strong> for this installed team, without modifying builtin recipes.
508
676
  </p>
509
677
 
678
+
510
679
  <div className="mt-6 flex flex-wrap gap-2">
511
680
  {(
512
681
  [
@@ -514,12 +683,21 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
514
683
  { id: "agents", label: "Agents" },
515
684
  { id: "skills", label: "Skills" },
516
685
  { id: "cron", label: "Cron" },
686
+ { id: "workflows", label: "Workflows" },
517
687
  { id: "files", label: "Files" },
688
+ { id: "orchestrator", label: "Orchestrator" },
518
689
  ] as const
519
690
  ).map((t) => (
520
691
  <button
521
692
  key={t.id}
522
- onClick={() => setActiveTab(t.id)}
693
+ onClick={() => {
694
+ setActiveTab(t.id);
695
+ try {
696
+ router.replace(`/teams/${encodeURIComponent(teamId)}?tab=${encodeURIComponent(t.id)}`, { scroll: false });
697
+ } catch {
698
+ // ignore
699
+ }
700
+ }}
523
701
  className={
524
702
  activeTab === t.id
525
703
  ? "rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)]"
@@ -1018,7 +1196,1475 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
1018
1196
  </div>
1019
1197
  ) : null}
1020
1198
 
1021
- {activeTab === "files" ? (
1199
+ {activeTab === "workflows" ? (
1200
+ <div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
1201
+ <div className={workflowEditorOpen ? "ck-glass-strong p-4" : "ck-glass-strong p-4 lg:col-span-3"}>
1202
+ <div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Workflows (file-first)</div>
1203
+ <div className="mt-2 text-xs text-[color:var(--ck-text-tertiary)]">
1204
+ Stored in <code>shared-context/workflows/&lt;id&gt;.workflow.json</code> inside the team workspace.
1205
+ </div>
1206
+
1207
+ {workflowFilesError ? (
1208
+ <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-3 text-sm text-red-100">
1209
+ {workflowFilesError}
1210
+ </div>
1211
+ ) : null}
1212
+
1213
+ <div className="mt-3 flex flex-wrap gap-2">
1214
+ <button
1215
+ disabled={workflowSaving}
1216
+ onClick={() => {
1217
+ setWorkflowCreateId("new-workflow");
1218
+ setWorkflowCreateName("New workflow");
1219
+ setWorkflowCreateOpen(true);
1220
+ }}
1221
+ className="rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)] disabled:opacity-50"
1222
+ >
1223
+ Add workflow
1224
+ </button>
1225
+
1226
+ {workflowFiles.length === 0 ? (
1227
+ <button
1228
+ disabled={workflowSaving}
1229
+ onClick={async () => {
1230
+ setWorkflowSaving(true);
1231
+ setWorkflowFilesError("");
1232
+ try {
1233
+ // Create the MVP built-in template (Marketing Cadence v1) for THIS TEAM.
1234
+ const workflow: WorkflowFileV1 = {
1235
+ schema: "clawkitchen.workflow.v1",
1236
+ id: "marketing-cadence",
1237
+ name: "Marketing Cadence (v1)",
1238
+ version: 1,
1239
+ timezone: "America/New_York",
1240
+ triggers: [
1241
+ { kind: "cron", id: "content-cadence", name: "Content cadence", enabled: true, expr: "0 9 * * 1,3,5", tz: "America/New_York" },
1242
+ { kind: "cron", id: "seasonal-scan", name: "Seasonal scan", enabled: true, expr: "0 8 * * *", tz: "America/New_York" },
1243
+ { kind: "cron", id: "weekly-recap", name: "Weekly recap", enabled: false, expr: "30 9 * * 1", tz: "America/New_York" },
1244
+ ],
1245
+ nodes: [
1246
+ { id: "start", type: "start", name: "Start", x: 80, y: 80 },
1247
+ { id: "research", type: "llm", name: "Research", x: 320, y: 80 },
1248
+ { id: "draft_assets", type: "llm", name: "Draft assets", x: 560, y: 80 },
1249
+ { id: "qc_brand", type: "llm", name: "QC brand", x: 800, y: 80 },
1250
+ { id: "approval", type: "human_approval", name: "Approval", x: 1040, y: 80 },
1251
+ { id: "post_x", type: "tool", name: "Post: X", x: 1040, y: 240 },
1252
+ { id: "post_instagram", type: "tool", name: "Post: Instagram", x: 1040, y: 360 },
1253
+ { id: "post_tiktok", type: "tool", name: "Post: TikTok", x: 1040, y: 480 },
1254
+ { id: "post_youtube", type: "tool", name: "Post: YouTube", x: 1040, y: 600 },
1255
+ { id: "writeback", type: "tool", name: "Writeback", x: 800, y: 600 },
1256
+ { id: "end", type: "end", name: "End", x: 560, y: 600 },
1257
+ ],
1258
+ edges: [
1259
+ { id: "e1", from: "start", to: "research" },
1260
+ { id: "e2", from: "research", to: "draft_assets" },
1261
+ { id: "e3", from: "draft_assets", to: "qc_brand" },
1262
+ { id: "e4", from: "qc_brand", to: "approval" },
1263
+ { id: "e5", from: "approval", to: "post_x", label: "approve" },
1264
+ { id: "e6", from: "post_x", to: "post_instagram" },
1265
+ { id: "e7", from: "post_instagram", to: "post_tiktok" },
1266
+ { id: "e8", from: "post_tiktok", to: "post_youtube" },
1267
+ { id: "e9", from: "post_youtube", to: "writeback" },
1268
+ { id: "e10", from: "writeback", to: "end" },
1269
+ ],
1270
+ meta: {
1271
+ templateId: "marketing-cadence-v1",
1272
+ approvalGateRequired: true,
1273
+ },
1274
+ };
1275
+
1276
+ const res = await fetch("/api/teams/workflows", {
1277
+ method: "POST",
1278
+ headers: { "content-type": "application/json" },
1279
+ body: JSON.stringify({ teamId, workflow }),
1280
+ });
1281
+ const json = await res.json();
1282
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to write workflow");
1283
+
1284
+ // Refresh list
1285
+ const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
1286
+ const listJson = await listRes.json();
1287
+ if (listRes.ok && listJson.ok) {
1288
+ const files = Array.isArray(listJson.files) ? listJson.files : [];
1289
+ const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
1290
+ setWorkflowFiles(list);
1291
+ setSelectedWorkflowFile("marketing-cadence.workflow.json");
1292
+ setWorkflowJsonText(JSON.stringify(workflow, null, 2) + "\n");
1293
+ setWorkflowEditorOpen(true);
1294
+ }
1295
+
1296
+ flashMessage("Created workflow template: marketing-cadence", "success");
1297
+ } catch (e: unknown) {
1298
+ setWorkflowFilesError(e instanceof Error ? e.message : String(e));
1299
+ } finally {
1300
+ setWorkflowSaving(false);
1301
+ }
1302
+ }}
1303
+ className="rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)] disabled:opacity-50"
1304
+ >
1305
+ {workflowSaving ? "Working…" : "Create Marketing Cadence template"}
1306
+ </button>
1307
+ ) : null}
1308
+
1309
+ <button
1310
+ disabled={workflowSaving}
1311
+ onClick={async () => {
1312
+ setWorkflowSaving(true);
1313
+ setWorkflowFilesError("");
1314
+ try {
1315
+ const res = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
1316
+ const json = await res.json();
1317
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to list workflows");
1318
+ const files = Array.isArray(json.files) ? json.files : [];
1319
+ const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
1320
+ setWorkflowFiles(list);
1321
+ flashMessage("Refreshed workflow list", "success");
1322
+ } catch (e: unknown) {
1323
+ setWorkflowFilesError(e instanceof Error ? e.message : String(e));
1324
+ } finally {
1325
+ setWorkflowSaving(false);
1326
+ }
1327
+ }}
1328
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
1329
+ >
1330
+ Refresh
1331
+ </button>
1332
+ </div>
1333
+
1334
+ {workflowCreateOpen ? (
1335
+ <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 p-4">
1336
+ <div className="ck-glass-strong w-full max-w-lg rounded-[var(--ck-radius-lg)] border border-white/10 p-4 shadow-[var(--ck-shadow-2)]">
1337
+ <div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Create workflow</div>
1338
+ <div className="mt-1 text-xs text-[color:var(--ck-text-tertiary)]">Creates a new file under shared-context/workflows/ for this team.</div>
1339
+
1340
+ <div className="mt-4 grid grid-cols-1 gap-3">
1341
+ <label className="grid gap-1">
1342
+ <span className="text-xs text-[color:var(--ck-text-secondary)]">Workflow id</span>
1343
+ <input
1344
+ value={workflowCreateId}
1345
+ onChange={(e) => setWorkflowCreateId(e.target.value)}
1346
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 px-3 py-2 text-sm text-[color:var(--ck-text-primary)]"
1347
+ placeholder="e.g. marketing-cadence"
1348
+ />
1349
+ <span className="text-xs text-[color:var(--ck-text-tertiary)]">lowercase letters, numbers, dashes</span>
1350
+ </label>
1351
+
1352
+ <label className="grid gap-1">
1353
+ <span className="text-xs text-[color:var(--ck-text-secondary)]">Name</span>
1354
+ <input
1355
+ value={workflowCreateName}
1356
+ onChange={(e) => setWorkflowCreateName(e.target.value)}
1357
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 px-3 py-2 text-sm text-[color:var(--ck-text-primary)]"
1358
+ placeholder="New workflow"
1359
+ />
1360
+ </label>
1361
+ </div>
1362
+
1363
+ <div className="mt-4 flex flex-wrap justify-end gap-2">
1364
+ <button
1365
+ type="button"
1366
+ onClick={() => setWorkflowCreateOpen(false)}
1367
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1368
+ >
1369
+ Cancel
1370
+ </button>
1371
+ <button
1372
+ type="button"
1373
+ onClick={() => {
1374
+ const safeId = String(workflowCreateId || "").trim();
1375
+ if (!safeId) {
1376
+ flashMessage("Workflow id is required", "error");
1377
+ return;
1378
+ }
1379
+
1380
+ const workflow: WorkflowFileV1 = {
1381
+ schema: "clawkitchen.workflow.v1",
1382
+ id: safeId,
1383
+ name: String(workflowCreateName || safeId),
1384
+ version: 1,
1385
+ timezone: "America/New_York",
1386
+ triggers: [],
1387
+ nodes: [
1388
+ { id: "start", type: "start", name: "Start", x: 120, y: 120 },
1389
+ { id: "end", type: "end", name: "End", x: 420, y: 120 },
1390
+ ],
1391
+ edges: [{ id: "e1", from: "start", to: "end" }],
1392
+ meta: {},
1393
+ };
1394
+
1395
+ setSelectedWorkflowFile(`${safeId}.workflow.json`);
1396
+ setWorkflowJsonText(JSON.stringify(workflow, null, 2) + "\n");
1397
+ setWorkflowCreateOpen(false);
1398
+ setWorkflowEditorOpen(true);
1399
+ }}
1400
+ className="rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)]"
1401
+ >
1402
+ Create
1403
+ </button>
1404
+ </div>
1405
+ </div>
1406
+ </div>
1407
+ ) : null}
1408
+
1409
+ <div className="mt-4 text-xs font-medium text-[color:var(--ck-text-secondary)]">Files</div>
1410
+ <ul className="mt-2 space-y-1">
1411
+ {workflowFilesLoading ? <li className="text-sm text-[color:var(--ck-text-secondary)]">Loading…</li> : null}
1412
+ {workflowFiles.length ? (
1413
+ workflowFiles.map((f) => {
1414
+ const isActive = selectedWorkflowFile === f;
1415
+ return (
1416
+ <li key={f} className="flex items-center justify-between gap-3 rounded-[var(--ck-radius-sm)] px-2 py-1 hover:bg-white/5">
1417
+ <button
1418
+ type="button"
1419
+ onClick={() => setSelectedWorkflowFile(f)}
1420
+ className="flex min-w-0 items-center gap-2 text-left"
1421
+ >
1422
+ <span className={isActive ? "text-green-400" : "text-white/25"}>{isActive ? "✓" : "•"}</span>
1423
+ <span
1424
+ className={
1425
+ isActive
1426
+ ? "truncate text-base font-medium text-[color:var(--ck-text-primary)]"
1427
+ : "truncate text-base text-[color:var(--ck-text-secondary)]"
1428
+ }
1429
+ >
1430
+ {f}
1431
+ </span>
1432
+ </button>
1433
+
1434
+ <div className="flex flex-wrap items-center gap-2">
1435
+ <button
1436
+ type="button"
1437
+ disabled={workflowSaving}
1438
+ onClick={async () => {
1439
+ setSelectedWorkflowFile(f);
1440
+ setWorkflowSaving(true);
1441
+ setWorkflowFilesError("");
1442
+ try {
1443
+ const id = String(f).replace(/\.workflow\.json$/i, "");
1444
+ const res = await fetch(
1445
+ `/api/teams/workflows?teamId=${encodeURIComponent(teamId)}&id=${encodeURIComponent(id)}`,
1446
+ { cache: "no-store" }
1447
+ );
1448
+ const json = await res.json();
1449
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load workflow");
1450
+ setWorkflowJsonText(JSON.stringify(json.workflow, null, 2) + "\n");
1451
+ setWorkflowEditorOpen(true);
1452
+ } catch (e: unknown) {
1453
+ setWorkflowFilesError(e instanceof Error ? e.message : String(e));
1454
+ } finally {
1455
+ setWorkflowSaving(false);
1456
+ }
1457
+ }}
1458
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
1459
+ >
1460
+ Edit
1461
+ </button>
1462
+
1463
+ <button
1464
+ type="button"
1465
+ disabled={workflowSaving}
1466
+ onClick={async () => {
1467
+ if (!confirm(`Delete workflow ${f}?`)) return;
1468
+ setWorkflowSaving(true);
1469
+ setWorkflowFilesError("");
1470
+ try {
1471
+ const id = String(f).replace(/\.workflow\.json$/i, "");
1472
+ const res = await fetch(
1473
+ `/api/teams/workflows?teamId=${encodeURIComponent(teamId)}&id=${encodeURIComponent(id)}`,
1474
+ { method: "DELETE" }
1475
+ );
1476
+ const json = await res.json();
1477
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to delete workflow");
1478
+
1479
+ const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
1480
+ const listJson = await listRes.json();
1481
+ if (listRes.ok && listJson.ok) {
1482
+ const files = Array.isArray(listJson.files) ? listJson.files : [];
1483
+ const list = files.map((x: unknown) => String(x ?? "").trim()).filter((x: string) => Boolean(x));
1484
+ setWorkflowFiles(list);
1485
+ }
1486
+
1487
+ if (selectedWorkflowFile === f) {
1488
+ setSelectedWorkflowFile("");
1489
+ setWorkflowJsonText("");
1490
+ }
1491
+
1492
+ flashMessage(`Deleted workflow: ${f}`, "success");
1493
+ } catch (e: unknown) {
1494
+ setWorkflowFilesError(e instanceof Error ? e.message : String(e));
1495
+ } finally {
1496
+ setWorkflowSaving(false);
1497
+ }
1498
+ }}
1499
+ className="rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm font-medium text-red-100 shadow-[var(--ck-shadow-1)] hover:bg-red-500/20 disabled:opacity-50"
1500
+ >
1501
+ Delete
1502
+ </button>
1503
+ </div>
1504
+ </li>
1505
+ );
1506
+ })
1507
+ ) : !workflowFilesLoading ? (
1508
+ <li className="text-sm text-[color:var(--ck-text-secondary)]">No workflows yet.</li>
1509
+ ) : null}
1510
+ </ul>
1511
+ </div>
1512
+
1513
+ {workflowEditorOpen ? (
1514
+ <div className="fixed inset-0 z-50 bg-black/70">
1515
+ <div className="ck-glass-strong h-full w-full overflow-hidden border border-white/10 shadow-[var(--ck-shadow-2)]">
1516
+ <div className="flex items-center justify-between border-b border-white/10 bg-black/20 p-3">
1517
+ <div className="text-sm font-medium text-[color:var(--ck-text-primary)]">
1518
+ Workflow editor{selectedWorkflowFile ? ` — ${selectedWorkflowFile}` : ""}
1519
+ </div>
1520
+ <button
1521
+ type="button"
1522
+ onClick={() => setWorkflowEditorOpen(false)}
1523
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1524
+ >
1525
+ Close
1526
+ </button>
1527
+ </div>
1528
+
1529
+ <div className="h-[calc(100%-3rem)] overflow-auto p-4">
1530
+ <div className="ck-glass-strong p-4 lg:col-span-2">
1531
+ <div className="flex flex-wrap items-center justify-between gap-3">
1532
+ <div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Workflow editor</div>
1533
+
1534
+ <div className="flex flex-wrap items-center gap-2">
1535
+ <div className="flex overflow-hidden rounded-[var(--ck-radius-sm)] border border-white/10">
1536
+ <button
1537
+ type="button"
1538
+ onClick={() => setWorkflowView("canvas")}
1539
+ className={
1540
+ workflowView === "canvas"
1541
+ ? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
1542
+ : "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
1543
+ }
1544
+ >
1545
+ Canvas
1546
+ </button>
1547
+ <button
1548
+ type="button"
1549
+ onClick={() => setWorkflowView("json")}
1550
+ className={
1551
+ workflowView === "json"
1552
+ ? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
1553
+ : "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
1554
+ }
1555
+ >
1556
+ JSON
1557
+ </button>
1558
+ </div>
1559
+
1560
+ <input
1561
+ ref={workflowImportInputRef}
1562
+ type="file"
1563
+ accept="application/json,.json"
1564
+ className="hidden"
1565
+ onChange={async (e) => {
1566
+ const file = e.target.files?.[0];
1567
+ // Reset the input so re-importing the same file still triggers onChange.
1568
+ e.target.value = "";
1569
+ if (!file) return;
1570
+
1571
+ setWorkflowFilesError("");
1572
+ try {
1573
+ const text = await file.text();
1574
+ const parsed = JSON.parse(text) as WorkflowFileV1;
1575
+ setWorkflowJsonText(JSON.stringify(parsed, null, 2) + "\n");
1576
+ setSelectedWorkflowFile("");
1577
+ flashMessage(`Imported workflow JSON: ${parsed.id || file.name}`, "success");
1578
+ } catch (err: unknown) {
1579
+ setWorkflowFilesError(err instanceof Error ? err.message : String(err));
1580
+ }
1581
+ }}
1582
+ />
1583
+
1584
+ <button
1585
+ type="button"
1586
+ disabled={workflowSaving}
1587
+ onClick={() => workflowImportInputRef.current?.click()}
1588
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
1589
+ >
1590
+ Import
1591
+ </button>
1592
+
1593
+ <button
1594
+ type="button"
1595
+ disabled={workflowSaving || !workflowParsed || Boolean(workflowParseError) || workflowValidation.errors.length > 0}
1596
+ onClick={() => {
1597
+ setWorkflowFilesError("");
1598
+ try {
1599
+ const wf = workflowParsed;
1600
+ if (!wf) throw new Error("No workflow loaded");
1601
+ if (workflowParseError) throw new Error(`Invalid JSON: ${workflowParseError}`);
1602
+ if (workflowValidation.errors.length) throw new Error("Fix workflow validation errors before exporting");
1603
+
1604
+ const filename = `${wf.id || "workflow"}.workflow.json`;
1605
+ const blob = new Blob([JSON.stringify(wf, null, 2) + "\n"], { type: "application/json" });
1606
+ const url = URL.createObjectURL(blob);
1607
+ const a = document.createElement("a");
1608
+ a.href = url;
1609
+ a.download = filename;
1610
+ document.body.appendChild(a);
1611
+ a.click();
1612
+ a.remove();
1613
+ URL.revokeObjectURL(url);
1614
+ flashMessage(`Exported: ${filename}`, "success");
1615
+ } catch (err: unknown) {
1616
+ setWorkflowFilesError(err instanceof Error ? err.message : String(err));
1617
+ }
1618
+ }}
1619
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
1620
+ >
1621
+ Export
1622
+ </button>
1623
+
1624
+ <button
1625
+ disabled={workflowSaving || !workflowParsed || Boolean(workflowParseError) || workflowValidation.errors.length > 0}
1626
+ onClick={async () => {
1627
+ setWorkflowSaving(true);
1628
+ setWorkflowFilesError("");
1629
+ try {
1630
+ const wf = workflowParsed;
1631
+ if (!wf) throw new Error("No workflow loaded");
1632
+ if (workflowParseError) throw new Error(`Invalid JSON: ${workflowParseError}`);
1633
+ if (workflowValidation.errors.length) throw new Error("Fix workflow validation errors before saving");
1634
+
1635
+ const res = await fetch("/api/teams/workflows", {
1636
+ method: "POST",
1637
+ headers: { "content-type": "application/json" },
1638
+ body: JSON.stringify({ teamId, workflow: wf }),
1639
+ });
1640
+ const json = await res.json();
1641
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to save workflow");
1642
+ flashMessage(`Saved workflow: ${wf.id}`, "success");
1643
+
1644
+ const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
1645
+ const listJson = await listRes.json();
1646
+ if (listRes.ok && listJson.ok) {
1647
+ const files = Array.isArray(listJson.files) ? listJson.files : [];
1648
+ const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
1649
+ setWorkflowFiles(list);
1650
+ setSelectedWorkflowFile(`${wf.id}.workflow.json`);
1651
+ }
1652
+ } catch (e: unknown) {
1653
+ setWorkflowFilesError(e instanceof Error ? e.message : String(e));
1654
+ } finally {
1655
+ setWorkflowSaving(false);
1656
+ }
1657
+ }}
1658
+ className="rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)] disabled:opacity-50"
1659
+ >
1660
+ {workflowSaving ? "Saving…" : "Save"}
1661
+ </button>
1662
+ </div>
1663
+ </div>
1664
+
1665
+ {workflowParseError ? (
1666
+ <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-yellow-400/30 bg-yellow-500/10 p-3 text-sm text-yellow-100">
1667
+ JSON parse error: {workflowParseError}
1668
+ </div>
1669
+ ) : null}
1670
+
1671
+ {!workflowParseError && workflowValidation.errors.length ? (
1672
+ <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-3 text-sm text-red-100">
1673
+ <div className="font-medium">Workflow validation errors</div>
1674
+ <ul className="mt-2 list-disc space-y-1 pl-5">
1675
+ {workflowValidation.errors.map((e) => (
1676
+ <li key={e}>{e}</li>
1677
+ ))}
1678
+ </ul>
1679
+ </div>
1680
+ ) : null}
1681
+
1682
+ {!workflowParseError && !workflowValidation.errors.length && workflowValidation.warnings.length ? (
1683
+ <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-yellow-400/30 bg-yellow-500/10 p-3 text-sm text-yellow-100">
1684
+ <div className="font-medium">Workflow validation warnings</div>
1685
+ <ul className="mt-2 list-disc space-y-1 pl-5">
1686
+ {workflowValidation.warnings.map((w) => (
1687
+ <li key={w}>{w}</li>
1688
+ ))}
1689
+ </ul>
1690
+ </div>
1691
+ ) : null}
1692
+
1693
+ {workflowView === "canvas" ? (
1694
+ <div className="mt-3 grid grid-cols-1 gap-3 lg:grid-cols-4">
1695
+ <div className="lg:col-span-3">
1696
+ <div
1697
+ ref={workflowCanvasRef}
1698
+ className="relative h-[calc(100vh-20rem)] min-h-[55vh] w-full overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20"
1699
+ >
1700
+ <div className="relative h-[900px] w-[1400px]">
1701
+ <svg className="absolute inset-0" width={1400} height={900}>
1702
+ {(workflowParsed?.edges ?? []).map((e) => {
1703
+ const wf = workflowParsed;
1704
+ if (!wf) return null;
1705
+ const a = wf.nodes.find((n) => n.id === e.from);
1706
+ const b = wf.nodes.find((n) => n.id === e.to);
1707
+ if (!a || !b) return null;
1708
+
1709
+ const ax = (typeof a.x === "number" ? a.x : 80) + 90;
1710
+ const ay = (typeof a.y === "number" ? a.y : 80) + 24;
1711
+ const bx = (typeof b.x === "number" ? b.x : 80) + 90;
1712
+ const by = (typeof b.y === "number" ? b.y : 80) + 24;
1713
+
1714
+ return (
1715
+ <g key={e.id}>
1716
+ <line x1={ax} y1={ay} x2={bx} y2={by} stroke="rgba(255,255,255,0.18)" strokeWidth={2} />
1717
+ {e.label ? (
1718
+ <text x={(ax + bx) / 2} y={(ay + by) / 2 - 6} fill="rgba(255,255,255,0.55)" fontSize={10} textAnchor="middle">
1719
+ {e.label}
1720
+ </text>
1721
+ ) : null}
1722
+ </g>
1723
+ );
1724
+ })}
1725
+ </svg>
1726
+
1727
+ {(workflowParsed?.nodes ?? []).map((n, idx) => {
1728
+ const x = typeof n.x === "number" ? n.x : 80 + idx * 220;
1729
+ const y = typeof n.y === "number" ? n.y : 80;
1730
+ const selected = workflowSelectedNodeId === n.id;
1731
+
1732
+ return (
1733
+ <div
1734
+ key={n.id}
1735
+ role="button"
1736
+ tabIndex={0}
1737
+ onClick={() => setWorkflowSelectedNodeId(n.id)}
1738
+ onPointerDown={(e) => {
1739
+ if (e.button !== 0) return;
1740
+ const el = workflowCanvasRef.current;
1741
+ if (!el) return;
1742
+ const rect = el.getBoundingClientRect();
1743
+ const dx = e.clientX - rect.left - x;
1744
+ const dy = e.clientY - rect.top - y;
1745
+ setWorkflowSelectedNodeId(n.id);
1746
+ setWorkflowDragging({ nodeId: n.id, dx, dy, containerLeft: rect.left, containerTop: rect.top });
1747
+ }}
1748
+ className={
1749
+ selected
1750
+ ? "absolute cursor-grab rounded-[var(--ck-radius-sm)] border border-white/25 bg-white/10 px-3 py-2 text-xs text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)]"
1751
+ : "absolute cursor-grab rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
1752
+ }
1753
+ style={{ left: x, top: y, width: 180 }}
1754
+ >
1755
+ <div className="font-medium text-[color:var(--ck-text-primary)]">{n.name || n.id}</div>
1756
+ <div className="mt-0.5 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</div>
1757
+ </div>
1758
+ );
1759
+ })}
1760
+ </div>
1761
+ </div>
1762
+
1763
+ <div className="mt-2 text-xs text-[color:var(--ck-text-tertiary)]">
1764
+ Tip: click a node to inspect it; drag to reposition. Positions are stored in the workflow JSON (file-first).
1765
+ </div>
1766
+ </div>
1767
+
1768
+ <div className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 p-3 lg:col-span-1">
1769
+ <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Workflow</div>
1770
+
1771
+ {workflowParsed ? (
1772
+ (() => {
1773
+ const wf = workflowParsed;
1774
+ const tz = String(wf.timezone ?? "").trim() || "UTC";
1775
+ const triggers = wf.triggers ?? [];
1776
+
1777
+ const meta = wf.meta && typeof wf.meta === "object" && !Array.isArray(wf.meta) ? (wf.meta as Record<string, unknown>) : {};
1778
+ const approvalProvider = String(meta.approvalProvider ?? "telegram").trim() || "telegram";
1779
+ const approvalTarget = String(meta.approvalTarget ?? "").trim();
1780
+
1781
+ const presets = [
1782
+ { label: "(no preset)", expr: "" },
1783
+ { label: "Mon/Wed/Fri 09:00 local", expr: "0 9 * * 1,3,5" },
1784
+ { label: "Daily 08:00 local", expr: "0 8 * * *" },
1785
+ { label: "Mon 09:30 local", expr: "30 9 * * 1" },
1786
+ ];
1787
+
1788
+ return (
1789
+ <div className="mt-2 space-y-4">
1790
+ <label className="block">
1791
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">timezone</div>
1792
+ <input
1793
+ value={tz}
1794
+ onChange={(e) => {
1795
+ const nextTz = String(e.target.value || "").trim() || "UTC";
1796
+ const next: WorkflowFileV1 = { ...wf, timezone: nextTz };
1797
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1798
+ }}
1799
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1800
+ placeholder="America/New_York"
1801
+ />
1802
+ </label>
1803
+
1804
+ <div className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1805
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">approval channel (mvp)</div>
1806
+ <div className="mt-2 space-y-2">
1807
+ <label className="block">
1808
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">provider</div>
1809
+ <input
1810
+ value={approvalProvider}
1811
+ onChange={(e) => {
1812
+ const nextProvider = String(e.target.value || "").trim() || "telegram";
1813
+ const nextMeta = { ...meta, approvalProvider: nextProvider };
1814
+ const next: WorkflowFileV1 = { ...wf, meta: nextMeta };
1815
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1816
+ }}
1817
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1818
+ placeholder="telegram"
1819
+ />
1820
+ </label>
1821
+
1822
+ <label className="block">
1823
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">target</div>
1824
+ <input
1825
+ value={approvalTarget}
1826
+ onChange={(e) => {
1827
+ const nextTarget = String(e.target.value || "").trim();
1828
+ const nextMeta = { ...meta, approvalTarget: nextTarget };
1829
+ const next: WorkflowFileV1 = { ...wf, meta: nextMeta };
1830
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1831
+ }}
1832
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1833
+ placeholder="(e.g. Telegram chat id)"
1834
+ />
1835
+ <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
1836
+ If set, sample runs that reach a human-approval node will send an approval packet via the gateway message tool.
1837
+ </div>
1838
+ </label>
1839
+ </div>
1840
+ </div>
1841
+
1842
+ <div>
1843
+ <div className="flex items-center justify-between gap-2">
1844
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">triggers</div>
1845
+ <button
1846
+ type="button"
1847
+ onClick={() => {
1848
+ const id = `t${Date.now()}`;
1849
+ const next: WorkflowFileV1 = {
1850
+ ...wf,
1851
+ triggers: [
1852
+ ...triggers,
1853
+ { kind: "cron", id, name: "New trigger", enabled: true, expr: "0 9 * * 1-5", tz },
1854
+ ],
1855
+ };
1856
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1857
+ }}
1858
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1859
+ >
1860
+ + Add
1861
+ </button>
1862
+ </div>
1863
+
1864
+ <div className="mt-2 space-y-2">
1865
+ {triggers.length ? (
1866
+ triggers.map((t, i) => {
1867
+ const kind = (t as { kind?: unknown }).kind;
1868
+ const isCron = kind === "cron";
1869
+ const id = String((t as { id?: unknown }).id ?? "");
1870
+ const name = String((t as { name?: unknown }).name ?? "");
1871
+ const enabled = Boolean((t as { enabled?: unknown }).enabled);
1872
+ const expr = String((t as { expr?: unknown }).expr ?? "");
1873
+ const trigTz = String((t as { tz?: unknown }).tz ?? tz);
1874
+ const cronFields = expr.trim().split(/\s+/).filter(Boolean);
1875
+ const cronLooksValid = !expr.trim() || cronFields.length === 5;
1876
+
1877
+ return (
1878
+ <div key={`${id}-${i}`} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1879
+ <div className="flex items-center justify-between gap-2">
1880
+ <div className="text-xs text-[color:var(--ck-text-primary)]">{name || id || `trigger-${i + 1}`}</div>
1881
+ <button
1882
+ type="button"
1883
+ onClick={() => {
1884
+ const next: WorkflowFileV1 = { ...wf, triggers: triggers.filter((_, idx) => idx !== i) };
1885
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1886
+ }}
1887
+ className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
1888
+ >
1889
+ Remove
1890
+ </button>
1891
+ </div>
1892
+
1893
+ {!isCron ? (
1894
+ <div className="mt-1 text-xs text-[color:var(--ck-text-secondary)]">Unsupported trigger kind: {String(kind)}</div>
1895
+ ) : null}
1896
+
1897
+ <div className="mt-2 grid grid-cols-1 gap-2">
1898
+ <label className="flex items-center gap-2 text-xs text-[color:var(--ck-text-secondary)]">
1899
+ <input
1900
+ type="checkbox"
1901
+ checked={enabled}
1902
+ onChange={(e) => {
1903
+ const nextEnabled = e.target.checked;
1904
+ const next: WorkflowFileV1 = {
1905
+ ...wf,
1906
+ triggers: triggers.map((x, idx) =>
1907
+ idx === i ? (x.kind === "cron" ? { ...x, enabled: nextEnabled } : x) : x
1908
+ ),
1909
+ };
1910
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1911
+ }}
1912
+ />
1913
+ Enabled
1914
+ </label>
1915
+
1916
+ <label className="block">
1917
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
1918
+ <input
1919
+ value={name}
1920
+ onChange={(e) => {
1921
+ const nextName = e.target.value;
1922
+ const next: WorkflowFileV1 = {
1923
+ ...wf,
1924
+ triggers: triggers.map((x, idx) =>
1925
+ idx === i ? (x.kind === "cron" ? { ...x, name: nextName } : x) : x
1926
+ ),
1927
+ };
1928
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1929
+ }}
1930
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1931
+ placeholder="Content cadence"
1932
+ />
1933
+ </label>
1934
+
1935
+ <label className="block">
1936
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">schedule (cron)</div>
1937
+ <input
1938
+ value={expr}
1939
+ onChange={(e) => {
1940
+ const nextExpr = e.target.value;
1941
+ const next: WorkflowFileV1 = {
1942
+ ...wf,
1943
+ triggers: triggers.map((x, idx) =>
1944
+ idx === i ? (x.kind === "cron" ? { ...x, expr: nextExpr } : x) : x
1945
+ ),
1946
+ };
1947
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1948
+ }}
1949
+ className={
1950
+ cronLooksValid
1951
+ ? "mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 font-mono text-[11px] text-[color:var(--ck-text-primary)]"
1952
+ : "mt-1 w-full rounded-[var(--ck-radius-sm)] border border-red-400/50 bg-black/25 px-2 py-1 font-mono text-[11px] text-[color:var(--ck-text-primary)]"
1953
+ }
1954
+ placeholder="0 9 * * 1,3,5"
1955
+ />
1956
+ {!cronLooksValid ? (
1957
+ <div className="mt-1 text-[10px] text-red-200">
1958
+ Cron should be 5 fields (min hour dom month dow). You entered {cronFields.length}.
1959
+ </div>
1960
+ ) : null}
1961
+ <div className="mt-1 grid grid-cols-1 gap-1">
1962
+ <select
1963
+ value={presets.some((p) => p.expr === expr) ? expr : ""}
1964
+ onChange={(e) => {
1965
+ const nextExpr = e.target.value;
1966
+ if (!nextExpr) return;
1967
+ const next: WorkflowFileV1 = {
1968
+ ...wf,
1969
+ triggers: triggers.map((x, idx) =>
1970
+ idx === i ? (x.kind === "cron" ? { ...x, expr: nextExpr } : x) : x
1971
+ ),
1972
+ };
1973
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
1974
+ }}
1975
+ className="w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-[11px] text-[color:var(--ck-text-secondary)]"
1976
+ >
1977
+ {presets.map((p) => (
1978
+ <option key={p.label} value={p.expr}>
1979
+ {p.label}
1980
+ </option>
1981
+ ))}
1982
+ </select>
1983
+ <div className="text-[10px] text-[color:var(--ck-text-tertiary)]">Presets set the cron; edit freely for advanced schedules.</div>
1984
+ </div>
1985
+ </label>
1986
+
1987
+ <label className="block">
1988
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">timezone override</div>
1989
+ <input
1990
+ value={trigTz}
1991
+ onChange={(e) => {
1992
+ const nextTz = String(e.target.value || "").trim() || tz;
1993
+ const next: WorkflowFileV1 = {
1994
+ ...wf,
1995
+ triggers: triggers.map((x, idx) =>
1996
+ idx === i ? (x.kind === "cron" ? { ...x, tz: nextTz } : x) : x
1997
+ ),
1998
+ };
1999
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2000
+ }}
2001
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2002
+ placeholder={tz}
2003
+ />
2004
+ </label>
2005
+ </div>
2006
+ </div>
2007
+ );
2008
+ })
2009
+ ) : (
2010
+ <div className="text-xs text-[color:var(--ck-text-secondary)]">No triggers yet.</div>
2011
+ )}
2012
+ </div>
2013
+ </div>
2014
+
2015
+ <div className="border-t border-white/10 pt-3">
2016
+ <div className="flex items-center justify-between gap-2">
2017
+ <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Runs (history)</div>
2018
+ <div className="flex items-center gap-2">
2019
+ <button
2020
+ type="button"
2021
+ disabled={workflowSaving}
2022
+ onClick={async () => {
2023
+ const wfId = String(wf.id ?? "").trim();
2024
+ if (!wfId) return;
2025
+ setWorkflowRunsError("");
2026
+ try {
2027
+ const res = await fetch("/api/teams/workflow-runs", {
2028
+ method: "POST",
2029
+ headers: { "content-type": "application/json" },
2030
+ body: JSON.stringify({ teamId, workflowId: wfId, mode: "sample" }),
2031
+ });
2032
+ const json = await res.json();
2033
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to create sample run");
2034
+
2035
+ const listRes = await fetch(
2036
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}`,
2037
+ { cache: "no-store" }
2038
+ );
2039
+ const listJson = await listRes.json();
2040
+ if (!listRes.ok || !listJson.ok) throw new Error(listJson.error || "Failed to refresh runs");
2041
+ const files = Array.isArray(listJson.files) ? listJson.files : [];
2042
+ const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
2043
+ setWorkflowRuns(list);
2044
+ flashMessage("Created sample run", "success");
2045
+ } catch (e: unknown) {
2046
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
2047
+ }
2048
+ }}
2049
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10 disabled:opacity-50"
2050
+ >
2051
+ + Sample run
2052
+ </button>
2053
+ </div>
2054
+ </div>
2055
+
2056
+ {workflowRunsError ? (
2057
+ <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-2 text-xs text-red-100">
2058
+ {workflowRunsError}
2059
+ </div>
2060
+ ) : null}
2061
+
2062
+ <div className="mt-2 space-y-1">
2063
+ {workflowRunsLoading ? (
2064
+ <div className="text-xs text-[color:var(--ck-text-secondary)]">Loading runs…</div>
2065
+ ) : workflowRuns.length ? (
2066
+ workflowRuns.slice(0, 8).map((f) => {
2067
+ const runId = String(f).replace(/\.run\.json$/i, "");
2068
+ const selected = selectedWorkflowRunId === runId;
2069
+ return (
2070
+ <button
2071
+ key={f}
2072
+ type="button"
2073
+ onClick={async () => {
2074
+ const wfId = String(wf.id ?? "").trim();
2075
+ if (!wfId) return;
2076
+ setSelectedWorkflowRunId(runId);
2077
+ setSelectedWorkflowRun(null);
2078
+ setWorkflowRunsError("");
2079
+ try {
2080
+ const res = await fetch(
2081
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(runId)}`,
2082
+ { cache: "no-store" }
2083
+ );
2084
+ const json = await res.json();
2085
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load run");
2086
+ setSelectedWorkflowRun(json.run);
2087
+ } catch (e: unknown) {
2088
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
2089
+ }
2090
+ }}
2091
+ className={
2092
+ selected
2093
+ ? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
2094
+ : "w-full rounded-[var(--ck-radius-sm)] px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-secondary)] hover:bg-white/5"
2095
+ }
2096
+ >
2097
+ {runId}
2098
+ </button>
2099
+ );
2100
+ })
2101
+ ) : (
2102
+ <div className="text-xs text-[color:var(--ck-text-secondary)]">No runs yet.</div>
2103
+ )}
2104
+ </div>
2105
+
2106
+ {selectedWorkflowRun ? (
2107
+ (() => {
2108
+ const run =
2109
+ selectedWorkflowRun && typeof selectedWorkflowRun === "object"
2110
+ ? (selectedWorkflowRun as Record<string, unknown>)
2111
+ : ({} as Record<string, unknown>);
2112
+ const nodesVal = (run as Record<string, unknown>).nodes;
2113
+ const nodes = Array.isArray(nodesVal) ? (nodesVal as unknown[]) : [];
2114
+ return (
2115
+ <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
2116
+ <div className="flex flex-wrap items-center justify-between gap-2">
2117
+ <div className="text-[11px] font-medium text-[color:var(--ck-text-primary)]">Run detail</div>
2118
+ <div className="text-[10px] text-[color:var(--ck-text-tertiary)]">
2119
+ <span className="font-mono">{String(run?.status ?? "")}</span>
2120
+ {run?.startedAt ? <span> · {String(run.startedAt)}</span> : null}
2121
+ </div>
2122
+ </div>
2123
+
2124
+ {run?.summary ? (
2125
+ <div className="mt-1 text-[11px] text-[color:var(--ck-text-secondary)]">{String(run.summary)}</div>
2126
+ ) : null}
2127
+
2128
+ {(() => {
2129
+ const status = String(run?.status ?? "");
2130
+ const approvalVal = (run as Record<string, unknown>).approval;
2131
+ const approval =
2132
+ approvalVal && typeof approvalVal === "object" ? (approvalVal as Record<string, unknown>) : ({} as Record<string, unknown>);
2133
+ const approvalState = String(approval.state ?? "");
2134
+ const approvalNodeId = String(approval.nodeId ?? "");
2135
+ const canAct = status === "waiting_for_approval" && approvalState === "pending" && approvalNodeId;
2136
+
2137
+ if (!canAct) return null;
2138
+
2139
+ return (
2140
+ <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-amber-300/30 bg-amber-500/10 p-2">
2141
+ <div className="text-[10px] uppercase tracking-wide text-amber-100">approval required</div>
2142
+ <div className="mt-1 text-[11px] text-amber-50">
2143
+ Waiting on <span className="font-mono">{approvalNodeId}</span>
2144
+ </div>
2145
+ <div className="mt-2 flex flex-wrap gap-2">
2146
+ <button
2147
+ type="button"
2148
+ onClick={async () => {
2149
+ const wfId = String(wf.id ?? "").trim();
2150
+ const rId = String((run as Record<string, unknown>).id ?? "").trim();
2151
+ if (!wfId || !rId) return;
2152
+ setWorkflowRunsError("");
2153
+ try {
2154
+ const res = await fetch("/api/teams/workflow-runs", {
2155
+ method: "POST",
2156
+ headers: { "content-type": "application/json" },
2157
+ body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "approve" }),
2158
+ });
2159
+ const json = await res.json();
2160
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to approve");
2161
+
2162
+ const detailRes = await fetch(
2163
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
2164
+ { cache: "no-store" }
2165
+ );
2166
+ const detailJson = await detailRes.json();
2167
+ if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
2168
+ setSelectedWorkflowRun(detailJson.run);
2169
+ flashMessage("Approved", "success");
2170
+ } catch (e: unknown) {
2171
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
2172
+ }
2173
+ }}
2174
+ className="rounded-[var(--ck-radius-sm)] border border-emerald-300/30 bg-emerald-500/10 px-2 py-1 text-[10px] font-medium text-emerald-50 hover:bg-emerald-500/20"
2175
+ >
2176
+ Approve &amp; continue
2177
+ </button>
2178
+ <button
2179
+ type="button"
2180
+ onClick={async () => {
2181
+ const wfId = String(wf.id ?? "").trim();
2182
+ const rId = String((run as Record<string, unknown>).id ?? "").trim();
2183
+ if (!wfId || !rId) return;
2184
+ setWorkflowRunsError("");
2185
+ try {
2186
+ const res = await fetch("/api/teams/workflow-runs", {
2187
+ method: "POST",
2188
+ headers: { "content-type": "application/json" },
2189
+ body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "request_changes" }),
2190
+ });
2191
+ const json = await res.json();
2192
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to request changes");
2193
+
2194
+ const detailRes = await fetch(
2195
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
2196
+ { cache: "no-store" }
2197
+ );
2198
+ const detailJson = await detailRes.json();
2199
+ if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
2200
+ setSelectedWorkflowRun(detailJson.run);
2201
+ flashMessage("Requested changes", "success");
2202
+ } catch (e: unknown) {
2203
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
2204
+ }
2205
+ }}
2206
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
2207
+ >
2208
+ Request changes
2209
+ </button>
2210
+ <button
2211
+ type="button"
2212
+ onClick={async () => {
2213
+ const wfId = String(wf.id ?? "").trim();
2214
+ const rId = String((run as Record<string, unknown>).id ?? "").trim();
2215
+ if (!wfId || !rId) return;
2216
+ setWorkflowRunsError("");
2217
+ try {
2218
+ const res = await fetch("/api/teams/workflow-runs", {
2219
+ method: "POST",
2220
+ headers: { "content-type": "application/json" },
2221
+ body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "cancel" }),
2222
+ });
2223
+ const json = await res.json();
2224
+ if (!res.ok || !json.ok) throw new Error(json.error || "Failed to cancel");
2225
+
2226
+ const detailRes = await fetch(
2227
+ `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
2228
+ { cache: "no-store" }
2229
+ );
2230
+ const detailJson = await detailRes.json();
2231
+ if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
2232
+ setSelectedWorkflowRun(detailJson.run);
2233
+ flashMessage("Canceled", "success");
2234
+ } catch (e: unknown) {
2235
+ setWorkflowRunsError(e instanceof Error ? e.message : String(e));
2236
+ }
2237
+ }}
2238
+ className="rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 px-2 py-1 text-[10px] font-medium text-red-50 hover:bg-red-500/20"
2239
+ >
2240
+ Cancel
2241
+ </button>
2242
+ </div>
2243
+ <div className="mt-2 text-[10px] text-amber-100/80">
2244
+ Note: this is currently an in-app approval stub; channel delivery/resume wiring still TBD.
2245
+ </div>
2246
+ </div>
2247
+ );
2248
+ })()}
2249
+
2250
+ <div className="mt-2">
2251
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">per-node results</div>
2252
+ {nodes.length ? (
2253
+ <div className="mt-1 space-y-1">
2254
+ {nodes.slice(0, 50).map((n, idx) => {
2255
+ const node = n && typeof n === "object" ? (n as Record<string, unknown>) : ({} as Record<string, unknown>);
2256
+ const status = String(node.status ?? "");
2257
+ const statusColor =
2258
+ status === "success"
2259
+ ? "text-emerald-200"
2260
+ : status === "error"
2261
+ ? "text-red-200"
2262
+ : status === "running" || status === "waiting"
2263
+ ? "text-amber-200"
2264
+ : "text-[color:var(--ck-text-secondary)]";
2265
+ const nodeId = String(node.nodeId ?? "");
2266
+ const errVal = node.error;
2267
+ const outputVal = node.output;
2268
+ return (
2269
+ <div key={`${nodeId || "node"}-${idx}`} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 p-2">
2270
+ <div className="flex items-center justify-between gap-2">
2271
+ <div className="text-[11px] text-[color:var(--ck-text-primary)]">
2272
+ <span className="font-mono">{nodeId}</span>
2273
+ </div>
2274
+ <div className={`text-[10px] font-medium ${statusColor}`}>{status}</div>
2275
+ </div>
2276
+ {errVal ? (
2277
+ <div className="mt-1 text-[10px] text-red-100">
2278
+ {typeof errVal === "string"
2279
+ ? errVal
2280
+ : typeof errVal === "object" && errVal && "message" in errVal
2281
+ ? String((errVal as Record<string, unknown>).message ?? "")
2282
+ : String(errVal)}
2283
+ </div>
2284
+ ) : null}
2285
+ {typeof outputVal !== "undefined" ? (
2286
+ <pre className="mt-1 max-h-[120px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
2287
+ {JSON.stringify(outputVal, null, 2)}
2288
+ </pre>
2289
+ ) : null}
2290
+ </div>
2291
+ );
2292
+ })}
2293
+ </div>
2294
+ ) : (
2295
+ <div className="mt-1 text-xs text-[color:var(--ck-text-secondary)]">No node results recorded on this run yet.</div>
2296
+ )}
2297
+ </div>
2298
+
2299
+ <details className="mt-2">
2300
+ <summary className="cursor-pointer select-none text-[10px] text-[color:var(--ck-text-tertiary)]">raw JSON</summary>
2301
+ <pre className="mt-2 max-h-[220px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
2302
+ {JSON.stringify(selectedWorkflowRun, null, 2)}
2303
+ </pre>
2304
+ </details>
2305
+ </div>
2306
+ );
2307
+ })()
2308
+ ) : null}
2309
+ </div>
2310
+
2311
+ <div className="border-t border-white/10 pt-3">
2312
+ <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Nodes</div>
2313
+
2314
+ <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
2315
+ <div className="grid grid-cols-1 gap-2">
2316
+ <label className="block">
2317
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
2318
+ <input
2319
+ value={workflowNewNodeId}
2320
+ onChange={(e) => setWorkflowNewNodeId(e.target.value)}
2321
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2322
+ placeholder="e.g. draft_assets"
2323
+ />
2324
+ <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
2325
+ Tip: ids are file-first + portable; use lowercase letters, numbers, and underscores.
2326
+ </div>
2327
+ </label>
2328
+
2329
+ <label className="block">
2330
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name (optional)</div>
2331
+ <input
2332
+ value={workflowNewNodeName}
2333
+ onChange={(e) => setWorkflowNewNodeName(e.target.value)}
2334
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2335
+ placeholder="Human-friendly label"
2336
+ />
2337
+ </label>
2338
+
2339
+ <label className="block">
2340
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
2341
+ <select
2342
+ value={workflowNewNodeType}
2343
+ onChange={(e) => setWorkflowNewNodeType(e.target.value as WorkflowFileV1["nodes"][number]["type"])}
2344
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2345
+ >
2346
+ <option value="start">start</option>
2347
+ <option value="end">end</option>
2348
+ <option value="llm">llm</option>
2349
+ <option value="tool">tool</option>
2350
+ <option value="condition">condition</option>
2351
+ <option value="delay">delay</option>
2352
+ <option value="human_approval">human_approval</option>
2353
+ </select>
2354
+ </label>
2355
+
2356
+ <button
2357
+ type="button"
2358
+ onClick={() => {
2359
+ const rawId = String(workflowNewNodeId || "").trim();
2360
+ const id = rawId.replace(/[^a-z0-9_\-]/gi, "_");
2361
+ if (!id) {
2362
+ flashMessage("Node id is required", "error");
2363
+ return;
2364
+ }
2365
+ if (wf.nodes.some((n) => n.id === id)) {
2366
+ flashMessage(`Node id already exists: ${id}`, "error");
2367
+ return;
2368
+ }
2369
+
2370
+ const maxX = wf.nodes.reduce((acc, n) => (typeof n.x === "number" ? Math.max(acc, n.x) : acc), 80);
2371
+ const nextNode = {
2372
+ id,
2373
+ type: workflowNewNodeType,
2374
+ name: String(workflowNewNodeName || "").trim() || id,
2375
+ x: maxX + 220,
2376
+ y: 80,
2377
+ } as WorkflowFileV1["nodes"][number];
2378
+
2379
+ const next: WorkflowFileV1 = { ...wf, nodes: [...wf.nodes, nextNode] };
2380
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2381
+ setWorkflowSelectedNodeId(id);
2382
+ setWorkflowNewNodeId("");
2383
+ setWorkflowNewNodeName("");
2384
+ }}
2385
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[11px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
2386
+ >
2387
+ + Add node
2388
+ </button>
2389
+ </div>
2390
+ </div>
2391
+
2392
+ <div className="mt-2 space-y-1">
2393
+ {wf.nodes.map((n) => {
2394
+ const selected = workflowSelectedNodeId === n.id;
2395
+ return (
2396
+ <button
2397
+ key={n.id}
2398
+ type="button"
2399
+ onClick={() => setWorkflowSelectedNodeId(n.id)}
2400
+ className={
2401
+ selected
2402
+ ? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
2403
+ : "w-full rounded-[var(--ck-radius-sm)] px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-secondary)] hover:bg-white/5"
2404
+ }
2405
+ >
2406
+ <span className="font-mono">{n.id}</span>
2407
+ <span className="ml-2 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</span>
2408
+ </button>
2409
+ );
2410
+ })}
2411
+ </div>
2412
+ </div>
2413
+
2414
+ <div className="border-t border-white/10 pt-3">
2415
+ <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Edges</div>
2416
+
2417
+ <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
2418
+ <div className="grid grid-cols-1 gap-2">
2419
+ <label className="block">
2420
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">from</div>
2421
+ <select
2422
+ value={workflowNewEdgeFrom}
2423
+ onChange={(e) => setWorkflowNewEdgeFrom(e.target.value)}
2424
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2425
+ >
2426
+ <option value="">(select)</option>
2427
+ {wf.nodes.map((n) => (
2428
+ <option key={n.id} value={n.id}>
2429
+ {n.id}
2430
+ </option>
2431
+ ))}
2432
+ </select>
2433
+ </label>
2434
+ <label className="block">
2435
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">to</div>
2436
+ <select
2437
+ value={workflowNewEdgeTo}
2438
+ onChange={(e) => setWorkflowNewEdgeTo(e.target.value)}
2439
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2440
+ >
2441
+ <option value="">(select)</option>
2442
+ {wf.nodes.map((n) => (
2443
+ <option key={n.id} value={n.id}>
2444
+ {n.id}
2445
+ </option>
2446
+ ))}
2447
+ </select>
2448
+ </label>
2449
+ <label className="block">
2450
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">label (optional)</div>
2451
+ <input
2452
+ value={workflowNewEdgeLabel}
2453
+ onChange={(e) => setWorkflowNewEdgeLabel(e.target.value)}
2454
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2455
+ placeholder="e.g. approve"
2456
+ />
2457
+ </label>
2458
+ <button
2459
+ type="button"
2460
+ onClick={() => {
2461
+ const from = String(workflowNewEdgeFrom || "").trim();
2462
+ const to = String(workflowNewEdgeTo || "").trim();
2463
+ if (!from || !to) {
2464
+ flashMessage("Edge requires from + to", "error");
2465
+ return;
2466
+ }
2467
+ if (from === to) {
2468
+ flashMessage("Edge from/to must be different", "error");
2469
+ return;
2470
+ }
2471
+ const id = `e${Date.now()}`;
2472
+ const nextEdge: WorkflowFileV1["edges"][number] = {
2473
+ id,
2474
+ from,
2475
+ to,
2476
+ ...(String(workflowNewEdgeLabel || "").trim() ? { label: String(workflowNewEdgeLabel).trim() } : {}),
2477
+ };
2478
+ const next: WorkflowFileV1 = { ...wf, edges: [...(wf.edges ?? []), nextEdge] };
2479
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2480
+ setWorkflowNewEdgeLabel("");
2481
+ }}
2482
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[11px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
2483
+ >
2484
+ + Add edge
2485
+ </button>
2486
+ </div>
2487
+ </div>
2488
+
2489
+ <div className="mt-2 space-y-2">
2490
+ {(wf.edges ?? []).length ? (
2491
+ (wf.edges ?? []).map((e) => (
2492
+ <div key={e.id} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
2493
+ <div className="flex items-center justify-between gap-2">
2494
+ <div className="text-[11px] text-[color:var(--ck-text-secondary)]">
2495
+ <span className="font-mono">{e.from}</span> → <span className="font-mono">{e.to}</span>
2496
+ {e.label ? <span className="ml-2 text-[10px] text-[color:var(--ck-text-tertiary)]">({e.label})</span> : null}
2497
+ </div>
2498
+ <button
2499
+ type="button"
2500
+ onClick={() => {
2501
+ const next: WorkflowFileV1 = { ...wf, edges: (wf.edges ?? []).filter((x) => x.id !== e.id) };
2502
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2503
+ }}
2504
+ className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
2505
+ >
2506
+ Remove
2507
+ </button>
2508
+ </div>
2509
+ </div>
2510
+ ))
2511
+ ) : (
2512
+ <div className="text-xs text-[color:var(--ck-text-secondary)]">No edges yet.</div>
2513
+ )}
2514
+ </div>
2515
+ </div>
2516
+
2517
+ <div className="border-t border-white/10 pt-3">
2518
+ <div className="flex items-center justify-between gap-2">
2519
+ <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Node inspector</div>
2520
+ {workflowSelectedNodeId ? (
2521
+ <button
2522
+ type="button"
2523
+ onClick={() => {
2524
+ const nodeId = workflowSelectedNodeId;
2525
+ const nextNodes = wf.nodes.filter((n) => n.id !== nodeId);
2526
+ const nextEdges = (wf.edges ?? []).filter((e) => e.from !== nodeId && e.to !== nodeId);
2527
+ const next: WorkflowFileV1 = { ...wf, nodes: nextNodes, edges: nextEdges };
2528
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2529
+ setWorkflowSelectedNodeId("");
2530
+ }}
2531
+ className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-red-100 hover:bg-white/10"
2532
+ >
2533
+ Delete node
2534
+ </button>
2535
+ ) : null}
2536
+ </div>
2537
+
2538
+ {workflowSelectedNodeId ? (
2539
+ (() => {
2540
+ const node = wf.nodes.find((n) => n.id === workflowSelectedNodeId);
2541
+ if (!node) return <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">No node selected.</div>;
2542
+
2543
+ return (
2544
+ <div className="mt-3 space-y-3">
2545
+ <div>
2546
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
2547
+ <div className="mt-1 rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]">
2548
+ {node.id}
2549
+ </div>
2550
+ </div>
2551
+
2552
+ <label className="block">
2553
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
2554
+ <input
2555
+ value={String(node.name ?? "")}
2556
+ onChange={(e) => {
2557
+ const nextName = e.target.value;
2558
+ const next: WorkflowFileV1 = {
2559
+ ...wf,
2560
+ nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, name: nextName } : n)),
2561
+ };
2562
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2563
+ }}
2564
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2565
+ placeholder="Optional"
2566
+ />
2567
+ </label>
2568
+
2569
+ <label className="block">
2570
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
2571
+ <select
2572
+ value={node.type}
2573
+ onChange={(e) => {
2574
+ const nextType = e.target.value as WorkflowFileV1["nodes"][number]["type"];
2575
+ const next: WorkflowFileV1 = {
2576
+ ...wf,
2577
+ nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, type: nextType } : n)),
2578
+ };
2579
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2580
+ }}
2581
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2582
+ >
2583
+ <option value="start">start</option>
2584
+ <option value="end">end</option>
2585
+ <option value="llm">llm</option>
2586
+ <option value="tool">tool</option>
2587
+ <option value="condition">condition</option>
2588
+ <option value="delay">delay</option>
2589
+ <option value="human_approval">human_approval</option>
2590
+ </select>
2591
+ </label>
2592
+
2593
+ <div className="grid grid-cols-2 gap-2">
2594
+ <label className="block">
2595
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">x</div>
2596
+ <input
2597
+ type="number"
2598
+ value={typeof node.x === "number" ? node.x : 0}
2599
+ onChange={(e) => {
2600
+ const nextX = Number(e.target.value);
2601
+ const next: WorkflowFileV1 = {
2602
+ ...wf,
2603
+ nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, x: nextX } : n)),
2604
+ };
2605
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2606
+ }}
2607
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2608
+ />
2609
+ </label>
2610
+ <label className="block">
2611
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">y</div>
2612
+ <input
2613
+ type="number"
2614
+ value={typeof node.y === "number" ? node.y : 0}
2615
+ onChange={(e) => {
2616
+ const nextY = Number(e.target.value);
2617
+ const next: WorkflowFileV1 = {
2618
+ ...wf,
2619
+ nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, y: nextY } : n)),
2620
+ };
2621
+ setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
2622
+ }}
2623
+ className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
2624
+ />
2625
+ </label>
2626
+ </div>
2627
+
2628
+ <div>
2629
+ <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">config</div>
2630
+ <pre className="mt-1 max-h-[200px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
2631
+ {JSON.stringify(node.config ?? {}, null, 2)}
2632
+ </pre>
2633
+ </div>
2634
+ </div>
2635
+ );
2636
+ })()
2637
+ ) : (
2638
+ <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">Select a node.</div>
2639
+ )}
2640
+ </div>
2641
+ </div>
2642
+ );
2643
+ })()
2644
+ ) : (
2645
+ <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">Load or create a workflow to edit triggers.</div>
2646
+ )}
2647
+ </div>
2648
+ </div>
2649
+ ) : (
2650
+ <textarea
2651
+ value={workflowJsonText}
2652
+ onChange={(e) => setWorkflowJsonText(e.target.value)}
2653
+ className="mt-3 h-[55vh] w-full resize-none rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-3 font-mono text-xs text-[color:var(--ck-text-primary)]"
2654
+ spellCheck={false}
2655
+ placeholder="Select a workflow from the left (or create the template)."
2656
+ />
2657
+ )}
2658
+ </div>
2659
+ </div>
2660
+ </div>
2661
+ </div>
2662
+ ) : null}
2663
+
2664
+ </div>
2665
+ ) : null}
2666
+
2667
+ {activeTab === "files" ? (
1022
2668
  <div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
1023
2669
  <div className="ck-glass-strong p-4">
1024
2670
  <div className="flex items-center justify-between gap-3">
@@ -1092,6 +2738,8 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
1092
2738
  </div>
1093
2739
  ) : null}
1094
2740
 
2741
+ {activeTab === "orchestrator" ? <OrchestratorPanel teamId={teamId} /> : null}
2742
+
1095
2743
  {/* markdown editor lives below for convenience */}
1096
2744
  {activeTab === "recipe" ? (
1097
2745
  <div className="mt-6 ck-glass-strong p-4">