makersclaw 0.8.0 → 0.8.2

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 (401) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/r/activity-log.json +52 -0
  4. package/r/agent-changes.json +51 -0
  5. package/r/agent-chat.json +53 -0
  6. package/r/agent-column.json +85 -0
  7. package/r/agent-dock.json +57 -0
  8. package/r/agent-kit.json +49 -0
  9. package/r/agent-marks.json +49 -0
  10. package/r/agent-pane.json +93 -0
  11. package/r/ai-action.json +56 -0
  12. package/r/annotate.json +3 -3
  13. package/r/approval-request.json +2 -2
  14. package/r/approval.json +54 -0
  15. package/r/ask-bar.json +50 -0
  16. package/r/audio-player.json +2 -2
  17. package/r/auth-form.json +2 -2
  18. package/r/automation-editor.json +66 -0
  19. package/r/automation-list.json +57 -0
  20. package/r/automation-model.json +118 -0
  21. package/r/automations-screen.json +71 -0
  22. package/r/board.json +2 -2
  23. package/r/calendar-view.json +3 -3
  24. package/r/cart.json +2 -2
  25. package/r/chart.json +2 -2
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +2 -2
  28. package/r/code-editor.json +2 -2
  29. package/r/comment-box.json +3 -3
  30. package/r/connect-card.json +51 -0
  31. package/r/contact-section.json +2 -2
  32. package/r/core.json +13 -7
  33. package/r/cost.json +51 -0
  34. package/r/csv-import.json +3 -3
  35. package/r/data-picker.json +60 -0
  36. package/r/data-table.json +2 -2
  37. package/r/diagram.json +2 -2
  38. package/r/doc-editor.json +2 -2
  39. package/r/doc-page.json +3 -3
  40. package/r/email-render.json +2 -2
  41. package/r/faq.json +2 -2
  42. package/r/file-upload.json +2 -2
  43. package/r/first-run.json +63 -0
  44. package/r/fix-note.json +57 -0
  45. package/r/flow-canvas.json +2 -2
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +2 -2
  48. package/r/gantt.json +3 -3
  49. package/r/job-panel.json +2 -2
  50. package/r/json-editor.json +3 -3
  51. package/r/library-shelf.json +56 -0
  52. package/r/map.json +2 -2
  53. package/r/margin-notes.json +67 -0
  54. package/r/menubar.json +2 -2
  55. package/r/mind-map.json +3 -3
  56. package/r/navigation-menu.json +2 -2
  57. package/r/needs-you.json +55 -0
  58. package/r/order-detail.json +2 -2
  59. package/r/org-chart.json +3 -3
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +2 -2
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +2 -2
  65. package/r/product-detail.json +2 -2
  66. package/r/product-grid.json +3 -3
  67. package/r/record-form.json +2 -2
  68. package/r/record-grid.json +52 -0
  69. package/r/record-notes.json +4 -4
  70. package/r/registry.json +4086 -1512
  71. package/r/run-history.json +52 -0
  72. package/r/run-view.json +80 -0
  73. package/r/schedule-card.json +54 -0
  74. package/r/schedule-engine.json +79 -0
  75. package/r/schedule-gantt.json +83 -0
  76. package/r/schedule-ruler.json +64 -0
  77. package/r/search-dialog.json +2 -2
  78. package/r/settings-list.json +2 -2
  79. package/r/setup-checklist.json +2 -2
  80. package/r/share-dialog.json +2 -2
  81. package/r/sidebar.json +2 -2
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +2 -2
  84. package/r/source-receipt.json +52 -0
  85. package/r/spreadsheet.json +3 -3
  86. package/r/stat-cards.json +3 -3
  87. package/r/step-picker.json +53 -0
  88. package/r/step-settings.json +72 -0
  89. package/r/suggestion.json +58 -0
  90. package/r/sum-bar.json +64 -0
  91. package/r/task-list.json +3 -3
  92. package/r/task-model.json +2 -2
  93. package/r/testimonials.json +2 -2
  94. package/r/tier-map.json +67 -0
  95. package/r/time-grid.json +73 -0
  96. package/r/time-strip.json +64 -0
  97. package/r/top-bar.json +66 -0
  98. package/r/transaction-list.json +2 -2
  99. package/r/trigger-picker.json +61 -0
  100. package/r/whiteboard.json +3 -3
  101. package/r/workflow-view.json +55 -0
  102. package/registry/blocks/activity-log/README.md +112 -0
  103. package/registry/blocks/activity-log/activity-log.jsx +351 -0
  104. package/registry/blocks/activity-log/block.json +33 -0
  105. package/registry/blocks/activity-log/demo.jsx +48 -0
  106. package/registry/blocks/activity-log/index.js +4 -0
  107. package/registry/blocks/agent-changes/README.md +94 -0
  108. package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
  109. package/registry/blocks/agent-changes/block.json +28 -0
  110. package/registry/blocks/agent-changes/demo.jsx +39 -0
  111. package/registry/blocks/agent-changes/index.js +3 -0
  112. package/registry/blocks/agent-chat/README.md +145 -0
  113. package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
  114. package/registry/blocks/agent-chat/block.json +30 -0
  115. package/registry/blocks/agent-chat/demo.jsx +165 -0
  116. package/registry/blocks/agent-chat/index.js +3 -0
  117. package/registry/blocks/agent-column/README.md +197 -0
  118. package/registry/blocks/agent-column/agent-column.jsx +492 -0
  119. package/registry/blocks/agent-column/block.json +67 -0
  120. package/registry/blocks/agent-column/column-picker.jsx +163 -0
  121. package/registry/blocks/agent-column/demo.jsx +50 -0
  122. package/registry/blocks/agent-column/index.js +7 -0
  123. package/registry/blocks/agent-column/model.js +164 -0
  124. package/registry/blocks/agent-column/receipt.jsx +483 -0
  125. package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
  126. package/registry/blocks/agent-dock/README.md +210 -0
  127. package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
  128. package/registry/blocks/agent-dock/block.json +47 -0
  129. package/registry/blocks/agent-dock/demo.jsx +153 -0
  130. package/registry/blocks/agent-dock/dock-model.js +118 -0
  131. package/registry/blocks/agent-dock/index.js +5 -0
  132. package/registry/blocks/agent-kit/README.md +73 -0
  133. package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
  134. package/registry/blocks/agent-kit/block.json +10 -0
  135. package/registry/blocks/agent-kit/demo.jsx +35 -0
  136. package/registry/blocks/agent-kit/index.js +3 -0
  137. package/registry/blocks/agent-marks/README.md +80 -0
  138. package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
  139. package/registry/blocks/agent-marks/block.json +27 -0
  140. package/registry/blocks/agent-marks/demo.jsx +49 -0
  141. package/registry/blocks/agent-marks/index.js +3 -0
  142. package/registry/blocks/agent-pane/README.md +234 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +246 -0
  144. package/registry/blocks/agent-pane/block.json +41 -0
  145. package/registry/blocks/agent-pane/composer.jsx +6 -0
  146. package/registry/blocks/agent-pane/demo.jsx +233 -0
  147. package/registry/blocks/agent-pane/dock-context.js +10 -0
  148. package/registry/blocks/agent-pane/index.js +4 -0
  149. package/registry/blocks/agent-pane/prose.jsx +84 -0
  150. package/registry/blocks/agent-pane/questions.jsx +134 -0
  151. package/registry/blocks/agent-pane/transcript.jsx +268 -0
  152. package/registry/blocks/agent-pane/work.jsx +368 -0
  153. package/registry/blocks/ai-action/README.md +94 -0
  154. package/registry/blocks/ai-action/ai-action.jsx +188 -0
  155. package/registry/blocks/ai-action/block.json +34 -0
  156. package/registry/blocks/ai-action/demo.jsx +93 -0
  157. package/registry/blocks/ai-action/index.js +3 -0
  158. package/registry/blocks/approval/README.md +145 -0
  159. package/registry/blocks/approval/approval.jsx +163 -0
  160. package/registry/blocks/approval/block.json +31 -0
  161. package/registry/blocks/approval/demo.jsx +121 -0
  162. package/registry/blocks/approval/index.js +3 -0
  163. package/registry/blocks/ask-bar/README.md +105 -0
  164. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  165. package/registry/blocks/ask-bar/block.json +26 -0
  166. package/registry/blocks/ask-bar/demo.jsx +78 -0
  167. package/registry/blocks/ask-bar/index.js +3 -0
  168. package/registry/blocks/automation-editor/README.md +291 -0
  169. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  170. package/registry/blocks/automation-editor/block.json +39 -0
  171. package/registry/blocks/automation-editor/demo.jsx +284 -0
  172. package/registry/blocks/automation-editor/index.js +4 -0
  173. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  174. package/registry/blocks/automation-list/README.md +129 -0
  175. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  176. package/registry/blocks/automation-list/block.json +35 -0
  177. package/registry/blocks/automation-list/demo.jsx +67 -0
  178. package/registry/blocks/automation-list/index.js +3 -0
  179. package/registry/blocks/automation-model/README.md +109 -0
  180. package/registry/blocks/automation-model/block.json +135 -0
  181. package/registry/blocks/automation-model/catalog.js +256 -0
  182. package/registry/blocks/automation-model/check.js +161 -0
  183. package/registry/blocks/automation-model/demo.jsx +42 -0
  184. package/registry/blocks/automation-model/edit.js +262 -0
  185. package/registry/blocks/automation-model/fixes.js +287 -0
  186. package/registry/blocks/automation-model/glyph.jsx +13 -0
  187. package/registry/blocks/automation-model/graph.js +410 -0
  188. package/registry/blocks/automation-model/index.js +33 -0
  189. package/registry/blocks/automation-model/overlay.js +175 -0
  190. package/registry/blocks/automation-model/parts.jsx +94 -0
  191. package/registry/blocks/automation-model/schedule.js +53 -0
  192. package/registry/blocks/automation-model/templates.js +128 -0
  193. package/registry/blocks/automation-model/values.js +29 -0
  194. package/registry/blocks/automation-model/words.js +439 -0
  195. package/registry/blocks/automations-screen/README.md +207 -0
  196. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  197. package/registry/blocks/automations-screen/block.json +47 -0
  198. package/registry/blocks/automations-screen/demo.jsx +364 -0
  199. package/registry/blocks/automations-screen/index.js +4 -0
  200. package/registry/blocks/automations-screen/screen-model.js +129 -0
  201. package/registry/blocks/chat-pane/README.md +143 -0
  202. package/registry/blocks/chat-pane/block.json +48 -0
  203. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  204. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  205. package/registry/blocks/chat-pane/chat-context.js +17 -0
  206. package/registry/blocks/chat-pane/chat-model.js +43 -0
  207. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  208. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  209. package/registry/blocks/chat-pane/demo.jsx +103 -0
  210. package/registry/blocks/chat-pane/index.js +9 -0
  211. package/registry/blocks/connect-card/README.md +99 -0
  212. package/registry/blocks/connect-card/block.json +10 -0
  213. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  214. package/registry/blocks/connect-card/demo.jsx +49 -0
  215. package/registry/blocks/connect-card/index.js +3 -0
  216. package/registry/blocks/cost/README.md +115 -0
  217. package/registry/blocks/cost/block.json +10 -0
  218. package/registry/blocks/cost/cost.jsx +176 -0
  219. package/registry/blocks/cost/demo.jsx +66 -0
  220. package/registry/blocks/cost/index.js +3 -0
  221. package/registry/blocks/data-picker/README.md +81 -0
  222. package/registry/blocks/data-picker/block.json +38 -0
  223. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  224. package/registry/blocks/data-picker/demo.jsx +34 -0
  225. package/registry/blocks/data-picker/fields.js +89 -0
  226. package/registry/blocks/data-picker/index.js +4 -0
  227. package/registry/blocks/data-table/demo.jsx +1 -1
  228. package/registry/blocks/first-run/README.md +118 -0
  229. package/registry/blocks/first-run/block.json +11 -0
  230. package/registry/blocks/first-run/demo.jsx +88 -0
  231. package/registry/blocks/first-run/first-run-model.js +51 -0
  232. package/registry/blocks/first-run/first-run.jsx +156 -0
  233. package/registry/blocks/first-run/index.js +5 -0
  234. package/registry/blocks/first-run/probe-rules.js +26 -0
  235. package/registry/blocks/fix-note/README.md +79 -0
  236. package/registry/blocks/fix-note/block.json +35 -0
  237. package/registry/blocks/fix-note/demo.jsx +30 -0
  238. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  239. package/registry/blocks/fix-note/index.js +3 -0
  240. package/registry/blocks/flow-editor/README.md +108 -0
  241. package/registry/blocks/flow-editor/block.json +44 -0
  242. package/registry/blocks/flow-editor/demo.jsx +115 -0
  243. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  244. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  245. package/registry/blocks/flow-editor/index.js +6 -0
  246. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  247. package/registry/blocks/flow-editor/outline.jsx +134 -0
  248. package/registry/blocks/library-shelf/README.md +117 -0
  249. package/registry/blocks/library-shelf/block.json +28 -0
  250. package/registry/blocks/library-shelf/demo.jsx +68 -0
  251. package/registry/blocks/library-shelf/index.js +3 -0
  252. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  253. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  254. package/registry/blocks/margin-notes/README.md +222 -0
  255. package/registry/blocks/margin-notes/block.json +10 -0
  256. package/registry/blocks/margin-notes/demo.jsx +144 -0
  257. package/registry/blocks/margin-notes/index.js +20 -0
  258. package/registry/blocks/margin-notes/margin-model.js +224 -0
  259. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  260. package/registry/blocks/needs-you/README.md +177 -0
  261. package/registry/blocks/needs-you/block.json +34 -0
  262. package/registry/blocks/needs-you/demo.jsx +143 -0
  263. package/registry/blocks/needs-you/index.js +3 -0
  264. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  265. package/registry/blocks/pace-bar/README.md +230 -0
  266. package/registry/blocks/pace-bar/block.json +10 -0
  267. package/registry/blocks/pace-bar/demo.jsx +255 -0
  268. package/registry/blocks/pace-bar/index.js +22 -0
  269. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  270. package/registry/blocks/pace-bar/pace-model.js +288 -0
  271. package/registry/blocks/record-grid/README.md +100 -0
  272. package/registry/blocks/record-grid/block.json +28 -0
  273. package/registry/blocks/record-grid/demo.jsx +43 -0
  274. package/registry/blocks/record-grid/index.js +3 -0
  275. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  276. package/registry/blocks/run-history/README.md +138 -0
  277. package/registry/blocks/run-history/block.json +10 -0
  278. package/registry/blocks/run-history/demo.jsx +74 -0
  279. package/registry/blocks/run-history/index.js +3 -0
  280. package/registry/blocks/run-history/run-history.jsx +218 -0
  281. package/registry/blocks/run-view/README.md +174 -0
  282. package/registry/blocks/run-view/block.json +50 -0
  283. package/registry/blocks/run-view/demo.jsx +286 -0
  284. package/registry/blocks/run-view/index.js +5 -0
  285. package/registry/blocks/run-view/run-controls.jsx +266 -0
  286. package/registry/blocks/run-view/run-view.jsx +236 -0
  287. package/registry/blocks/run-view/runs-list.jsx +99 -0
  288. package/registry/blocks/schedule-card/README.md +121 -0
  289. package/registry/blocks/schedule-card/block.json +10 -0
  290. package/registry/blocks/schedule-card/demo.jsx +82 -0
  291. package/registry/blocks/schedule-card/index.js +3 -0
  292. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  293. package/registry/blocks/schedule-engine/README.md +137 -0
  294. package/registry/blocks/schedule-engine/block.json +10 -0
  295. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  296. package/registry/blocks/schedule-engine/engine.js +769 -0
  297. package/registry/blocks/schedule-engine/index.js +9 -0
  298. package/registry/blocks/schedule-engine/ops.js +71 -0
  299. package/registry/blocks/schedule-engine/place.js +294 -0
  300. package/registry/blocks/schedule-engine/tree.js +79 -0
  301. package/registry/blocks/schedule-engine/waits.js +80 -0
  302. package/registry/blocks/schedule-gantt/README.md +159 -0
  303. package/registry/blocks/schedule-gantt/block.json +10 -0
  304. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  305. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  306. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  307. package/registry/blocks/schedule-gantt/index.js +26 -0
  308. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  309. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  310. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  311. package/registry/blocks/schedule-ruler/README.md +148 -0
  312. package/registry/blocks/schedule-ruler/block.json +10 -0
  313. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  314. package/registry/blocks/schedule-ruler/index.js +21 -0
  315. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  316. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  317. package/registry/blocks/source-receipt/README.md +97 -0
  318. package/registry/blocks/source-receipt/block.json +30 -0
  319. package/registry/blocks/source-receipt/demo.jsx +71 -0
  320. package/registry/blocks/source-receipt/index.js +3 -0
  321. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  322. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  323. package/registry/blocks/step-picker/README.md +78 -0
  324. package/registry/blocks/step-picker/block.json +31 -0
  325. package/registry/blocks/step-picker/demo.jsx +23 -0
  326. package/registry/blocks/step-picker/index.js +3 -0
  327. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  328. package/registry/blocks/step-settings/README.md +100 -0
  329. package/registry/blocks/step-settings/block.json +44 -0
  330. package/registry/blocks/step-settings/demo.jsx +93 -0
  331. package/registry/blocks/step-settings/fields.jsx +58 -0
  332. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  333. package/registry/blocks/step-settings/index.js +5 -0
  334. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  335. package/registry/blocks/suggestion/README.md +171 -0
  336. package/registry/blocks/suggestion/block.json +40 -0
  337. package/registry/blocks/suggestion/demo.jsx +91 -0
  338. package/registry/blocks/suggestion/index.js +3 -0
  339. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  340. package/registry/blocks/sum-bar/README.md +122 -0
  341. package/registry/blocks/sum-bar/block.json +10 -0
  342. package/registry/blocks/sum-bar/demo.jsx +110 -0
  343. package/registry/blocks/sum-bar/index.js +5 -0
  344. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  345. package/registry/blocks/sum-bar/sum-model.js +126 -0
  346. package/registry/blocks/tier-map/README.md +217 -0
  347. package/registry/blocks/tier-map/block.json +10 -0
  348. package/registry/blocks/tier-map/demo.jsx +222 -0
  349. package/registry/blocks/tier-map/index.js +24 -0
  350. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  351. package/registry/blocks/tier-map/tier-model.js +318 -0
  352. package/registry/blocks/time-grid/README.md +305 -0
  353. package/registry/blocks/time-grid/block.json +10 -0
  354. package/registry/blocks/time-grid/demo.jsx +279 -0
  355. package/registry/blocks/time-grid/grid-model.js +322 -0
  356. package/registry/blocks/time-grid/index.js +25 -0
  357. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  358. package/registry/blocks/time-strip/README.md +141 -0
  359. package/registry/blocks/time-strip/block.json +10 -0
  360. package/registry/blocks/time-strip/demo.jsx +129 -0
  361. package/registry/blocks/time-strip/index.js +19 -0
  362. package/registry/blocks/time-strip/strip-model.js +178 -0
  363. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  364. package/registry/blocks/top-bar/README.md +149 -0
  365. package/registry/blocks/top-bar/block.json +35 -0
  366. package/registry/blocks/top-bar/demo.jsx +78 -0
  367. package/registry/blocks/top-bar/index.js +5 -0
  368. package/registry/blocks/top-bar/probe-rules.js +74 -0
  369. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  370. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  371. package/registry/blocks/trigger-picker/README.md +98 -0
  372. package/registry/blocks/trigger-picker/block.json +38 -0
  373. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  374. package/registry/blocks/trigger-picker/index.js +3 -0
  375. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  376. package/registry/blocks/workflow-view/README.md +123 -0
  377. package/registry/blocks/workflow-view/block.json +10 -0
  378. package/registry/blocks/workflow-view/demo.jsx +90 -0
  379. package/registry/blocks/workflow-view/index.js +3 -0
  380. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  381. package/src/install.mjs +77 -16
  382. package/src/registry.mjs +3 -0
  383. package/templates/next/browser/lib/data.js +6 -1
  384. package/templates/next/files/README.md +627 -24
  385. package/templates/next/files/check/automation-format.ts +2103 -0
  386. package/templates/next/files/check/automation-package.ts +142 -0
  387. package/templates/next/files/check/run.mjs +968 -9
  388. package/templates/next/files/components/ui/field.jsx +17 -4
  389. package/templates/next/files/components/ui/icon.jsx +13 -0
  390. package/templates/next/files/components/ui/index.js +2 -1
  391. package/templates/next/files/components/ui/layout.jsx +77 -2
  392. package/templates/next/files/components/ui/notice.jsx +70 -0
  393. package/templates/next/files/components/ui/page.jsx +124 -0
  394. package/templates/next/files/components/ui/stat.jsx +22 -7
  395. package/templates/next/files/components/ui/toast.jsx +6 -3
  396. package/templates/next/files/lib/automations.js +660 -0
  397. package/templates/next/files/lib/claw.js +241 -5
  398. package/templates/next/files/lib/door.js +92 -0
  399. package/templates/next/files/lib/hooks.js +1677 -8
  400. package/templates/next/files/lib/platform.js +529 -21
  401. package/templates/next/template.json +13 -3
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "automation-editor",
4
+ "type": "registry:block",
5
+ "title": "AutomationEditor",
6
+ "description": "The whole editor for one automation: a header with its name, a status (New, Draft, Draft ahead, Live, Off, Unsaved changes), a test chip (Not tested, Tested with the steps that would run, Last test failed), the run picker, the Live switch, Test run (a dry run: it reads real data and changes nothing) with its price on it, Keep this version and Save; a strip that says what is wrong with it, in words and by step; the node canvas (flow-editor) with an undo history; and the panel for whatever is pressed (the trigger, a step, or the automation as a whole), over the canvas on a narrow width and beside it on a wide one. It holds the working copy of the definition and knows nothing of the network: Save, Keep, Live and Test are callbacks that resolve the platform's answer. Keep and Live need a passed test; when the platform refuses for want of one, the editor offers a test or, after one more question, a deliberate skip. A draft shared with an agent is never buried: the editor remembers the version its copy began on, saves against it, and when a newer version arrives while there are edits not saved it says so (Look at it, Keep mine, or save on top of it on purpose); a line under the header says when the agent drew the draft. Delete automation is in a menu at the end of the header and asks first.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/automation-model.json",
10
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/fix-note.json",
11
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/flow-editor.json",
12
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/step-settings.json",
13
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/trigger-picker.json"
14
+ ],
15
+ "dependencies": [],
16
+ "files": [
17
+ {
18
+ "path": "registry/blocks/automation-editor/index.js",
19
+ "type": "registry:component",
20
+ "target": "@components/makersclaw/blocks/automation-editor/index.js",
21
+ "content": "// The AutomationEditor block — `makersclaw add automation-editor`.\n// Fixed: compose it through its props (README.md beside this file).\nexport { AutomationEditor } from \"./automation-editor\";\nexport { RunInputs } from \"./run-inputs\";\n"
22
+ },
23
+ {
24
+ "path": "registry/blocks/automation-editor/automation-editor.jsx",
25
+ "type": "registry:component",
26
+ "target": "@components/makersclaw/blocks/automation-editor/automation-editor.jsx",
27
+ "content": "\"use client\";\n\n// The builder: one automation on the canvas, with its header, its side\n// panel and its outline view. Everything an app's Automations page needs\n// to let a member open, edit, test and switch on an automation.\n//\n// It holds the working copy of the definition (an undo stack of pure\n// edits from automation-model) and says what is wrong with it (the quick\n// check); it knows nothing of the network. The screen around it saves,\n// keeps, switches live and runs, and hands the answers back as callbacks\n// that resolve the platform's answer: `{ ok: true }` or `{ ok: false,\n// error, problems? }` (what useAutomation's save, keep, setLive and test\n// resolve), or throw an Error with a message.\n//\n// The model of saving (\"done is evidence\"):\n// Save a draft version; the live one keeps running as it was\n// Test a DRY run of the draft (or live) version: it reads real data and\n// changes nothing (nothing is sent, nothing is written)\n// Keep the draft becomes the version that runs\n// Live the switch: it fires on its trigger, or it does not\n// A draft never goes live by itself. Keep and Live need a passed test of that\n// version (the platform refuses with `needs_test` otherwise), unless the\n// member deliberately skips it: the second, inline-confirmed press calls\n// onKeep({ skipTest: true }) / onLive(on, { skipTest: true }). Never silently,\n// and never by an agent.\n//\n// The draft is shared with the agent: it may save a newer version while a\n// member has edits that are not saved. So the editor remembers the version\n// its working copy began on (`baseVersion`) and sends THAT with every save,\n// never \"the newest I can see\": the platform answers `changed` when the draft\n// moved, and the editor says so in a strip (Look at the new one, or Keep\n// mine and save on top of it on purpose). A save never buries a newer\n// version by accident.\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Confirm, Menu, Switch, cx } from \"../../ui\";\nimport {\n Chip,\n RunPicker,\n addStep,\n agentDraftOf,\n availableBefore,\n canonicalJson,\n changeLinesBetween,\n checkAutomation,\n creditsWords,\n deleteWords,\n duplicateStep,\n findStep,\n flaggedSteps,\n issuesFor,\n latestVersionOf,\n newerVersionWords,\n priceMap,\n removeStep,\n slotAfter,\n slotAtEnd,\n stepTitle,\n updateStep,\n} from \"../automation-model\";\nimport { FixProposal } from \"../fix-note\";\nimport { FlowEditor } from \"../flow-editor\";\nimport { FlowSettings, StepSettings } from \"../step-settings\";\nimport { TriggerPicker } from \"../trigger-picker\";\nimport { RunInputs } from \"./run-inputs\";\n\nconst MAX_UNDO = 60;\n\n/** The version a successful save made: `{ ok, version }`, or the hook's `{ ok, data: { version } }`. */\nfunction versionOf(r) {\n const n = r?.version ?? r?.data?.version;\n return Number.isInteger(n) ? n : null;\n}\n\n/** Words for a refusal the platform sent, or a thrown error. `latest` is the\n * newest version the platform has, when it said (a `changed` refusal does). */\nfunction said(r, fallback) {\n if (r && r.ok === false) return { text: r.error || fallback, lines: Array.isArray(r.problems) ? r.problems : [], code: typeof r.code === \"string\" ? r.code : null, latest: Number.isInteger(r.latest) ? r.latest : null };\n return null;\n}\n\n/** The test chip for the version being looked at: the draft if there is one,\n * else the live one. Null when the app gave none, or the version is the app's\n * own shipped one (it needs no test). */\nfunction testChip(tests) {\n if (!tests) return null;\n const v = tests.draft ?? tests.live ?? null;\n if (!v || v.exempt) return null;\n if (v.passed) {\n const n = Number(v.last?.would ?? 0);\n return { tone: \"green\", text: n > 0 ? `Tested · ${n} ${n === 1 ? \"step\" : \"steps\"} would run` : \"Tested\" };\n }\n const last = v.last?.status;\n if (last && last !== \"done\") return { tone: \"red\", text: \"Last test failed\" };\n return { tone: \"amber\", text: \"Not tested\" };\n}\n\n/**\n * automationId the saved automation's id, or null for one not saved yet\n * status draft | on | off | needs_owner | \"new\"\n * liveVersion, draftVersion the numbers, or null\n * baseline the definition as last saved (or the starting point)\n * versions [{ version, kind, note, created_at }]\n * runs [{ id, status, created_at, is_test, credits }], newest first\n * apps [{ id, name }], for an action in a team automation\n * actions the app's declared actions, [{ name, description? }]\n * ownedByApp the automation belongs to an app\n * estimate what a run is expected to cost (GET automation's estimate)\n * samples { [stepId]: a real output }, for the data picker\n * runnable a Set of the kinds the platform runs\n * backLabel, onBack\n * onSave(def, { baseVersion }), onKeep(opts?), onLive(on, opts?), onTest(inputs), onOpenRun(runId)\n * `baseVersion` is the version the working copy began on: send it to the platform as\n * `expect_version`, never the newest one you can see. A save the platform refuses with\n * the code \"changed\" (and `latest`, if it says) draws the conflict strip. A save that\n * works may resolve `{ ok: true, version }` (or the hook's `{ ok: true, data: { version } }`): the editor then begins from that version.\n * opts is `{ skipTest: true }` ONLY after the member pressed \"Go live without a\n * test\" and then confirmed \"Skip the test\" in the note. Resolve like the others;\n * a refusal keeps its `code` (\"needs_test\" shows the note with its two buttons).\n * tests { live: TestView|null, draft: TestView|null } (useAutomation().tests), TestView =\n * { version, passed, exempt, last: { run_id, status, ended_at, credits, would } | null };\n * draws the test chip (Not tested · Tested · 3 steps would run · Last test failed).\n * Without it, or for the app's own shipped version, no chip.\n * onReset \"Reset to the app's version\", when it was edited from a shipped one\n * tabs the Flow / Runs tabs, drawn in the header\n * suggestions the changes the agent drafted from failed runs, waiting for a person\n * [{ id, why, stale, patch }] (useAutomation().suggestions)\n * onKeepSuggestion(id), onDropSuggestion(id) keep (a new version that runs) or drop one;\n * resolve the platform's answer. Without them the strip is not drawn.\n * triggers what can start it (useAutomations().triggers, the list door's\n * `triggers`): the picker offers events, an app's actions, records,\n * a web address and another automation. Without it: by hand and a schedule.\n * selfKey this automation's key (it cannot follow itself)\n * hook a webhook trigger's address once saved (useAutomation().hook)\n * onRotateHook makes (or replaces) the webhook's secret; resolves { ok, hook: { secret } }\n * compareTo { version, definition }: what \"What changed\" compares the agent's draft with\n * (the version the member kept, usually the live one). Without it, no What changed.\n * onDelete(automation) async: \"Delete automation\", in a menu at the end of the header (never beside Save),\n * after a question that says what it does: Deletes \"<name>\". Its runs stay in history.\n * (and, for a live one, that it is switched off). `automation` is `{ id, name, status }`.\n * Resolves `{ ok: true }` (the screen leaves the editor and says what followed) or\n * `{ ok: false, error, code? }`: the words are shown as they are. Only for a saved\n * automation the member may edit; without it, no menu\n * canDelete offer it at all (default true; hide it where the member may not)\n * source the automation's source (member · agent · package · legacy). An app's own job and what an\n * app shipped are not deleted from here: the item is disabled and points at the app's settings\n * onFocus `{ selected, unsaved, baseVersion }` whenever any of the three changes: the step\n * pressed (a step id, \"trigger\", \"flow\" or null), whether the working copy has edits\n * that are not saved, and the version it began on. For a pane beside the editor\n * (an agent) that needs to know what the member is looking at; it never re-renders it.\n * canEdit false: read-only (default true)\n */\nexport function AutomationEditor({\n automationId = null,\n status = \"new\",\n liveVersion = null,\n draftVersion = null,\n baseline,\n versions = [],\n runs = [],\n apps = [],\n actions,\n ownedByApp = false,\n estimate = null,\n samples = null,\n runnable,\n backLabel = \"Automations\",\n onBack,\n onSave,\n onKeep,\n onLive,\n onTest,\n tests = null,\n onOpenRun,\n onReset,\n tabs,\n suggestions = [],\n onKeepSuggestion,\n onDropSuggestion,\n triggers = null,\n selfKey = null,\n hook = null,\n onRotateHook,\n compareTo = null,\n onFocus,\n onDelete,\n canDelete = true,\n source = null,\n canEdit = true,\n className,\n}) {\n const [hist, setHist] = useState({ past: [], present: baseline, future: [] });\n const def = hist.present;\n // The version this was opened on moved (a suggestion was kept, another tab\n // saved): if nothing here was changed since, open the new one.\n const [seen, setSeen] = useState(baseline);\n if (canonicalJson(seen) !== canonicalJson(baseline)) {\n setSeen(baseline);\n if (canonicalJson(hist.present) === canonicalJson(seen)) setHist({ past: [], present: baseline, future: [] });\n }\n // The version the working copy began on (`baseVersion`). It moves to the\n // newest only while the copy is in step with the saved one: a copy with\n // edits keeps the version it was made from, so a newer version (the agent's)\n // is seen as newer, and a save names the base, not the newest.\n const latest = useMemo(() => latestVersionOf({ versions, draftVersion, liveVersion }), [versions, draftVersion, liveVersion]);\n const [begun, setBegun] = useState(latest);\n // What the member decided about a newer version: a conflict the platform\n // reported ({ latest }), the newest version they chose to keep theirs over,\n // and what \"Look at\" replaced (so Undo can bring the edits, and their base, back).\n const [conflict, setConflict] = useState(null);\n const [keptOver, setKeptOver] = useState(null);\n const looked = useRef(null);\n const [showChanges, setShowChanges] = useState(false);\n if (canonicalJson(def) === canonicalJson(baseline)) {\n if (begun !== latest) setBegun(latest);\n if (conflict) setConflict(null);\n }\n const [selected, setSelected] = useState(null);\n const [reveal, setReveal] = useState(null);\n const [busy, setBusy] = useState(null);\n const [note, setNote] = useState(null);\n const [asking, setAsking] = useState(false);\n const [skipAsk, setSkipAsk] = useState(false);\n const [deleting, setDeleting] = useState(false);\n\n const commit = useCallback((next, keepSelection = true) => {\n setHist((h) => (canonicalJson(h.present) === canonicalJson(next) ? h : { past: [...h.past, h.present].slice(-MAX_UNDO), present: next, future: [] }));\n if (!keepSelection) setSelected(null);\n setNote(null);\n }, []);\n const undo = () => {\n if (hist.past.length === 0) return;\n const back = hist.past[hist.past.length - 1];\n setHist((h) => (h.past.length ? { past: h.past.slice(0, -1), present: h.past[h.past.length - 1], future: [h.present, ...h.future] } : h));\n // Undoing \"Look at\" gives back the member's edits and the version they were made on.\n const was = looked.current;\n if (was && canonicalJson(back) === was.def) {\n looked.current = null;\n setBegun(was.begun);\n setKeptOver(null);\n }\n };\n const redo = () => setHist((h) => (h.future.length ? { past: [...h.past, h.present], present: h.future[0], future: h.future.slice(1) } : h));\n\n const isNew = automationId === null;\n // Something not saved yet is always a change to save.\n const edited = useMemo(() => canonicalJson(def) !== canonicalJson(baseline), [def, baseline]);\n const dirty = isNew || edited;\n const issues = useMemo(() => checkAutomation(def, runnable ? { runnable } : undefined), [def, runnable]);\n const flagged = useMemo(() => flaggedSteps(issues), [issues]);\n const prices = useMemo(() => priceMap(estimate), [estimate]);\n const hasLive = liveVersion !== null && liveVersion !== undefined;\n const canKeep = !isNew && !dirty && draftVersion !== null && draftVersion !== undefined && draftVersion !== liveVersion;\n const live = status === \"on\";\n\n // Tell a pane beside the editor what the member is on: never re-renders the editor.\n const focusRef = useRef(onFocus);\n useEffect(() => {\n focusRef.current = onFocus;\n });\n useEffect(() => {\n focusRef.current?.({ selected, unsaved: edited, baseVersion: begun });\n }, [selected, edited, begun]);\n\n // A newer version arrived while the working copy has edits made on an older one.\n const newerThanMine = !isNew && edited && begun !== null && latest !== null && latest > begun;\n const sawNewer = conflict ? Math.max(latest ?? 0, conflict.latest ?? 0) : latest;\n const strip = !isNew && edited && (conflict ? \"conflict\" : newerThanMine && keptOver !== latest ? \"behind\" : null);\n const agentDraft = useMemo(() => (isNew ? null : agentDraftOf({ versions, draftVersion, liveVersion })), [isNew, versions, draftVersion, liveVersion]);\n const changeLines = useMemo(() => (showChanges && compareTo?.definition ? changeLinesBetween(compareTo.definition, baseline) : []), [showChanges, compareTo, baseline]);\n\n const inputsAsked = useMemo(() => Object.entries(def.inputs ?? {}).map(([name, spec]) => ({ name, label: spec.label })), [def.inputs]);\n\n /* ---- editing ------------------------------------------------------- */\n\n const insert = (slot, kind) => {\n const r = addStep(def, slot, kind);\n commit(r.def);\n setSelected(r.id);\n setReveal(r.id);\n };\n const addAtSelection = (kind) => {\n const slot = (selected && selected !== \"trigger\" && selected !== \"flow\" ? slotAfter(def, selected) : null) ?? slotAtEnd(def);\n insert(slot, kind);\n };\n const selectedStep = selected && selected !== \"trigger\" && selected !== \"flow\" ? findStep(def.steps, selected) : null;\n\n /* ---- the server ---------------------------------------------------- */\n\n // `again` says how to ask a second time, deliberately without the test, when\n // the platform answers needs_test: { kind: \"keep\" } or { kind: \"live\", on }.\n const run = async (kind, work, ok, again = null) => {\n setBusy(kind);\n setNote(null);\n setSkipAsk(false);\n try {\n const r = await work();\n const bad = said(r, \"That did not work.\");\n if (bad && bad.code === \"changed\" && (kind === \"save\" || kind === \"test\")) {\n // Someone (the agent, another tab) saved a newer version: the strip says so.\n setConflict({ latest: bad.latest });\n setKeptOver(null);\n } else if (bad) setNote({ tone: \"bad\", text: bad.text, lines: bad.lines, code: bad.code, again: bad.code === \"needs_test\" ? again : null });\n else {\n if (kind === \"save\") {\n setConflict(null);\n if (versionOf(r) !== null) setBegun(versionOf(r));\n }\n if (ok) setNote({ tone: \"ok\", text: ok });\n }\n } catch (e) {\n if (e?.code === \"changed\" && (kind === \"save\" || kind === \"test\")) {\n setConflict({ latest: Number.isInteger(e.latest) ? e.latest : null });\n setKeptOver(null);\n } else setNote({ tone: \"bad\", text: e?.message || \"That did not work.\", lines: Array.isArray(e?.problems) ? e.problems : [] });\n } finally {\n setBusy(null);\n }\n };\n\n const save = () => run(\"save\", () => onSave?.(def, { baseVersion: begun }), \"Saved as a draft.\");\n // \"Save on top of vN\": the member's deliberate press, naming the newest version as the base.\n const saveOnTop = () => run(\"save\", () => onSave?.(def, { baseVersion: sawNewer ?? begun }), \"Saved as a draft.\");\n // Replace the working copy with the saved one, as one step Undo can take back.\n const lookAtNewer = () => {\n looked.current = { def: canonicalJson(def), begun };\n commit(baseline, false);\n setBegun(latest);\n setConflict(null);\n setKeptOver(null);\n setShowChanges(false);\n };\n const test = async (values) => {\n setAsking(false);\n await run(\"test\", async () => {\n if (dirty) {\n const r = await onSave?.(def, { baseVersion: begun });\n if (r && r.ok === false) return r;\n if (versionOf(r) !== null) setBegun(versionOf(r));\n }\n return onTest?.(values);\n });\n };\n const goLive = (on, opts) => run(\"live\", () => (opts ? onLive?.(on, opts) : onLive?.(on)), undefined, { kind: \"live\", on });\n const keep = (opts) => run(\"keep\", () => (opts ? onKeep?.(opts) : onKeep?.()), \"Kept. This version runs now.\", { kind: \"keep\" });\n // The member's deliberate second press: the same callback again, with the skip.\n const skipTheTest = () => {\n const again = note?.again;\n if (!again) return;\n if (again.kind === \"keep\") void keep({ skipTest: true });\n else void goLive(again.on, { skipTest: true });\n };\n // The member said yes to the question: the platform answers, and a refusal is shown as it is.\n const confirmDelete = () => {\n setDeleting(false);\n return run(\"delete\", () => onDelete?.({ id: automationId, name: baseline.name, status }));\n };\n const pressTest = () => {\n if (Object.keys(def.inputs ?? {}).length > 0) setAsking(true);\n else void test({});\n };\n\n const onKeyDown = (e) => {\n if (e.target.closest?.(\"input, textarea, select, [contenteditable]\")) return;\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"z\") {\n e.preventDefault();\n if (e.shiftKey) redo();\n else undo();\n } else if ((e.key === \"Delete\" || e.key === \"Backspace\") && selectedStep && canEdit) {\n e.preventDefault();\n commit(removeStep(def, selectedStep.id), false);\n }\n };\n\n /* ---- the panel ----------------------------------------------------- */\n\n let panel = null;\n if (selected === \"trigger\") {\n panel = (\n <TriggerPicker\n trigger={def.trigger}\n readOnly={!canEdit}\n catalog={triggers}\n ownedByApp={ownedByApp}\n selfKey={selfKey ?? def.key ?? null}\n hook={hook}\n onRotateHook={onRotateHook}\n onChange={(trigger) => commit({ ...def, trigger })}\n onClose={() => setSelected(null)}\n />\n );\n } else if (selected === \"flow\") {\n panel = <FlowSettings def={def} readOnly={!canEdit} versions={versions} liveVersion={liveVersion} onChange={commit} onClose={() => setSelected(null)} onReset={onReset} />;\n } else if (selectedStep) {\n panel = (\n <StepSettings\n key={selectedStep.id}\n step={selectedStep}\n available={{ ...availableBefore(def, selectedStep.id, stepTitle), inputs: inputsAsked }}\n samples={samples}\n apps={apps}\n actions={actions}\n ownedByApp={ownedByApp}\n problems={issuesFor(issues, selectedStep.id)}\n readOnly={!canEdit}\n onChange={(patch) => commit(updateStep(def, selectedStep.id, patch))}\n onRemove={() => commit(removeStep(def, selectedStep.id), false)}\n onDuplicate={() => {\n const d = duplicateStep(def, selectedStep.id);\n if (d) {\n commit(d.def);\n setSelected(d.id);\n setReveal(d.id);\n }\n }}\n onClose={() => setSelected(null)}\n />\n );\n }\n\n const chip = isNew\n ? { tone: \"grey\", text: \"New\" }\n : !hasLive\n ? { tone: \"amber\", text: \"Draft\" }\n : canKeep\n ? { tone: \"amber\", text: \"Draft ahead\" }\n : live\n ? { tone: \"green\", text: \"Live\" }\n : { tone: \"grey\", text: \"Off\" };\n\n const cost = estimate && !dirty ? creditsWords(estimate.credits) : null;\n const testTitle = isNew\n ? \"Create it first\"\n : issues.length\n ? \"Fix what is marked first\"\n : estimate\n ? `Reads your real data; nothing is sent or changed. About ${creditsWords(estimate.credits)}, at most ${creditsWords(estimate.credits_max)}.`\n : \"Reads your real data; nothing is sent or changed\";\n const tested = testChip(tests);\n\n return (\n <div onKeyDown={onKeyDown} className={cx(\"@container flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground\", className)}>\n <header className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1.5 border-b border-border px-3 py-2\">\n {onBack ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"arrow-left\" onClick={onBack}>\n {backLabel}\n </Button>\n ) : null}\n <label className=\"min-w-[10rem] max-w-md flex-1\">\n <span className=\"sr-only\">Name</span>\n <input\n value={def.name}\n maxLength={120}\n placeholder=\"Name this automation\"\n readOnly={!canEdit}\n onChange={(e) => commit({ ...def, name: e.target.value })}\n className=\"min-h-9 w-full rounded-md border border-transparent bg-transparent px-2 text-sm font-semibold outline-none hover:border-border focus-visible:border-input focus-visible:ring-2 focus-visible:ring-ring\"\n />\n </label>\n <Chip tone={edited && !isNew ? \"amber\" : chip.tone}>{edited && !isNew ? \"Unsaved changes\" : chip.text}</Chip>\n {tested ? <Chip tone={tested.tone}>{tested.text}</Chip> : null}\n {tabs}\n <span className=\"ml-auto\" />\n {!isNew && runs.length > 0 ? <RunPicker runs={runs} onPick={onOpenRun} /> : null}\n {canEdit ? (\n <>\n <span title={hasLive ? undefined : \"Keep a version first\"}>\n <Switch checked={live} disabled={!hasLive || busy !== null || dirty} label=\"Live\" onChange={(e) => void goLive(e.target.checked)} />\n </span>\n <Button size=\"sm\" variant=\"secondary\" icon=\"play\" disabled={isNew || busy !== null || issues.length > 0 || def.steps.length === 0} onClick={pressTest} title={testTitle}>\n Test run\n {cost ? <span className=\"font-normal text-muted-foreground\">about {cost}</span> : null}\n </Button>\n {canKeep ? (\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => void keep()}>\n Keep this version\n </Button>\n ) : null}\n <Button size=\"sm\" variant=\"primary\" loading={busy === \"save\"} disabled={busy !== null || !dirty || issues.length > 0 || !def.name.trim()} onClick={() => void save()} title={issues.length ? \"Fix what is marked first\" : undefined}>\n {isNew ? \"Create\" : \"Save\"}\n </Button>\n {!isNew && onDelete && canDelete ? (\n <Menu\n label=\"More\"\n items={\n source === \"legacy\" || source === \"package\"\n ? [{ label: \"Delete automation\", icon: \"trash\", disabled: true, description: \"Comes with the app. Switch it off, or remove it in the app's settings.\" }]\n : [{ label: \"Delete automation\", icon: \"trash\", tone: \"danger\", onSelect: () => setDeleting(true) }]\n }\n />\n ) : null}\n </>\n ) : null}\n </header>\n\n {agentDraft ? (\n <div role=\"status\" className=\"border-b border-border bg-muted px-3 py-1.5 text-[13px] text-muted-foreground\">\n <span>\n Drawn by the agent{agentDraft.note ? <> · from: {agentDraft.note}</> : null}\n </span>\n {compareTo?.definition ? (\n <>\n {\" · \"}\n <button type=\"button\" aria-expanded={showChanges} onClick={() => setShowChanges((v) => !v)} className=\"text-foreground underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n What changed\n </button>\n </>\n ) : null}\n {showChanges && compareTo?.definition ? (\n <div className=\"mt-1\">\n <p className=\"m-0 text-xs\">Compared with {compareTo.version ? `v${compareTo.version}` : \"the version you kept\"}:</p>\n {changeLines.length ? (\n <ul className=\"m-0 mt-0.5 list-disc pl-5\">\n {changeLines.map((l) => (\n <li key={l}>{l}</li>\n ))}\n </ul>\n ) : (\n <p className=\"m-0 mt-0.5\">Nothing in the steps differs.</p>\n )}\n </div>\n ) : null}\n </div>\n ) : null}\n\n {strip && canEdit ? (\n <div role=\"status\" className=\"flex flex-wrap items-center gap-2 border-b border-warning/50 bg-warning/10 px-3 py-2 text-sm\">\n <span className=\"min-w-0 flex-1\">{newerVersionWords(sawNewer, versions)}</span>\n {strip === \"conflict\" ? (\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => void saveOnTop()}>\n Save on top of v{sawNewer}\n </Button>\n ) : null}\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={lookAtNewer}>\n Look at v{sawNewer}\n </Button>\n {strip === \"behind\" ? (\n <Button size=\"sm\" variant=\"quiet\" onClick={() => setKeptOver(latest)}>\n Keep mine\n </Button>\n ) : null}\n </div>\n ) : null}\n\n {note || (issues.length > 0 && canEdit) ? (\n <div\n role={note?.tone === \"bad\" ? \"alert\" : \"status\"}\n className={cx(\"border-b px-3 py-2 text-sm\", note?.tone === \"bad\" ? \"border-destructive/50 bg-destructive/10\" : note?.tone === \"ok\" ? \"border-success/40 bg-success/10\" : \"border-warning/50 bg-warning/10\")}\n >\n {note ? (\n <>\n <span>{note.text}</span>\n {note.code === \"needs_test\" && note.again && canEdit ? (\n skipAsk ? (\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <span>Skip the test? It will run for real the first time it starts.</span>\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={skipTheTest}>\n Skip the test\n </Button>\n <Button size=\"sm\" variant=\"quiet\" onClick={() => setSkipAsk(false)}>\n Cancel\n </Button>\n </div>\n ) : (\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" variant=\"secondary\" icon=\"play\" disabled={busy !== null || isNew || issues.length > 0 || def.steps.length === 0} onClick={pressTest}>\n Run a test\n </Button>\n <Button size=\"sm\" variant=\"quiet\" disabled={busy !== null} onClick={() => setSkipAsk(true)}>\n {note.again.kind === \"keep\" ? \"Keep without a test\" : \"Go live without a test\"}\n </Button>\n </div>\n )\n ) : null}\n {note.lines?.length ? (\n <ul className=\"m-0 mt-1 list-disc pl-5 text-[13px]\">\n {note.lines.slice(0, 3).map((l) => (\n <li key={l}>{l}</li>\n ))}\n </ul>\n ) : null}\n </>\n ) : (\n <>\n <span>{issues.length === 1 ? \"One thing to fix before it can be saved.\" : `${issues.length} things to fix before it can be saved.`}</span>\n <ul className=\"m-0 mt-1 list-disc pl-5 text-[13px]\">\n {issues.slice(0, 3).map((i) => (\n <li key={i.path + i.message}>\n {i.stepId ? (\n <button type=\"button\" onClick={() => setSelected(i.stepId)} className=\"text-left underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n {i.message}\n </button>\n ) : (\n i.message\n )}\n </li>\n ))}\n </ul>\n </>\n )}\n </div>\n ) : null}\n\n {canEdit && suggestions.length > 0 && (onKeepSuggestion || onDropSuggestion) ? (\n <div className=\"grid max-h-[40%] gap-2 overflow-auto border-b border-border px-3 py-2\">\n {suggestions.slice(0, 2).map((s) => (\n <FixProposal\n key={s.id}\n proposal={s}\n def={def}\n blocked={dirty && !isNew ? \"Save or undo your changes first, so what is kept is what you see.\" : null}\n onKeep={onKeepSuggestion ? () => onKeepSuggestion(s.id) : undefined}\n onDrop={onDropSuggestion ? () => onDropSuggestion(s.id) : undefined}\n />\n ))}\n {suggestions.length > 2 ? <p className=\"m-0 text-xs text-muted-foreground\">{suggestions.length - 2} more suggestions after these.</p> : null}\n </div>\n ) : null}\n\n <div className=\"relative flex min-h-0 flex-1\">\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n <FlowEditor\n def={def}\n selectedId={selected}\n onSelect={setSelected}\n editable={canEdit}\n onInsert={insert}\n onAddAtSelection={addAtSelection}\n flagged={flagged}\n prices={prices}\n fitKey={automationId ?? \"new\"}\n revealId={reveal}\n runnable={runnable}\n undo={canEdit ? { canUndo: hist.past.length > 0, canRedo: hist.future.length > 0, onUndo: undo, onRedo: redo } : null}\n onOpenFlow={() => setSelected(\"flow\")}\n />\n </div>\n {panel ? (\n <div className=\"absolute inset-x-0 bottom-0 z-10 flex max-h-[75%] flex-col overflow-hidden rounded-t-xl border-t border-border shadow-lg [&>aside]:min-h-0 [&>aside]:flex-1 @3xl:relative @3xl:inset-auto @3xl:max-h-none @3xl:w-[380px] @3xl:shrink-0 @3xl:rounded-none @3xl:border-l @3xl:border-t-0 @3xl:shadow-none\">{panel}</div>\n ) : null}\n </div>\n\n <Confirm\n open={deleting}\n onClose={() => setDeleting(false)}\n onConfirm={() => void confirmDelete()}\n title=\"Delete this automation?\"\n body={deleteWords(baseline.name, status)}\n confirmLabel=\"Delete\"\n tone=\"danger\"\n />\n\n {asking ? <RunInputs open inputs={def.inputs} onCancel={() => setAsking(false)} onRun={(v) => void test(v)} /> : null}\n </div>\n );\n}\n"
28
+ },
29
+ {
30
+ "path": "registry/blocks/automation-editor/run-inputs.jsx",
31
+ "type": "registry:component",
32
+ "target": "@components/makersclaw/blocks/automation-editor/run-inputs.jsx",
33
+ "content": "\"use client\";\n\n// The small question a manual run asks first: one field for each input the\n// automation declares, with its default. The values are typed to the\n// input's kind before they go (the platform checks them again).\n\nimport { useId, useState } from \"react\";\nimport { Button, Checkbox, Field, Input, Select, Sheet } from \"../../ui\";\n\nfunction initial(spec) {\n if (spec.type === \"boolean\") return spec.default === true;\n return spec.default === undefined ? \"\" : String(spec.default);\n}\n\n/**\n * open, inputs { name: { type: string|number|boolean|date, label, default, required, options } }\n * onRun(values), onCancel()\n */\nexport function RunInputs({ open, inputs, onRun, onCancel }) {\n const names = Object.keys(inputs ?? {});\n const [values, setValues] = useState(() => Object.fromEntries(names.map((n) => [n, initial(inputs[n])])));\n const id = useId();\n const missing = names.filter((n) => inputs[n].required && inputs[n].type !== \"boolean\" && String(values[n] ?? \"\").trim() === \"\");\n\n const submit = () => {\n const out = {};\n for (const n of names) {\n const spec = inputs[n];\n const v = values[n];\n if (spec.type === \"boolean\") out[n] = v === true;\n else if (String(v ?? \"\").trim() === \"\") continue;\n else out[n] = spec.type === \"number\" ? Number(v) : String(v);\n }\n onRun(out);\n };\n\n return (\n <Sheet\n open={open}\n onClose={onCancel}\n side=\"center\"\n title=\"Test run\"\n footer={\n <>\n <Button variant=\"quiet\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" disabled={missing.length > 0} onClick={submit}>\n Run it\n </Button>\n </>\n }\n >\n <form\n className=\"grid gap-4\"\n onSubmit={(e) => {\n e.preventDefault();\n if (!missing.length) submit();\n }}\n >\n <p className=\"m-0 text-sm text-muted-foreground\">It runs once with real data. Nothing is sent or changed by a step marked as a pencil draft.</p>\n {names.map((n, i) => {\n const spec = inputs[n];\n const fid = `${id}-${i}`;\n const label = `${spec.label || n}${spec.required ? \" *\" : \"\"}`;\n if (spec.type === \"boolean\") {\n return <Checkbox key={n} id={fid} label={label} checked={values[n] === true} onChange={(e) => setValues({ ...values, [n]: e.target.checked })} />;\n }\n return (\n <Field key={n} id={fid} label={label}>\n {spec.options?.length ? (\n <Select id={fid} value={String(values[n] ?? \"\")} options={[{ value: \"\", label: \"\" }, ...spec.options.map((o) => ({ value: o, label: o }))]} onChange={(e) => setValues({ ...values, [n]: e.target.value })} />\n ) : (\n <Input id={fid} type={spec.type === \"number\" ? \"number\" : spec.type === \"date\" ? \"date\" : \"text\"} value={String(values[n] ?? \"\")} onChange={(e) => setValues({ ...values, [n]: e.target.value })} />\n )}\n </Field>\n );\n })}\n </form>\n </Sheet>\n );\n}\n"
34
+ },
35
+ {
36
+ "path": "registry/blocks/automation-editor/README.md",
37
+ "type": "registry:file",
38
+ "target": "@components/makersclaw/blocks/automation-editor/README.md",
39
+ "content": "# AutomationEditor\n\nThe whole editor for one automation, for an app's Automations page. It\nanswers \"how do I open, edit, test and switch on my app's automations?\"\ninside the app, in the app's theme.\n\n- **The header** — Back, the name (editable), a status (New · Draft · Draft\n ahead · Live · Off · Unsaved changes), the Flow / Runs tabs you hand it,\n a **test chip** (see below), the run picker, the **Live** switch, **Test\n run** with its price on it (\"about 3 cr\"), **Keep this version** and\n **Save**.\n- **A strip** that says what is wrong with it, in words and by step, and\n what the platform refused (with its lines).\n- **The canvas** (`flow-editor`) with undo and redo (Ctrl or ⌘ + Z), Delete\n on the selected step, and a **list view** of the same flow.\n- **The panel** for whatever is pressed: the trigger (`trigger-picker`), a\n step (`step-settings`) or the automation as a whole (`FlowSettings`).\n Beside the canvas on a wide width; over it, from the bottom, on a narrow\n one.\n- **Test run** asks the questions a manual run needs first (`RunInputs`).\n\nHow saving works (nothing here is hidden from a member):\n\n1. **Save** makes a draft version; the live one keeps running as it was.\n2. **Test run** is a dry run of the draft (or the live one): it reads your\n real data and changes nothing (nothing is sent, nothing is written). Its\n run opens as a test, with what each step would have done.\n3. **Keep this version** makes the draft the one that runs.\n4. **Live** is the switch: it fires on its trigger, or it does not.\n\nKeep and Live need a passed test of that version. Without one the platform\nrefuses (`needs_test`) and the editor shows its words with two buttons:\n**Run a test** (the same as Test run) and **Go live without a test** (**Keep\nwithout a test** for Keep). The second asks once more, inline: \"Skip the\ntest? It will run for real the first time it starts.\" with **Skip the test**\nand **Cancel**; only then does it call `onKeep({ skipTest: true })` or\n`onLive(on, { skipTest: true })` again. The skip is the member's own\ndeliberate second press (some automations cannot be tested meaningfully);\nan agent never has it, and it is never silent.\n\nThe **test chip** sits beside the status for the version being looked at\n(the draft if there is one, else the live one): \"Not tested\" (amber),\n\"Tested · 3 steps would run\" (green), \"Last test failed\" (red). Nothing\nwithout `tests`, or for the app's own shipped version (`exempt`).\n\nA draft never goes live by itself. An automation shipped with the app is\nedited the same way: a member's edit is a new version, the shipped one\nstays on file, and **Reset to the app's version** is one press (`onReset`).\n\n```jsx\nimport { AutomationEditor } from \"../components/blocks/automation-editor\"; // a Makersclaw app\nimport { AutomationEditor } from \"@/components/makersclaw/blocks/automation-editor\"; // a shadcn project\n```\n\n`makersclaw add automation-editor` lays `flow-editor`, `step-settings`,\n`trigger-picker`, `fix-note` and `automation-model` with it. A client component. It\nfills its parent's height (give it a parent with one) and its own width\ndecides its layout: the panel sits beside the canvas from about 768 px.\n\n## Props\n\n- `automationId` — the saved id, or `null` for one not saved yet (the\n button says Create).\n- `status` — `draft` · `on` · `off` · `needs_owner` · `\"new\"`.\n `liveVersion`, `draftVersion` — the numbers, or null.\n- `baseline` — the definition as last saved (or the starting point). Key the\n builder by id, never by version: when the saved version moves (a kept\n suggestion, an agent's draft, another tab) an editor with nothing edited\n opens the new one by itself, and one with edits keeps them and says so\n (the strip below). Keying by version would throw the edits away.\n- `versions`, `runs` (for the picker), `apps`, `actions` (the app's declared\n actions, for the action step), `ownedByApp`.\n- `estimate` — what a run is expected to cost (`useAutomation().estimate`):\n paid steps wear a price, Test run says what it will spend.\n- `samples` — `{ [stepId]: a real output }` for the data picker.\n- `runnable` — a `Set` of the kinds the platform runs.\n- `backLabel`, `onBack`, `tabs`, `canEdit` (false: read-only).\n- `onSave(def, { baseVersion })`, `onKeep(opts?)`, `onLive(on, opts?)`, `onTest(inputs)` — each\n resolves the platform's answer (`{ ok: true }` or `{ ok: false, error,\n code?, problems }`, which is what `useAutomation`'s `save`, `keep`,\n `setLive` and `test` resolve) or throws an Error. The builder shows the\n words and the lines, and keeps the `code`. `opts` is `{ skipTest: true }`\n only after the member's confirmed skip (above); otherwise they are called\n as before, with none. Pass `keep` and `setLive` straight through. `onSave`'s\n second argument is `{ baseVersion }`: the version the working copy began\n on. **Send it to the platform as `expect_version`, never the newest\n version you can see** (`useAutomation().save(def, { baseVersion })` does).\n A refusal with the code `changed` (and `latest`, when it says) draws the\n conflict strip (next section). A save that works may resolve `{ ok: true,\n version }` (or the hook's `{ ok: true, data: { version } }`): the editor\n then begins from that version.\n- `onFocus({ selected, unsaved, baseVersion })` — called whenever any of the\n three changes: the pressed step (a step id, `\"trigger\"`, `\"flow\"` or\n `null`), whether the working copy has edits that are not saved, and the\n version it began on. For a pane beside the editor (the agent's) that\n must know what the member is looking at. It never re-renders the editor:\n keep the last value in a ref.\n- `compareTo` — `{ version, definition }`: what \"What changed\" compares an\n agent's draft with (the version the member kept, usually the live one).\n Without it the agent's line has no What changed.\n- `tests` — `{ live, draft }`, each a `TestView` `{ version, passed,\n exempt, last: { run_id, status, ended_at, credits, would } | null }`\n (`useAutomation().tests`): draws the test chip. Optional.\n- `suggestions` — the changes the agent drafted from failed runs, waiting\n for a person: `[{ id, why, stale, patch }]` (`useAutomation().suggestions`).\n `onKeepSuggestion(id)` keeps one (a new version that runs; refused with\n `stale` when the automation moved on) and `onDropSuggestion(id)` forgets\n it. They sit in a strip above the canvas; Keep waits while the working\n copy is unsaved. Without the callbacks, or when `canEdit` is false, the\n strip is not drawn. When the saved version moves under an editor with no\n edits of its own, it opens the new one.\n- `triggers` — what can start an automation here (`useAutomations().triggers`):\n the trigger panel offers events, an app's actions, records, a web address\n and another automation, as the platform runs them. Without it: by hand and\n a schedule. `selfKey` is this automation's key (it cannot follow itself,\n defaults to the definition's), `hook` its webhook address once it is saved\n (`useAutomation().hook`) and `onRotateHook` makes or replaces the webhook's\n secret (`useAutomation().rotateHook`; the secret is shown once).\n- `onDelete(automation)` — async: **Delete automation**, in a menu at the end\n of the header (never a button beside Save), for a saved automation the\n member may edit. It asks first, in words that say what it does: `Deletes\n \"<name>\". Its runs stay in history.` (and, for one that is on, that\n deleting also switches it off). `automation` is `{ id, name, status }`.\n Resolves `{ ok: true }` (the screen leaves the editor and says what\n followed: an Undo, the automations that followed this one) or `{ ok:\n false, error, code? }`, whose words are shown as they are (it is running,\n only the owner or an admin may delete it…). `canDelete` hides the item\n (default true). `source` is the automation's source: an app's own job\n (`legacy`) and what an app shipped (`package`) get the item disabled, with\n a line pointing at the app's settings.\n- `onOpenRun(runId)`, `onReset()`.\n\n## Shared with the agent\n\nThe draft is shared: an agent (or another tab) may save a newer version while\na member has edits that are not saved. The editor never lets a save bury it.\n\n- **The base.** The editor remembers the version its working copy began on\n and sends that with every save. Nothing edited: it follows the saved\n version, so a draft the agent saves simply appears on the canvas.\n- **Edits not saved, a newer version arrives:** a strip says \"The agent drew\n v5 from your last save. You have changes that are not saved.\" (or \"Version 5\n was saved after you opened this…\" when a person saved it). **Look at v5**\n replaces the working copy with it as one undoable step (Undo brings the\n edits and the version they were made on back). **Keep mine** dismisses the\n strip.\n- **A save the platform refuses as `changed`** (after Keep mine, or from\n another tab) draws the same strip with **Save on top of v5**, the member's\n deliberate press that names v5 as the base, and Look at v5.\n- **While the draft is the agent's** (its version is `kind: \"agent\"` and not\n yet kept), a line under the header says \"Drawn by the agent · from: <its\n note> · What changed\". What changed lists, in plain words, the steps added,\n removed and changed against `compareTo` (`changeLinesBetween` in\n automation-model).\n\n## Hooks (lib/hooks.js)\n\n`useAutomation(id)` → `{ automation, live, draft, definition, versions,\nestimate, tests, runs, suggestions, keepSuggestion(id), dropSuggestion(id),\nbusy, save(def), keep({ skipTest }?), setLive(on, { skipTest }?),\ntest({ inputs, sample, dry }?), loading, error, code, unavailable, reload }`.\n`test` is a dry run unless you pass `dry: false`. `definition` is the draft if\nthere is one, else the live one: what the builder opens. A save names the\nversion it was made against, so two people editing at once get \"changed\"\nrather than overwriting each other.\n\n## Who may do what\n\nAny member who opens the app edits and runs its automations (roles come\nlater, decided in one place on the platform). A step's input and output are\nshown only to the automation's owner, the admins and whoever started the\nrun; the others see status and the report. The platform refuses what a\nmember may not do; a hidden button is never the guard.\n\n## Never\n\n- Never a draft that goes live by itself, or a switch that is on without a\n version kept.\n- Never an approval step: an automation has none. When a yes is truly\n needed it goes to the automation's owner, on the desk.\n- Never a step the platform cannot run without saying so: it says \"soon\"\n and the save waits.\n\n## Example\n\nThe whole Automations page of an app: the list, an automation, a run.\n\n```jsx\n\"use client\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { useAutomation, useAutomationRun, useAutomations } from \"../lib/hooks\";\nimport { AutomationList } from \"../components/blocks/automation-list\";\nimport { AutomationEditor } from \"../components/blocks/automation-editor\";\nimport { RunView, RunsList } from \"../components/blocks/run-view\";\nimport { STARTING_POINTS, blankAutomation, keyFromName } from \"../components/blocks/automation-model\";\n\nfunction Tabs({ tab, onTab }) {\n return (\n <div role=\"tablist\" className=\"flex gap-1\">\n {[[\"flow\", \"Flow\"], [\"runs\", \"Runs\"]].map(([id, label]) => (\n <button key={id} type=\"button\" role=\"tab\" aria-selected={tab === id} onClick={() => onTab(id)} className={tab === id ? \"rounded-md bg-accent px-3 py-1 text-sm font-medium\" : \"rounded-md px-3 py-1 text-sm text-muted-foreground\"}>\n {label}\n </button>\n ))}\n </div>\n );\n}\n\nfunction One({ id, runId, tab, go, triggers }) {\n const a = useAutomation(id);\n const run = useAutomationRun(runId);\n if (a.unavailable) return <p className=\"p-4 text-sm\">Automations are not set up here yet.</p>;\n if (!a.automation || !a.definition) return <p className=\"p-4 text-sm text-muted-foreground\">Opening…</p>;\n const tabs = <Tabs tab={tab} onTab={(t) => go({ automation: id, tab: t })} />;\n if (runId && run.detail) {\n return <RunView def={run.definition ?? a.definition} detail={run.detail} runs={a.runs} tz={a.automation.tz} tabs={tabs} name={a.automation.name} onBack={() => go({ automation: id })} onPickRun={(r) => go({ automation: id, run: r })} onRunAgain={async () => { const r = await a.test({ dry: run.run?.dry === true }); if (r.ok) go({ automation: id, run: r.data.run_id }); }} onControl={run.control} controlBusy={run.busy} onKeepFix={run.keepFix} onDropFix={run.dropFix} />;\n }\n if (tab === \"runs\") return <RunsList runs={a.runs} tz={a.automation.tz} onOpen={(r) => go({ automation: id, run: r })} />;\n return (\n <AutomationEditor\n key={id}\n automationId={id}\n status={a.automation.status}\n liveVersion={a.automation.live_version}\n draftVersion={a.automation.draft_version}\n baseline={a.definition}\n versions={a.versions}\n runs={a.runs}\n estimate={a.estimate}\n ownedByApp\n tabs={tabs}\n onBack={() => go({})}\n onSave={a.save}\n onKeep={a.keep}\n onLive={a.setLive}\n tests={a.tests}\n onTest={async (inputs) => { const r = await a.test({ inputs }); if (r.ok) go({ automation: id, run: r.data.run_id }); return r; }}\n onOpenRun={(r) => go({ automation: id, run: r })}\n suggestions={a.suggestions}\n onKeepSuggestion={a.keepSuggestion}\n onDropSuggestion={a.dropSuggestion}\n triggers={triggers}\n selfKey={a.automation.key}\n hook={a.hook}\n onRotateHook={a.rotateHook}\n />\n );\n}\n\n// A new one is not saved until Create: the platform wants a first step.\nfunction NewOne({ start, automations, create, go, triggers }) {\n const taken = new Set(automations.map((a) => a.key));\n const picked = STARTING_POINTS.find((s) => s.id === start);\n return (\n <AutomationEditor\n key=\"new\"\n automationId={null}\n status=\"new\"\n baseline={picked ? picked.build(taken) : blankAutomation(\"\", keyFromName(\"New automation\", taken))}\n ownedByApp\n triggers={triggers}\n onBack={() => go({})}\n onSave={async (def) => {\n const r = await create({ ...def, key: keyFromName(def.name, taken) });\n if (r.ok) go({ automation: r.data.id });\n return r;\n }}\n />\n );\n}\n\nexport default function Automations() {\n const router = useRouter();\n const q = useSearchParams();\n const go = (p) => router.push(`?${new URLSearchParams(p)}`);\n const { automations, triggers, setStatus, create, unavailable } = useAutomations();\n const id = q.get(\"automation\");\n if (id === \"new\") return <div className=\"flex h-[640px] flex-col\"><NewOne start={q.get(\"start\")} automations={automations} create={create} go={go} triggers={triggers} /></div>;\n if (id) return <div className=\"flex h-[640px] flex-col\"><One id={id} runId={q.get(\"run\")} tab={q.get(\"tab\") ?? \"flow\"} go={go} triggers={triggers} /></div>;\n if (unavailable) return <p className=\"p-4 text-sm\">Automations are not set up here yet.</p>;\n return (\n <AutomationList\n rows={automations}\n grouped={false}\n onOpen={(i) => go({ automation: i })}\n onToggle={async (row, on) => { const r = await setStatus(row.id, on); if (!r.ok) throw new Error(r.error); }}\n onNew={(start) => go({ automation: \"new\", ...(start ? { start: start.id } : {}) })}\n />\n );\n}\n```\n"
40
+ }
41
+ ],
42
+ "categories": [
43
+ "automation",
44
+ "workflow",
45
+ "builder",
46
+ "editor",
47
+ "canvas",
48
+ "save",
49
+ "test run",
50
+ "keep",
51
+ "live",
52
+ "draft",
53
+ "undo",
54
+ "n8n",
55
+ "make",
56
+ "zapier",
57
+ "page"
58
+ ],
59
+ "docs": "Installed to components/makersclaw/blocks/automation-editor. Import: import { AutomationEditor, RunInputs } from \"@/components/makersclaw/blocks/automation-editor\". Props and an example: components/makersclaw/blocks/automation-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
+ "meta": {
61
+ "makersclaw": {
62
+ "version": "0.8.2",
63
+ "useWhen": "A member opens, edits, tests and switches on an automation inside an app. Pair it with automation-list (the rows) and run-view (a run), and the useAutomations / useAutomation / useAutomationRun hooks (lib/hooks.js)."
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "automation-list",
4
+ "type": "registry:block",
5
+ "title": "AutomationList",
6
+ "description": "An app's automations as rows, each a WHOLE row you press to open: what it is called, when it runs (in words, in the automation's own time zone), how its last run went as a dot and a word, when the next is due, what the last one cost, and an on/off switch that keeps its own press. A line above counts what is on, what failed and what waits for its owner; a search box; grouping into Team and each app when asked. With none yet it is the first-run screen: a few ready-made starting points. StartingPoints is the picker New automation opens. With onDescribe it opens with \"Describe it\": a sentence in a box that the platform turns into a draft; the platform's own starting points say what they need and cost. Each row has a menu with Delete automation, which asks first, says what it does and leaves the runs in history.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.2/r/automation-model.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/automation-list/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/automation-list/index.js",
17
+ "content": "// The AutomationList block — `makersclaw add automation-list`. Fixed:\n// compose it through its props (README.md beside this file).\nexport { AutomationList, StartingPoints } from \"./automation-list\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/automation-list/automation-list.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/automation-list/automation-list.jsx",
23
+ "content": "\"use client\";\n\n// The list: every automation, each a WHOLE row you press to open. A row\n// says what it does, when it runs, how its last run went, when the next is\n// due (in the automation's own zone) and what the last one cost, and\n// carries its on and off switch. The switch and the row keep their own\n// press: the row is a stretched button under the switch.\n//\n// With none yet, the screen is the starting points: a few ready-made flows\n// and a \"New automation\" button. With `onDescribe` it also opens with\n// \"Describe it\": a sentence in a box, and the platform drafts the automation\n// for the member to read, test and switch on. Props in, callbacks out; it\n// formats nothing it was not given a clock for (`now`), and reads no door.\n\nimport { useMemo, useRef, useState } from \"react\";\nimport { Button, Confirm, Icon, Menu, Sheet, Switch, cx } from \"../../ui\";\nimport { Dot, Glyph, STARTING_POINTS, agoWords, clockWords, creditsWords, deleteWords, healthOf, originWords, startsFromPlatform, summaryOf, triggerWords } from \"../automation-model\";\n\nconst DESCRIBE_PRICE = \"1 call · about 1 cr\";\nconst DESCRIBE_CHIPS = [\"Every Monday, mail me…\", \"When a deal is won, tell…\", \"When a message arrives, sort it…\"];\n\n/** Starting points as the card draws them: the app's own (`build`) as they are,\n * the platform's raw ones (`definition`) turned into the same shape. */\nfunction ready(starts) {\n const out = [];\n for (const s of starts ?? []) {\n if (s && typeof s.build === \"function\") out.push(s);\n else if (s && s.definition) out.push(...startsFromPlatform([s]));\n }\n return out;\n}\n\n/** \"needs payments, a mailbox · a model writes three lines · about 6 cr\" (or \"free\"). */\nfunction startLine(s) {\n const n = Number(s.price?.credits ?? 0);\n const price = n > 0 ? `about ${creditsWords(n)}${s.price?.per === \"each\" ? \" each\" : \"\"}` : \"free\";\n return [s.needs ? `needs ${s.needs}` : null, s.ai || null, price].filter(Boolean).join(\" · \");\n}\n\n/** \"Describe it\": say what it should do in a sentence; the platform drafts it.\n * On success the new automation opens; a question is listed under the box. */\nfunction DescribeIt({ onDescribe, onOpen, price, chips, className }) {\n const [text, setText] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [said, setSaid] = useState(null);\n const box = useRef(null);\n const draft = async () => {\n const ask = text.trim();\n if (!ask || busy) return;\n setBusy(true);\n setSaid(null);\n try {\n const r = await onDescribe(ask);\n if (r && r.ok) {\n // An id opens the new automation; none means the sentence was handed on\n // (to an agent beside the list, say): the box is empty for the next one.\n if (r.id !== undefined && r.id !== null) onOpen?.(r.id);\n else setText(\"\");\n } else if (r && Array.isArray(r.questions) && r.questions.length > 0) {\n setSaid({ questions: r.questions });\n } else {\n setSaid({ error: (r && r.error) || \"That did not work. Try saying it another way.\" });\n }\n } catch (e) {\n setSaid({ error: e instanceof Error && e.message ? e.message : \"That did not work. Try saying it another way.\" });\n } finally {\n setBusy(false);\n }\n };\n return (\n <section aria-label=\"Describe it\" className={cx(\"grid gap-2 rounded-lg border border-border bg-card p-3 text-card-foreground\", className)}>\n <h3 className=\"m-0 flex items-center gap-1.5 text-sm font-semibold\">\n <Icon name=\"wand\" size={15} className=\"text-primary\" /> Describe it\n </h3>\n <p className=\"m-0 text-xs text-muted-foreground\">Say what you want in a sentence. It draws the steps for you to read, test and switch on.</p>\n <textarea\n ref={box}\n rows={2}\n value={text}\n disabled={busy}\n aria-label=\"What should it do?\"\n placeholder=\"Every Monday morning, mail me the deals that have gone quiet\"\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n void draft();\n }\n }}\n className=\"min-h-16 w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60\"\n />\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button variant=\"primary\" icon=\"wand\" aria-busy={busy || undefined} disabled={busy || !text.trim()} onClick={() => void draft()}>\n {busy ? \"Drawing it…\" : \"Draft it\"}\n {!busy && price ? <span className=\"font-normal opacity-80\">{price}</span> : null}\n </Button>\n {chips.map((c) => (\n <button\n key={c}\n type=\"button\"\n disabled={busy}\n onClick={() => {\n setText(c);\n box.current?.focus();\n }}\n className=\"inline-flex min-h-8 items-center rounded-full border border-border px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60\"\n >\n {c}\n </button>\n ))}\n </div>\n {said?.questions ? (\n <div role=\"status\" className=\"grid gap-1 text-[13px]\">\n <b className=\"font-semibold\">Ask me about:</b>\n <ul className=\"m-0 list-disc pl-5\">\n {said.questions.map((q) => (\n <li key={q}>{q}</li>\n ))}\n </ul>\n </div>\n ) : null}\n {said?.error ? (\n <p role=\"alert\" className=\"m-0 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-[13px]\">\n {said.error}\n </p>\n ) : null}\n </section>\n );\n}\n\nfunction groupRows(rows, appName) {\n const team = rows.filter((r) => r.owner_kind === \"workspace\");\n const byApp = new Map();\n for (const r of rows) if (r.owner_kind === \"app\" && r.app_id) byApp.set(r.app_id, [...(byApp.get(r.app_id) ?? []), r]);\n const groups = [];\n if (team.length) groups.push({ key: \"team\", title: \"Team\", rows: team });\n [...byApp.entries()]\n .map(([id, rs]) => ({ key: id, title: `From ${appName?.(id) ?? \"an app\"}`, rows: rs }))\n .sort((a, b) => a.title.localeCompare(b.title))\n .forEach((g) => groups.push(g));\n return groups;\n}\n\n/** The sources the platform never deletes from here: an app's own job and what it shipped. */\nconst KEEPS = new Set([\"legacy\", \"package\"]);\n\n/** The row's menu: Delete automation, or why it cannot be. */\nfunction rowMenu(row, onDelete) {\n if (!onDelete) return null;\n if (KEEPS.has(row.source)) {\n return [{ label: \"Delete automation\", icon: \"trash\", disabled: true, description: \"Comes with the app. Switch it off, or remove it in the app's settings.\" }];\n }\n return [{ label: \"Delete automation\", icon: \"trash\", tone: \"danger\", onSelect: () => onDelete(row) }];\n}\n\nfunction Row({ row, appName, now, busy, canToggle, showOrigin, onOpen, onToggle, onDelete }) {\n const menu = rowMenu(row, onDelete);\n const health = healthOf(row);\n const trig = triggerWords(row.trigger);\n const last = row.last_run;\n const next = row.status === \"on\" ? clockWords(row.next_run_at, row.tz, now) : null;\n const lastWhen = last ? (agoWords(last.started_at ?? row.last_run_at, now) ?? \"\") : \"\";\n return (\n <li className={cx(\"@container relative flex min-w-0 items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-card-foreground hover:bg-accent/30\", row.status === \"off\" && \"opacity-75\")}>\n {canToggle ? (\n <span className=\"relative z-10 shrink-0\">\n <Switch\n checked={row.status === \"on\"}\n disabled={busy || row.live_version === null || row.live_version === undefined}\n label={<span className=\"sr-only\">{`${row.name} is ${row.status === \"on\" ? \"on\" : \"off\"}`}</span>}\n onChange={(e) => onToggle(row, e.target.checked)}\n />\n </span>\n ) : null}\n <button\n type=\"button\"\n data-card-link\n onClick={() => onOpen(row.id)}\n className=\"grid min-w-0 flex-1 items-center gap-x-4 gap-y-1 text-left after:absolute after:inset-0 after:content-[''] focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-ring after:rounded-lg @xl:grid-cols-[minmax(0,2fr)_minmax(0,1.2fr)_minmax(0,0.9fr)_auto]\"\n >\n <span className=\"grid min-w-0 gap-0.5\">\n <b className=\"truncate text-sm font-semibold\">{row.live_name ?? row.name}</b>\n <span className=\"truncate text-xs text-muted-foreground\">\n {showOrigin ? `${originWords(row, appName)} · ` : \"\"}\n {trig}\n {row.edited ? \" · edited\" : \"\"}\n </span>\n </span>\n <span className=\"grid min-w-0 gap-0.5\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5 text-[13px]\">\n <Dot tone={health.tone} />\n <span className=\"truncate\">{health.text}</span>\n </span>\n {lastWhen ? <span className=\"text-xs text-muted-foreground\">{lastWhen}</span> : null}\n </span>\n <span className=\"grid min-w-0 gap-0.5\">\n <span className=\"text-xs text-muted-foreground\">Next</span>\n <b className=\"truncate text-[13px] font-medium\">{next ?? \"—\"}</b>\n </span>\n <span className=\"font-mono text-xs text-muted-foreground @xl:text-right\">{last ? creditsWords(last.credits) : \"\"}</span>\n </button>\n {menu ? (\n <span className=\"relative z-10 shrink-0\">\n <Menu label={`More for ${row.name}`} items={menu} />\n </span>\n ) : null}\n <Icon name=\"chevron-right\" size={16} className=\"shrink-0 text-muted-foreground\" />\n </li>\n );\n}\n\nfunction Start({ s, onPick, autoFocus }) {\n return (\n <button type=\"button\" onClick={() => onPick(s)} autoFocus={autoFocus} className=\"flex min-w-0 items-start gap-3 rounded-lg border border-border bg-card p-3 text-left text-card-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <span className=\"inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground\">{s.glyph ? <Glyph name={s.glyph} size={20} /> : <Icon name=\"plus\" size={20} />}</span>\n <span className=\"grid min-w-0 gap-0.5\">\n <b className=\"text-sm font-semibold\">{s.name}</b>\n <span className=\"text-xs text-muted-foreground\">{s.blurb}</span>\n {s.price ? <span className=\"text-xs text-muted-foreground\">{startLine(s)}</span> : s.needs ? <span className=\"text-xs text-muted-foreground\">You add: {s.needs}</span> : null}\n </span>\n </button>\n );\n}\n\n/**\n * The picker \"New automation\" opens when there are already some.\n * open, onClose\n * starts the starting points (default automation-model's STARTING_POINTS);\n * [{ id, name, blurb, glyph, needs, build(taken) }], or the platform's\n * (startsFromPlatform): plus { ai, price: { credits, per } }, drawn as\n * \"needs payments, a mailbox · a model writes three lines · about 6 cr\"\n * onPick(s), onBlank()\n * onDescribe, onOpen, describePrice, describeChips as AutomationList's: \"Describe it\" at the top\n */\nexport function StartingPoints({ open, onClose, starts = STARTING_POINTS, onPick, onBlank, onDescribe, onOpen, describePrice = DESCRIBE_PRICE, describeChips = DESCRIBE_CHIPS }) {\n const list = useMemo(() => ready(starts), [starts]);\n return (\n <Sheet open={open} onClose={onClose} side=\"center\" title=\"New automation\">\n <div className=\"grid gap-2\">\n {onDescribe ? <DescribeIt onDescribe={onDescribe} onOpen={onOpen} price={describePrice} chips={describeChips} /> : null}\n <Start s={{ name: \"Start empty\", blurb: \"A trigger and a blank canvas.\" }} onPick={() => onBlank?.()} autoFocus={!onDescribe} />\n {onDescribe && list.length > 0 ? <h3 className=\"m-0 mt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground\">Start from a ready one</h3> : null}\n {list.map((s) => (\n <Start key={s.id} s={s} onPick={onPick} />\n ))}\n </div>\n </Sheet>\n );\n}\n\n/**\n * rows the list: useAutomations().automations\n * appName(id) an app's name, or null (a team list names apps)\n * appFilter the app the list is filtered to, if any\n * grouped group by Team and each app (default true); an app's own\n * page passes false\n * starts the first-run starting points (default automation-model's), or the\n * platform's (useAutomations().starts, raw or through startsFromPlatform):\n * each card also says what it needs, what a model does and what it costs\n * onDescribe(ask) async: \"Describe it\". Resolves { ok, id? } (the new automation opens through\n * onOpen(id)), { ok: false, questions: [..] } (listed under the box: \"Ask me\n * about: ...\") or { ok: false, error } (shown). Without it, no Describe it card\n * describePrice what the button says it costs (default \"1 call · about 1 cr\")\n * describeChips sentences that start the box (default three: Monday mail, a won deal, a message)\n * onOpen(id) a whole row opens\n * onNew(start?) New automation, or a starting point from the first-run screen\n * onToggle(row, on) async: the switch; a refusal throws its words\n * onDelete(row) async: \"Delete automation\", in each row's menu, after a question that says what it does\n * (Deletes \"<name>\". Its runs stay in history.; and, for one that is on, that it is\n * switched off). Resolves { ok: true } (the screen says what followed: Undo, the\n * automations that followed it) or { ok: false, error } (shown as it is). An app's own\n * job and what an app shipped (`row.source` legacy, package) get the item disabled,\n * pointing at the app's settings. Without it, no menu\n * canDelete offer Delete automation at all (default true; hide it where the member may not)\n * canToggle draw the switch (default true; hide it where the member may not)\n * canCreate draw New automation (default true)\n * onClearFilter \"Showing one app. Show all\"\n * query the search box's words to start with (default \"\"). A screen that leaves the list\n * (to open an automation) and comes back hands back what the member typed\n * onQueryChange(text) the search box changed: keep it where the screen keeps its place\n * title, intro the head's words\n * now the clock (a test hands one in)\n */\nexport function AutomationList({ rows, appName, appFilter = null, grouped = true, starts = STARTING_POINTS, onDescribe, describePrice = DESCRIBE_PRICE, describeChips = DESCRIBE_CHIPS, onOpen, onNew, onToggle, canToggle = true, canCreate = true, onDelete, canDelete = true, onClearFilter, query = \"\", onQueryChange, title = \"Automations\", intro = \"What runs on its own, for your team and for each app.\", now: nowProp, className }) {\n const [q, setQ] = useState(typeof query === \"string\" ? query : \"\");\n const [pending, setPending] = useState(null);\n const [error, setError] = useState(null);\n const [choosing, setChoosing] = useState(false);\n const [deleting, setDeleting] = useState(null);\n const [deletingBusy, setDeletingBusy] = useState(false);\n const now = useMemo(() => nowProp ?? new Date(), [nowProp]);\n const startList = useMemo(() => ready(starts), [starts]);\n const describeBox = onDescribe && canCreate ? <DescribeIt onDescribe={onDescribe} onOpen={onOpen} price={describePrice} chips={describeChips} /> : null;\n const shown = useMemo(() => {\n const t = q.trim().toLowerCase();\n return t ? rows.filter((r) => `${r.name} ${r.key}`.toLowerCase().includes(t)) : rows;\n }, [rows, q]);\n const groups = useMemo(() => (grouped ? groupRows(shown, appName) : [{ key: \"all\", title: \"\", rows: shown }]), [shown, appName, grouped]);\n const sum = useMemo(() => summaryOf(rows, now), [rows, now]);\n\n const toggle = async (row, on) => {\n setPending(row.id);\n setError(null);\n try {\n await onToggle?.(row, on);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"That did not work.\");\n } finally {\n setPending(null);\n }\n };\n\n const askDelete = onDelete && canDelete ? (row) => setDeleting(row) : null;\n const confirmDelete = async () => {\n const row = deleting;\n if (!row) return;\n setDeletingBusy(true);\n setError(null);\n try {\n const r = await onDelete(row);\n if (r && r.ok === false) setError(r.error || \"That did not work.\");\n } catch (e) {\n setError(e instanceof Error && e.message ? e.message : \"That did not work.\");\n } finally {\n setDeletingBusy(false);\n setDeleting(null);\n }\n };\n\n const newButton = canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" onClick={() => (rows.length ? setChoosing(true) : onNew?.())}>\n New automation\n </Button>\n ) : null;\n\n if (rows.length === 0) {\n return (\n <div className={cx(\"grid min-w-0 content-start gap-4 p-4\", className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"grid gap-0.5\">\n <h2 className=\"m-0 text-lg font-semibold\">{title}</h2>\n <p className=\"m-0 text-sm text-muted-foreground\">{appFilter ? \"This app has none yet.\" : \"Nothing runs on its own yet.\"}</p>\n </div>\n {canCreate ? (\n <Button variant=\"primary\" icon=\"plus\" onClick={() => onNew?.()}>\n New automation\n </Button>\n ) : null}\n </header>\n {canCreate ? (\n <>\n {describeBox}\n <section className=\"grid gap-1 rounded-lg border border-dashed border-border p-4\">\n <h3 className=\"m-0 text-sm font-semibold\">Start from a ready one</h3>\n <p className=\"m-0 text-sm text-muted-foreground\">{onDescribe ? \"Pick one, change what you like, test it with real data, then switch it on.\" : \"Pick one, change what you like, test it with real data, then switch it on. Or ask the agent to draw one from a sentence.\"}</p>\n </section>\n <div className=\"@container\">\n <div className=\"grid gap-2 @xl:grid-cols-2\">\n {startList.map((s) => (\n <Start key={s.id} s={s} onPick={onNew} />\n ))}\n </div>\n </div>\n </>\n ) : null}\n </div>\n );\n }\n\n return (\n <div className={cx(\"@container grid min-w-0 content-start gap-3 p-4\", className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"grid gap-0.5\">\n <h2 className=\"m-0 text-lg font-semibold\">{title}</h2>\n <p className=\"m-0 text-sm text-muted-foreground\">{intro}</p>\n </div>\n {newButton}\n </header>\n\n {describeBox}\n\n <div role=\"status\" className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground\">\n <span>\n <b className=\"font-semibold text-foreground\">{sum.on}</b> on\n </span>\n {sum.failing > 0 ? (\n <span className=\"text-destructive\">\n <b className=\"font-semibold\">{sum.failing}</b> failed last time\n </span>\n ) : null}\n {sum.needs > 0 ? (\n <span className=\"text-warning\">\n <b className=\"font-semibold\">{sum.needs}</b> need{sum.needs === 1 ? \"s\" : \"\"} their owner\n </span>\n ) : null}\n {sum.next ? (\n <span>\n Next: <b className=\"font-semibold text-foreground\">{sum.next.name}</b>, {clockWords(sum.next.next_run_at, sum.next.tz, now)}\n </span>\n ) : null}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <label className=\"relative block min-w-0 max-w-sm flex-1\">\n <Icon name=\"search\" size={14} className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\" />\n <input\n type=\"search\"\n placeholder=\"Search automations\"\n aria-label=\"Search automations\"\n value={q}\n onChange={(e) => {\n setQ(e.target.value);\n onQueryChange?.(e.target.value);\n }}\n className=\"min-h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n />\n </label>\n {appFilter && onClearFilter ? (\n <Button size=\"sm\" variant=\"secondary\" onClick={onClearFilter}>\n Showing one app. Show all\n </Button>\n ) : null}\n </div>\n\n {error ? (\n <p role=\"alert\" className=\"m-0 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm\">\n {error}\n </p>\n ) : null}\n\n {shown.length === 0 ? <p className=\"m-0 text-sm text-muted-foreground\">No automation matches “{q}”.</p> : null}\n {groups.map((g) => (\n <section key={g.key} aria-label={g.title || \"Automations\"} className=\"grid gap-1.5\">\n {g.title ? <h3 className=\"m-0 text-xs font-semibold uppercase tracking-wide text-muted-foreground\">{g.title}</h3> : null}\n <ul className=\"m-0 grid list-none gap-1.5 p-0\">\n {g.rows.map((r) => (\n <Row key={r.id} row={r} appName={appName} now={now} busy={pending === r.id} canToggle={canToggle} showOrigin={grouped && !appFilter} onOpen={onOpen} onToggle={toggle} onDelete={askDelete} />\n ))}\n </ul>\n </section>\n ))}\n\n <Confirm\n open={deleting !== null}\n onClose={() => (deletingBusy ? undefined : setDeleting(null))}\n onConfirm={() => void confirmDelete()}\n title=\"Delete this automation?\"\n body={deleting ? deleteWords(deleting.name, deleting.status) : \"\"}\n confirmLabel=\"Delete\"\n tone=\"danger\"\n busy={deletingBusy}\n />\n\n <StartingPoints\n open={choosing}\n onClose={() => setChoosing(false)}\n starts={startList}\n onDescribe={onDescribe}\n describePrice={describePrice}\n describeChips={describeChips}\n onOpen={(id) => {\n setChoosing(false);\n onOpen?.(id);\n }}\n onBlank={() => {\n setChoosing(false);\n onNew?.();\n }}\n onPick={(s) => {\n setChoosing(false);\n onNew?.(s);\n }}\n />\n </div>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/automation-list/README.md",
27
+ "type": "registry:file",
28
+ "target": "@components/makersclaw/blocks/automation-list/README.md",
29
+ "content": "# AutomationList\n\nAn app's automations as rows. **The whole row opens the automation** (a\nstretched button under the switch), so a member is never hunting for a\nsmall arrow. Each row says what it is called and when it runs (in words, in\nthe automation's own time zone), how its last run went as a dot and a word\n(Healthy, Last run failed, Needs its owner, Waiting, Running, Off, Not live\nyet), when the next is due, what the last one cost, and an on/off switch\nthat keeps its own press.\n\n- A line above the rows counts what is on, what failed last time, what\n waits for its owner, and which runs next.\n- A search box; **New automation** (a picker of starting points once there\n are some, straight to a blank when there are none).\n- **First run** — with no automations, a few ready-made flows to start\n from (a Monday reminder, check then tell me, wait then follow up, an\n app's action every morning, ask the model for a line).\n- **Describe it** (with `onDescribe`) — a box at the top, above the rows, on\n the first-run screen and in the New automation sheet: say what it should\n do in a sentence, press \"Draft it\" (the button carries its price), and the\n platform draws it; it opens when ready. It says \"Drawing it…\" while it\n works. A question back is listed under the box (\"Ask me about: …\"); any\n other refusal shows its words. Three chips start the sentence. Without\n `onDescribe` nothing changes.\n- On a team page, the rows are grouped Team and From &lt;app&gt;; on an app's\n own page pass `grouped={false}`.\n\n```jsx\nimport { AutomationList } from \"../components/blocks/automation-list\"; // a Makersclaw app\nimport { AutomationList } from \"@/components/makersclaw/blocks/automation-list\"; // a shadcn project\n```\n\n`makersclaw add automation-list` lays `automation-model` with it. A client\ncomponent. It replaces `schedule-card` for an app whose jobs run on the\nautomations engine (the card still works for an app that has not moved).\nIts own width decides its layout: under about 576 px a row stacks its\nfacts under its name.\n\n## Props\n\n- `rows` — the list: `useAutomations().automations`. Each: `{ id, name,\n key, status (on · off · draft · needs_owner), live_version, trigger,\n next_run_at, tz, last_run: { status, started_at, credits }, active_runs,\n edited, owner_kind, app_id }`.\n- `onOpen(id)` — a whole row was pressed.\n- `onNew(start?)` — New automation; `start` is a starting point (`{ id,\n name, build(takenKeys) }`) when one was picked, else a blank is meant.\n- `onDelete(row)` — async: **Delete automation**, in a menu on each row. It\n asks first, in words that say what it does (`Deletes \"<name>\". Its runs\n stay in history.`, and for a row that is on, that deleting also switches it\n off). Resolves `{ ok: true }` (the screen says what followed) or `{ ok:\n false, error }` (shown above the rows as it is). A row from an app's own job\n or one an app shipped (`row.source` `legacy`, `package`) has the item\n disabled, pointing at the app's settings. `canDelete` hides the menu\n (default true). Without `onDelete`, no menu.\n- `onToggle(row, on)` — async; the switch. A refusal throws its words\n (shown above the rows). `canToggle` — draw the switch (default true).\n `canCreate` — draw New automation (default true).\n- `appName(id)`, `appFilter`, `onClearFilter`, `grouped`.\n- `query`, `onQueryChange(text)`: the search box starts with `query` and reports each change. A screen that opens an automation and comes back keeps the words in its own place and hands them back, so the list returns as it was left.\n- `starts` — your own starting points (default automation-model's\n `STARTING_POINTS`). An app offers flows that name its own actions here.\n The platform's own come through `startsFromPlatform(useAutomations().starts)`\n (automation-model): each card then also says what it needs, what a model\n does and what it costs (\"needs payments, a mailbox · a model writes three\n lines · about 6 cr\", or \"free\"). The raw list is also accepted.\n- `onDescribe(ask)` — async: `useAutomations().describe`. Resolves `{ ok,\n id }` (the list calls `onOpen(id)`), `{ ok: true }` with no id (the\n sentence was handed on, to an agent beside the list say: the box empties),\n `{ ok: false, questions }` or `{ ok: false, error }`. `describePrice` — what the button says it costs (default\n \"1 call · about 1 cr\"); `describeChips` — the sentences that start the\n box (default three).\n- `title`, `intro`, `now` (the clock, for a test), `className`.\n\n### StartingPoints\n\nThe picker New automation opens: `open`, `onClose`, `starts`, `onPick(s)`,\n`onBlank()`, and (for Describe it) `onDescribe`, `onOpen`, `describePrice`,\n`describeChips`. With Describe it, the ready-made ones sit under \"Start from\na ready one\".\n\n## Hooks (lib/hooks.js)\n\n`useAutomations()` → `{ automations, triggers, starts, describe(ask, app?),\ndescribing, busy, creating, setStatus(id, on), create(definition), loading,\nerror, unavailable, reload }`. It reads\n`GET _claw/automations`; `setStatus` patches `status`; `create` posts a\ndefinition and resolves `{ ok, data: { id } }` or `{ ok: false, error,\nproblems }`. Looks every 30 s (5 s while one runs), not at all while the\npage is hidden. On a claw from before the engine it is `unavailable`:\nshow a quiet \"not set up yet\". `starts` is `[]`: the desk's starting points are not served to an app\n(`_claw/automation-starts` does not exist), so the lib asks nothing and\nan app brings its own; `describe(ask, app?)` posts `_claw/automation-draft`\nand resolves `{ ok: true, id, … }`, `{ ok: false, error, code, questions? }`\nor `{ ok: false, code: \"unavailable\" }`, never thrown.\n\n## Never\n\n- Never switch an automation on that has no version kept: the switch is\n disabled until one is (`live_version`).\n- Never show a time in the viewer's zone: `next_run_at` is shown in `tz`.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport { useAutomations } from \"../lib/hooks\";\nimport { AutomationList } from \"../components/blocks/automation-list\";\n\nexport default function Automations() {\n const router = useRouter();\n const { automations, setStatus, unavailable } = useAutomations();\n if (unavailable) return <p className=\"p-4 text-sm text-muted-foreground\">Automations are not set up here yet.</p>;\n return (\n <AutomationList\n rows={automations}\n grouped={false}\n title=\"Automations\"\n onOpen={(id) => router.push(`?automation=${id}`)}\n onToggle={async (row, on) => {\n const r = await setStatus(row.id, on);\n if (!r.ok) throw new Error(r.error);\n }}\n onNew={(start) => router.push(`?automation=new${start ? `&start=${start.id}` : \"\"}`)}\n />\n );\n}\n```\n"
30
+ }
31
+ ],
32
+ "categories": [
33
+ "automation",
34
+ "workflow",
35
+ "list",
36
+ "rows",
37
+ "schedule",
38
+ "job",
39
+ "health",
40
+ "next run",
41
+ "last run",
42
+ "cost",
43
+ "switch",
44
+ "enabled",
45
+ "starting points",
46
+ "templates",
47
+ "n8n",
48
+ "make"
49
+ ],
50
+ "docs": "Installed to components/makersclaw/blocks/automation-list. Import: import { AutomationList, StartingPoints } from \"@/components/makersclaw/blocks/automation-list\". Props and an example: components/makersclaw/blocks/automation-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
51
+ "meta": {
52
+ "makersclaw": {
53
+ "version": "0.8.2",
54
+ "useWhen": "The Automations page of an app, or of the desk's own Automations window: the rows a member opens, switches and starts from. It replaces schedule-card for an app whose jobs run on the automations engine."
55
+ }
56
+ }
57
+ }