makersclaw 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/r/activity-log.json +52 -0
  4. package/r/agent-changes.json +51 -0
  5. package/r/agent-chat.json +53 -0
  6. package/r/agent-column.json +85 -0
  7. package/r/agent-dock.json +57 -0
  8. package/r/agent-kit.json +49 -0
  9. package/r/agent-marks.json +49 -0
  10. package/r/agent-pane.json +93 -0
  11. package/r/ai-action.json +56 -0
  12. package/r/annotate.json +3 -3
  13. package/r/approval-request.json +2 -2
  14. package/r/approval.json +54 -0
  15. package/r/ask-bar.json +50 -0
  16. package/r/audio-player.json +2 -2
  17. package/r/auth-form.json +2 -2
  18. package/r/automation-editor.json +66 -0
  19. package/r/automation-list.json +57 -0
  20. package/r/automation-model.json +118 -0
  21. package/r/automations-screen.json +71 -0
  22. package/r/board.json +2 -2
  23. package/r/calendar-view.json +3 -3
  24. package/r/cart.json +2 -2
  25. package/r/chart.json +2 -2
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +2 -2
  28. package/r/code-editor.json +2 -2
  29. package/r/comment-box.json +3 -3
  30. package/r/connect-card.json +51 -0
  31. package/r/contact-section.json +2 -2
  32. package/r/core.json +13 -7
  33. package/r/cost.json +51 -0
  34. package/r/csv-import.json +3 -3
  35. package/r/data-picker.json +60 -0
  36. package/r/data-table.json +2 -2
  37. package/r/diagram.json +2 -2
  38. package/r/doc-editor.json +2 -2
  39. package/r/doc-page.json +3 -3
  40. package/r/email-render.json +2 -2
  41. package/r/faq.json +2 -2
  42. package/r/file-upload.json +2 -2
  43. package/r/first-run.json +63 -0
  44. package/r/fix-note.json +57 -0
  45. package/r/flow-canvas.json +2 -2
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +2 -2
  48. package/r/gantt.json +3 -3
  49. package/r/job-panel.json +2 -2
  50. package/r/json-editor.json +3 -3
  51. package/r/library-shelf.json +56 -0
  52. package/r/map.json +2 -2
  53. package/r/margin-notes.json +67 -0
  54. package/r/menubar.json +2 -2
  55. package/r/mind-map.json +3 -3
  56. package/r/navigation-menu.json +2 -2
  57. package/r/needs-you.json +55 -0
  58. package/r/order-detail.json +2 -2
  59. package/r/org-chart.json +3 -3
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +2 -2
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +2 -2
  65. package/r/product-detail.json +2 -2
  66. package/r/product-grid.json +3 -3
  67. package/r/record-form.json +2 -2
  68. package/r/record-grid.json +52 -0
  69. package/r/record-notes.json +4 -4
  70. package/r/registry.json +4086 -1512
  71. package/r/run-history.json +52 -0
  72. package/r/run-view.json +80 -0
  73. package/r/schedule-card.json +54 -0
  74. package/r/schedule-engine.json +79 -0
  75. package/r/schedule-gantt.json +83 -0
  76. package/r/schedule-ruler.json +64 -0
  77. package/r/search-dialog.json +2 -2
  78. package/r/settings-list.json +2 -2
  79. package/r/setup-checklist.json +2 -2
  80. package/r/share-dialog.json +2 -2
  81. package/r/sidebar.json +2 -2
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +2 -2
  84. package/r/source-receipt.json +52 -0
  85. package/r/spreadsheet.json +3 -3
  86. package/r/stat-cards.json +3 -3
  87. package/r/step-picker.json +53 -0
  88. package/r/step-settings.json +72 -0
  89. package/r/suggestion.json +58 -0
  90. package/r/sum-bar.json +64 -0
  91. package/r/task-list.json +3 -3
  92. package/r/task-model.json +2 -2
  93. package/r/testimonials.json +2 -2
  94. package/r/tier-map.json +67 -0
  95. package/r/time-grid.json +73 -0
  96. package/r/time-strip.json +64 -0
  97. package/r/top-bar.json +66 -0
  98. package/r/transaction-list.json +2 -2
  99. package/r/trigger-picker.json +61 -0
  100. package/r/whiteboard.json +3 -3
  101. package/r/workflow-view.json +55 -0
  102. package/registry/blocks/activity-log/README.md +112 -0
  103. package/registry/blocks/activity-log/activity-log.jsx +351 -0
  104. package/registry/blocks/activity-log/block.json +33 -0
  105. package/registry/blocks/activity-log/demo.jsx +48 -0
  106. package/registry/blocks/activity-log/index.js +4 -0
  107. package/registry/blocks/agent-changes/README.md +94 -0
  108. package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
  109. package/registry/blocks/agent-changes/block.json +28 -0
  110. package/registry/blocks/agent-changes/demo.jsx +39 -0
  111. package/registry/blocks/agent-changes/index.js +3 -0
  112. package/registry/blocks/agent-chat/README.md +145 -0
  113. package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
  114. package/registry/blocks/agent-chat/block.json +30 -0
  115. package/registry/blocks/agent-chat/demo.jsx +165 -0
  116. package/registry/blocks/agent-chat/index.js +3 -0
  117. package/registry/blocks/agent-column/README.md +197 -0
  118. package/registry/blocks/agent-column/agent-column.jsx +492 -0
  119. package/registry/blocks/agent-column/block.json +67 -0
  120. package/registry/blocks/agent-column/column-picker.jsx +163 -0
  121. package/registry/blocks/agent-column/demo.jsx +50 -0
  122. package/registry/blocks/agent-column/index.js +7 -0
  123. package/registry/blocks/agent-column/model.js +164 -0
  124. package/registry/blocks/agent-column/receipt.jsx +483 -0
  125. package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
  126. package/registry/blocks/agent-dock/README.md +210 -0
  127. package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
  128. package/registry/blocks/agent-dock/block.json +47 -0
  129. package/registry/blocks/agent-dock/demo.jsx +153 -0
  130. package/registry/blocks/agent-dock/dock-model.js +118 -0
  131. package/registry/blocks/agent-dock/index.js +5 -0
  132. package/registry/blocks/agent-kit/README.md +73 -0
  133. package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
  134. package/registry/blocks/agent-kit/block.json +10 -0
  135. package/registry/blocks/agent-kit/demo.jsx +35 -0
  136. package/registry/blocks/agent-kit/index.js +3 -0
  137. package/registry/blocks/agent-marks/README.md +80 -0
  138. package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
  139. package/registry/blocks/agent-marks/block.json +27 -0
  140. package/registry/blocks/agent-marks/demo.jsx +49 -0
  141. package/registry/blocks/agent-marks/index.js +3 -0
  142. package/registry/blocks/agent-pane/README.md +234 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +246 -0
  144. package/registry/blocks/agent-pane/block.json +41 -0
  145. package/registry/blocks/agent-pane/composer.jsx +6 -0
  146. package/registry/blocks/agent-pane/demo.jsx +233 -0
  147. package/registry/blocks/agent-pane/dock-context.js +10 -0
  148. package/registry/blocks/agent-pane/index.js +4 -0
  149. package/registry/blocks/agent-pane/prose.jsx +84 -0
  150. package/registry/blocks/agent-pane/questions.jsx +134 -0
  151. package/registry/blocks/agent-pane/transcript.jsx +268 -0
  152. package/registry/blocks/agent-pane/work.jsx +368 -0
  153. package/registry/blocks/ai-action/README.md +94 -0
  154. package/registry/blocks/ai-action/ai-action.jsx +188 -0
  155. package/registry/blocks/ai-action/block.json +34 -0
  156. package/registry/blocks/ai-action/demo.jsx +93 -0
  157. package/registry/blocks/ai-action/index.js +3 -0
  158. package/registry/blocks/approval/README.md +145 -0
  159. package/registry/blocks/approval/approval.jsx +163 -0
  160. package/registry/blocks/approval/block.json +31 -0
  161. package/registry/blocks/approval/demo.jsx +121 -0
  162. package/registry/blocks/approval/index.js +3 -0
  163. package/registry/blocks/ask-bar/README.md +105 -0
  164. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  165. package/registry/blocks/ask-bar/block.json +26 -0
  166. package/registry/blocks/ask-bar/demo.jsx +78 -0
  167. package/registry/blocks/ask-bar/index.js +3 -0
  168. package/registry/blocks/automation-editor/README.md +291 -0
  169. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  170. package/registry/blocks/automation-editor/block.json +39 -0
  171. package/registry/blocks/automation-editor/demo.jsx +284 -0
  172. package/registry/blocks/automation-editor/index.js +4 -0
  173. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  174. package/registry/blocks/automation-list/README.md +129 -0
  175. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  176. package/registry/blocks/automation-list/block.json +35 -0
  177. package/registry/blocks/automation-list/demo.jsx +67 -0
  178. package/registry/blocks/automation-list/index.js +3 -0
  179. package/registry/blocks/automation-model/README.md +109 -0
  180. package/registry/blocks/automation-model/block.json +135 -0
  181. package/registry/blocks/automation-model/catalog.js +256 -0
  182. package/registry/blocks/automation-model/check.js +161 -0
  183. package/registry/blocks/automation-model/demo.jsx +42 -0
  184. package/registry/blocks/automation-model/edit.js +262 -0
  185. package/registry/blocks/automation-model/fixes.js +287 -0
  186. package/registry/blocks/automation-model/glyph.jsx +13 -0
  187. package/registry/blocks/automation-model/graph.js +410 -0
  188. package/registry/blocks/automation-model/index.js +33 -0
  189. package/registry/blocks/automation-model/overlay.js +175 -0
  190. package/registry/blocks/automation-model/parts.jsx +94 -0
  191. package/registry/blocks/automation-model/schedule.js +53 -0
  192. package/registry/blocks/automation-model/templates.js +128 -0
  193. package/registry/blocks/automation-model/values.js +29 -0
  194. package/registry/blocks/automation-model/words.js +439 -0
  195. package/registry/blocks/automations-screen/README.md +207 -0
  196. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  197. package/registry/blocks/automations-screen/block.json +47 -0
  198. package/registry/blocks/automations-screen/demo.jsx +364 -0
  199. package/registry/blocks/automations-screen/index.js +4 -0
  200. package/registry/blocks/automations-screen/screen-model.js +129 -0
  201. package/registry/blocks/chat-pane/README.md +143 -0
  202. package/registry/blocks/chat-pane/block.json +48 -0
  203. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  204. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  205. package/registry/blocks/chat-pane/chat-context.js +17 -0
  206. package/registry/blocks/chat-pane/chat-model.js +43 -0
  207. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  208. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  209. package/registry/blocks/chat-pane/demo.jsx +103 -0
  210. package/registry/blocks/chat-pane/index.js +9 -0
  211. package/registry/blocks/connect-card/README.md +99 -0
  212. package/registry/blocks/connect-card/block.json +10 -0
  213. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  214. package/registry/blocks/connect-card/demo.jsx +49 -0
  215. package/registry/blocks/connect-card/index.js +3 -0
  216. package/registry/blocks/cost/README.md +115 -0
  217. package/registry/blocks/cost/block.json +10 -0
  218. package/registry/blocks/cost/cost.jsx +176 -0
  219. package/registry/blocks/cost/demo.jsx +66 -0
  220. package/registry/blocks/cost/index.js +3 -0
  221. package/registry/blocks/data-picker/README.md +81 -0
  222. package/registry/blocks/data-picker/block.json +38 -0
  223. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  224. package/registry/blocks/data-picker/demo.jsx +34 -0
  225. package/registry/blocks/data-picker/fields.js +89 -0
  226. package/registry/blocks/data-picker/index.js +4 -0
  227. package/registry/blocks/data-table/demo.jsx +1 -1
  228. package/registry/blocks/first-run/README.md +118 -0
  229. package/registry/blocks/first-run/block.json +11 -0
  230. package/registry/blocks/first-run/demo.jsx +88 -0
  231. package/registry/blocks/first-run/first-run-model.js +51 -0
  232. package/registry/blocks/first-run/first-run.jsx +156 -0
  233. package/registry/blocks/first-run/index.js +5 -0
  234. package/registry/blocks/first-run/probe-rules.js +26 -0
  235. package/registry/blocks/fix-note/README.md +79 -0
  236. package/registry/blocks/fix-note/block.json +35 -0
  237. package/registry/blocks/fix-note/demo.jsx +30 -0
  238. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  239. package/registry/blocks/fix-note/index.js +3 -0
  240. package/registry/blocks/flow-editor/README.md +108 -0
  241. package/registry/blocks/flow-editor/block.json +44 -0
  242. package/registry/blocks/flow-editor/demo.jsx +115 -0
  243. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  244. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  245. package/registry/blocks/flow-editor/index.js +6 -0
  246. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  247. package/registry/blocks/flow-editor/outline.jsx +134 -0
  248. package/registry/blocks/library-shelf/README.md +117 -0
  249. package/registry/blocks/library-shelf/block.json +28 -0
  250. package/registry/blocks/library-shelf/demo.jsx +68 -0
  251. package/registry/blocks/library-shelf/index.js +3 -0
  252. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  253. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  254. package/registry/blocks/margin-notes/README.md +222 -0
  255. package/registry/blocks/margin-notes/block.json +10 -0
  256. package/registry/blocks/margin-notes/demo.jsx +144 -0
  257. package/registry/blocks/margin-notes/index.js +20 -0
  258. package/registry/blocks/margin-notes/margin-model.js +224 -0
  259. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  260. package/registry/blocks/needs-you/README.md +177 -0
  261. package/registry/blocks/needs-you/block.json +34 -0
  262. package/registry/blocks/needs-you/demo.jsx +143 -0
  263. package/registry/blocks/needs-you/index.js +3 -0
  264. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  265. package/registry/blocks/pace-bar/README.md +230 -0
  266. package/registry/blocks/pace-bar/block.json +10 -0
  267. package/registry/blocks/pace-bar/demo.jsx +255 -0
  268. package/registry/blocks/pace-bar/index.js +22 -0
  269. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  270. package/registry/blocks/pace-bar/pace-model.js +288 -0
  271. package/registry/blocks/record-grid/README.md +100 -0
  272. package/registry/blocks/record-grid/block.json +28 -0
  273. package/registry/blocks/record-grid/demo.jsx +43 -0
  274. package/registry/blocks/record-grid/index.js +3 -0
  275. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  276. package/registry/blocks/run-history/README.md +138 -0
  277. package/registry/blocks/run-history/block.json +10 -0
  278. package/registry/blocks/run-history/demo.jsx +74 -0
  279. package/registry/blocks/run-history/index.js +3 -0
  280. package/registry/blocks/run-history/run-history.jsx +218 -0
  281. package/registry/blocks/run-view/README.md +174 -0
  282. package/registry/blocks/run-view/block.json +50 -0
  283. package/registry/blocks/run-view/demo.jsx +286 -0
  284. package/registry/blocks/run-view/index.js +5 -0
  285. package/registry/blocks/run-view/run-controls.jsx +266 -0
  286. package/registry/blocks/run-view/run-view.jsx +236 -0
  287. package/registry/blocks/run-view/runs-list.jsx +99 -0
  288. package/registry/blocks/schedule-card/README.md +121 -0
  289. package/registry/blocks/schedule-card/block.json +10 -0
  290. package/registry/blocks/schedule-card/demo.jsx +82 -0
  291. package/registry/blocks/schedule-card/index.js +3 -0
  292. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  293. package/registry/blocks/schedule-engine/README.md +137 -0
  294. package/registry/blocks/schedule-engine/block.json +10 -0
  295. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  296. package/registry/blocks/schedule-engine/engine.js +769 -0
  297. package/registry/blocks/schedule-engine/index.js +9 -0
  298. package/registry/blocks/schedule-engine/ops.js +71 -0
  299. package/registry/blocks/schedule-engine/place.js +294 -0
  300. package/registry/blocks/schedule-engine/tree.js +79 -0
  301. package/registry/blocks/schedule-engine/waits.js +80 -0
  302. package/registry/blocks/schedule-gantt/README.md +159 -0
  303. package/registry/blocks/schedule-gantt/block.json +10 -0
  304. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  305. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  306. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  307. package/registry/blocks/schedule-gantt/index.js +26 -0
  308. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  309. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  310. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  311. package/registry/blocks/schedule-ruler/README.md +148 -0
  312. package/registry/blocks/schedule-ruler/block.json +10 -0
  313. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  314. package/registry/blocks/schedule-ruler/index.js +21 -0
  315. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  316. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  317. package/registry/blocks/source-receipt/README.md +97 -0
  318. package/registry/blocks/source-receipt/block.json +30 -0
  319. package/registry/blocks/source-receipt/demo.jsx +71 -0
  320. package/registry/blocks/source-receipt/index.js +3 -0
  321. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  322. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  323. package/registry/blocks/step-picker/README.md +78 -0
  324. package/registry/blocks/step-picker/block.json +31 -0
  325. package/registry/blocks/step-picker/demo.jsx +23 -0
  326. package/registry/blocks/step-picker/index.js +3 -0
  327. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  328. package/registry/blocks/step-settings/README.md +100 -0
  329. package/registry/blocks/step-settings/block.json +44 -0
  330. package/registry/blocks/step-settings/demo.jsx +93 -0
  331. package/registry/blocks/step-settings/fields.jsx +58 -0
  332. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  333. package/registry/blocks/step-settings/index.js +5 -0
  334. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  335. package/registry/blocks/suggestion/README.md +171 -0
  336. package/registry/blocks/suggestion/block.json +40 -0
  337. package/registry/blocks/suggestion/demo.jsx +91 -0
  338. package/registry/blocks/suggestion/index.js +3 -0
  339. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  340. package/registry/blocks/sum-bar/README.md +122 -0
  341. package/registry/blocks/sum-bar/block.json +10 -0
  342. package/registry/blocks/sum-bar/demo.jsx +110 -0
  343. package/registry/blocks/sum-bar/index.js +5 -0
  344. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  345. package/registry/blocks/sum-bar/sum-model.js +126 -0
  346. package/registry/blocks/tier-map/README.md +217 -0
  347. package/registry/blocks/tier-map/block.json +10 -0
  348. package/registry/blocks/tier-map/demo.jsx +222 -0
  349. package/registry/blocks/tier-map/index.js +24 -0
  350. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  351. package/registry/blocks/tier-map/tier-model.js +318 -0
  352. package/registry/blocks/time-grid/README.md +305 -0
  353. package/registry/blocks/time-grid/block.json +10 -0
  354. package/registry/blocks/time-grid/demo.jsx +279 -0
  355. package/registry/blocks/time-grid/grid-model.js +322 -0
  356. package/registry/blocks/time-grid/index.js +25 -0
  357. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  358. package/registry/blocks/time-strip/README.md +141 -0
  359. package/registry/blocks/time-strip/block.json +10 -0
  360. package/registry/blocks/time-strip/demo.jsx +129 -0
  361. package/registry/blocks/time-strip/index.js +19 -0
  362. package/registry/blocks/time-strip/strip-model.js +178 -0
  363. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  364. package/registry/blocks/top-bar/README.md +149 -0
  365. package/registry/blocks/top-bar/block.json +35 -0
  366. package/registry/blocks/top-bar/demo.jsx +78 -0
  367. package/registry/blocks/top-bar/index.js +5 -0
  368. package/registry/blocks/top-bar/probe-rules.js +74 -0
  369. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  370. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  371. package/registry/blocks/trigger-picker/README.md +98 -0
  372. package/registry/blocks/trigger-picker/block.json +38 -0
  373. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  374. package/registry/blocks/trigger-picker/index.js +3 -0
  375. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  376. package/registry/blocks/workflow-view/README.md +123 -0
  377. package/registry/blocks/workflow-view/block.json +10 -0
  378. package/registry/blocks/workflow-view/demo.jsx +90 -0
  379. package/registry/blocks/workflow-view/index.js +3 -0
  380. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  381. package/src/install.mjs +77 -16
  382. package/src/registry.mjs +3 -0
  383. package/templates/next/browser/lib/data.js +6 -1
  384. package/templates/next/files/README.md +627 -24
  385. package/templates/next/files/check/automation-format.ts +2103 -0
  386. package/templates/next/files/check/automation-package.ts +142 -0
  387. package/templates/next/files/check/run.mjs +968 -9
  388. package/templates/next/files/components/ui/field.jsx +17 -4
  389. package/templates/next/files/components/ui/icon.jsx +13 -0
  390. package/templates/next/files/components/ui/index.js +2 -1
  391. package/templates/next/files/components/ui/layout.jsx +77 -2
  392. package/templates/next/files/components/ui/notice.jsx +70 -0
  393. package/templates/next/files/components/ui/page.jsx +124 -0
  394. package/templates/next/files/components/ui/stat.jsx +22 -7
  395. package/templates/next/files/components/ui/toast.jsx +6 -3
  396. package/templates/next/files/lib/automations.js +660 -0
  397. package/templates/next/files/lib/claw.js +241 -5
  398. package/templates/next/files/lib/door.js +92 -0
  399. package/templates/next/files/lib/hooks.js +1677 -8
  400. package/templates/next/files/lib/platform.js +529 -21
  401. package/templates/next/template.json +13 -3
@@ -1,16 +1,61 @@
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, llm, 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
@@ -19,7 +64,10 @@ import { ask, cancelJob, llm, openRequest, requests as readRequests, startJob, s
19
64
  // from a surface: the asks in flight on this app, so an empty table can
20
65
  // show the work coming instead of a blank. `useLlm` (0.8.0) is neither:
21
66
  // one call to a model, answered in the page, for text work on what the
22
- // screen holds — nothing to follow, nothing on the desk.
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.
23
71
  //
24
72
  // While the gateway has no _claw routes yet (`unavailable`), polls back
25
73
  // off to once a minute so a preview that is still being wired does not
@@ -698,7 +746,8 @@ const QUIET = { loading: false, result: null, error: null, unavailable: false };
698
746
  * { ok: false, aborted: true } and changes nothing, so a slow answer never
699
747
  * lands over a newer one. The hook owns the signal (one in opts is
700
748
  * replaced). Unmounting aborts the call in flight. Nothing is called until
701
- * `run` is.
749
+ * `run` is. `reset()` (0.8.1) clears what is shown and drops a call in
750
+ * flight.
702
751
  *
703
752
  * const { run, loading, result, error, unavailable } = useLlm();
704
753
  * run(`Translate to French: ${text}`, { task: "translate" });
@@ -739,5 +788,1625 @@ export function useLlm() {
739
788
  return answer;
740
789
  }, []);
741
790
 
742
- return { ...state, run };
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
+ // A pending question is identified by its request and input ids, not the
1130
+ // time its GET/stream frame arrived. Accepted answers must not reappear
1131
+ // while the agent resumes, and a genuinely new question must still show.
1132
+ function threadQuestionKey(data) {
1133
+ if (data?.state !== "needs_you" || data.needs?.kind !== "question") return null;
1134
+ const ids = (data.view?.questions ?? []).map((q) => q.id).filter(Boolean).sort();
1135
+ return JSON.stringify([data.session_id, data.request?.id, data.needs.at ?? null, ids, ids.length ? null : data.needs.text]);
1136
+ }
1137
+
1138
+ function withoutThreadQuestion(data) {
1139
+ return { ...data, state: "working", needs: null, ...(data.view ? { view: { ...data.view, questions: [], thinking: true } } : {}) };
1140
+ }
1141
+
1142
+
1143
+ /**
1144
+ * The member's own thread with the app's agent, for the app's own chat
1145
+ * (the `agent-chat` block): the same eve session the desk's agent column
1146
+ * shows — one conversation, two views, never a copy.
1147
+ *
1148
+ * const chat = useAgentThread({ row: { kind: "person", id, name } });
1149
+ * <AgentChat chat={chat} title={`Ask about ${name}`} />
1150
+ *
1151
+ * Options (all optional):
1152
+ * row the record the chat is about ({ kind, id, name }): it rides
1153
+ * with every ask as context, and the thread is kept for it
1154
+ * how instructions for the agent only (the AGENTS.md section)
1155
+ * job a declared job's key, or true; by default none — a chat is
1156
+ * a conversation, not a job
1157
+ * key where this browser keeps the thread (default `<kind>:<id>`
1158
+ * of the row, else "app"); null keeps none
1159
+ * Readers on the same key in one page follow each other: one that
1160
+ * starts, drops or switches the thread is heard by the rest (the
1161
+ * dock's pane, the Today ask bar and a screen's buttons share it)
1162
+ * view true asks the door for the thread as the desk's sidebar draws
1163
+ * it (0.8.1, D27: the `agent-pane` block; useAgentPane sets it)
1164
+ *
1165
+ * State:
1166
+ * sessionId the thread, or null before the first ask
1167
+ * state queued | working | needs_you | done | stopped | failed, or
1168
+ * null before the first ask
1169
+ * needs { kind, text, at, in_app, options?, free? } while it waits
1170
+ * on the member. `in_app` is how the app can settle it:
1171
+ * "answer" (a question: answer({ optionId | text }), with
1172
+ * its `options` and whether `free` words are welcome),
1173
+ * "press" (the one ask's yes: the `approval` block's press
1174
+ * frame, useApproval(request.id)), or null (on the desk:
1175
+ * openOnDesk)
1176
+ * turns [{ id, member, parts: [{ type: "text", text } |
1177
+ * { type: "step", label, status }], writing, status }]
1178
+ * pending the member's words just sent, until the thread has them
1179
+ * view with `view: true`, the sidebar's view: { messages, thinking,
1180
+ * failure, questions } (platform.js thread), else null
1181
+ * sameConversation the next message continues this request (D29: also
1182
+ * after a Stop, a finish or a failed turn); false for a plan,
1183
+ * a piece, a change sent up, a counted job (a new request)
1184
+ * place "app", or "makersclaw" for an ask made in Makersclaw (the
1185
+ * chat shows a pointer, not the turns)
1186
+ * request the request the thread is on (as useRequests reads one)
1187
+ * sending, loading, error, code, unavailable (a claw with no door yet)
1188
+ *
1189
+ * Actions: send(text, { files }) · stop() · answer({ optionId, text } | { answers }) ·
1190
+ * openOnDesk() · newChat() · openSession(sessionId) · reload(). answer is
1191
+ * the agent's question answered in the chat where it was asked (0.8.1,
1192
+ * D19): → { ok } | { ok: false, error, code }. `answers` is
1193
+ * [{ requestId, optionId | text }], one per question `view.questions`
1194
+ * lists, sent as one resume (D27). openSession switches to another of the
1195
+ * member's threads (the pane's Chats list). `files` are [{ name, type, data }]
1196
+ * (data URLs, at most 4 and 3 MB together) sent with the words (D27 phase 4);
1197
+ * `notice` says when they did not go (the ask is queued). With `view: true`
1198
+ * a working thread is streamed (platform.js streamThread) and polled as the
1199
+ * safety net.
1200
+ * send continues the thread (ask({ sessionId })) — after a Stop, a finish
1201
+ * or a failed turn too: the platform opens the closed request again, in the
1202
+ * same session, so there is no new card (D29) — and starts a fresh one only
1203
+ * when the old one is a teammate's or gone. The desk's column is never
1204
+ * opened by an ask from here (`open: false`); openOnDesk is the member's
1205
+ * press, and opens it.
1206
+ */
1207
+ export function useAgentThread({ row, how, job = false, key, view = false } = {}) {
1208
+ const storeKey = key === null ? null : (key ?? (row?.kind && row?.id ? `${row.kind}:${row.id}` : "app"));
1209
+ const [sessionId, setSessionId] = useState(() => keptThread(storeKey));
1210
+ const me = useRef({}).current;
1211
+ const [state, setState] = useState({
1212
+ data: null,
1213
+ loading: Boolean(sessionId),
1214
+ sending: false,
1215
+ pending: null,
1216
+ error: null,
1217
+ code: null,
1218
+ unavailable: false,
1219
+ });
1220
+ const [tick, setTick] = useState(0);
1221
+ const reload = useCallback(() => setTick((t) => t + 1), []);
1222
+ const aliveRef = useRef(true);
1223
+ const dataRef = useRef(state.data);
1224
+ dataRef.current = state.data;
1225
+ const dismissedQuestions = useRef(new Map());
1226
+ const protectQuestions = (data) => dismissedQuestions.current.has(threadQuestionKey(data)) ? withoutThreadQuestion(data) : data;
1227
+ const dismissQuestion = () => {
1228
+ const snapshot = dataRef.current;
1229
+ const questionKey = threadQuestionKey(snapshot);
1230
+ if (!questionKey) return null;
1231
+ const pending = { key: questionKey, snapshot };
1232
+ dismissedQuestions.current.set(questionKey, pending);
1233
+ setState((s) => ({ ...s, error: null, data: protectQuestions(s.data) }));
1234
+ return pending;
1235
+ };
1236
+ const restoreQuestion = (pending, error) => {
1237
+ if (!pending || dismissedQuestions.current.get(pending.key) !== pending) return;
1238
+ dismissedQuestions.current.delete(pending.key);
1239
+ const before = pending.snapshot;
1240
+ setState((s) => {
1241
+ const current = s.data;
1242
+ const otherQuestion = threadQuestionKey(current);
1243
+ if (!current || current.session_id !== before.session_id || current.request?.id !== before.request?.id || (otherQuestion && otherQuestion !== pending.key)) return s;
1244
+ return { ...s, error, data: { ...current, state: before.state, needs: before.needs, ...(before.view ? { view: { ...current.view, questions: before.view.questions, thinking: before.view.thinking } } : {}) } };
1245
+ });
1246
+ };
1247
+ const confirmQuestion = (pending) => { if (pending) pending.snapshot = null; };
1248
+ const sessionRef = useRef(sessionId);
1249
+ sessionRef.current = sessionId;
1250
+ const rowRef = useRef(row);
1251
+ rowRef.current = row;
1252
+ const viewRef = useRef(view);
1253
+ viewRef.current = view;
1254
+ // Words sent and not in the thread yet: the agent's turn has not opened,
1255
+ // so the chat keeps looking (for a minute at most) though the last turn
1256
+ // has ended.
1257
+ const pendingRef = useRef(null);
1258
+ pendingRef.current = state.pending ? (pendingRef.current?.words === state.pending ? pendingRef.current : { words: state.pending, at: Date.now() }) : null;
1259
+
1260
+ useEffect(() => {
1261
+ aliveRef.current = true;
1262
+ return () => {
1263
+ aliveRef.current = false;
1264
+ };
1265
+ }, []);
1266
+
1267
+ // Another record opened in the same chat: its own thread.
1268
+ const lastKey = useRef(storeKey);
1269
+ useEffect(() => {
1270
+ if (lastKey.current === storeKey) return;
1271
+ lastKey.current = storeKey;
1272
+ const kept = keptThread(storeKey);
1273
+ dismissedQuestions.current.clear();
1274
+ setSessionId(kept);
1275
+ setState({ data: null, loading: Boolean(kept), sending: false, pending: null, error: null, code: null, unavailable: false });
1276
+ }, [storeKey]);
1277
+
1278
+ // Another reader of this thread started it, dropped it or opened another
1279
+ // chat in it: follow (a pane that stays mounted beside the work, the Today
1280
+ // ask bar, a button on a screen). The one that made the change does not
1281
+ // hear itself.
1282
+ useEffect(() => {
1283
+ if (!storeKey || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
1284
+ const on = (e) => {
1285
+ const d = e.detail;
1286
+ if (!d || d.key !== storeKey || d.from === me) return;
1287
+ const next = d.sessionId ?? null;
1288
+ if (next === sessionRef.current) return;
1289
+ dismissedQuestions.current.clear();
1290
+ setSessionId(next);
1291
+ setState({ data: null, loading: Boolean(next), sending: false, pending: null, error: null, code: null, unavailable: false });
1292
+ };
1293
+ window.addEventListener(THREAD_KEPT, on);
1294
+ return () => window.removeEventListener(THREAD_KEPT, on);
1295
+ }, [storeKey, me]);
1296
+
1297
+ const forget = useCallback(() => {
1298
+ keepThread(storeKey, null, me);
1299
+ setSessionId(null);
1300
+ }, [storeKey, me]);
1301
+
1302
+ // Follow the thread: at once, then while the agent works or waits. With
1303
+ // the sidebar's view (the `agent-pane` block) a working thread is also
1304
+ // STREAMED (0.8.1, D27 phase 4): the gateway pushes the thread after each
1305
+ // burst of the agent's events, so text and steps appear as they happen.
1306
+ // The poll stays as the safety net: it looks every few seconds while the
1307
+ // stream is alive and takes over when it ends, breaks, or does not exist.
1308
+ useEffect(() => {
1309
+ if (!sessionId) return undefined;
1310
+ const controller = new AbortController();
1311
+ let alive = true;
1312
+ let timer = null;
1313
+ let inflight = false;
1314
+ let streaming = false;
1315
+ let streamOff = false;
1316
+ let quickEnds = 0;
1317
+ let lastLine = 0;
1318
+ let streamStop = null;
1319
+ const schedule = (ms) => {
1320
+ clearTimeout(timer);
1321
+ timer = null;
1322
+ if (alive && ms && screen.shown()) timer = setTimeout(load, ms);
1323
+ };
1324
+ const wordsLanded = (data, words) =>
1325
+ Boolean(words) && ((data.turns ?? []).some((t) => t.member === words) || (data.view?.messages ?? []).some((m) => m.role === "user" && m.text === words));
1326
+ const applyFrame = (frame) => {
1327
+ if (!alive) return;
1328
+ if (frame.desk_origin) publishDesk(frame.desk_origin);
1329
+ setState((s) => ({
1330
+ ...s,
1331
+ data: protectQuestions({ ...(s.data ?? {}), ...frame, turns: s.data?.turns ?? [] }),
1332
+ loading: false,
1333
+ error: null,
1334
+ code: null,
1335
+ unavailable: false,
1336
+ pending: wordsLanded(frame, s.pending) ? null : s.pending,
1337
+ }));
1338
+ };
1339
+ const openStream = () => {
1340
+ if (!viewRef.current || streaming || streamOff || !alive || !screen.shown()) return;
1341
+ streaming = true;
1342
+ lastLine = Date.now();
1343
+ const began = Date.now();
1344
+ const stop = new AbortController();
1345
+ streamStop = stop;
1346
+ // No line (not even a ping) for 25 s: the stream is dead, not quiet.
1347
+ const watchdog = setInterval(() => {
1348
+ if (Date.now() - lastLine > 25000) stop.abort();
1349
+ }, 5000);
1350
+ streamThread(sessionId, { signal: stop.signal, onLine: () => (lastLine = Date.now()), onThread: applyFrame }).then((r) => {
1351
+ clearInterval(watchdog);
1352
+ streaming = false;
1353
+ streamStop = null;
1354
+ if (!alive) return;
1355
+ // A claw with no stream, or a thread it will not stream: poll.
1356
+ if (!r.ok && !r.aborted && (r.unavailable || r.status === 404 || r.status === 403)) streamOff = true;
1357
+ // Three streams in a row that ended at once: stop trying.
1358
+ quickEnds = Date.now() - began < 2000 ? quickEnds + 1 : 0;
1359
+ if (quickEnds >= 3) streamOff = true;
1360
+ load(); // the canonical read, and a new stream if it is still going
1361
+ });
1362
+ };
1363
+ const load = async () => {
1364
+ clearTimeout(timer);
1365
+ timer = null;
1366
+ if (!alive || inflight || !screen.shown()) return;
1367
+ // While a stream is alive it is the fresher view: look rarely.
1368
+ if (streaming && Date.now() - lastLine < 20000) {
1369
+ schedule(CHAT_WORKING_MS * 3);
1370
+ return;
1371
+ }
1372
+ inflight = true;
1373
+ const r = await readThread(sessionId, { signal: controller.signal, view: viewRef.current });
1374
+ inflight = false;
1375
+ if (!alive || r.aborted) return;
1376
+ if (r.ok && r.data) {
1377
+ const data = r.data;
1378
+ if (data.desk_origin) publishDesk(data.desk_origin);
1379
+ setState((s) => ({
1380
+ ...s,
1381
+ data: protectQuestions(data),
1382
+ loading: false,
1383
+ error: null,
1384
+ code: null,
1385
+ unavailable: false,
1386
+ pending: wordsLanded(data, s.pending) ? null : s.pending,
1387
+ }));
1388
+ const turns = data.turns ?? [];
1389
+ const last = turns[turns.length - 1];
1390
+ const waitingForTurn = pendingRef.current && Date.now() - pendingRef.current.at < 60000;
1391
+ const working = CHAT_LIVE.has(data.state) || last?.status === "working" || Boolean(waitingForTurn);
1392
+ if (working && CHAT_LIVE.has(data.state)) openStream();
1393
+ schedule(working ? CHAT_WORKING_MS : data.state === "needs_you" ? CHAT_WAITING_MS : 0);
1394
+ return;
1395
+ }
1396
+ if (r.status === 403 || r.status === 404) {
1397
+ if (r.code) {
1398
+ // A teammate's thread, or one this app no longer has: start fresh.
1399
+ forget();
1400
+ setState((s) => ({ ...s, data: null, loading: false }));
1401
+ return;
1402
+ }
1403
+ setState((s) => ({ ...s, loading: false, unavailable: true }));
1404
+ return;
1405
+ }
1406
+ setState((s) => ({ ...s, loading: false, error: r.error ?? null }));
1407
+ schedule(CHAT_WORKING_MS * 4); // a blip: look again, a little later
1408
+ };
1409
+ const screen = watchShown(load);
1410
+ load();
1411
+ return () => {
1412
+ alive = false;
1413
+ controller.abort();
1414
+ streamStop?.abort();
1415
+ clearTimeout(timer);
1416
+ screen.stop();
1417
+ };
1418
+ }, [sessionId, tick, forget]);
1419
+
1420
+ const send = useCallback(
1421
+ async (message, { files } = {}) => {
1422
+ const text = String(message ?? "").trim();
1423
+ if (!text) return { ok: false, error: "nothing to ask" };
1424
+ const dismissed = dismissQuestion();
1425
+ setState((s) => ({ ...s, sending: true, pending: text, error: null, code: null, notice: null }));
1426
+ const asking = (session) =>
1427
+ ask({ text, row: rowRef.current ?? undefined, how, job, files, sessionId: session ?? undefined, open: false });
1428
+ let result = await asking(sessionRef.current);
1429
+ if (!result.ok && sessionRef.current && START_FRESH.has(result.code)) result = await asking(null);
1430
+ if (!aliveRef.current) return result;
1431
+ if (!result.ok) {
1432
+ restoreQuestion(dismissed, result.error ?? "The ask could not be sent.");
1433
+ const unavailable = result.status === 404 && !result.code;
1434
+ setState((s) => ({
1435
+ ...s,
1436
+ sending: false,
1437
+ pending: null,
1438
+ error: unavailable ? null : (result.error ?? "The ask could not be sent."),
1439
+ code: result.code ?? null,
1440
+ unavailable: unavailable || s.unavailable,
1441
+ }));
1442
+ return result;
1443
+ }
1444
+ confirmQuestion(dismissed);
1445
+ announceAsk();
1446
+ const request = result.data?.request ?? null;
1447
+ const next = request?.app_session_id ?? sessionRef.current ?? null;
1448
+ if (next) {
1449
+ keepThread(storeKey, next, me);
1450
+ if (next !== sessionRef.current) setSessionId(next);
1451
+ else reload();
1452
+ }
1453
+ setState((s) => ({
1454
+ ...s,
1455
+ sending: false,
1456
+ // The ask was taken but waits its turn: a file is not kept for later.
1457
+ notice:
1458
+ result.data?.files_attached === false
1459
+ ? "Your files did not go with that message because it is waiting its turn. Attach them again once it starts."
1460
+ : null,
1461
+ data: s.data && next === s.data.session_id ? (threadQuestionKey(s.data) && threadQuestionKey(s.data) !== dismissed?.key ? s.data : { ...s.data, state: "working", request: request ?? s.data.request }) : s.data,
1462
+ }));
1463
+ return result;
1464
+ },
1465
+ [how, job, storeKey, reload, me],
1466
+ );
1467
+
1468
+ const request = state.data?.request ?? null;
1469
+ const stop = useCallback(async () => {
1470
+ if (!request?.id) return { ok: false, error: "nothing to stop" };
1471
+ const r = await stopRequest(request.id);
1472
+ reload();
1473
+ return r;
1474
+ }, [request?.id, reload]);
1475
+ const needsAt = state.data?.needs?.at ?? null;
1476
+ const answer = useCallback(
1477
+ async ({ optionId, text, answers } = {}) => {
1478
+ if (!request?.id) return { ok: false, error: "nothing to answer" };
1479
+ const dismissed = dismissQuestion();
1480
+ const r = await sendAnswer(request.id, { optionId, text, answers, at: needsAt ?? undefined });
1481
+ if (!aliveRef.current) return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1482
+ if (!r.ok) restoreQuestion(dismissed, r.error ?? "That answer did not go through. Try again.");
1483
+ if (r.ok) {
1484
+ confirmQuestion(dismissed);
1485
+ announceAsk();
1486
+ setState((s) => ({ ...s, error: null }));
1487
+ }
1488
+ if (r.ok) reload();
1489
+ return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
1490
+ },
1491
+ [request?.id, needsAt, reload],
1492
+ );
1493
+ const openOnDesk = useCallback(() => (request ? openRequest(request) : false), [request]);
1494
+ const newChat = useCallback(() => {
1495
+ forget();
1496
+ dismissedQuestions.current.clear();
1497
+ setState({ data: null, loading: false, sending: false, pending: null, error: null, code: null, unavailable: false });
1498
+ }, [forget]);
1499
+ // Another of the member's threads in this app (the pane's Chats list):
1500
+ // kept as this chat's, so the same record opens on it next time.
1501
+ const openSession = useCallback(
1502
+ (next) => {
1503
+ const id = typeof next === "string" && next ? next : null;
1504
+ if (!id || id === sessionRef.current) return;
1505
+ dismissedQuestions.current.clear();
1506
+ keepThread(storeKey, id, me);
1507
+ setSessionId(id);
1508
+ setState({ data: null, loading: true, sending: false, pending: null, error: null, code: null, unavailable: false });
1509
+ },
1510
+ [storeKey, me],
1511
+ );
1512
+
1513
+ const data = state.data;
1514
+ return {
1515
+ sessionId,
1516
+ state: data?.state ?? (state.sending ? "working" : null),
1517
+ needs: data?.needs ?? null,
1518
+ turns: data?.turns ?? [],
1519
+ view: data?.view ?? null,
1520
+ // Does the next message go on in this same request (D29)? True while it
1521
+ // is open and after a Stop, a finish or a failed turn (the platform
1522
+ // opens the closed request again, in the same session), false only for
1523
+ // a plan or a piece of one, a change sent up, a counted job and a
1524
+ // hand-off never started: those get a new request. Unknown (an older
1525
+ // claw, or before the first read): true.
1526
+ sameConversation: data?.same_conversation ?? true,
1527
+ place: data?.thread ?? "app",
1528
+ request,
1529
+ pending: state.pending,
1530
+ sending: state.sending,
1531
+ loading: state.loading,
1532
+ error: state.error,
1533
+ code: state.code,
1534
+ // A line to show beside the thread (files that did not go), or null.
1535
+ notice: state.notice ?? null,
1536
+ unavailable: state.unavailable,
1537
+ send,
1538
+ stop,
1539
+ answer,
1540
+ openOnDesk,
1541
+ newChat,
1542
+ openSession,
1543
+ reload,
1544
+ };
743
1545
  }
1546
+
1547
+ /** How often the Chats list looks while a thread is going, and otherwise. */
1548
+ const CHATS_WORKING_MS = 6000;
1549
+ const CHATS_IDLE_MS = 30000;
1550
+
1551
+ /** A thread's request status as the Chats list marks it: waiting on the
1552
+ * member, or the agent is on it. */
1553
+ function chatMark(status) {
1554
+ if (status === "needs_you") return "needs";
1555
+ if (status === "queued" || status === "routing" || status === "installing" || status === "setting_up" || status === "building" || status === "working") return "working";
1556
+ return null;
1557
+ }
1558
+
1559
+ /**
1560
+ * The member's own threads in this app, newest first, for the `agent-pane`
1561
+ * block's Chats list (GET _claw/threads, 0.8.1, D27):
1562
+ *
1563
+ * const { items, loading, error, unavailable } = useThreadList();
1564
+ *
1565
+ * items [{ sessionId, requestId, title, mark: "needs" | "working" | null, at }]
1566
+ *
1567
+ * Looks every 30 s, every 6 s while one is going or waits, and at once
1568
+ * when an ask is sent. A claw from before the door is `unavailable`: the
1569
+ * pane draws no list.
1570
+ */
1571
+ export function useThreadList({ every = CHATS_IDLE_MS } = {}) {
1572
+ const watched = useWatched((signal) => readThreads({ signal }), "threads", {
1573
+ next: (data) => (listOf(data?.threads).some((t) => chatMark(t?.status)) ? CHATS_WORKING_MS : every),
1574
+ listen: onAsked,
1575
+ });
1576
+ const items = useMemo(
1577
+ () =>
1578
+ listOf(watched.data?.threads)
1579
+ .filter((t) => t && typeof t.session_id === "string")
1580
+ .map((t) => ({
1581
+ sessionId: t.session_id,
1582
+ requestId: t.request_id ?? null,
1583
+ title: typeof t.title === "string" && t.title.trim() ? t.title.trim() : "Untitled",
1584
+ mark: chatMark(t.status),
1585
+ at: t.updated_at ?? null,
1586
+ })),
1587
+ [watched.data],
1588
+ );
1589
+ return { items, loading: watched.loading, error: watched.error, unavailable: watched.unavailable, reload: watched.reload };
1590
+ }
1591
+
1592
+ /**
1593
+ * The desk's agent sidebar, as the app's own pane (0.8.1, D27; the
1594
+ * `agent-pane` block). useAgentThread with the sidebar's view, plus what
1595
+ * the sidebar has around its chat:
1596
+ *
1597
+ * const chat = useAgentPane({ row: { kind: "person", id, name } });
1598
+ * <AgentPane chat={chat} title={`Ask about ${name}`} />
1599
+ *
1600
+ * Everything useAgentThread returns (`view` is set), and
1601
+ *
1602
+ * chats { items, loading, error, unavailable }: the member's
1603
+ * threads in this app (useThreadList); openSession(id)
1604
+ * switches to one
1605
+ * callDetail (callId) → Promise<{ ok, call }>: one step of this
1606
+ * thread, opened (platform.js threadCall); the pane asks it
1607
+ * when a member opens a step
1608
+ * openRequest (requestId): opens a request's thread on the desk
1609
+ * open ({ kind: "app", id } | { kind: "file", path, where,
1610
+ * appRef }): opens an app or a file it made on the desk
1611
+ * (the pane's default "Open")
1612
+ *
1613
+ * Options are useAgentThread's: `row` (the record on screen, kept as that
1614
+ * record's own thread), `how` (for the agent only), `job`, `key`.
1615
+ */
1616
+ export function useAgentPane(options = {}) {
1617
+ const chat = useAgentThread({ ...options, view: true });
1618
+ const list = useThreadList();
1619
+ const sessionId = chat.sessionId;
1620
+ const callDetail = useCallback(
1621
+ (callId) => (sessionId ? readThreadCall(sessionId, callId) : Promise.resolve({ ok: false, status: 0, error: "no thread", unavailable: false })),
1622
+ [sessionId],
1623
+ );
1624
+ return {
1625
+ ...chat,
1626
+ chats: { items: list.items, loading: list.loading, error: list.error, unavailable: list.unavailable },
1627
+ callDetail,
1628
+ // "Open in Makersclaw" under a change the agent sent up: the desk opens
1629
+ // that request's thread (a member's press).
1630
+ openRequest: (id) => openRequest(id),
1631
+ // "Open" under an app or a file the agent made (D27 phase 4): the desk
1632
+ // opens it, on the member's press. → true when the desk was asked.
1633
+ open: (target) => (target?.kind === "app" ? openApp(target.id) : target?.kind === "file" ? openFile(target) : false),
1634
+ };
1635
+ }
1636
+
1637
+ // ---------------------------------------------------------------------------
1638
+ // The kit (0.8.1, D15): the app shows its agent's work itself. Each hook
1639
+ // feeds one or more of the kit's blocks; the blocks take the hook's
1640
+ // answer as props and never read a door themselves.
1641
+
1642
+ /** What a kit read shows before its first answer, and with nothing to
1643
+ * read (`enabled` false, no id). `key` names the read the state belongs
1644
+ * to: a state kept for another read is not shown. */
1645
+ const WATCH_READING = { key: null, data: null, loading: true, error: null, code: null, unavailable: false };
1646
+ const WATCH_NOTHING = { ...WATCH_READING, loading: false };
1647
+
1648
+ /**
1649
+ * One kit door, followed: read when `key` changes and on reload(), then
1650
+ * again after `next(data)` ms while the page is shown (0 or nothing: no
1651
+ * timer). Hidden, nothing is asked; coming back looks at once. A missing
1652
+ * door (`unavailable`) is looked at once a minute and leaves `error`
1653
+ * null; a refusal keeps the last answer and sets `error` and `code`. A
1654
+ * `listen` subscription (onAsked) makes it look at once. Aborts what is
1655
+ * out on unmount and when the key changes.
1656
+ * → { data, loading, error, code, unavailable, reload }
1657
+ */
1658
+ function useWatched(read, key, { enabled = true, next, listen } = {}) {
1659
+ const [state, setState] = useState(WATCH_READING);
1660
+ const [tick, setTick] = useState(0);
1661
+ const readRef = useRef(read);
1662
+ readRef.current = read;
1663
+ const nextRef = useRef(next);
1664
+ nextRef.current = next;
1665
+ const reload = useCallback(() => setTick((t) => t + 1), []);
1666
+
1667
+ useEffect(() => {
1668
+ if (!enabled) return undefined;
1669
+ const controller = new AbortController();
1670
+ let alive = true;
1671
+ let timer = null;
1672
+ let inflight = false;
1673
+ let again = false;
1674
+ let unavailable = false;
1675
+ let last = null;
1676
+
1677
+ const schedule = () => {
1678
+ clearTimeout(timer);
1679
+ timer = null;
1680
+ if (!alive || !screen.shown()) return;
1681
+ if (again) {
1682
+ again = false;
1683
+ load();
1684
+ return;
1685
+ }
1686
+ const ms = unavailable ? BACKOFF_MS : Number(nextRef.current?.(last)) || 0;
1687
+ if (ms > 0) timer = setTimeout(load, ms);
1688
+ };
1689
+ const load = async () => {
1690
+ clearTimeout(timer);
1691
+ timer = null;
1692
+ if (!alive || !screen.shown()) return;
1693
+ if (inflight) {
1694
+ again = true;
1695
+ return;
1696
+ }
1697
+ inflight = true;
1698
+ const r = await readRef.current(controller.signal);
1699
+ inflight = false;
1700
+ if (!alive || r?.aborted) return;
1701
+ unavailable = Boolean(r?.unavailable);
1702
+ if (r?.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
1703
+ last = r.data;
1704
+ setState({ key, data: r.data, loading: false, error: null, code: null, unavailable: false });
1705
+ } else {
1706
+ const why = r?.ok
1707
+ ? { error: "the platform answered something unexpected", code: null, unavailable: false }
1708
+ : unavailable
1709
+ ? { error: null, code: null, unavailable: true }
1710
+ : { error: r?.error || "the platform did not answer", code: r?.code ?? null, unavailable: false };
1711
+ setState((s) => ({ key, data: s.key === key ? s.data : null, loading: false, ...why }));
1712
+ }
1713
+ schedule();
1714
+ };
1715
+
1716
+ const screen = watchShown(load);
1717
+ const unsubscribe = listen ? listen(load) : null;
1718
+ load();
1719
+ return () => {
1720
+ alive = false;
1721
+ controller.abort();
1722
+ clearTimeout(timer);
1723
+ screen.stop();
1724
+ unsubscribe?.();
1725
+ };
1726
+ }, [key, enabled, tick]);
1727
+
1728
+ const mine = enabled ? (state.key === key ? state : WATCH_READING) : WATCH_NOTHING;
1729
+ return { data: mine.data, loading: mine.loading, error: mine.error, code: mine.code, unavailable: mine.unavailable, reload };
1730
+ }
1731
+
1732
+ /** How often useRuns and useJobs look while a run is going. */
1733
+ const RUNNING_MS = 5000;
1734
+ /** How often useRun looks at a run that is going. */
1735
+ const RUN_FOLLOW_MS = 2500;
1736
+ /** How often useConnections looks while an account is being connected,
1737
+ * and for how long. */
1738
+ const CONNECTING_MS = 3000;
1739
+ const CONNECTING_FOR_MS = 120000;
1740
+
1741
+ const NO_MAY = Object.freeze({ run: false, switch: false, pause: false });
1742
+ const listOf = (value) => (Array.isArray(value) ? value : NONE);
1743
+
1744
+ /**
1745
+ * The app's declared jobs (its schedule), for the `schedule-card` and
1746
+ * `workflow-view` blocks (GET _claw/jobs):
1747
+ *
1748
+ * const { jobs, app, budget, may, busy, runNow, setEnabled } = useJobs();
1749
+ *
1750
+ * jobs [{ id, name, kind, topic, does, cron, tz, words, enabled,
1751
+ * next_run_at, last_run_at, last_run, plan }]
1752
+ * app { status, live } or null: live is the app's Live switch
1753
+ * budget { week_cap_credits, week_used_credits } or null (no cap)
1754
+ * credits the workspace's credits, as _claw/me gives them, or null
1755
+ * may { run, switch, pause }: what this member may press. Hide a
1756
+ * control they may not; the door refuses it anyway
1757
+ * busy { [job id]: "run" | "switch" } while a press is out
1758
+ * appBusy "pause" | "resume" while the Live switch's press is out
1759
+ * loading, error, code, unavailable, reload
1760
+ *
1761
+ * Actions, each resolving the platform call's answer and looking again
1762
+ * after: runNow(id) (spends: show its price on the button), setEnabled(id,
1763
+ * on), pause(), resume(). Looks every `every` ms (30 s), every 5 s while
1764
+ * a job's last run is going.
1765
+ */
1766
+ export function useJobs({ every = 30000 } = {}) {
1767
+ const watched = useWatched((signal) => readJobs({ signal }), "jobs", {
1768
+ next: (data) => (listOf(data?.jobs).some((j) => j?.last_run?.status === "running") ? RUNNING_MS : every),
1769
+ listen: onAsked,
1770
+ });
1771
+ const { data, reload } = watched;
1772
+ const [busy, setBusy] = useState({});
1773
+ const [appBusy, setAppBusy] = useState(null);
1774
+ const [patched, setPatched] = useState({}); // jobs as a switch's answer gave them, until the next read
1775
+ const aliveRef = useRef(true);
1776
+ useEffect(() => {
1777
+ aliveRef.current = true;
1778
+ return () => {
1779
+ aliveRef.current = false;
1780
+ };
1781
+ }, []);
1782
+ useEffect(() => setPatched((p) => (Object.keys(p).length ? {} : p)), [data]);
1783
+
1784
+ const mark = useCallback((id, what) => {
1785
+ if (!aliveRef.current) return;
1786
+ setBusy((b) => {
1787
+ const nextBusy = { ...b };
1788
+ if (what) nextBusy[id] = what;
1789
+ else delete nextBusy[id];
1790
+ return nextBusy;
1791
+ });
1792
+ }, []);
1793
+
1794
+ const runNow = useCallback(
1795
+ async (id) => {
1796
+ mark(id, "run");
1797
+ const r = await startRun(id);
1798
+ mark(id, null);
1799
+ if (r.ok) announceAsk(); // the requests and the runs on the page look at once
1800
+ if (aliveRef.current) reload();
1801
+ return r;
1802
+ },
1803
+ [mark, reload],
1804
+ );
1805
+
1806
+ const setEnabled = useCallback(
1807
+ async (id, on) => {
1808
+ mark(id, "switch");
1809
+ const r = await setJob(id, { enabled: Boolean(on) });
1810
+ mark(id, null);
1811
+ if (!aliveRef.current) return r;
1812
+ if (r.ok && r.data?.job?.id) setPatched((p) => ({ ...p, [r.data.job.id]: r.data.job }));
1813
+ reload();
1814
+ return r;
1815
+ },
1816
+ [mark, reload],
1817
+ );
1818
+
1819
+ const live = useCallback(
1820
+ async (on) => {
1821
+ setAppBusy(on ? "resume" : "pause");
1822
+ const r = await (on ? resumeApp() : pauseApp());
1823
+ if (!aliveRef.current) return r;
1824
+ setAppBusy(null);
1825
+ reload();
1826
+ return r;
1827
+ },
1828
+ [reload],
1829
+ );
1830
+ const pause = useCallback(() => live(false), [live]);
1831
+ const resume = useCallback(() => live(true), [live]);
1832
+
1833
+ const jobs = useMemo(() => listOf(data?.jobs).map((j) => (j && patched[j.id] ? { ...j, ...patched[j.id] } : j)), [data, patched]);
1834
+ return {
1835
+ jobs,
1836
+ app: data?.app ?? null,
1837
+ budget: data?.budget ?? null,
1838
+ credits: data?.credits ?? null,
1839
+ may: data?.may ? { ...NO_MAY, ...data.may } : NO_MAY,
1840
+ busy,
1841
+ appBusy,
1842
+ loading: watched.loading,
1843
+ error: watched.error,
1844
+ code: watched.code,
1845
+ unavailable: watched.unavailable,
1846
+ reload,
1847
+ runNow,
1848
+ setEnabled,
1849
+ pause,
1850
+ resume,
1851
+ };
1852
+ }
1853
+
1854
+ /**
1855
+ * The app's runs, newest first, and their overview, for the `run-history`
1856
+ * and `workflow-view` blocks (GET _claw/runs):
1857
+ *
1858
+ * const { runs, overview } = useRuns({ job: wakeId, limit: 20 });
1859
+ *
1860
+ * runs [{ id, title, state, wake_id, job, trigger, by, number,
1861
+ * started_at, ended_at, duration_ms, credits, report, status }]
1862
+ * `state` is running | done | failed | stopped | needs_you
1863
+ * overview { completed, failed, running, avg_ms, credits_week,
1864
+ * cap_week_credits } or null
1865
+ * loading, error, code, unavailable, reload
1866
+ *
1867
+ * `job` narrows to one job (its wake id), `limit` (20, at most 60). The
1868
+ * jobs' runs and the member's own asks, never a teammate's ask. Looks
1869
+ * every 5 s while a run is going, else every `every` ms (30 s), and at
1870
+ * once after useJobs' Run now.
1871
+ */
1872
+ export function useRuns({ job, limit = 20, every = 30000 } = {}) {
1873
+ const key = JSON.stringify(["runs", job ?? null, limit ?? null]);
1874
+ const watched = useWatched((signal) => readRuns({ job, limit, signal }), key, {
1875
+ next: (data) => (listOf(data?.runs).some((r) => r?.state === "running") ? RUNNING_MS : every),
1876
+ listen: onAsked,
1877
+ });
1878
+ const { data, ...rest } = watched;
1879
+ return { runs: listOf(data?.runs), overview: data?.overview ?? null, ...rest };
1880
+ }
1881
+
1882
+ /**
1883
+ * One run's account, for the `run-history` block's RunAccount and the
1884
+ * `workflow-view` graph (GET _claw/runs/<id>):
1885
+ *
1886
+ * const { run, steps, text, needs } = useRun(selectedId);
1887
+ *
1888
+ * run the run, as useRuns lists it, or null
1889
+ * steps [{ label, status: running | done | failed }], in order
1890
+ * text the agent's report, or null
1891
+ * needs { kind, text } while it waits on the member (answered on the
1892
+ * desk), or null
1893
+ *
1894
+ * Nothing is read while `id` is empty. Looks every 2.5 s while the run is
1895
+ * going; a run that has ended is read once.
1896
+ */
1897
+ export function useRun(id) {
1898
+ const enabled = Boolean(id);
1899
+ const watched = useWatched((signal) => readRun(id, { signal }), JSON.stringify(["run", id ?? null]), {
1900
+ enabled,
1901
+ next: (data) => (data?.run?.state === "running" ? RUN_FOLLOW_MS : 0),
1902
+ });
1903
+ const { data, ...rest } = watched;
1904
+ return { run: data?.run ?? null, steps: listOf(data?.steps), text: data?.text ?? null, needs: data?.needs ?? null, ...rest };
1905
+ }
1906
+
1907
+ /** A request's needs as a needs-you item's kind: the yes (an approval is
1908
+ * one), a question, a spend (a limit), a connect; else as it is. */
1909
+ const NEEDS_KIND = { approval: "yes", yes: "yes", question: "question", limit: "spend", connect: "connect" };
1910
+
1911
+ /**
1912
+ * What waits on this member in this app, as a needs-you item, or null.
1913
+ * Pure, so the tests hold it to each case: only the member's own
1914
+ * requests (`mine`), with a need that blocks (a `note` never does). A
1915
+ * claw whose requests carry no `needs` still says `needs_you`: the item
1916
+ * is then a question, in the request's own line.
1917
+ *
1918
+ * An item also says how the app can settle it (0.8.1, D18/D19): `via` is
1919
+ * "answer" (a question: answer it in place with its `options` and, when
1920
+ * `free`, the member's own words), "press" (the one ask's yes: the
1921
+ * `approval` block's press frame) or null (on the desk: connect, spend,
1922
+ * a tool approval, several questions at once). `needsAt` is the version
1923
+ * of the need an answer names.
1924
+ */
1925
+ export function needsYouItem(request) {
1926
+ if (!request || typeof request !== "object" || !request.mine || ended(request)) return null;
1927
+ const needs = request.needs && typeof request.needs === "object" ? request.needs : null;
1928
+ if (needs && needs.kind === "note") return null;
1929
+ if (!needs && request.status !== "needs_you") return null;
1930
+ const line = typeof request.line === "string" ? request.line.replace(/^Needs you\s*·\s*/, "") : "";
1931
+ return {
1932
+ id: request.id,
1933
+ request,
1934
+ kind: needs ? (NEEDS_KIND[needs.kind] ?? needs.kind ?? "question") : "question",
1935
+ text: (needs?.text || line || request.title || "").trim(),
1936
+ title: request.title ?? "",
1937
+ at: request.updated_at ?? request.created_at ?? null,
1938
+ via: needs?.in_app === "answer" || needs?.in_app === "press" ? needs.in_app : null,
1939
+ options: needs?.in_app === "answer" && Array.isArray(needs.options) ? needs.options : null,
1940
+ free: needs?.free !== false,
1941
+ needsAt: typeof needs?.at === "string" ? needs.at : null,
1942
+ };
1943
+ }
1944
+
1945
+ /**
1946
+ * What waits on this member here, for the `needs-you` block (GET
1947
+ * _claw/requests, the member's own only):
1948
+ *
1949
+ * const { items, working, open, takeBack, stop } = useNeedsYou();
1950
+ *
1951
+ * items [{ id, request, kind: question | yes | spend | connect | …,
1952
+ * text, title, at, via: "answer" | "press" | null, options,
1953
+ * free, needsAt }]: the agent's questions and yeses, newest
1954
+ * first. A question with via "answer" is answered in place
1955
+ * (answer(item, …)); a yes with via "press" is the `approval`
1956
+ * block's press frame; anything else, open(item) takes to
1957
+ * the desk
1958
+ * working the member's own requests the agent is working on now
1959
+ * open(item) the request's conversation on the desk
1960
+ * answer(item, { optionId, text }) answer a question in place (0.8.1):
1961
+ * → { ok } | { ok: false, error, code }; the page's
1962
+ * other readers look again at once
1963
+ * takeBack(item) stop the ask that waits (the frozen "Ready to send")
1964
+ * stop(request) stop a working one ("Stop keeps everything written
1965
+ * so far")
1966
+ * loading, error, code, unavailable, reload
1967
+ *
1968
+ * Looks every `every` ms (8 s) while the page is shown, and at once after
1969
+ * an ask from this page.
1970
+ */
1971
+ export function useNeedsYou({ every = 8000 } = {}) {
1972
+ const often = Math.max(Number(every) || 8000, 1000);
1973
+ const watched = useWatched((signal) => readRequests({ signal }), "needs-you", { next: () => often, listen: onAsked });
1974
+ const { data, reload } = watched;
1975
+ const list = listOf(data?.requests);
1976
+ const items = useMemo(
1977
+ () =>
1978
+ list
1979
+ .map(needsYouItem)
1980
+ .filter(Boolean)
1981
+ .sort((a, b) => String(b.at ?? "").localeCompare(String(a.at ?? ""))),
1982
+ [list],
1983
+ );
1984
+ const working = useMemo(() => list.filter((q) => q?.mine && LIVE.has(q.status) && q.status !== "needs_you" && !needsYouItem(q)), [list]);
1985
+ useEffect(() => {
1986
+ if (data?.desk_origin) publishDesk(data.desk_origin);
1987
+ }, [data]);
1988
+
1989
+ const open = useCallback((item) => openRequest(item?.request ?? item), []);
1990
+ const stopOne = useCallback(
1991
+ async (request) => {
1992
+ const id = request?.id;
1993
+ if (!id) return { ok: false, error: "nothing to stop" };
1994
+ const r = await stopRequest(id);
1995
+ reload();
1996
+ return r;
1997
+ },
1998
+ [reload],
1999
+ );
2000
+ const takeBack = useCallback((item) => stopOne(item?.request ?? item), [stopOne]);
2001
+ const answerOne = useCallback(
2002
+ async (item, { optionId, text } = {}) => {
2003
+ const id = item?.request?.id ?? item?.id;
2004
+ if (!id) return { ok: false, error: "nothing to answer" };
2005
+ const r = await sendAnswer(id, { optionId, text, at: item?.needsAt ?? undefined });
2006
+ if (r.ok) announceAsk();
2007
+ reload();
2008
+ return r.ok ? { ok: true } : { ok: false, error: r.error, code: r.code ?? null };
2009
+ },
2010
+ [reload],
2011
+ );
2012
+
2013
+ return {
2014
+ items,
2015
+ working,
2016
+ loading: watched.loading,
2017
+ error: watched.error,
2018
+ code: watched.code,
2019
+ unavailable: watched.unavailable,
2020
+ reload,
2021
+ open,
2022
+ answer: answerOne,
2023
+ takeBack,
2024
+ stop: stopOne,
2025
+ };
2026
+ }
2027
+
2028
+ /** How long the press frame has to say it is ready before the card
2029
+ * falls back to the desk's (a frame that cannot load says nothing). */
2030
+ const FRAME_READY_MS = 7000;
2031
+ /** The frame's height, as it asks for it: kept between these. */
2032
+ const FRAME_MIN = 96;
2033
+ const FRAME_MAX = 520;
2034
+ /** What a card shows for a request whose yes is no longer there. */
2035
+ const APPROVAL_GONE = new Set(["nothing_to_approve", "unknown_request", "not_one_yes", "not_your_request"]);
2036
+ const NO_PRESS = { for: null, state: null, height: null, always: false, ready: false };
2037
+
2038
+ /**
2039
+ * The yes, given in the app (0.8.1, D18): the facts of one request's yes
2040
+ * and the Makersclaw-held press frame to embed in the app's own card, for
2041
+ * the `approval` block.
2042
+ *
2043
+ * const approval = useApproval(item.id); // item from useNeedsYou
2044
+ * <Approval approval={approval} title="Send 3 follow-ups"> …the draft… </Approval>
2045
+ *
2046
+ * The app draws the card in its theme; the press is a page of
2047
+ * Makersclaw's in another origin, which shows the SERVER's words (the
2048
+ * agent's question, who, from which account, the first message, the rule
2049
+ * "Always allow this kind" would save) and only counts a press a person
2050
+ * made on a card they can see. App code cannot press it, and this hook
2051
+ * only listens to it.
2052
+ *
2053
+ * approval { request_id, question, label, covers, always, cap_credits,
2054
+ * at, frame: { src, origin }, expires_at } or null
2055
+ * state idle (no request) · loading · ready (the frame is up) ·
2056
+ * approved · declined · stale (the question moved on; looks
2057
+ * again by itself) · gone (nothing waits any more: answered
2058
+ * elsewhere) · signin (the frame needs the member signed in
2059
+ * on the desk) · blocked (the frame did not load in time) ·
2060
+ * unavailable (a claw from before this door) · error
2061
+ * always true once "Always allow this kind" was saved with the yes
2062
+ * fallback true when the frame cannot be used (signin, blocked,
2063
+ * unavailable, error): draw WaitingYes and open the desk
2064
+ * frame { src, ref, title } to spread on the <iframe>
2065
+ * height the px the frame asked for, kept between 96 and 520
2066
+ * loading, error, code, unavailable, reload
2067
+ *
2068
+ * Nothing here approves. A request with no id asks nothing.
2069
+ */
2070
+ export function useApproval(requestId, { fallbackMs = FRAME_READY_MS } = {}) {
2071
+ const id = requestId ? String(requestId) : null;
2072
+ const watched = useWatched((signal) => readApproval(id, { signal }), JSON.stringify(["approval", id]), {
2073
+ enabled: Boolean(id),
2074
+ next: () => 0,
2075
+ });
2076
+ const { data, reload } = watched;
2077
+ const approval = data?.approval ?? null;
2078
+ const src = approval?.frame?.src ?? null;
2079
+ const origin = approval?.frame?.origin ?? null;
2080
+ const frameRef = useRef(null);
2081
+ const [heard, setHeard] = useState(NO_PRESS);
2082
+ const [timedOut, setTimedOut] = useState(null);
2083
+ const mine = heard.for === src ? heard : NO_PRESS;
2084
+
2085
+ useEffect(() => {
2086
+ if (!src || !origin || typeof window === "undefined" || typeof window.addEventListener !== "function") return undefined;
2087
+ const onMessage = (event) => {
2088
+ const m = pressMessage(event, { origin, source: frameRef.current?.contentWindow, request: id });
2089
+ if (!m) return;
2090
+ if (m.state === "ready") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, ready: true }));
2091
+ else if (m.state === "size") {
2092
+ const px = Math.round(Number(m.height));
2093
+ 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) }));
2094
+ } else if (m.state === "approved" || m.state === "declined") {
2095
+ setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: m.state, always: m.state === "approved" && m.always === true }));
2096
+ announceAsk();
2097
+ } else if (m.state === "stale") {
2098
+ // The question moved on since this card was drawn: a new ticket, or the news that nothing waits.
2099
+ setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "stale" }));
2100
+ reload();
2101
+ } else if (m.state === "signin") setHeard((h) => ({ ...(h.for === src ? h : NO_PRESS), for: src, state: "signin" }));
2102
+ };
2103
+ window.addEventListener("message", onMessage);
2104
+ const timer = setTimeout(() => setTimedOut(src), Math.max(Number(fallbackMs) || FRAME_READY_MS, 1000));
2105
+ return () => {
2106
+ window.removeEventListener?.("message", onMessage);
2107
+ clearTimeout(timer);
2108
+ };
2109
+ }, [src, origin, id, fallbackMs, reload]);
2110
+
2111
+ let state;
2112
+ if (!id) state = "idle";
2113
+ else if (watched.unavailable) state = "unavailable";
2114
+ else if (watched.error) state = APPROVAL_GONE.has(watched.code) ? "gone" : "error";
2115
+ else if (!approval) state = "loading";
2116
+ else if (mine.state) state = mine.state;
2117
+ else if (mine.ready) state = "ready";
2118
+ else state = timedOut === src ? "blocked" : "ready";
2119
+ const fallback = state === "signin" || state === "blocked" || state === "unavailable" || state === "error";
2120
+
2121
+ return {
2122
+ approval,
2123
+ state,
2124
+ always: mine.always,
2125
+ fallback,
2126
+ frame: approval && src ? { src, ref: frameRef, title: "Approve" } : null,
2127
+ height: mine.height,
2128
+ loading: watched.loading,
2129
+ error: watched.error,
2130
+ code: watched.code,
2131
+ unavailable: watched.unavailable,
2132
+ reload,
2133
+ };
2134
+ }
2135
+
2136
+ /** A time as the key of a read: to the minute, so a `since` made fresh
2137
+ * each render (new Date(Date.now() - …)) does not read again each
2138
+ * render. */
2139
+ function minuteOf(time) {
2140
+ if (time instanceof Date) return Number.isNaN(time.getTime()) ? null : time.toISOString().slice(0, 16);
2141
+ return typeof time === "string" && time ? time.slice(0, 16) : null;
2142
+ }
2143
+
2144
+ /**
2145
+ * Who did what in this app, newest first, for the `activity-log` and
2146
+ * `agent-changes` blocks (GET _claw/data/activity):
2147
+ *
2148
+ * const { rows } = useActivity({ row: deal.id }); // one record's timeline
2149
+ * const { rows } = useActivity({ agent: true, since: askedAt }); // what the agent did
2150
+ *
2151
+ * rows [{ id, at, action, surface, row_ref, detail, data, by }]; `by`
2152
+ * is user:<email>, agent:<email> (on a member's ask) or agent:job
2153
+ * loading, error, code, unavailable, reload
2154
+ *
2155
+ * `row` narrows to one record's lines, `surface` to one surface, `since`
2156
+ * (ISO or a Date, compared to the minute) to lines after it, `limit` (50);
2157
+ * `agent: true` keeps the agent's lines only. Looks every `every` ms (20
2158
+ * s) while the page is shown. lib/door.js's useActivity is the same lines
2159
+ * read once, with no `row` and no polling; this one is the kit's.
2160
+ */
2161
+ export function useActivity({ row, surface, since, limit = 50, agent = false, every = 20000 } = {}) {
2162
+ const sinceRef = useRef(since);
2163
+ sinceRef.current = since;
2164
+ const key = JSON.stringify(["activity", row ?? null, surface ?? null, minuteOf(since), limit ?? null]);
2165
+ const watched = useWatched((signal) => readActivity({ row, surface, since: sinceRef.current, limit, signal }), key, {
2166
+ next: () => every,
2167
+ });
2168
+ const { data, ...rest } = watched;
2169
+ const all = listOf(data?.rows);
2170
+ const rows = useMemo(() => (agent ? all.filter((r) => typeof r?.by === "string" && r.by.startsWith("agent:")) : all), [all, agent]);
2171
+ return { rows, ...rest };
2172
+ }
2173
+
2174
+ /**
2175
+ * Connect one of the app's declared accounts, the way the member's press
2176
+ * should go: inside the desk, ask it to open its connections panel for
2177
+ * that need (every provider of the kind shows; no agent turn); on the
2178
+ * app's own host, fetch the link (POST _claw/connect) and open it in a new
2179
+ * tab. Signing in stays the member's, at the provider.
2180
+ *
2181
+ * The tab is opened before the link is fetched, on the press itself, and
2182
+ * pointed at the link once it comes: a tab opened after an await is a
2183
+ * popup the browser blocks. → { opened: "desk" | "tab" | false,
2184
+ * connected?, error?, code? }. Never throws. useConnections calls it.
2185
+ */
2186
+ export async function connectAccount(key, win = typeof window === "undefined" ? undefined : window) {
2187
+ if (openConnections(key, win)) return { opened: "desk" };
2188
+ let tab = null;
2189
+ try {
2190
+ tab = win?.open?.("about:blank", "_blank") ?? null;
2191
+ if (tab) tab.opener = null;
2192
+ } catch {
2193
+ tab = null;
2194
+ }
2195
+ const r = await connectLink(key);
2196
+ const url = r.ok && typeof r.data?.url === "string" && /^https?:\/\//i.test(r.data.url) ? r.data.url : null;
2197
+ if (!url) {
2198
+ try {
2199
+ tab?.close?.();
2200
+ } catch {
2201
+ /* a tab the browser already took back */
2202
+ }
2203
+ if (r.ok && r.data?.connected) return { opened: false, connected: true };
2204
+ if (r.ok) return { opened: false, error: "no link came back" };
2205
+ return { opened: false, error: r.unavailable ? null : r.error, code: r.code ?? null, unavailable: Boolean(r.unavailable) };
2206
+ }
2207
+ try {
2208
+ if (tab) {
2209
+ tab.location.href = url;
2210
+ return { opened: "tab" };
2211
+ }
2212
+ const opened = win?.open?.(url, "_blank", "noopener");
2213
+ return { opened: opened === undefined ? false : "tab" };
2214
+ } catch {
2215
+ return { opened: false, error: "the browser would not open the link" };
2216
+ }
2217
+ }
2218
+
2219
+ /** Is a real account behind this connection? `connected` is the
2220
+ * workspace listing the toolkit, which can be true while the list of
2221
+ * accounts that meet it is empty: a toolkit that needs no sign-in, or a
2222
+ * leftover (an account removed at the provider). A screen that needs an
2223
+ * account (a mailbox to read, a calendar to sync) asks this, not
2224
+ * `connected`, before it says "Connected" or offers the read: the CRM's
2225
+ * "Connect your mailbox" card said "Connected" over nothing (2026-10-11).
2226
+ * False without an `accounts` list (a claw from before it) only when
2227
+ * `connected` is false too. */
2228
+ export function hasAccount(c) {
2229
+ if (!c || !c.connected) return false;
2230
+ return Array.isArray(c.accounts) ? c.accounts.filter(Boolean).length > 0 : true;
2231
+ }
2232
+
2233
+ /**
2234
+ * The app's declared accounts, for the `connect-card` block (GET
2235
+ * _claw/connect):
2236
+ *
2237
+ * const { connections, connect } = useConnections();
2238
+ * <ConnectCard connections={connections} onConnect={connect} />
2239
+ *
2240
+ * connections [{ key, label, toolkits, required, connected, accounts,
2241
+ * category, category_label }]
2242
+ * connecting the key being connected while it is watched, or null
2243
+ * connect(key) → { opened: "desk" | "tab" | false, error? }
2244
+ * (connectAccount), then looks every 3 s for two minutes
2245
+ * until that account is connected
2246
+ * loading, error, code, unavailable, reload
2247
+ *
2248
+ * Read once, then only while an account is being connected (or every
2249
+ * `every` ms when given), and at once when the page comes back.
2250
+ */
2251
+ export function useConnections({ every } = {}) {
2252
+ const watchRef = useRef(null); // { key, until }
2253
+ const [connecting, setConnecting] = useState(null);
2254
+ const watched = useWatched((signal) => readConnections({ signal }), "connections", {
2255
+ next: (data) => {
2256
+ const watch = watchRef.current;
2257
+ if (watch) {
2258
+ const met = listOf(data?.connections).some((c) => c?.key === watch.key && c.connected);
2259
+ if (met || Date.now() > watch.until) {
2260
+ watchRef.current = null;
2261
+ setConnecting(null);
2262
+ } else return CONNECTING_MS;
2263
+ }
2264
+ return every ?? 0;
2265
+ },
2266
+ });
2267
+ const { data, reload } = watched;
2268
+
2269
+ const connect = useCallback(
2270
+ async (key) => {
2271
+ const r = await connectAccount(key);
2272
+ if (r.opened) {
2273
+ watchRef.current = { key, until: Date.now() + CONNECTING_FOR_MS };
2274
+ setConnecting(key);
2275
+ }
2276
+ reload();
2277
+ return r;
2278
+ },
2279
+ [reload],
2280
+ );
2281
+
2282
+ return {
2283
+ connections: listOf(data?.connections),
2284
+ connecting,
2285
+ loading: watched.loading,
2286
+ error: watched.error,
2287
+ code: watched.code,
2288
+ unavailable: watched.unavailable,
2289
+ reload,
2290
+ connect,
2291
+ };
2292
+ }
2293
+
2294
+ /** What the agent knows and can do, from GET _claw/agent's answer. Pure:
2295
+ * the app's own skills first (from: "app"), then those added on top
2296
+ * (from: "added", installed or not), then its tools — the declared
2297
+ * actions the agent may run. */
2298
+ export function agentKitOf(data) {
2299
+ const own = listOf(data?.own_skills)
2300
+ .filter((s) => s && typeof s.name === "string")
2301
+ .map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "app", installed: true }));
2302
+ const ownNames = new Set(own.map((s) => s.name));
2303
+ const added = listOf(data?.skills)
2304
+ .filter((s) => s && typeof s.name === "string" && !ownNames.has(s.name))
2305
+ .map((s) => ({ name: s.name, title: s.title || s.name, when: s.when ?? null, from: "added", installed: Boolean(s.installed) }));
2306
+ const tools = listOf(data?.actions)
2307
+ .filter((a) => a && typeof a.name === "string" && a.agent !== false)
2308
+ .map((a) => ({ name: a.name, description: a.description ?? "" }));
2309
+ return { skills: [...own, ...added], tools };
2310
+ }
2311
+
2312
+ /**
2313
+ * What the app's agent knows and can do, for the `agent-kit` block (GET
2314
+ * _claw/agent): `skills` [{ name, title, when, from: "app" | "added",
2315
+ * installed }] and `tools` [{ name, description }] (agentKitOf), with
2316
+ * loading, error, code, unavailable, reload. Read once.
2317
+ */
2318
+ export function useAgentKit() {
2319
+ const watched = useWatched((signal) => readAgentKit({ signal }), "agent-kit");
2320
+ const { data, ...rest } = watched;
2321
+ const kit = useMemo(() => agentKitOf(data), [data]);
2322
+ return { ...kit, ...rest };
2323
+ }
2324
+
2325
+ /** How long useLlmAction waits for the input to settle before it asks
2326
+ * for a price. */
2327
+ const ESTIMATE_AFTER_MS = 400;
2328
+
2329
+ /**
2330
+ * One model call on what the screen holds, with its price before the
2331
+ * press, for the `ai-action` and `cost` blocks:
2332
+ *
2333
+ * const shorter = useLlmAction({ input: text, system: "Make it shorter.", task: "shorten" });
2334
+ * <AiAction label="Shorter" action={shorter} onUse={(r) => setText(r.text)} />
2335
+ *
2336
+ * estimate { credits, credits_max } or null: the price, asked of the
2337
+ * platform (llmEstimate: priced, never sent to a model,
2338
+ * never charged) once per distinct input, 400 ms after it
2339
+ * settles. Null for an empty input, and on a claw with no
2340
+ * estimate yet. While a new input is priced the last price
2341
+ * stays, with `estimating` true
2342
+ * estimating a price is being asked for
2343
+ * run() the call, with the same arguments → llm()'s answer
2344
+ * result, loading, error, unavailable as useLlm
2345
+ * reset() back to nothing shown
2346
+ *
2347
+ * Options: llm()'s — input, system, schema, tier, task, maxTokens — and
2348
+ * `estimate` (default true; false asks no price).
2349
+ */
2350
+ export function useLlmAction({ input, system, schema, tier, task, maxTokens, estimate = true } = {}) {
2351
+ const call = useLlm();
2352
+ const argsRef = useRef(null);
2353
+ argsRef.current = { input, opts: { system, schema, tier, task, maxTokens } };
2354
+ const priced = Boolean(estimate) && saidSomething(input);
2355
+ let key = null;
2356
+ if (priced) {
2357
+ try {
2358
+ key = JSON.stringify([input, system ?? null, schema ?? null, tier ?? null, task ?? null, maxTokens ?? null]);
2359
+ } catch {
2360
+ key = null; // an input that is not JSON: llm() refuses it too
2361
+ }
2362
+ }
2363
+ const cacheRef = useRef(new Map());
2364
+ const [price, setPrice] = useState({ key: null, value: null, estimating: false });
2365
+
2366
+ useEffect(() => {
2367
+ if (!key) return undefined;
2368
+ const known = cacheRef.current.get(key);
2369
+ if (known !== undefined) {
2370
+ setPrice({ key, value: known, estimating: false });
2371
+ return undefined;
2372
+ }
2373
+ const controller = new AbortController();
2374
+ const timer = setTimeout(async () => {
2375
+ setPrice((p) => ({ ...p, estimating: true }));
2376
+ const { input: now, opts } = argsRef.current;
2377
+ const r = await llmEstimate(now, { ...opts, signal: controller.signal });
2378
+ if (controller.signal.aborted || r.aborted) return;
2379
+ const value =
2380
+ r.ok && r.estimate && typeof r.estimate === "object" && typeof r.estimate.credits === "number"
2381
+ ? { credits: r.estimate.credits, credits_max: typeof r.estimate.credits_max === "number" ? r.estimate.credits_max : r.estimate.credits }
2382
+ : null;
2383
+ if (r.ok) cacheRef.current.set(key, value);
2384
+ setPrice({ key, value, estimating: false });
2385
+ }, ESTIMATE_AFTER_MS);
2386
+ return () => {
2387
+ clearTimeout(timer);
2388
+ controller.abort();
2389
+ };
2390
+ }, [key]);
2391
+
2392
+ const run = useCallback(() => {
2393
+ const { input: now, opts } = argsRef.current;
2394
+ return call.run(now, opts);
2395
+ }, [call.run]);
2396
+
2397
+ return {
2398
+ estimate: key ? price.value : null,
2399
+ estimating: Boolean(key) && (price.estimating || (price.key !== key && !cacheRef.current.has(key))),
2400
+ run,
2401
+ result: call.result,
2402
+ loading: call.loading,
2403
+ error: call.error,
2404
+ unavailable: call.unavailable,
2405
+ reset: call.reset,
2406
+ };
2407
+ }
2408
+
2409
+ // The automations engine's hooks (useAutomations, useAutomation,
2410
+ // useAutomationRun) live in lib/automations.js, with their doors; a surface
2411
+ // imports them from here with the others.
2412
+ export * from "./automations";