makersclaw 0.7.0 → 0.8.1

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 (419) hide show
  1. package/README.md +17 -4
  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 +4 -4
  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 +5 -5
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +3 -3
  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 +18 -6
  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 +3 -3
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +3 -3
  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 +4 -4
  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 +4 -4
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +3 -3
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +3 -3
  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 +4089 -1510
  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 +3 -3
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +3 -3
  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 +4 -4
  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 +225 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +244 -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/annotate/README.md +14 -11
  159. package/registry/blocks/approval/README.md +145 -0
  160. package/registry/blocks/approval/approval.jsx +163 -0
  161. package/registry/blocks/approval/block.json +31 -0
  162. package/registry/blocks/approval/demo.jsx +121 -0
  163. package/registry/blocks/approval/index.js +3 -0
  164. package/registry/blocks/ask-bar/README.md +105 -0
  165. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  166. package/registry/blocks/ask-bar/block.json +26 -0
  167. package/registry/blocks/ask-bar/demo.jsx +78 -0
  168. package/registry/blocks/ask-bar/index.js +3 -0
  169. package/registry/blocks/automation-editor/README.md +291 -0
  170. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  171. package/registry/blocks/automation-editor/block.json +39 -0
  172. package/registry/blocks/automation-editor/demo.jsx +284 -0
  173. package/registry/blocks/automation-editor/index.js +4 -0
  174. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  175. package/registry/blocks/automation-list/README.md +129 -0
  176. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  177. package/registry/blocks/automation-list/block.json +35 -0
  178. package/registry/blocks/automation-list/demo.jsx +67 -0
  179. package/registry/blocks/automation-list/index.js +3 -0
  180. package/registry/blocks/automation-model/README.md +109 -0
  181. package/registry/blocks/automation-model/block.json +135 -0
  182. package/registry/blocks/automation-model/catalog.js +256 -0
  183. package/registry/blocks/automation-model/check.js +161 -0
  184. package/registry/blocks/automation-model/demo.jsx +42 -0
  185. package/registry/blocks/automation-model/edit.js +262 -0
  186. package/registry/blocks/automation-model/fixes.js +287 -0
  187. package/registry/blocks/automation-model/glyph.jsx +13 -0
  188. package/registry/blocks/automation-model/graph.js +410 -0
  189. package/registry/blocks/automation-model/index.js +33 -0
  190. package/registry/blocks/automation-model/overlay.js +175 -0
  191. package/registry/blocks/automation-model/parts.jsx +94 -0
  192. package/registry/blocks/automation-model/schedule.js +53 -0
  193. package/registry/blocks/automation-model/templates.js +128 -0
  194. package/registry/blocks/automation-model/values.js +29 -0
  195. package/registry/blocks/automation-model/words.js +439 -0
  196. package/registry/blocks/automations-screen/README.md +207 -0
  197. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  198. package/registry/blocks/automations-screen/block.json +47 -0
  199. package/registry/blocks/automations-screen/demo.jsx +364 -0
  200. package/registry/blocks/automations-screen/index.js +4 -0
  201. package/registry/blocks/automations-screen/screen-model.js +129 -0
  202. package/registry/blocks/chart/cartesian.jsx +1 -1
  203. package/registry/blocks/chart/core.jsx +33 -23
  204. package/registry/blocks/chart/pie.jsx +1 -1
  205. package/registry/blocks/chat/README.md +4 -1
  206. package/registry/blocks/chat/demo.jsx +2 -2
  207. package/registry/blocks/chat-pane/README.md +143 -0
  208. package/registry/blocks/chat-pane/block.json +48 -0
  209. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  210. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  211. package/registry/blocks/chat-pane/chat-context.js +17 -0
  212. package/registry/blocks/chat-pane/chat-model.js +43 -0
  213. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  214. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  215. package/registry/blocks/chat-pane/demo.jsx +103 -0
  216. package/registry/blocks/chat-pane/index.js +9 -0
  217. package/registry/blocks/connect-card/README.md +99 -0
  218. package/registry/blocks/connect-card/block.json +10 -0
  219. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  220. package/registry/blocks/connect-card/demo.jsx +49 -0
  221. package/registry/blocks/connect-card/index.js +3 -0
  222. package/registry/blocks/cost/README.md +115 -0
  223. package/registry/blocks/cost/block.json +10 -0
  224. package/registry/blocks/cost/cost.jsx +176 -0
  225. package/registry/blocks/cost/demo.jsx +66 -0
  226. package/registry/blocks/cost/index.js +3 -0
  227. package/registry/blocks/data-picker/README.md +81 -0
  228. package/registry/blocks/data-picker/block.json +38 -0
  229. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  230. package/registry/blocks/data-picker/demo.jsx +34 -0
  231. package/registry/blocks/data-picker/fields.js +89 -0
  232. package/registry/blocks/data-picker/index.js +4 -0
  233. package/registry/blocks/data-table/demo.jsx +1 -1
  234. package/registry/blocks/first-run/README.md +118 -0
  235. package/registry/blocks/first-run/block.json +11 -0
  236. package/registry/blocks/first-run/demo.jsx +88 -0
  237. package/registry/blocks/first-run/first-run-model.js +51 -0
  238. package/registry/blocks/first-run/first-run.jsx +156 -0
  239. package/registry/blocks/first-run/index.js +5 -0
  240. package/registry/blocks/first-run/probe-rules.js +26 -0
  241. package/registry/blocks/fix-note/README.md +79 -0
  242. package/registry/blocks/fix-note/block.json +35 -0
  243. package/registry/blocks/fix-note/demo.jsx +30 -0
  244. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  245. package/registry/blocks/fix-note/index.js +3 -0
  246. package/registry/blocks/flow-canvas/README.md +15 -8
  247. package/registry/blocks/flow-editor/README.md +108 -0
  248. package/registry/blocks/flow-editor/block.json +44 -0
  249. package/registry/blocks/flow-editor/demo.jsx +115 -0
  250. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  251. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  252. package/registry/blocks/flow-editor/index.js +6 -0
  253. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  254. package/registry/blocks/flow-editor/outline.jsx +134 -0
  255. package/registry/blocks/footer/README.md +3 -2
  256. package/registry/blocks/library-shelf/README.md +117 -0
  257. package/registry/blocks/library-shelf/block.json +28 -0
  258. package/registry/blocks/library-shelf/demo.jsx +68 -0
  259. package/registry/blocks/library-shelf/index.js +3 -0
  260. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  261. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  262. package/registry/blocks/margin-notes/README.md +222 -0
  263. package/registry/blocks/margin-notes/block.json +10 -0
  264. package/registry/blocks/margin-notes/demo.jsx +144 -0
  265. package/registry/blocks/margin-notes/index.js +20 -0
  266. package/registry/blocks/margin-notes/margin-model.js +224 -0
  267. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  268. package/registry/blocks/mind-map/README.md +4 -2
  269. package/registry/blocks/needs-you/README.md +177 -0
  270. package/registry/blocks/needs-you/block.json +34 -0
  271. package/registry/blocks/needs-you/demo.jsx +143 -0
  272. package/registry/blocks/needs-you/index.js +3 -0
  273. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  274. package/registry/blocks/org-chart/README.md +4 -2
  275. package/registry/blocks/pace-bar/README.md +230 -0
  276. package/registry/blocks/pace-bar/block.json +10 -0
  277. package/registry/blocks/pace-bar/demo.jsx +255 -0
  278. package/registry/blocks/pace-bar/index.js +22 -0
  279. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  280. package/registry/blocks/pace-bar/pace-model.js +288 -0
  281. package/registry/blocks/page-builder/README.md +1 -1
  282. package/registry/blocks/page-builder/demo.jsx +3 -3
  283. package/registry/blocks/pdf-viewer/README.md +4 -1
  284. package/registry/blocks/record-grid/README.md +100 -0
  285. package/registry/blocks/record-grid/block.json +28 -0
  286. package/registry/blocks/record-grid/demo.jsx +43 -0
  287. package/registry/blocks/record-grid/index.js +3 -0
  288. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  289. package/registry/blocks/run-history/README.md +138 -0
  290. package/registry/blocks/run-history/block.json +10 -0
  291. package/registry/blocks/run-history/demo.jsx +74 -0
  292. package/registry/blocks/run-history/index.js +3 -0
  293. package/registry/blocks/run-history/run-history.jsx +218 -0
  294. package/registry/blocks/run-view/README.md +174 -0
  295. package/registry/blocks/run-view/block.json +50 -0
  296. package/registry/blocks/run-view/demo.jsx +286 -0
  297. package/registry/blocks/run-view/index.js +5 -0
  298. package/registry/blocks/run-view/run-controls.jsx +266 -0
  299. package/registry/blocks/run-view/run-view.jsx +236 -0
  300. package/registry/blocks/run-view/runs-list.jsx +99 -0
  301. package/registry/blocks/schedule-card/README.md +121 -0
  302. package/registry/blocks/schedule-card/block.json +10 -0
  303. package/registry/blocks/schedule-card/demo.jsx +82 -0
  304. package/registry/blocks/schedule-card/index.js +3 -0
  305. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  306. package/registry/blocks/schedule-engine/README.md +137 -0
  307. package/registry/blocks/schedule-engine/block.json +10 -0
  308. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  309. package/registry/blocks/schedule-engine/engine.js +769 -0
  310. package/registry/blocks/schedule-engine/index.js +9 -0
  311. package/registry/blocks/schedule-engine/ops.js +71 -0
  312. package/registry/blocks/schedule-engine/place.js +294 -0
  313. package/registry/blocks/schedule-engine/tree.js +79 -0
  314. package/registry/blocks/schedule-engine/waits.js +80 -0
  315. package/registry/blocks/schedule-gantt/README.md +159 -0
  316. package/registry/blocks/schedule-gantt/block.json +10 -0
  317. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  318. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  319. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  320. package/registry/blocks/schedule-gantt/index.js +26 -0
  321. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  322. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  323. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  324. package/registry/blocks/schedule-ruler/README.md +148 -0
  325. package/registry/blocks/schedule-ruler/block.json +10 -0
  326. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  327. package/registry/blocks/schedule-ruler/index.js +21 -0
  328. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  329. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  330. package/registry/blocks/sidebar/README.md +23 -13
  331. package/registry/blocks/sketch-canvas/README.md +15 -9
  332. package/registry/blocks/source-receipt/README.md +97 -0
  333. package/registry/blocks/source-receipt/block.json +30 -0
  334. package/registry/blocks/source-receipt/demo.jsx +71 -0
  335. package/registry/blocks/source-receipt/index.js +3 -0
  336. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  337. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  338. package/registry/blocks/step-picker/README.md +78 -0
  339. package/registry/blocks/step-picker/block.json +31 -0
  340. package/registry/blocks/step-picker/demo.jsx +23 -0
  341. package/registry/blocks/step-picker/index.js +3 -0
  342. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  343. package/registry/blocks/step-settings/README.md +100 -0
  344. package/registry/blocks/step-settings/block.json +44 -0
  345. package/registry/blocks/step-settings/demo.jsx +93 -0
  346. package/registry/blocks/step-settings/fields.jsx +58 -0
  347. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  348. package/registry/blocks/step-settings/index.js +5 -0
  349. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  350. package/registry/blocks/suggestion/README.md +171 -0
  351. package/registry/blocks/suggestion/block.json +40 -0
  352. package/registry/blocks/suggestion/demo.jsx +91 -0
  353. package/registry/blocks/suggestion/index.js +3 -0
  354. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  355. package/registry/blocks/sum-bar/README.md +122 -0
  356. package/registry/blocks/sum-bar/block.json +10 -0
  357. package/registry/blocks/sum-bar/demo.jsx +110 -0
  358. package/registry/blocks/sum-bar/index.js +5 -0
  359. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  360. package/registry/blocks/sum-bar/sum-model.js +126 -0
  361. package/registry/blocks/tier-map/README.md +217 -0
  362. package/registry/blocks/tier-map/block.json +10 -0
  363. package/registry/blocks/tier-map/demo.jsx +222 -0
  364. package/registry/blocks/tier-map/index.js +24 -0
  365. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  366. package/registry/blocks/tier-map/tier-model.js +318 -0
  367. package/registry/blocks/time-grid/README.md +305 -0
  368. package/registry/blocks/time-grid/block.json +10 -0
  369. package/registry/blocks/time-grid/demo.jsx +279 -0
  370. package/registry/blocks/time-grid/grid-model.js +322 -0
  371. package/registry/blocks/time-grid/index.js +25 -0
  372. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  373. package/registry/blocks/time-strip/README.md +141 -0
  374. package/registry/blocks/time-strip/block.json +10 -0
  375. package/registry/blocks/time-strip/demo.jsx +129 -0
  376. package/registry/blocks/time-strip/index.js +19 -0
  377. package/registry/blocks/time-strip/strip-model.js +178 -0
  378. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  379. package/registry/blocks/top-bar/README.md +149 -0
  380. package/registry/blocks/top-bar/block.json +35 -0
  381. package/registry/blocks/top-bar/demo.jsx +78 -0
  382. package/registry/blocks/top-bar/index.js +5 -0
  383. package/registry/blocks/top-bar/probe-rules.js +74 -0
  384. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  385. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  386. package/registry/blocks/trigger-picker/README.md +98 -0
  387. package/registry/blocks/trigger-picker/block.json +38 -0
  388. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  389. package/registry/blocks/trigger-picker/index.js +3 -0
  390. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  391. package/registry/blocks/whiteboard/README.md +20 -14
  392. package/registry/blocks/workflow-view/README.md +123 -0
  393. package/registry/blocks/workflow-view/block.json +10 -0
  394. package/registry/blocks/workflow-view/demo.jsx +90 -0
  395. package/registry/blocks/workflow-view/index.js +3 -0
  396. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  397. package/src/cli.mjs +2 -2
  398. package/src/install.mjs +100 -22
  399. package/src/registry.mjs +37 -3
  400. package/templates/next/browser/lib/data.js +6 -1
  401. package/templates/next/files/README.md +909 -26
  402. package/templates/next/files/app/shell.jsx +54 -37
  403. package/templates/next/files/check/automation-format.ts +2103 -0
  404. package/templates/next/files/check/automation-package.ts +142 -0
  405. package/templates/next/files/check/run.mjs +1043 -12
  406. package/templates/next/files/components/ui/field.jsx +17 -4
  407. package/templates/next/files/components/ui/icon.jsx +13 -0
  408. package/templates/next/files/components/ui/index.js +3 -1
  409. package/templates/next/files/components/ui/layout.jsx +382 -0
  410. package/templates/next/files/components/ui/notice.jsx +70 -0
  411. package/templates/next/files/components/ui/page.jsx +124 -0
  412. package/templates/next/files/components/ui/stat.jsx +22 -7
  413. package/templates/next/files/components/ui/toast.jsx +6 -3
  414. package/templates/next/files/lib/automations.js +660 -0
  415. package/templates/next/files/lib/claw.js +244 -5
  416. package/templates/next/files/lib/door.js +92 -0
  417. package/templates/next/files/lib/hooks.js +1694 -6
  418. package/templates/next/files/lib/platform.js +611 -14
  419. package/templates/next/template.json +22 -3
@@ -1,23 +1,73 @@
1
1
  "use client";
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { claw, publishDesk, publishStatus } from "./claw";
5
- import { ask, cancelJob, openRequest, requests as readRequests, startJob, stopRequest } from "./platform";
4
+ import { claw, openApp, openConnections, openFile, pressMessage, publishDesk, publishStatus } from "./claw";
5
+ import {
6
+ activity as readActivity,
7
+ agentKit as readAgentKit,
8
+ answer as sendAnswer,
9
+ approval as readApproval,
10
+ ask,
11
+ cancelJob,
12
+ connectLink,
13
+ connections as readConnections,
14
+ jobs as readJobs,
15
+ library,
16
+ llm,
17
+ llmEstimate,
18
+ openRequest,
19
+ pauseApp,
20
+ requests as readRequests,
21
+ resumeApp,
22
+ run as readRun,
23
+ runNow as startRun,
24
+ runs as readRuns,
25
+ saidSomething,
26
+ setJob,
27
+ startJob,
28
+ stopRequest,
29
+ streamThread,
30
+ thread as readThread,
31
+ threadCall as readThreadCall,
32
+ threads as readThreads,
33
+ } from "./platform";
6
34
 
7
35
  // The React side of the _claw API, without any look: fetching, polling,
8
36
  // streaming and cleanup. A surface renders what comes back and owns every
9
37
  // pixel. "use client" only: the _claw API is browser-side.
10
38
  //
11
- // There is no chat, runs or activity hook here, and there will not be:
12
- // those are STANDING surfaces and the OS draws them as tabs on the app
13
- // window (docs/ARTIFACT-CONTRACTS.md §9). `useJob` is the other thing —
39
+ // Up to 0.8.1 this header said there was no runs or activity hook here
40
+ // and there would not be: those were STANDING surfaces, and the OS drew
41
+ // them as tabs on the app window (docs/ARTIFACT-CONTRACTS.md §9) so that
42
+ // every app showed its agent's work in one look. The kit (0.8.1, D15)
43
+ // changed that. One look now lives in the kit's blocks, drawn in the
44
+ // app's theme, so the app shows its agent's work itself, beside the rows
45
+ // it wrote: `useJobs` (the schedule, Run now, the switches), `useRuns` and
46
+ // `useRun` (the runs and one run's account), `useNeedsYou` (what waits on
47
+ // this member), `useActivity` (who did what, a record's timeline),
48
+ // `useConnections` (the app's accounts), `useAgentKit` (what its agent
49
+ // knows and can do) and `useLlmAction` (a model call with its price
50
+ // first). The OS tabs stay, as the full history; the yes, the account
51
+ // sign-in and money stay on the desk. Each kit hook polls only while the
52
+ // page is shown, backs off while its door is missing, aborts on unmount,
53
+ // and never throws: a missing door is `unavailable`, a quiet "not
54
+ // connected yet", never an error.
55
+ //
56
+ // The chat came first (0.8.1): `useAgentThread` follows the member's own
57
+ // thread with the app's agent, the same conversation the desk's column
58
+ // shows, for the `agent-chat` block. `useJob` is the other thing —
14
59
  // work a founder asks for from inside the app, which is not a surface at
15
60
  // all: it opens because they pressed something and it closes when the
16
61
  // work lands (SOP §2.3, ARTIFACT-CONTRACTS §9f). Since 0.4.0 that work is
17
62
  // a request: the conversation lives in the desk's agent column, and the
18
63
  // app follows only its status. `useRequests` is the same requests seen
19
64
  // from a surface: the asks in flight on this app, so an empty table can
20
- // show the work coming instead of a blank.
65
+ // show the work coming instead of a blank. `useLlm` (0.8.0) is neither:
66
+ // one call to a model, answered in the page, for text work on what the
67
+ // screen holds — nothing to follow, nothing on the desk. Nor are the
68
+ // Library's hooks (0.8.1): they read the collections Makersclaw keeps,
69
+ // once, when the screen asks (a collection's items and rows a page at a
70
+ // time), and have nothing to poll.
21
71
  //
22
72
  // While the gateway has no _claw routes yet (`unavailable`), polls back
23
73
  // off to once a minute so a preview that is still being wired does not
@@ -672,3 +722,1641 @@ export function useJob() {
672
722
 
673
723
  return { ...state, start, cancel, reset, open };
674
724
  }
725
+
726
+ /** What useLlm shows with no call in flight and nothing to show. */
727
+ const QUIET = { loading: false, result: null, error: null, unavailable: false };
728
+
729
+ /**
730
+ * One model call from a component (lib/platform.js, llm). `run(input,
731
+ * opts)` takes llm()'s arguments and resolves llm()'s answer, so a caller
732
+ * that needs the refusal's `code` reads it there; the state is for the
733
+ * screen:
734
+ *
735
+ * loading a call is out
736
+ * result the answer when it was ok: { ok: true, text } or { ok: true,
737
+ * data } (null while loading and after a failure)
738
+ * error the failure's words (null while loading, after an answer,
739
+ * and on a claw with no door)
740
+ * unavailable the platform has no model door yet (a claw from before
741
+ * it) or there is no network: show the quiet "not connected
742
+ * yet", as for useClaw's `unavailable`. Then `error` stays
743
+ * null, so a refusal and a missing door are never both set
744
+ *
745
+ * A run supersedes the one before it: that one is aborted, resolves
746
+ * { ok: false, aborted: true } and changes nothing, so a slow answer never
747
+ * lands over a newer one. The hook owns the signal (one in opts is
748
+ * replaced). Unmounting aborts the call in flight. Nothing is called until
749
+ * `run` is. `reset()` (0.8.1) clears what is shown and drops a call in
750
+ * flight.
751
+ *
752
+ * const { run, loading, result, error, unavailable } = useLlm();
753
+ * run(`Translate to French: ${text}`, { task: "translate" });
754
+ * …{loading ? <Spinner /> : result?.text}
755
+ */
756
+ export function useLlm() {
757
+ const [state, setState] = useState(QUIET);
758
+ const abortRef = useRef(null);
759
+ const runRef = useRef(0); // the run that owns the state
760
+ const aliveRef = useRef(true);
761
+
762
+ // React runs this cleanup and mounts the hook again in development (its
763
+ // double mount; a fast refresh does the same): a call cut off by that
764
+ // cleanup is closed quietly in run(), below, once the answer comes back.
765
+ useEffect(() => {
766
+ aliveRef.current = true;
767
+ return () => {
768
+ aliveRef.current = false;
769
+ abortRef.current?.abort();
770
+ };
771
+ }, []);
772
+
773
+ const run = useCallback(async (input, opts) => {
774
+ abortRef.current?.abort();
775
+ const controller = new AbortController();
776
+ abortRef.current = controller;
777
+ const mine = ++runRef.current;
778
+ setState({ loading: true, result: null, error: null, unavailable: false });
779
+ const answer = await llm(input, { ...opts, signal: controller.signal });
780
+ if (runRef.current !== mine || !aliveRef.current) return answer; // a newer run owns the state, or the page is gone
781
+ if (controller.signal.aborted) {
782
+ setState(QUIET); // cut off by an unmount the hook came back from, with nothing started since
783
+ return answer;
784
+ }
785
+ if (answer.ok) setState({ loading: false, result: answer, error: null, unavailable: false });
786
+ else if (answer.unavailable) setState({ loading: false, result: null, error: null, unavailable: true });
787
+ else setState({ loading: false, result: null, error: answer.error || "the model did not answer", unavailable: false });
788
+ return answer;
789
+ }, []);
790
+
791
+ // Back to nothing shown: a call in flight is aborted and its answer
792
+ // changes nothing.
793
+ const reset = useCallback(() => {
794
+ abortRef.current?.abort();
795
+ runRef.current += 1;
796
+ setState(QUIET);
797
+ }, []);
798
+
799
+ return { ...state, run, reset };
800
+ }
801
+
802
+ // ---------------------------------------------------------------------------
803
+ // The Library (0.8.1): lib/platform.js, library
804
+
805
+ /** What a Library read shows while it is out, and what it shows with
806
+ * nothing to read (no id yet, or `enabled` false). `key` names the read
807
+ * the state belongs to: a state kept for another read is not shown. */
808
+ const READING = { key: null, data: null, loading: true, error: null, code: null, unavailable: false };
809
+ const NOTHING = { key: null, data: null, loading: false, error: null, code: null, unavailable: false };
810
+ const NONE = [];
811
+
812
+ /** A read that did not give the hooks an answer to hold: the platform's
813
+ * refusal, a claw with no door, or an answer that is not the door's.
814
+ * `unavailable` (a claw from before the door, or no network) leaves
815
+ * `error` null, so a refusal and a missing door are never both set (as
816
+ * useLlm). `code` is the refusal's (`needs_plan`), when it named one. */
817
+ function unanswered(r) {
818
+ if (r.unavailable) return { error: null, code: null, unavailable: true };
819
+ return { error: r.error || "the Library did not answer", code: r.code ?? null, unavailable: false };
820
+ }
821
+
822
+ /** Is this a read the door answered with an object? A 200 of another kind
823
+ * (a login page in front of the app) is not the Library's. */
824
+ function answered(r) {
825
+ if (!r.ok) return r;
826
+ if (r.data && typeof r.data === "object" && !Array.isArray(r.data)) return r;
827
+ return { ok: false, status: r.status, error: "the platform answered something unexpected", unavailable: false };
828
+ }
829
+
830
+ /**
831
+ * One read of the Library, once: fetches when `key` changes (the key names
832
+ * the read: its id and its options) and on reload(). While the key is the
833
+ * same a failed reload keeps the last data and sets `error`; a new key
834
+ * shows `loading` and nothing else, never the last read's answer. Aborts a
835
+ * read it no longer wants. → { data, loading, error, code, unavailable,
836
+ * reload }
837
+ */
838
+ function useLibraryRead(read, key, enabled) {
839
+ const [state, setState] = useState(READING);
840
+ const [tick, setTick] = useState(0);
841
+ const readRef = useRef(read);
842
+ readRef.current = read;
843
+ const reload = useCallback(() => setTick((t) => t + 1), []);
844
+
845
+ useEffect(() => {
846
+ if (!enabled) return undefined;
847
+ const controller = new AbortController();
848
+ let alive = true;
849
+ (async () => {
850
+ const r = answered(await readRef.current(controller.signal));
851
+ if (!alive || r.aborted) return;
852
+ if (r.ok) setState({ key, data: r.data, loading: false, error: null, code: null, unavailable: false });
853
+ else setState((s) => ({ key, data: s.key === key ? s.data : null, loading: false, ...unanswered(r) }));
854
+ })();
855
+ return () => {
856
+ alive = false;
857
+ controller.abort();
858
+ };
859
+ }, [key, enabled, tick]);
860
+
861
+ const mine = enabled ? (state.key === key ? state : READING) : NOTHING;
862
+ return { data: mine.data, loading: mine.loading, error: mine.error, code: mine.code, unavailable: mine.unavailable, reload };
863
+ }
864
+
865
+ /** A page's list, total and next cursor from a door's answer. */
866
+ function pageOf(data, name) {
867
+ return {
868
+ list: Array.isArray(data?.[name]) ? data[name] : NONE,
869
+ total: typeof data?.total === "number" ? data.total : null,
870
+ previewOnly: Boolean(data?.preview_only),
871
+ next: typeof data?.next_cursor === "string" && data.next_cursor ? data.next_cursor : null,
872
+ };
873
+ }
874
+
875
+ const READING_PAGE = {
876
+ key: null,
877
+ list: NONE,
878
+ total: null,
879
+ previewOnly: false,
880
+ next: null,
881
+ loading: true,
882
+ loadingMore: false,
883
+ error: null,
884
+ code: null,
885
+ unavailable: false,
886
+ };
887
+ const NOTHING_PAGE = { ...READING_PAGE, loading: false };
888
+
889
+ /**
890
+ * A collection's items or rows, a page at a time. `read(cursor, signal)`
891
+ * asks for one page. The first page is read when `key` changes and on
892
+ * reload(); loadMore() reads the one after it and adds it to the list (a
893
+ * row seen twice is kept once). A new key, a reload and an unmount abort a
894
+ * loadMore in flight, so a late page never lands on another list. →
895
+ * { list, total, previewOnly, hasMore, loading, loadingMore, error, code,
896
+ * unavailable, loadMore, reload }
897
+ */
898
+ function useLibraryPage(name, read, key, enabled) {
899
+ const [state, setState] = useState(READING_PAGE);
900
+ const [tick, setTick] = useState(0);
901
+ const readRef = useRef(read);
902
+ readRef.current = read;
903
+ const nowRef = useRef({ key, state });
904
+ nowRef.current = { key, state };
905
+ const moreRef = useRef(null); // the loadMore in flight: its AbortController
906
+ const reload = useCallback(() => setTick((t) => t + 1), []);
907
+
908
+ useEffect(() => {
909
+ if (!enabled) return undefined;
910
+ const controller = new AbortController();
911
+ let alive = true;
912
+ (async () => {
913
+ const r = answered(await readRef.current(null, controller.signal));
914
+ if (!alive || r.aborted) return;
915
+ if (r.ok) setState({ ...READING_PAGE, ...pageOf(r.data, name), key, loading: false });
916
+ else setState((s) => ({ ...(s.key === key ? s : READING_PAGE), key, loading: false, loadingMore: false, ...unanswered(r) }));
917
+ })();
918
+ return () => {
919
+ alive = false;
920
+ controller.abort();
921
+ moreRef.current?.abort();
922
+ moreRef.current = null;
923
+ };
924
+ }, [name, key, enabled, tick]);
925
+
926
+ const loadMore = useCallback(async () => {
927
+ const { key: now, state: held } = nowRef.current;
928
+ if (held.key !== now || !held.next || moreRef.current) return undefined;
929
+ const controller = new AbortController();
930
+ moreRef.current = controller;
931
+ setState((s) => (s.key === now ? { ...s, loadingMore: true, error: null, code: null } : s));
932
+ const r = answered(await readRef.current(held.next, controller.signal));
933
+ if (moreRef.current === controller) moreRef.current = null;
934
+ if (controller.signal.aborted) return r; // a new key, a reload or an unmount took the list
935
+ setState((s) => {
936
+ if (s.key !== now) return s;
937
+ if (!r.ok) return { ...s, loadingMore: false, ...unanswered(r) };
938
+ const page = pageOf(r.data, name);
939
+ const seen = new Set(s.list.map((x) => x?.id));
940
+ return {
941
+ ...s,
942
+ list: [...s.list, ...page.list.filter((x) => !seen.has(x?.id))],
943
+ total: page.total ?? s.total,
944
+ previewOnly: page.previewOnly,
945
+ next: page.next,
946
+ loadingMore: false,
947
+ error: null,
948
+ code: null,
949
+ unavailable: false,
950
+ };
951
+ });
952
+ return r;
953
+ }, [name]);
954
+
955
+ const mine = enabled ? (state.key === key ? state : READING_PAGE) : NOTHING_PAGE;
956
+ return {
957
+ list: mine.list,
958
+ total: mine.total,
959
+ previewOnly: mine.previewOnly,
960
+ hasMore: Boolean(mine.next),
961
+ loading: mine.loading,
962
+ loadingMore: mine.loadingMore,
963
+ error: mine.error,
964
+ code: mine.code,
965
+ unavailable: mine.unavailable,
966
+ loadMore,
967
+ reload,
968
+ };
969
+ }
970
+
971
+ /** Filters as a key that does not depend on the order they were written
972
+ * in: the ones with a value, sorted by column. */
973
+ function filtersKey(filters) {
974
+ if (!filters || typeof filters !== "object" || Array.isArray(filters)) return [];
975
+ return Object.entries(filters)
976
+ .filter(([, v]) => (typeof v === "string" ? v.trim() !== "" : typeof v === "number" || typeof v === "boolean"))
977
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
978
+ }
979
+
980
+ /**
981
+ * The collections on the Library's shelves (library.collections), for a
982
+ * screen that offers a choice. Read once; reload() reads again.
983
+ *
984
+ * const { collections, loading, error, unavailable } = useLibraryCollections({ shelf: "decks" });
985
+ *
986
+ * → { collections, loading, error, code, unavailable, reload }. Each
987
+ * collection is a card: { id, slug, title, blurb, kind, shelf, tags,
988
+ * access, status, public_page, cover_url, item_count, row_count, usable,
989
+ * preview_only }. `enabled: false` reads nothing.
990
+ */
991
+ export function useLibraryCollections(opts) {
992
+ const { q, shelf, enabled = true } = opts ?? {};
993
+ const key = JSON.stringify(["collections", q ?? "", shelf ?? ""]);
994
+ const { data, ...rest } = useLibraryRead((signal) => library.collections({ q, shelf, signal }), key, Boolean(enabled));
995
+ return { collections: Array.isArray(data?.collections) ? data.collections : NONE, ...rest };
996
+ }
997
+
998
+ /**
999
+ * One collection whole (library.collection): its card and `body`,
1000
+ * `columns` (a rows collection's: { key, label, type, filter, … }),
1001
+ * `source`, `licence`, `preview_count`, `personal_data`, `published_at`.
1002
+ * `id` is its slug or uuid. Nothing is read until there is an id.
1003
+ * → { collection, loading, error, code, unavailable, reload }
1004
+ */
1005
+ export function useLibraryCollection(id, opts) {
1006
+ const enabled = Boolean(id) && (opts?.enabled ?? true);
1007
+ const key = JSON.stringify(["collection", id ?? null]);
1008
+ const { data, ...rest } = useLibraryRead((signal) => library.collection(id, { signal }), key, enabled);
1009
+ return { collection: data?.collection ?? null, ...rest };
1010
+ }
1011
+
1012
+ /**
1013
+ * A files collection's items, a page at a time (library.items):
1014
+ *
1015
+ * const { items, total, previewOnly, hasMore, loadMore, loading, loadingMore } =
1016
+ * useLibraryItems("pitch-decks", { q: search, tag: "seed", limit: 24 });
1017
+ *
1018
+ * items the pages read so far, in the Library's order
1019
+ * total how many there are in all (null before the first page);
1020
+ * on a preview, the true count, not the count shown
1021
+ * previewOnly a paid collection on a Free plan: `items` are the
1022
+ * preview's only (the ones past it are not listed, and
1023
+ * useLibraryItem on one says `needs_plan`)
1024
+ * hasMore loadMore() has a page to read
1025
+ * loading the first page is out loadingMore a later one is
1026
+ * error, code, unavailable as for every Library hook
1027
+ * loadMore() reads the next page and adds it; nothing when there is
1028
+ * none, or while one is out
1029
+ * reload() reads the first page again
1030
+ *
1031
+ * A new `id`, `q`, `tag` or `limit` starts the list over (a `q` typed key
1032
+ * by key should be debounced by the screen). Nothing is read until there
1033
+ * is an id.
1034
+ */
1035
+ export function useLibraryItems(id, opts) {
1036
+ const { q, tag, limit, enabled = true } = opts ?? {};
1037
+ const key = JSON.stringify(["items", id ?? null, q ?? "", tag ?? "", limit ?? null]);
1038
+ const { list, ...rest } = useLibraryPage(
1039
+ "items",
1040
+ (cursor, signal) => library.items(id, { q, tag, limit, cursor, signal }),
1041
+ key,
1042
+ Boolean(id) && Boolean(enabled),
1043
+ );
1044
+ return { items: list, ...rest };
1045
+ }
1046
+
1047
+ /**
1048
+ * A rows collection's rows, a page at a time (library.rows): the same
1049
+ * state as useLibraryItems, with `rows` ({ id, data, source_url }) in
1050
+ * place of `items`. `filters` is { <column key>: value }, for the columns
1051
+ * the collection marks as filters; a value that is empty is left out, and
1052
+ * the order they are written in does not matter. An `image` cell of a
1053
+ * row's `data` is a signed address, good for ten minutes: reload() to
1054
+ * read the page again when the screen has been open longer.
1055
+ */
1056
+ export function useLibraryRows(id, opts) {
1057
+ const { q, filters, limit, enabled = true } = opts ?? {};
1058
+ const key = JSON.stringify(["rows", id ?? null, q ?? "", filtersKey(filters), limit ?? null]);
1059
+ const { list, ...rest } = useLibraryPage(
1060
+ "rows",
1061
+ (cursor, signal) => library.rows(id, { q, filters, limit, cursor, signal }),
1062
+ key,
1063
+ Boolean(id) && Boolean(enabled),
1064
+ );
1065
+ return { rows: list, ...rest };
1066
+ }
1067
+
1068
+ /**
1069
+ * One item (library.item): the card's fields as `item`, its file's signed
1070
+ * address as `fileUrl` (good for ten minutes: show or download from it,
1071
+ * never keep it; reload() signs another) and the first 4,000 characters of
1072
+ * its text as `textExcerpt`. An item the member's plan does not open is
1073
+ * `code: "needs_plan"` with the platform's words in `error`, and no
1074
+ * `fileUrl`. Nothing is read until there is an id.
1075
+ * → { item, fileUrl, textExcerpt, loading, error, code, unavailable, reload }
1076
+ */
1077
+ export function useLibraryItem(id, opts) {
1078
+ const enabled = Boolean(id) && (opts?.enabled ?? true);
1079
+ const key = JSON.stringify(["item", id ?? null]);
1080
+ const { data, ...rest } = useLibraryRead((signal) => library.item(id, { signal }), key, enabled);
1081
+ return { item: data?.item ?? null, fileUrl: data?.file_url ?? null, textExcerpt: data?.text_excerpt ?? null, ...rest };
1082
+ }
1083
+
1084
+ // ---------------------------------------------------------------------------
1085
+ // The app's own chat (0.8.1)
1086
+
1087
+ /** How often the chat looks while the agent works, and while it waits on
1088
+ * the member on the desk. Nothing is polled once a turn has ended. */
1089
+ const CHAT_WORKING_MS = 1500;
1090
+ const CHAT_WAITING_MS = 6000;
1091
+ /** Where the chat keeps its thread for a key (`<kind>:<id>`, or "app"),
1092
+ * so the person's chat is there again when the person is opened again.
1093
+ * This browser's only; the platform keeps the thread itself. */
1094
+ const CHAT_KEY = "claw.chat.";
1095
+
1096
+ function keptThread(key) {
1097
+ if (!key) return null;
1098
+ try {
1099
+ return localStorage.getItem(CHAT_KEY + key) || null;
1100
+ } catch {
1101
+ return null;
1102
+ }
1103
+ }
1104
+
1105
+ // Who else reads the same thread in this page, so a reader that was not the
1106
+ // one to start it follows it (2026-10-12): the agent's dock keeps its pane
1107
+ // mounted beside the work, and a button on a screen, the Today ask bar and
1108
+ // the dock are each a useAgentThread on the same key. `from` is the reader
1109
+ // that made the change; it does not hear itself.
1110
+ const THREAD_KEPT = "claw:thread-kept";
1111
+
1112
+ function keepThread(key, sessionId, from) {
1113
+ if (!key) return;
1114
+ try {
1115
+ if (sessionId) localStorage.setItem(CHAT_KEY + key, sessionId);
1116
+ else localStorage.removeItem(CHAT_KEY + key);
1117
+ } catch {
1118
+ /* private mode: the chat starts fresh next time */
1119
+ }
1120
+ try {
1121
+ window.dispatchEvent(new CustomEvent(THREAD_KEPT, { detail: { key, sessionId: sessionId || null, from } }));
1122
+ } catch {
1123
+ /* no window: nobody else is reading */
1124
+ }
1125
+ }
1126
+
1127
+ const CHAT_LIVE = new Set(["queued", "working"]);
1128
+
1129
+ /**
1130
+ * The member's own thread with the app's agent, for the app's own chat
1131
+ * (the `agent-chat` block): the same eve session the desk's agent column
1132
+ * shows — one conversation, two views, never a copy.
1133
+ *
1134
+ * const chat = useAgentThread({ row: { kind: "person", id, name } });
1135
+ * <AgentChat chat={chat} title={`Ask about ${name}`} />
1136
+ *
1137
+ * Options (all optional):
1138
+ * row the record the chat is about ({ kind, id, name }): it rides
1139
+ * with every ask as context, and the thread is kept for it
1140
+ * how instructions for the agent only (the AGENTS.md section)
1141
+ * job a declared job's key, or true; by default none — a chat is
1142
+ * a conversation, not a job
1143
+ * key where this browser keeps the thread (default `<kind>:<id>`
1144
+ * of the row, else "app"); null keeps none
1145
+ * Readers on the same key in one page follow each other: one that
1146
+ * starts, drops or switches the thread is heard by the rest (the
1147
+ * dock's pane, the Today ask bar and a screen's buttons share it)
1148
+ * view true asks the door for the thread as the desk's sidebar draws
1149
+ * it (0.8.1, D27: the `agent-pane` block; useAgentPane sets it)
1150
+ *
1151
+ * State:
1152
+ * sessionId the thread, or null before the first ask
1153
+ * state queued | working | needs_you | done | stopped | failed, or
1154
+ * null before the first ask
1155
+ * needs { kind, text, at, in_app, options?, free? } while it waits
1156
+ * on the member. `in_app` is how the app can settle it:
1157
+ * "answer" (a question: answer({ optionId | text }), with
1158
+ * its `options` and whether `free` words are welcome),
1159
+ * "press" (the one ask's yes: the `approval` block's press
1160
+ * frame, useApproval(request.id)), or null (on the desk:
1161
+ * openOnDesk)
1162
+ * turns [{ id, member, parts: [{ type: "text", text } |
1163
+ * { type: "step", label, status }], writing, status }]
1164
+ * pending the member's words just sent, until the thread has them
1165
+ * view with `view: true`, the sidebar's view: { messages, thinking,
1166
+ * failure, questions } (platform.js thread), else null
1167
+ * sameConversation the next message continues this request (D29: also
1168
+ * after a Stop, a finish or a failed turn); false for a plan,
1169
+ * a piece, a change sent up, a counted job (a new request)
1170
+ * place "app", or "makersclaw" for an ask made in Makersclaw (the
1171
+ * chat shows a pointer, not the turns)
1172
+ * request the request the thread is on (as useRequests reads one)
1173
+ * sending, loading, error, code, unavailable (a claw with no door yet)
1174
+ *
1175
+ * Actions: send(text, { files }) · stop() · answer({ optionId, text } | { answers }) ·
1176
+ * openOnDesk() · newChat() · openSession(sessionId) · reload(). answer is
1177
+ * the agent's question answered in the chat where it was asked (0.8.1,
1178
+ * D19): → { ok } | { ok: false, error, code }. `answers` is
1179
+ * [{ requestId, optionId | text }], one per question `view.questions`
1180
+ * lists, sent as one resume (D27). openSession switches to another of the
1181
+ * member's threads (the pane's Chats list). `files` are [{ name, type, data }]
1182
+ * (data URLs, at most 4 and 3 MB together) sent with the words (D27 phase 4);
1183
+ * `notice` says when they did not go (the ask is queued). With `view: true`
1184
+ * a working thread is streamed (platform.js streamThread) and polled as the
1185
+ * safety net.
1186
+ * send continues the thread (ask({ sessionId })) — after a Stop, a finish
1187
+ * or a failed turn too: the platform opens the closed request again, in the
1188
+ * same session, so there is no new card (D29) — and starts a fresh one only
1189
+ * when the old one is a teammate's or gone. The desk's column is never
1190
+ * opened by an ask from here (`open: false`); openOnDesk is the member's
1191
+ * press, and opens it.
1192
+ */
1193
+ export function useAgentThread({ row, how, job = false, key, view = false } = {}) {
1194
+ const storeKey = key === null ? null : (key ?? (row?.kind && row?.id ? `${row.kind}:${row.id}` : "app"));
1195
+ const [sessionId, setSessionId] = useState(() => keptThread(storeKey));
1196
+ const me = useRef({}).current;
1197
+ const [state, setState] = useState({
1198
+ data: null,
1199
+ loading: Boolean(sessionId),
1200
+ sending: false,
1201
+ pending: null,
1202
+ error: null,
1203
+ code: null,
1204
+ unavailable: false,
1205
+ });
1206
+ const [tick, setTick] = useState(0);
1207
+ const reload = useCallback(() => setTick((t) => t + 1), []);
1208
+ const aliveRef = useRef(true);
1209
+ const sessionRef = useRef(sessionId);
1210
+ sessionRef.current = sessionId;
1211
+ const rowRef = useRef(row);
1212
+ rowRef.current = row;
1213
+ const viewRef = useRef(view);
1214
+ viewRef.current = view;
1215
+ // Words sent and not in the thread yet: the agent's turn has not opened,
1216
+ // so the chat keeps looking (for a minute at most) though the last turn
1217
+ // has ended.
1218
+ const pendingRef = useRef(null);
1219
+ pendingRef.current = state.pending ? (pendingRef.current?.words === state.pending ? pendingRef.current : { words: state.pending, at: Date.now() }) : null;
1220
+
1221
+ useEffect(() => {
1222
+ aliveRef.current = true;
1223
+ return () => {
1224
+ aliveRef.current = false;
1225
+ };
1226
+ }, []);
1227
+
1228
+ // Another record opened in the same chat: its own thread.
1229
+ const lastKey = useRef(storeKey);
1230
+ useEffect(() => {
1231
+ if (lastKey.current === storeKey) return;
1232
+ lastKey.current = storeKey;
1233
+ const kept = keptThread(storeKey);
1234
+ setSessionId(kept);
1235
+ setState({ data: null, loading: Boolean(kept), sending: false, pending: null, error: null, code: null, unavailable: false });
1236
+ }, [storeKey]);
1237
+
1238
+ // Another reader of this thread started it, dropped it or opened another
1239
+ // chat in it: follow (a pane that stays mounted beside the work, the Today
1240
+ // ask bar, a button on a screen). The one that made the change does not
1241
+ // hear itself.
1242
+ useEffect(() => {
1243
+ if (!storeKey || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
1244
+ const on = (e) => {
1245
+ const d = e.detail;
1246
+ if (!d || d.key !== storeKey || d.from === me) return;
1247
+ const next = d.sessionId ?? null;
1248
+ if (next === sessionRef.current) return;
1249
+ setSessionId(next);
1250
+ setState({ data: null, loading: Boolean(next), sending: false, pending: null, error: null, code: null, unavailable: false });
1251
+ };
1252
+ window.addEventListener(THREAD_KEPT, on);
1253
+ return () => window.removeEventListener(THREAD_KEPT, on);
1254
+ }, [storeKey, me]);
1255
+
1256
+ const forget = useCallback(() => {
1257
+ keepThread(storeKey, null, me);
1258
+ setSessionId(null);
1259
+ }, [storeKey, me]);
1260
+
1261
+ // Follow the thread: at once, then while the agent works or waits. With
1262
+ // the sidebar's view (the `agent-pane` block) a working thread is also
1263
+ // STREAMED (0.8.1, D27 phase 4): the gateway pushes the thread after each
1264
+ // burst of the agent's events, so text and steps appear as they happen.
1265
+ // The poll stays as the safety net: it looks every few seconds while the
1266
+ // stream is alive and takes over when it ends, breaks, or does not exist.
1267
+ useEffect(() => {
1268
+ if (!sessionId) return undefined;
1269
+ const controller = new AbortController();
1270
+ let alive = true;
1271
+ let timer = null;
1272
+ let inflight = false;
1273
+ let streaming = false;
1274
+ let streamOff = false;
1275
+ let quickEnds = 0;
1276
+ let lastLine = 0;
1277
+ let streamStop = null;
1278
+ const schedule = (ms) => {
1279
+ clearTimeout(timer);
1280
+ timer = null;
1281
+ if (alive && ms && screen.shown()) timer = setTimeout(load, ms);
1282
+ };
1283
+ const wordsLanded = (data, words) =>
1284
+ Boolean(words) && ((data.turns ?? []).some((t) => t.member === words) || (data.view?.messages ?? []).some((m) => m.role === "user" && m.text === words));
1285
+ const applyFrame = (frame) => {
1286
+ if (!alive) return;
1287
+ if (frame.desk_origin) publishDesk(frame.desk_origin);
1288
+ setState((s) => ({
1289
+ ...s,
1290
+ data: { ...(s.data ?? {}), ...frame, turns: s.data?.turns ?? [] },
1291
+ loading: false,
1292
+ error: null,
1293
+ code: null,
1294
+ unavailable: false,
1295
+ pending: wordsLanded(frame, s.pending) ? null : s.pending,
1296
+ }));
1297
+ };
1298
+ const openStream = () => {
1299
+ if (!viewRef.current || streaming || streamOff || !alive || !screen.shown()) return;
1300
+ streaming = true;
1301
+ lastLine = Date.now();
1302
+ const began = Date.now();
1303
+ const stop = new AbortController();
1304
+ streamStop = stop;
1305
+ // No line (not even a ping) for 25 s: the stream is dead, not quiet.
1306
+ const watchdog = setInterval(() => {
1307
+ if (Date.now() - lastLine > 25000) stop.abort();
1308
+ }, 5000);
1309
+ streamThread(sessionId, { signal: stop.signal, onLine: () => (lastLine = Date.now()), onThread: applyFrame }).then((r) => {
1310
+ clearInterval(watchdog);
1311
+ streaming = false;
1312
+ streamStop = null;
1313
+ if (!alive) return;
1314
+ // A claw with no stream, or a thread it will not stream: poll.
1315
+ if (!r.ok && !r.aborted && (r.unavailable || r.status === 404 || r.status === 403)) streamOff = true;
1316
+ // Three streams in a row that ended at once: stop trying.
1317
+ quickEnds = Date.now() - began < 2000 ? quickEnds + 1 : 0;
1318
+ if (quickEnds >= 3) streamOff = true;
1319
+ load(); // the canonical read, and a new stream if it is still going
1320
+ });
1321
+ };
1322
+ const load = async () => {
1323
+ clearTimeout(timer);
1324
+ timer = null;
1325
+ if (!alive || inflight || !screen.shown()) return;
1326
+ // While a stream is alive it is the fresher view: look rarely.
1327
+ if (streaming && Date.now() - lastLine < 20000) {
1328
+ schedule(CHAT_WORKING_MS * 3);
1329
+ return;
1330
+ }
1331
+ inflight = true;
1332
+ const r = await readThread(sessionId, { signal: controller.signal, view: viewRef.current });
1333
+ inflight = false;
1334
+ if (!alive || r.aborted) return;
1335
+ if (r.ok && r.data) {
1336
+ const data = r.data;
1337
+ if (data.desk_origin) publishDesk(data.desk_origin);
1338
+ setState((s) => ({
1339
+ ...s,
1340
+ data,
1341
+ loading: false,
1342
+ error: null,
1343
+ code: null,
1344
+ unavailable: false,
1345
+ pending: wordsLanded(data, s.pending) ? null : s.pending,
1346
+ }));
1347
+ const turns = data.turns ?? [];
1348
+ const last = turns[turns.length - 1];
1349
+ const waitingForTurn = pendingRef.current && Date.now() - pendingRef.current.at < 60000;
1350
+ const working = CHAT_LIVE.has(data.state) || last?.status === "working" || Boolean(waitingForTurn);
1351
+ if (working && CHAT_LIVE.has(data.state)) openStream();
1352
+ schedule(working ? CHAT_WORKING_MS : data.state === "needs_you" ? CHAT_WAITING_MS : 0);
1353
+ return;
1354
+ }
1355
+ if (r.status === 403 || r.status === 404) {
1356
+ if (r.code) {
1357
+ // A teammate's thread, or one this app no longer has: start fresh.
1358
+ forget();
1359
+ setState((s) => ({ ...s, data: null, loading: false }));
1360
+ return;
1361
+ }
1362
+ setState((s) => ({ ...s, loading: false, unavailable: true }));
1363
+ return;
1364
+ }
1365
+ setState((s) => ({ ...s, loading: false, error: r.error ?? null }));
1366
+ schedule(CHAT_WORKING_MS * 4); // a blip: look again, a little later
1367
+ };
1368
+ const screen = watchShown(load);
1369
+ load();
1370
+ return () => {
1371
+ alive = false;
1372
+ controller.abort();
1373
+ streamStop?.abort();
1374
+ clearTimeout(timer);
1375
+ screen.stop();
1376
+ };
1377
+ }, [sessionId, tick, forget]);
1378
+
1379
+ const send = useCallback(
1380
+ async (message, { files } = {}) => {
1381
+ const text = String(message ?? "").trim();
1382
+ if (!text) return { ok: false, error: "nothing to ask" };
1383
+ setState((s) => ({ ...s, sending: true, pending: text, error: null, code: null, notice: null }));
1384
+ const asking = (session) =>
1385
+ ask({ text, row: rowRef.current ?? undefined, how, job, files, sessionId: session ?? undefined, open: false });
1386
+ let result = await asking(sessionRef.current);
1387
+ if (!result.ok && sessionRef.current && START_FRESH.has(result.code)) result = await asking(null);
1388
+ if (!aliveRef.current) return result;
1389
+ if (!result.ok) {
1390
+ const unavailable = result.status === 404 && !result.code;
1391
+ setState((s) => ({
1392
+ ...s,
1393
+ sending: false,
1394
+ pending: null,
1395
+ error: unavailable ? null : (result.error ?? "The ask could not be sent."),
1396
+ code: result.code ?? null,
1397
+ unavailable: unavailable || s.unavailable,
1398
+ }));
1399
+ return result;
1400
+ }
1401
+ announceAsk();
1402
+ const request = result.data?.request ?? null;
1403
+ const next = request?.app_session_id ?? sessionRef.current ?? null;
1404
+ if (next) {
1405
+ keepThread(storeKey, next, me);
1406
+ if (next !== sessionRef.current) setSessionId(next);
1407
+ else reload();
1408
+ }
1409
+ setState((s) => ({
1410
+ ...s,
1411
+ sending: false,
1412
+ // The ask was taken but waits its turn: a file is not kept for later.
1413
+ notice:
1414
+ result.data?.files_attached === false
1415
+ ? "Your files did not go with that message because it is waiting its turn. Attach them again once it starts."
1416
+ : null,
1417
+ data: s.data && next === s.data.session_id ? { ...s.data, state: "working", request: request ?? s.data.request } : s.data,
1418
+ }));
1419
+ return result;
1420
+ },
1421
+ [how, job, storeKey, reload, me],
1422
+ );
1423
+
1424
+ const request = state.data?.request ?? null;
1425
+ const stop = useCallback(async () => {
1426
+ if (!request?.id) return { ok: false, error: "nothing to stop" };
1427
+ const r = await stopRequest(request.id);
1428
+ reload();
1429
+ return r;
1430
+ }, [request?.id, reload]);
1431
+ const needsAt = state.data?.needs?.at ?? null;
1432
+ const answer = useCallback(
1433
+ async ({ optionId, text, answers } = {}) => {
1434
+ if (!request?.id) return { ok: false, error: "nothing to answer" };
1435
+ const r = await sendAnswer(request.id, { optionId, text, answers, at: needsAt ?? undefined });
1436
+ if (r.ok) {
1437
+ announceAsk();
1438
+ setState((s) => (s.data ? { ...s, error: null, data: { ...s.data, state: "working", needs: null } } : s));
1439
+ }
1440
+ reload();
1441
+ return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1442
+ },
1443
+ [request?.id, needsAt, reload],
1444
+ );
1445
+ const openOnDesk = useCallback(() => (request ? openRequest(request) : false), [request]);
1446
+ const newChat = useCallback(() => {
1447
+ forget();
1448
+ setState({ data: null, loading: false, sending: false, pending: null, error: null, code: null, unavailable: false });
1449
+ }, [forget]);
1450
+ // Another of the member's threads in this app (the pane's Chats list):
1451
+ // kept as this chat's, so the same record opens on it next time.
1452
+ const openSession = useCallback(
1453
+ (next) => {
1454
+ const id = typeof next === "string" && next ? next : null;
1455
+ if (!id || id === sessionRef.current) return;
1456
+ keepThread(storeKey, id, me);
1457
+ setSessionId(id);
1458
+ setState({ data: null, loading: true, sending: false, pending: null, error: null, code: null, unavailable: false });
1459
+ },
1460
+ [storeKey, me],
1461
+ );
1462
+
1463
+ const data = state.data;
1464
+ return {
1465
+ sessionId,
1466
+ state: data?.state ?? (state.sending ? "working" : null),
1467
+ needs: data?.needs ?? null,
1468
+ turns: data?.turns ?? [],
1469
+ view: data?.view ?? null,
1470
+ // Does the next message go on in this same request (D29)? True while it
1471
+ // is open and after a Stop, a finish or a failed turn (the platform
1472
+ // opens the closed request again, in the same session), false only for
1473
+ // a plan or a piece of one, a change sent up, a counted job and a
1474
+ // hand-off never started: those get a new request. Unknown (an older
1475
+ // claw, or before the first read): true.
1476
+ sameConversation: data?.same_conversation ?? true,
1477
+ place: data?.thread ?? "app",
1478
+ request,
1479
+ pending: state.pending,
1480
+ sending: state.sending,
1481
+ loading: state.loading,
1482
+ error: state.error,
1483
+ code: state.code,
1484
+ // A line to show beside the thread (files that did not go), or null.
1485
+ notice: state.notice ?? null,
1486
+ unavailable: state.unavailable,
1487
+ send,
1488
+ stop,
1489
+ answer,
1490
+ openOnDesk,
1491
+ newChat,
1492
+ openSession,
1493
+ reload,
1494
+ };
1495
+ }
1496
+
1497
+ /** How often the Chats list looks while a thread is going, and otherwise. */
1498
+ const CHATS_WORKING_MS = 6000;
1499
+ const CHATS_IDLE_MS = 30000;
1500
+
1501
+ /** A thread's request status as the Chats list marks it: waiting on the
1502
+ * member, or the agent is on it. */
1503
+ function chatMark(status) {
1504
+ if (status === "needs_you") return "needs";
1505
+ if (status === "queued" || status === "routing" || status === "installing" || status === "setting_up" || status === "building" || status === "working") return "working";
1506
+ return null;
1507
+ }
1508
+
1509
+ /**
1510
+ * The member's own threads in this app, newest first, for the `agent-pane`
1511
+ * block's Chats list (GET _claw/threads, 0.8.1, D27):
1512
+ *
1513
+ * const { items, loading, error, unavailable } = useThreadList();
1514
+ *
1515
+ * items [{ sessionId, requestId, title, mark: "needs" | "working" | null, at }]
1516
+ *
1517
+ * Looks every 30 s, every 6 s while one is going or waits, and at once
1518
+ * when an ask is sent. A claw from before the door is `unavailable`: the
1519
+ * pane draws no list.
1520
+ */
1521
+ export function useThreadList({ every = CHATS_IDLE_MS } = {}) {
1522
+ const watched = useWatched((signal) => readThreads({ signal }), "threads", {
1523
+ next: (data) => (listOf(data?.threads).some((t) => chatMark(t?.status)) ? CHATS_WORKING_MS : every),
1524
+ listen: onAsked,
1525
+ });
1526
+ const items = useMemo(
1527
+ () =>
1528
+ listOf(watched.data?.threads)
1529
+ .filter((t) => t && typeof t.session_id === "string")
1530
+ .map((t) => ({
1531
+ sessionId: t.session_id,
1532
+ requestId: t.request_id ?? null,
1533
+ title: typeof t.title === "string" && t.title.trim() ? t.title.trim() : "Untitled",
1534
+ mark: chatMark(t.status),
1535
+ at: t.updated_at ?? null,
1536
+ })),
1537
+ [watched.data],
1538
+ );
1539
+ return { items, loading: watched.loading, error: watched.error, unavailable: watched.unavailable, reload: watched.reload };
1540
+ }
1541
+
1542
+ /**
1543
+ * The desk's agent sidebar, as the app's own pane (0.8.1, D27; the
1544
+ * `agent-pane` block). useAgentThread with the sidebar's view, plus what
1545
+ * the sidebar has around its chat:
1546
+ *
1547
+ * const chat = useAgentPane({ row: { kind: "person", id, name } });
1548
+ * <AgentPane chat={chat} title={`Ask about ${name}`} />
1549
+ *
1550
+ * Everything useAgentThread returns (`view` is set), and
1551
+ *
1552
+ * chats { items, loading, error, unavailable }: the member's
1553
+ * threads in this app (useThreadList); openSession(id)
1554
+ * switches to one
1555
+ * callDetail (callId) → Promise<{ ok, call }>: one step of this
1556
+ * thread, opened (platform.js threadCall); the pane asks it
1557
+ * when a member opens a step
1558
+ * openRequest (requestId): opens a request's thread on the desk
1559
+ * open ({ kind: "app", id } | { kind: "file", path, where,
1560
+ * appRef }): opens an app or a file it made on the desk
1561
+ * (the pane's default "Open")
1562
+ *
1563
+ * Options are useAgentThread's: `row` (the record on screen, kept as that
1564
+ * record's own thread), `how` (for the agent only), `job`, `key`.
1565
+ */
1566
+ export function useAgentPane(options = {}) {
1567
+ const chat = useAgentThread({ ...options, view: true });
1568
+ const list = useThreadList();
1569
+ const sessionId = chat.sessionId;
1570
+ const callDetail = useCallback(
1571
+ (callId) => (sessionId ? readThreadCall(sessionId, callId) : Promise.resolve({ ok: false, status: 0, error: "no thread", unavailable: false })),
1572
+ [sessionId],
1573
+ );
1574
+ return {
1575
+ ...chat,
1576
+ chats: { items: list.items, loading: list.loading, error: list.error, unavailable: list.unavailable },
1577
+ callDetail,
1578
+ // "Open in Makersclaw" under a change the agent sent up: the desk opens
1579
+ // that request's thread (a member's press).
1580
+ openRequest: (id) => openRequest(id),
1581
+ // "Open" under an app or a file the agent made (D27 phase 4): the desk
1582
+ // opens it, on the member's press. → true when the desk was asked.
1583
+ open: (target) => (target?.kind === "app" ? openApp(target.id) : target?.kind === "file" ? openFile(target) : false),
1584
+ };
1585
+ }
1586
+
1587
+ // ---------------------------------------------------------------------------
1588
+ // The kit (0.8.1, D15): the app shows its agent's work itself. Each hook
1589
+ // feeds one or more of the kit's blocks; the blocks take the hook's
1590
+ // answer as props and never read a door themselves.
1591
+
1592
+ /** What a kit read shows before its first answer, and with nothing to
1593
+ * read (`enabled` false, no id). `key` names the read the state belongs
1594
+ * to: a state kept for another read is not shown. */
1595
+ const WATCH_READING = { key: null, data: null, loading: true, error: null, code: null, unavailable: false };
1596
+ const WATCH_NOTHING = { ...WATCH_READING, loading: false };
1597
+
1598
+ /**
1599
+ * One kit door, followed: read when `key` changes and on reload(), then
1600
+ * again after `next(data)` ms while the page is shown (0 or nothing: no
1601
+ * timer). Hidden, nothing is asked; coming back looks at once. A missing
1602
+ * door (`unavailable`) is looked at once a minute and leaves `error`
1603
+ * null; a refusal keeps the last answer and sets `error` and `code`. A
1604
+ * `listen` subscription (onAsked) makes it look at once. Aborts what is
1605
+ * out on unmount and when the key changes.
1606
+ * → { data, loading, error, code, unavailable, reload }
1607
+ */
1608
+ function useWatched(read, key, { enabled = true, next, listen } = {}) {
1609
+ const [state, setState] = useState(WATCH_READING);
1610
+ const [tick, setTick] = useState(0);
1611
+ const readRef = useRef(read);
1612
+ readRef.current = read;
1613
+ const nextRef = useRef(next);
1614
+ nextRef.current = next;
1615
+ const reload = useCallback(() => setTick((t) => t + 1), []);
1616
+
1617
+ useEffect(() => {
1618
+ if (!enabled) return undefined;
1619
+ const controller = new AbortController();
1620
+ let alive = true;
1621
+ let timer = null;
1622
+ let inflight = false;
1623
+ let again = false;
1624
+ let unavailable = false;
1625
+ let last = null;
1626
+
1627
+ const schedule = () => {
1628
+ clearTimeout(timer);
1629
+ timer = null;
1630
+ if (!alive || !screen.shown()) return;
1631
+ if (again) {
1632
+ again = false;
1633
+ load();
1634
+ return;
1635
+ }
1636
+ const ms = unavailable ? BACKOFF_MS : Number(nextRef.current?.(last)) || 0;
1637
+ if (ms > 0) timer = setTimeout(load, ms);
1638
+ };
1639
+ const load = async () => {
1640
+ clearTimeout(timer);
1641
+ timer = null;
1642
+ if (!alive || !screen.shown()) return;
1643
+ if (inflight) {
1644
+ again = true;
1645
+ return;
1646
+ }
1647
+ inflight = true;
1648
+ const r = await readRef.current(controller.signal);
1649
+ inflight = false;
1650
+ if (!alive || r?.aborted) return;
1651
+ unavailable = Boolean(r?.unavailable);
1652
+ if (r?.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
1653
+ last = r.data;
1654
+ setState({ key, data: r.data, loading: false, error: null, code: null, unavailable: false });
1655
+ } else {
1656
+ const why = r?.ok
1657
+ ? { error: "the platform answered something unexpected", code: null, unavailable: false }
1658
+ : unavailable
1659
+ ? { error: null, code: null, unavailable: true }
1660
+ : { error: r?.error || "the platform did not answer", code: r?.code ?? null, unavailable: false };
1661
+ setState((s) => ({ key, data: s.key === key ? s.data : null, loading: false, ...why }));
1662
+ }
1663
+ schedule();
1664
+ };
1665
+
1666
+ const screen = watchShown(load);
1667
+ const unsubscribe = listen ? listen(load) : null;
1668
+ load();
1669
+ return () => {
1670
+ alive = false;
1671
+ controller.abort();
1672
+ clearTimeout(timer);
1673
+ screen.stop();
1674
+ unsubscribe?.();
1675
+ };
1676
+ }, [key, enabled, tick]);
1677
+
1678
+ const mine = enabled ? (state.key === key ? state : WATCH_READING) : WATCH_NOTHING;
1679
+ return { data: mine.data, loading: mine.loading, error: mine.error, code: mine.code, unavailable: mine.unavailable, reload };
1680
+ }
1681
+
1682
+ /** How often useRuns and useJobs look while a run is going. */
1683
+ const RUNNING_MS = 5000;
1684
+ /** How often useRun looks at a run that is going. */
1685
+ const RUN_FOLLOW_MS = 2500;
1686
+ /** How often useConnections looks while an account is being connected,
1687
+ * and for how long. */
1688
+ const CONNECTING_MS = 3000;
1689
+ const CONNECTING_FOR_MS = 120000;
1690
+
1691
+ const NO_MAY = Object.freeze({ run: false, switch: false, pause: false });
1692
+ const listOf = (value) => (Array.isArray(value) ? value : NONE);
1693
+
1694
+ /**
1695
+ * The app's declared jobs (its schedule), for the `schedule-card` and
1696
+ * `workflow-view` blocks (GET _claw/jobs):
1697
+ *
1698
+ * const { jobs, app, budget, may, busy, runNow, setEnabled } = useJobs();
1699
+ *
1700
+ * jobs [{ id, name, kind, topic, does, cron, tz, words, enabled,
1701
+ * next_run_at, last_run_at, last_run, plan }]
1702
+ * app { status, live } or null: live is the app's Live switch
1703
+ * budget { week_cap_credits, week_used_credits } or null (no cap)
1704
+ * credits the workspace's credits, as _claw/me gives them, or null
1705
+ * may { run, switch, pause }: what this member may press. Hide a
1706
+ * control they may not; the door refuses it anyway
1707
+ * busy { [job id]: "run" | "switch" } while a press is out
1708
+ * appBusy "pause" | "resume" while the Live switch's press is out
1709
+ * loading, error, code, unavailable, reload
1710
+ *
1711
+ * Actions, each resolving the platform call's answer and looking again
1712
+ * after: runNow(id) (spends: show its price on the button), setEnabled(id,
1713
+ * on), pause(), resume(). Looks every `every` ms (30 s), every 5 s while
1714
+ * a job's last run is going.
1715
+ */
1716
+ export function useJobs({ every = 30000 } = {}) {
1717
+ const watched = useWatched((signal) => readJobs({ signal }), "jobs", {
1718
+ next: (data) => (listOf(data?.jobs).some((j) => j?.last_run?.status === "running") ? RUNNING_MS : every),
1719
+ listen: onAsked,
1720
+ });
1721
+ const { data, reload } = watched;
1722
+ const [busy, setBusy] = useState({});
1723
+ const [appBusy, setAppBusy] = useState(null);
1724
+ const [patched, setPatched] = useState({}); // jobs as a switch's answer gave them, until the next read
1725
+ const aliveRef = useRef(true);
1726
+ useEffect(() => {
1727
+ aliveRef.current = true;
1728
+ return () => {
1729
+ aliveRef.current = false;
1730
+ };
1731
+ }, []);
1732
+ useEffect(() => setPatched((p) => (Object.keys(p).length ? {} : p)), [data]);
1733
+
1734
+ const mark = useCallback((id, what) => {
1735
+ if (!aliveRef.current) return;
1736
+ setBusy((b) => {
1737
+ const nextBusy = { ...b };
1738
+ if (what) nextBusy[id] = what;
1739
+ else delete nextBusy[id];
1740
+ return nextBusy;
1741
+ });
1742
+ }, []);
1743
+
1744
+ const runNow = useCallback(
1745
+ async (id) => {
1746
+ mark(id, "run");
1747
+ const r = await startRun(id);
1748
+ mark(id, null);
1749
+ if (r.ok) announceAsk(); // the requests and the runs on the page look at once
1750
+ if (aliveRef.current) reload();
1751
+ return r;
1752
+ },
1753
+ [mark, reload],
1754
+ );
1755
+
1756
+ const setEnabled = useCallback(
1757
+ async (id, on) => {
1758
+ mark(id, "switch");
1759
+ const r = await setJob(id, { enabled: Boolean(on) });
1760
+ mark(id, null);
1761
+ if (!aliveRef.current) return r;
1762
+ if (r.ok && r.data?.job?.id) setPatched((p) => ({ ...p, [r.data.job.id]: r.data.job }));
1763
+ reload();
1764
+ return r;
1765
+ },
1766
+ [mark, reload],
1767
+ );
1768
+
1769
+ const live = useCallback(
1770
+ async (on) => {
1771
+ setAppBusy(on ? "resume" : "pause");
1772
+ const r = await (on ? resumeApp() : pauseApp());
1773
+ if (!aliveRef.current) return r;
1774
+ setAppBusy(null);
1775
+ reload();
1776
+ return r;
1777
+ },
1778
+ [reload],
1779
+ );
1780
+ const pause = useCallback(() => live(false), [live]);
1781
+ const resume = useCallback(() => live(true), [live]);
1782
+
1783
+ const jobs = useMemo(() => listOf(data?.jobs).map((j) => (j && patched[j.id] ? { ...j, ...patched[j.id] } : j)), [data, patched]);
1784
+ return {
1785
+ jobs,
1786
+ app: data?.app ?? null,
1787
+ budget: data?.budget ?? null,
1788
+ credits: data?.credits ?? null,
1789
+ may: data?.may ? { ...NO_MAY, ...data.may } : NO_MAY,
1790
+ busy,
1791
+ appBusy,
1792
+ loading: watched.loading,
1793
+ error: watched.error,
1794
+ code: watched.code,
1795
+ unavailable: watched.unavailable,
1796
+ reload,
1797
+ runNow,
1798
+ setEnabled,
1799
+ pause,
1800
+ resume,
1801
+ };
1802
+ }
1803
+
1804
+ /**
1805
+ * The app's runs, newest first, and their overview, for the `run-history`
1806
+ * and `workflow-view` blocks (GET _claw/runs):
1807
+ *
1808
+ * const { runs, overview } = useRuns({ job: wakeId, limit: 20 });
1809
+ *
1810
+ * runs [{ id, title, state, wake_id, job, trigger, by, number,
1811
+ * started_at, ended_at, duration_ms, credits, report, status }]
1812
+ * `state` is running | done | failed | stopped | needs_you
1813
+ * overview { completed, failed, running, avg_ms, credits_week,
1814
+ * cap_week_credits } or null
1815
+ * loading, error, code, unavailable, reload
1816
+ *
1817
+ * `job` narrows to one job (its wake id), `limit` (20, at most 60). The
1818
+ * jobs' runs and the member's own asks, never a teammate's ask. Looks
1819
+ * every 5 s while a run is going, else every `every` ms (30 s), and at
1820
+ * once after useJobs' Run now.
1821
+ */
1822
+ export function useRuns({ job, limit = 20, every = 30000 } = {}) {
1823
+ const key = JSON.stringify(["runs", job ?? null, limit ?? null]);
1824
+ const watched = useWatched((signal) => readRuns({ job, limit, signal }), key, {
1825
+ next: (data) => (listOf(data?.runs).some((r) => r?.state === "running") ? RUNNING_MS : every),
1826
+ listen: onAsked,
1827
+ });
1828
+ const { data, ...rest } = watched;
1829
+ return { runs: listOf(data?.runs), overview: data?.overview ?? null, ...rest };
1830
+ }
1831
+
1832
+ /**
1833
+ * One run's account, for the `run-history` block's RunAccount and the
1834
+ * `workflow-view` graph (GET _claw/runs/<id>):
1835
+ *
1836
+ * const { run, steps, text, needs } = useRun(selectedId);
1837
+ *
1838
+ * run the run, as useRuns lists it, or null
1839
+ * steps [{ label, status: running | done | failed }], in order
1840
+ * text the agent's report, or null
1841
+ * needs { kind, text } while it waits on the member (answered on the
1842
+ * desk), or null
1843
+ *
1844
+ * Nothing is read while `id` is empty. Looks every 2.5 s while the run is
1845
+ * going; a run that has ended is read once.
1846
+ */
1847
+ export function useRun(id) {
1848
+ const enabled = Boolean(id);
1849
+ const watched = useWatched((signal) => readRun(id, { signal }), JSON.stringify(["run", id ?? null]), {
1850
+ enabled,
1851
+ next: (data) => (data?.run?.state === "running" ? RUN_FOLLOW_MS : 0),
1852
+ });
1853
+ const { data, ...rest } = watched;
1854
+ return { run: data?.run ?? null, steps: listOf(data?.steps), text: data?.text ?? null, needs: data?.needs ?? null, ...rest };
1855
+ }
1856
+
1857
+ /** A request's needs as a needs-you item's kind: the yes (an approval is
1858
+ * one), a question, a spend (a limit), a connect; else as it is. */
1859
+ const NEEDS_KIND = { approval: "yes", yes: "yes", question: "question", limit: "spend", connect: "connect" };
1860
+
1861
+ /**
1862
+ * What waits on this member in this app, as a needs-you item, or null.
1863
+ * Pure, so the tests hold it to each case: only the member's own
1864
+ * requests (`mine`), with a need that blocks (a `note` never does). A
1865
+ * claw whose requests carry no `needs` still says `needs_you`: the item
1866
+ * is then a question, in the request's own line.
1867
+ *
1868
+ * An item also says how the app can settle it (0.8.1, D18/D19): `via` is
1869
+ * "answer" (a question: answer it in place with its `options` and, when
1870
+ * `free`, the member's own words), "press" (the one ask's yes: the
1871
+ * `approval` block's press frame) or null (on the desk: connect, spend,
1872
+ * a tool approval, several questions at once). `needsAt` is the version
1873
+ * of the need an answer names.
1874
+ */
1875
+ export function needsYouItem(request) {
1876
+ if (!request || typeof request !== "object" || !request.mine || ended(request)) return null;
1877
+ const needs = request.needs && typeof request.needs === "object" ? request.needs : null;
1878
+ if (needs && needs.kind === "note") return null;
1879
+ if (!needs && request.status !== "needs_you") return null;
1880
+ const line = typeof request.line === "string" ? request.line.replace(/^Needs you\s*·\s*/, "") : "";
1881
+ return {
1882
+ id: request.id,
1883
+ request,
1884
+ kind: needs ? (NEEDS_KIND[needs.kind] ?? needs.kind ?? "question") : "question",
1885
+ text: (needs?.text || line || request.title || "").trim(),
1886
+ title: request.title ?? "",
1887
+ at: request.updated_at ?? request.created_at ?? null,
1888
+ via: needs?.in_app === "answer" || needs?.in_app === "press" ? needs.in_app : null,
1889
+ options: needs?.in_app === "answer" && Array.isArray(needs.options) ? needs.options : null,
1890
+ free: needs?.free !== false,
1891
+ needsAt: typeof needs?.at === "string" ? needs.at : null,
1892
+ };
1893
+ }
1894
+
1895
+ /**
1896
+ * What waits on this member here, for the `needs-you` block (GET
1897
+ * _claw/requests, the member's own only):
1898
+ *
1899
+ * const { items, working, open, takeBack, stop } = useNeedsYou();
1900
+ *
1901
+ * items [{ id, request, kind: question | yes | spend | connect | …,
1902
+ * text, title, at, via: "answer" | "press" | null, options,
1903
+ * free, needsAt }]: the agent's questions and yeses, newest
1904
+ * first. A question with via "answer" is answered in place
1905
+ * (answer(item, …)); a yes with via "press" is the `approval`
1906
+ * block's press frame; anything else, open(item) takes to
1907
+ * the desk
1908
+ * working the member's own requests the agent is working on now
1909
+ * open(item) the request's conversation on the desk
1910
+ * answer(item, { optionId, text }) answer a question in place (0.8.1):
1911
+ * → { ok } | { ok: false, error, code }; the page's
1912
+ * other readers look again at once
1913
+ * takeBack(item) stop the ask that waits (the frozen "Ready to send")
1914
+ * stop(request) stop a working one ("Stop keeps everything written
1915
+ * so far")
1916
+ * loading, error, code, unavailable, reload
1917
+ *
1918
+ * Looks every `every` ms (8 s) while the page is shown, and at once after
1919
+ * an ask from this page.
1920
+ */
1921
+ export function useNeedsYou({ every = 8000 } = {}) {
1922
+ const often = Math.max(Number(every) || 8000, 1000);
1923
+ const watched = useWatched((signal) => readRequests({ signal }), "needs-you", { next: () => often, listen: onAsked });
1924
+ const { data, reload } = watched;
1925
+ const list = listOf(data?.requests);
1926
+ const items = useMemo(
1927
+ () =>
1928
+ list
1929
+ .map(needsYouItem)
1930
+ .filter(Boolean)
1931
+ .sort((a, b) => String(b.at ?? "").localeCompare(String(a.at ?? ""))),
1932
+ [list],
1933
+ );
1934
+ const working = useMemo(() => list.filter((q) => q?.mine && LIVE.has(q.status) && q.status !== "needs_you" && !needsYouItem(q)), [list]);
1935
+ useEffect(() => {
1936
+ if (data?.desk_origin) publishDesk(data.desk_origin);
1937
+ }, [data]);
1938
+
1939
+ const open = useCallback((item) => openRequest(item?.request ?? item), []);
1940
+ const stopOne = useCallback(
1941
+ async (request) => {
1942
+ const id = request?.id;
1943
+ if (!id) return { ok: false, error: "nothing to stop" };
1944
+ const r = await stopRequest(id);
1945
+ reload();
1946
+ return r;
1947
+ },
1948
+ [reload],
1949
+ );
1950
+ const takeBack = useCallback((item) => stopOne(item?.request ?? item), [stopOne]);
1951
+ const answerOne = useCallback(
1952
+ async (item, { optionId, text } = {}) => {
1953
+ const id = item?.request?.id ?? item?.id;
1954
+ if (!id) return { ok: false, error: "nothing to answer" };
1955
+ const r = await sendAnswer(id, { optionId, text, at: item?.needsAt ?? undefined });
1956
+ if (r.ok) announceAsk();
1957
+ reload();
1958
+ return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1959
+ },
1960
+ [reload],
1961
+ );
1962
+
1963
+ return {
1964
+ items,
1965
+ working,
1966
+ loading: watched.loading,
1967
+ error: watched.error,
1968
+ code: watched.code,
1969
+ unavailable: watched.unavailable,
1970
+ reload,
1971
+ open,
1972
+ answer: answerOne,
1973
+ takeBack,
1974
+ stop: stopOne,
1975
+ };
1976
+ }
1977
+
1978
+ /** How long the press frame has to say it is ready before the card
1979
+ * falls back to the desk's (a frame that cannot load says nothing). */
1980
+ const FRAME_READY_MS = 7000;
1981
+ /** The frame's height, as it asks for it: kept between these. */
1982
+ const FRAME_MIN = 96;
1983
+ const FRAME_MAX = 520;
1984
+ /** What a card shows for a request whose yes is no longer there. */
1985
+ const APPROVAL_GONE = new Set(["nothing_to_approve", "unknown_request", "not_one_yes", "not_your_request"]);
1986
+ const NO_PRESS = { for: null, state: null, height: null, always: false, ready: false };
1987
+
1988
+ /**
1989
+ * The yes, given in the app (0.8.1, D18): the facts of one request's yes
1990
+ * and the Makersclaw-held press frame to embed in the app's own card, for
1991
+ * the `approval` block.
1992
+ *
1993
+ * const approval = useApproval(item.id); // item from useNeedsYou
1994
+ * <Approval approval={approval} title="Send 3 follow-ups"> …the draft… </Approval>
1995
+ *
1996
+ * The app draws the card in its theme; the press is a page of
1997
+ * Makersclaw's in another origin, which shows the SERVER's words (the
1998
+ * agent's question, who, from which account, the first message, the rule
1999
+ * "Always allow this kind" would save) and only counts a press a person
2000
+ * made on a card they can see. App code cannot press it, and this hook
2001
+ * only listens to it.
2002
+ *
2003
+ * approval { request_id, question, label, covers, always, cap_credits,
2004
+ * at, frame: { src, origin }, expires_at } or null
2005
+ * state idle (no request) · loading · ready (the frame is up) ·
2006
+ * approved · declined · stale (the question moved on; looks
2007
+ * again by itself) · gone (nothing waits any more: answered
2008
+ * elsewhere) · signin (the frame needs the member signed in
2009
+ * on the desk) · blocked (the frame did not load in time) ·
2010
+ * unavailable (a claw from before this door) · error
2011
+ * always true once "Always allow this kind" was saved with the yes
2012
+ * fallback true when the frame cannot be used (signin, blocked,
2013
+ * unavailable, error): draw WaitingYes and open the desk
2014
+ * frame { src, ref, title } to spread on the <iframe>
2015
+ * height the px the frame asked for, kept between 96 and 520
2016
+ * loading, error, code, unavailable, reload
2017
+ *
2018
+ * Nothing here approves. A request with no id asks nothing.
2019
+ */
2020
+ export function useApproval(requestId, { fallbackMs = FRAME_READY_MS } = {}) {
2021
+ const id = requestId ? String(requestId) : null;
2022
+ const watched = useWatched((signal) => readApproval(id, { signal }), JSON.stringify(["approval", id]), {
2023
+ enabled: Boolean(id),
2024
+ next: () => 0,
2025
+ });
2026
+ const { data, reload } = watched;
2027
+ const approval = data?.approval ?? null;
2028
+ const src = approval?.frame?.src ?? null;
2029
+ const origin = approval?.frame?.origin ?? null;
2030
+ const frameRef = useRef(null);
2031
+ const [heard, setHeard] = useState(NO_PRESS);
2032
+ const [timedOut, setTimedOut] = useState(null);
2033
+ const mine = heard.for === src ? heard : NO_PRESS;
2034
+
2035
+ useEffect(() => {
2036
+ if (!src || !origin || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
2037
+ const onMessage = (event) => {
2038
+ const m = pressMessage(event, { origin, source: frameRef.current?.contentWindow, request: id });
2039
+ if (!m) return;
2040
+ if (m.state === "ready") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, ready: true }));
2041
+ else if (m.state === "size") {
2042
+ const px = Math.round(Number(m.height));
2043
+ if (Number.isFinite(px) && px > 0) setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, height: Math.min(Math.max(px, FRAME_MIN), FRAME_MAX) }));
2044
+ } else if (m.state === "approved" || m.state === "declined") {
2045
+ setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: m.state, always: m.state === "approved" && m.always === true }));
2046
+ announceAsk();
2047
+ } else if (m.state === "stale") {
2048
+ // The question moved on since this card was drawn: a new ticket, or the news that nothing waits.
2049
+ setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "stale" }));
2050
+ reload();
2051
+ } else if (m.state === "signin") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "signin" }));
2052
+ };
2053
+ window.addEventListener("message", onMessage);
2054
+ const timer = setTimeout(() => setTimedOut(src), Math.max(Number(fallbackMs) || FRAME_READY_MS, 1000));
2055
+ return () => {
2056
+ window.removeEventListener?.("message", onMessage);
2057
+ clearTimeout(timer);
2058
+ };
2059
+ }, [src, origin, id, fallbackMs, reload]);
2060
+
2061
+ let state;
2062
+ if (!id) state = "idle";
2063
+ else if (watched.unavailable) state = "unavailable";
2064
+ else if (watched.error) state = APPROVAL_GONE.has(watched.code) ? "gone" : "error";
2065
+ else if (!approval) state = "loading";
2066
+ else if (mine.state) state = mine.state;
2067
+ else if (mine.ready) state = "ready";
2068
+ else state = timedOut === src ? "blocked" : "ready";
2069
+ const fallback = state === "signin" || state === "blocked" || state === "unavailable" || state === "error";
2070
+
2071
+ return {
2072
+ approval,
2073
+ state,
2074
+ always: mine.always,
2075
+ fallback,
2076
+ frame: approval && src ? { src, ref: frameRef, title: "Approve" } : null,
2077
+ height: mine.height,
2078
+ loading: watched.loading,
2079
+ error: watched.error,
2080
+ code: watched.code,
2081
+ unavailable: watched.unavailable,
2082
+ reload,
2083
+ };
2084
+ }
2085
+
2086
+ /** A time as the key of a read: to the minute, so a `since` made fresh
2087
+ * each render (new Date(Date.now() - …)) does not read again each
2088
+ * render. */
2089
+ function minuteOf(time) {
2090
+ if (time instanceof Date) return Number.isNaN(time.getTime()) ? null : time.toISOString().slice(0, 16);
2091
+ return typeof time === "string" && time ? time.slice(0, 16) : null;
2092
+ }
2093
+
2094
+ /**
2095
+ * Who did what in this app, newest first, for the `activity-log` and
2096
+ * `agent-changes` blocks (GET _claw/data/activity):
2097
+ *
2098
+ * const { rows } = useActivity({ row: deal.id }); // one record's timeline
2099
+ * const { rows } = useActivity({ agent: true, since: askedAt }); // what the agent did
2100
+ *
2101
+ * rows [{ id, at, action, surface, row_ref, detail, data, by }]; `by`
2102
+ * is user:<email>, agent:<email> (on a member's ask) or agent:job
2103
+ * loading, error, code, unavailable, reload
2104
+ *
2105
+ * `row` narrows to one record's lines, `surface` to one surface, `since`
2106
+ * (ISO or a Date, compared to the minute) to lines after it, `limit` (50);
2107
+ * `agent: true` keeps the agent's lines only. Looks every `every` ms (20
2108
+ * s) while the page is shown. lib/door.js's useActivity is the same lines
2109
+ * read once, with no `row` and no polling; this one is the kit's.
2110
+ */
2111
+ export function useActivity({ row, surface, since, limit = 50, agent = false, every = 20000 } = {}) {
2112
+ const sinceRef = useRef(since);
2113
+ sinceRef.current = since;
2114
+ const key = JSON.stringify(["activity", row ?? null, surface ?? null, minuteOf(since), limit ?? null]);
2115
+ const watched = useWatched((signal) => readActivity({ row, surface, since: sinceRef.current, limit, signal }), key, {
2116
+ next: () => every,
2117
+ });
2118
+ const { data, ...rest } = watched;
2119
+ const all = listOf(data?.rows);
2120
+ const rows = useMemo(() => (agent ? all.filter((r) => typeof r?.by === "string" && r.by.startsWith("agent:")) : all), [all, agent]);
2121
+ return { rows, ...rest };
2122
+ }
2123
+
2124
+ /**
2125
+ * Connect one of the app's declared accounts, the way the member's press
2126
+ * should go: inside the desk, ask it to open its connections panel for
2127
+ * that need (every provider of the kind shows; no agent turn); on the
2128
+ * app's own host, fetch the link (POST _claw/connect) and open it in a new
2129
+ * tab. Signing in stays the member's, at the provider.
2130
+ *
2131
+ * The tab is opened before the link is fetched, on the press itself, and
2132
+ * pointed at the link once it comes: a tab opened after an await is a
2133
+ * popup the browser blocks. → { opened: "desk" | "tab" | false,
2134
+ * connected?, error?, code? }. Never throws. useConnections calls it.
2135
+ */
2136
+ export async function connectAccount(key, win = typeof window === "undefined" ? undefined : window) {
2137
+ if (openConnections(key, win)) return { opened: "desk" };
2138
+ let tab = null;
2139
+ try {
2140
+ tab = win?.open?.("about:blank", "_blank") ?? null;
2141
+ if (tab) tab.opener = null;
2142
+ } catch {
2143
+ tab = null;
2144
+ }
2145
+ const r = await connectLink(key);
2146
+ const url = r.ok && typeof r.data?.url === "string" && /^https?:\/\//i.test(r.data.url) ? r.data.url : null;
2147
+ if (!url) {
2148
+ try {
2149
+ tab?.close?.();
2150
+ } catch {
2151
+ /* a tab the browser already took back */
2152
+ }
2153
+ if (r.ok && r.data?.connected) return { opened: false, connected: true };
2154
+ if (r.ok) return { opened: false, error: "no link came back" };
2155
+ return { opened: false, error: r.unavailable ? null : r.error, code: r.code ?? null, unavailable: Boolean(r.unavailable) };
2156
+ }
2157
+ try {
2158
+ if (tab) {
2159
+ tab.location.href = url;
2160
+ return { opened: "tab" };
2161
+ }
2162
+ const opened = win?.open?.(url, "_blank", "noopener");
2163
+ return { opened: opened === undefined ? false : "tab" };
2164
+ } catch {
2165
+ return { opened: false, error: "the browser would not open the link" };
2166
+ }
2167
+ }
2168
+
2169
+ /** Is a real account behind this connection? `connected` is the
2170
+ * workspace listing the toolkit, which can be true while the list of
2171
+ * accounts that meet it is empty: a toolkit that needs no sign-in, or a
2172
+ * leftover (an account removed at the provider). A screen that needs an
2173
+ * account (a mailbox to read, a calendar to sync) asks this, not
2174
+ * `connected`, before it says "Connected" or offers the read: the CRM's
2175
+ * "Connect your mailbox" card said "Connected" over nothing (2026-10-11).
2176
+ * False without an `accounts` list (a claw from before it) only when
2177
+ * `connected` is false too. */
2178
+ export function hasAccount(c) {
2179
+ if (!c || !c.connected) return false;
2180
+ return Array.isArray(c.accounts) ? c.accounts.filter(Boolean).length > 0 : true;
2181
+ }
2182
+
2183
+ /**
2184
+ * The app's declared accounts, for the `connect-card` block (GET
2185
+ * _claw/connect):
2186
+ *
2187
+ * const { connections, connect } = useConnections();
2188
+ * <ConnectCard connections={connections} onConnect={connect} />
2189
+ *
2190
+ * connections [{ key, label, toolkits, required, connected, accounts,
2191
+ * category, category_label }]
2192
+ * connecting the key being connected while it is watched, or null
2193
+ * connect(key) → { opened: "desk" | "tab" | false, error? }
2194
+ * (connectAccount), then looks every 3 s for two minutes
2195
+ * until that account is connected
2196
+ * loading, error, code, unavailable, reload
2197
+ *
2198
+ * Read once, then only while an account is being connected (or every
2199
+ * `every` ms when given), and at once when the page comes back.
2200
+ */
2201
+ export function useConnections({ every } = {}) {
2202
+ const watchRef = useRef(null); // { key, until }
2203
+ const [connecting, setConnecting] = useState(null);
2204
+ const watched = useWatched((signal) => readConnections({ signal }), "connections", {
2205
+ next: (data) => {
2206
+ const watch = watchRef.current;
2207
+ if (watch) {
2208
+ const met = listOf(data?.connections).some((c) => c?.key === watch.key && c.connected);
2209
+ if (met || Date.now() > watch.until) {
2210
+ watchRef.current = null;
2211
+ setConnecting(null);
2212
+ } else return CONNECTING_MS;
2213
+ }
2214
+ return every ?? 0;
2215
+ },
2216
+ });
2217
+ const { data, reload } = watched;
2218
+
2219
+ const connect = useCallback(
2220
+ async (key) => {
2221
+ const r = await connectAccount(key);
2222
+ if (r.opened) {
2223
+ watchRef.current = { key, until: Date.now() + CONNECTING_FOR_MS };
2224
+ setConnecting(key);
2225
+ }
2226
+ reload();
2227
+ return r;
2228
+ },
2229
+ [reload],
2230
+ );
2231
+
2232
+ return {
2233
+ connections: listOf(data?.connections),
2234
+ connecting,
2235
+ loading: watched.loading,
2236
+ error: watched.error,
2237
+ code: watched.code,
2238
+ unavailable: watched.unavailable,
2239
+ reload,
2240
+ connect,
2241
+ };
2242
+ }
2243
+
2244
+ /** What the agent knows and can do, from GET _claw/agent's answer. Pure:
2245
+ * the app's own skills first (from: "app"), then those added on top
2246
+ * (from: "added", installed or not), then its tools — the declared
2247
+ * actions the agent may run. */
2248
+ export function agentKitOf(data) {
2249
+ const own = listOf(data?.own_skills)
2250
+ .filter((s) => s && typeof s.name === "string")
2251
+ .map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "app", installed: true }));
2252
+ const ownNames = new Set(own.map((s) => s.name));
2253
+ const added = listOf(data?.skills)
2254
+ .filter((s) => s && typeof s.name === "string" && !ownNames.has(s.name))
2255
+ .map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "added", installed: Boolean(s.installed) }));
2256
+ const tools = listOf(data?.actions)
2257
+ .filter((a) => a && typeof a.name === "string" && a.agent !== false)
2258
+ .map((a) => ({ name: a.name, description: a.description ?? "" }));
2259
+ return { skills: [...own, ...added], tools };
2260
+ }
2261
+
2262
+ /**
2263
+ * What the app's agent knows and can do, for the `agent-kit` block (GET
2264
+ * _claw/agent): `skills` [{ name, title, when, from: "app" | "added",
2265
+ * installed }] and `tools` [{ name, description }] (agentKitOf), with
2266
+ * loading, error, code, unavailable, reload. Read once.
2267
+ */
2268
+ export function useAgentKit() {
2269
+ const watched = useWatched((signal) => readAgentKit({ signal }), "agent-kit");
2270
+ const { data, ...rest } = watched;
2271
+ const kit = useMemo(() => agentKitOf(data), [data]);
2272
+ return { ...kit, ...rest };
2273
+ }
2274
+
2275
+ /** How long useLlmAction waits for the input to settle before it asks
2276
+ * for a price. */
2277
+ const ESTIMATE_AFTER_MS = 400;
2278
+
2279
+ /**
2280
+ * One model call on what the screen holds, with its price before the
2281
+ * press, for the `ai-action` and `cost` blocks:
2282
+ *
2283
+ * const shorter = useLlmAction({ input: text, system: "Make it shorter.", task: "shorten" });
2284
+ * <AiAction label="Shorter" action={shorter} onUse={(r) => setText(r.text)} />
2285
+ *
2286
+ * estimate { credits, credits_max } or null: the price, asked of the
2287
+ * platform (llmEstimate: priced, never sent to a model,
2288
+ * never charged) once per distinct input, 400 ms after it
2289
+ * settles. Null for an empty input, and on a claw with no
2290
+ * estimate yet. While a new input is priced the last price
2291
+ * stays, with `estimating` true
2292
+ * estimating a price is being asked for
2293
+ * run() the call, with the same arguments → llm()'s answer
2294
+ * result, loading, error, unavailable as useLlm
2295
+ * reset() back to nothing shown
2296
+ *
2297
+ * Options: llm()'s — input, system, schema, tier, task, maxTokens — and
2298
+ * `estimate` (default true; false asks no price).
2299
+ */
2300
+ export function useLlmAction({ input, system, schema, tier, task, maxTokens, estimate = true } = {}) {
2301
+ const call = useLlm();
2302
+ const argsRef = useRef(null);
2303
+ argsRef.current = { input, opts: { system, schema, tier, task, maxTokens } };
2304
+ const priced = Boolean(estimate) && saidSomething(input);
2305
+ let key = null;
2306
+ if (priced) {
2307
+ try {
2308
+ key = JSON.stringify([input, system ?? null, schema ?? null, tier ?? null, task ?? null, maxTokens ?? null]);
2309
+ } catch {
2310
+ key = null; // an input that is not JSON: llm() refuses it too
2311
+ }
2312
+ }
2313
+ const cacheRef = useRef(new Map());
2314
+ const [price, setPrice] = useState({ key: null, value: null, estimating: false });
2315
+
2316
+ useEffect(() => {
2317
+ if (!key) return undefined;
2318
+ const known = cacheRef.current.get(key);
2319
+ if (known !== undefined) {
2320
+ setPrice({ key, value: known, estimating: false });
2321
+ return undefined;
2322
+ }
2323
+ const controller = new AbortController();
2324
+ const timer = setTimeout(async () => {
2325
+ setPrice((p) => ({ ...p, estimating: true }));
2326
+ const { input: now, opts } = argsRef.current;
2327
+ const r = await llmEstimate(now, { ...opts, signal: controller.signal });
2328
+ if (controller.signal.aborted || r.aborted) return;
2329
+ const value =
2330
+ r.ok && r.estimate && typeof r.estimate === "object" && typeof r.estimate.credits === "number"
2331
+ ? { credits: r.estimate.credits, credits_max: typeof r.estimate.credits_max === "number" ? r.estimate.credits_max : r.estimate.credits }
2332
+ : null;
2333
+ if (r.ok) cacheRef.current.set(key, value);
2334
+ setPrice({ key, value, estimating: false });
2335
+ }, ESTIMATE_AFTER_MS);
2336
+ return () => {
2337
+ clearTimeout(timer);
2338
+ controller.abort();
2339
+ };
2340
+ }, [key]);
2341
+
2342
+ const run = useCallback(() => {
2343
+ const { input: now, opts } = argsRef.current;
2344
+ return call.run(now, opts);
2345
+ }, [call.run]);
2346
+
2347
+ return {
2348
+ estimate: key ? price.value : null,
2349
+ estimating: Boolean(key) && (price.estimating || (price.key !== key && !cacheRef.current.has(key))),
2350
+ run,
2351
+ result: call.result,
2352
+ loading: call.loading,
2353
+ error: call.error,
2354
+ unavailable: call.unavailable,
2355
+ reset: call.reset,
2356
+ };
2357
+ }
2358
+
2359
+ // The automations engine's hooks (useAutomations, useAutomation,
2360
+ // useAutomationRun) live in lib/automations.js, with their doors; a surface
2361
+ // imports them from here with the others.
2362
+ export * from "./automations";