@kitn.ai/ui 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (603) hide show
  1. package/README.md +17 -6
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon-b5A8hlHs.js → Icon-BnFKqqtY.js} +1 -1
  6. package/dist/action-icons-CBskfBMz.js +1 -0
  7. package/dist/arrow-down-Br0qtpsz.js +1 -0
  8. package/dist/arrow-left-Dj7xgE12.js +1 -0
  9. package/dist/artifact-CvXEm1lS.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-C43ZegEq.js +1 -0
  12. package/dist/audio-visualizer-B-79y1Tj.js +1 -0
  13. package/dist/badge-BpUbq8c0.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-sn17Ip9v.js +1 -0
  16. package/dist/{elements/chunks/button-B9okqG73.js → button-BH6rN0U-.js} +1 -1
  17. package/dist/card-renderer-Cb1gQmth.js +1 -0
  18. package/dist/card-routing-DifQgS7n.js +1 -0
  19. package/dist/check-DPeeMUYx.js +1 -0
  20. package/dist/checkbox-Dc9Yov_B.js +1 -0
  21. package/dist/checkbox-group-B1kDLLYk.js +1 -0
  22. package/dist/chevron-down-CHSWk7ZT.js +1 -0
  23. package/dist/chevron-right-B7l_fMgx.js +1 -0
  24. package/dist/choice-card-BT3c1S8g.js +1 -0
  25. package/dist/circle-Bb0yOpqM.js +1 -0
  26. package/dist/circle-check-BGZpCiz7.js +1 -0
  27. package/dist/circle-x-wobWkoSx.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/code-block-CUQ5mtv0.js +1 -0
  30. package/dist/collapsible-CPJbf6FM.js +1 -0
  31. package/dist/components/attachment-types.d.ts +53 -1
  32. package/dist/components/attachments.d.ts +31 -0
  33. package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
  34. package/dist/components/audio-visualizer/index.d.ts +15 -1
  35. package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
  36. package/dist/components/card-renderer.d.ts +13 -0
  37. package/dist/components/card.d.ts +4 -0
  38. package/dist/components/chat-thread.d.ts +49 -42
  39. package/dist/components/code-block.d.ts +22 -0
  40. package/dist/components/conversation-item.d.ts +66 -1
  41. package/dist/components/conversation-list.d.ts +77 -0
  42. package/dist/components/form-widgets.d.ts +67 -2
  43. package/dist/components/form.d.ts +89 -0
  44. package/dist/components/message.d.ts +29 -0
  45. package/dist/components/reasoning.d.ts +6 -0
  46. package/dist/components/scroll-button.d.ts +11 -0
  47. package/dist/components/thread.d.ts +5 -0
  48. package/dist/components/toast.d.ts +2 -1
  49. package/dist/components/tool-types.d.ts +9 -0
  50. package/dist/components/voice-input.d.ts +8 -0
  51. package/dist/components/voice-output.d.ts +12 -1
  52. package/dist/components/workspace-shell.d.ts +87 -0
  53. package/dist/composer-CjngLzal.js +106 -0
  54. package/dist/confirm-card-DCx_A4bU.js +1 -0
  55. package/dist/construct-cli.es.js +1169 -0
  56. package/dist/context-BY8V-Nlp.js +1 -0
  57. package/dist/controllable-D43BKmtX.js +1 -0
  58. package/dist/conversation-list-D4lLU_yS.js +1 -0
  59. package/dist/copy-B6DO4PqB.js +1 -0
  60. package/dist/core-AYMC6_lb.js +12 -5874
  61. package/dist/{create-tween-CUzOlaMS.js → create-tween-BPzaefTp.js} +1 -1
  62. package/dist/{create-tween-B5Y4Im8G.js → create-tween-CnMHviKB.js} +1 -1
  63. package/dist/{create-tween-4z1XYLkR.js → create-tween-D1aiGZZh.js} +1 -1
  64. package/dist/{elements/chunks/create-tween-CUrM6D_o.js → create-tween-DUSQUKpr.js} +2 -2
  65. package/dist/css-M7EaDHN_.js +1 -6
  66. package/dist/custom-elements.json +3468 -1927
  67. package/dist/default-input-CucQEOqH.js +1 -0
  68. package/dist/define-CGGX-7Vg.js +1 -0
  69. package/dist/define.d.ts +4 -0
  70. package/dist/define.js +510 -0
  71. package/dist/define.server.d.ts +5 -0
  72. package/dist/define.server.js +506 -0
  73. package/dist/diagnostics/hook.d.ts +81 -0
  74. package/dist/diagnostics/index.d.ts +7 -0
  75. package/dist/diagnostics/report-request.d.ts +61 -0
  76. package/dist/diagnostics.d.ts +5 -0
  77. package/dist/diagnostics.js +209 -0
  78. package/dist/disclosure-CbLtajUQ.js +1 -0
  79. package/dist/dropdown-BwhViJFd.js +1 -0
  80. package/dist/elements/agent-card.js +1 -1
  81. package/dist/elements/artifact.js +1 -1
  82. package/dist/elements/attachments.js +1 -1
  83. package/dist/elements/audio-visualizer.js +1 -1
  84. package/dist/elements/autoloader.js +1 -1
  85. package/dist/elements/avatar.js +1 -1
  86. package/dist/elements/badge.js +1 -1
  87. package/dist/elements/button.js +1 -1
  88. package/dist/elements/card.js +1 -1
  89. package/dist/elements/cards.js +1 -1
  90. package/dist/elements/chain-of-thought.js +1 -1
  91. package/dist/elements/chat-scope-picker.js +1 -1
  92. package/dist/elements/chat-workspace.js +1 -1
  93. package/dist/elements/chat.js +1 -1
  94. package/dist/elements/checkbox-group.d.ts +1 -0
  95. package/dist/elements/checkbox-group.js +1 -0
  96. package/dist/elements/checkbox.d.ts +1 -0
  97. package/dist/elements/checkbox.js +1 -0
  98. package/dist/elements/checkpoint.js +1 -1
  99. package/dist/elements/choice.js +1 -1
  100. package/dist/elements/coachmark.js +1 -1
  101. package/dist/elements/code-block.js +1 -1
  102. package/dist/elements/command.js +1 -1
  103. package/dist/elements/compare.js +1 -1
  104. package/dist/elements/composer.js +1 -1
  105. package/dist/elements/confirm-card.js +1 -1
  106. package/dist/elements/context-meter.js +1 -1
  107. package/dist/elements/conversation-item.d.ts +1 -0
  108. package/dist/elements/conversation-item.js +1 -0
  109. package/dist/elements/conversation-list.js +1 -1
  110. package/dist/elements/default-input.d.ts +3 -3
  111. package/dist/elements/define-entry.d.ts +12 -0
  112. package/dist/elements/define.d.ts +27 -0
  113. package/dist/elements/diagnostic-events.d.ts +74 -0
  114. package/dist/elements/dialog.js +1 -1
  115. package/dist/elements/dock.d.ts +1 -0
  116. package/dist/elements/dock.js +243 -0
  117. package/dist/elements/dropdown.d.ts +1 -0
  118. package/dist/elements/dropdown.js +1 -0
  119. package/dist/elements/editable-label.js +1 -1
  120. package/dist/elements/element-data-types.d.ts +37 -0
  121. package/dist/elements/element-diagnostics.d.ts +81 -0
  122. package/dist/elements/embed.js +1 -1
  123. package/dist/elements/empty.js +1 -1
  124. package/dist/elements/feedback-bar.js +1 -1
  125. package/dist/elements/file-tree.js +1 -1
  126. package/dist/elements/file-upload.js +1 -1
  127. package/dist/elements/form.js +1 -1
  128. package/dist/elements/hover-card.js +1 -1
  129. package/dist/elements/icon.js +1 -1
  130. package/dist/elements/image.js +1 -1
  131. package/dist/elements/input.js +1 -1
  132. package/dist/elements/kbd.js +1 -1
  133. package/dist/elements/link-preview.js +1 -1
  134. package/dist/elements/loader.js +1 -1
  135. package/dist/elements/markdown.js +1 -1
  136. package/dist/elements/menu.js +1 -1
  137. package/dist/elements/message-skills.js +1 -1
  138. package/dist/elements/message.js +1 -1
  139. package/dist/elements/model-switcher.js +1 -1
  140. package/dist/elements/nav.js +1 -1
  141. package/dist/elements/notice.js +1 -1
  142. package/dist/elements/pane-grid.d.ts +1 -0
  143. package/dist/elements/pane-grid.js +1 -0
  144. package/dist/elements/pane-group.js +1 -1
  145. package/dist/elements/pane.js +1 -1
  146. package/dist/elements/popover.js +1 -1
  147. package/dist/elements/progress-bar.js +1 -1
  148. package/dist/elements/prompt-dock.js +1 -1
  149. package/dist/elements/prompt-input.js +1 -1
  150. package/dist/elements/prompt-suggestions.js +1 -1
  151. package/dist/elements/radio-group.d.ts +1 -0
  152. package/dist/elements/radio-group.js +1 -0
  153. package/dist/elements/reasoning.js +1 -1
  154. package/dist/elements/register.d.ts +2 -0
  155. package/dist/elements/remote.js +1 -1
  156. package/dist/elements/resizable.js +1 -1
  157. package/dist/elements/response-stream.js +3 -3
  158. package/dist/elements/screen.js +1 -1
  159. package/dist/elements/scroll-area.js +1 -1
  160. package/dist/elements/scroll-button.js +1 -1
  161. package/dist/elements/search.js +1 -1
  162. package/dist/elements/segmented.js +1 -1
  163. package/dist/elements/select.d.ts +1 -0
  164. package/dist/elements/select.js +1 -0
  165. package/dist/elements/separator.js +1 -1
  166. package/dist/elements/setting-item.js +1 -1
  167. package/dist/elements/settings-group.js +1 -1
  168. package/dist/elements/skeleton.js +1 -1
  169. package/dist/elements/slider.d.ts +1 -0
  170. package/dist/elements/slider.js +1 -0
  171. package/dist/elements/slots.d.ts +36 -4
  172. package/dist/elements/source.js +1 -1
  173. package/dist/elements/status.js +1 -1
  174. package/dist/elements/switch.js +1 -1
  175. package/dist/elements/tabs.js +1 -1
  176. package/dist/elements/tasks.js +1 -1
  177. package/dist/elements/text-shimmer.js +1 -1
  178. package/dist/elements/thinking-bar.js +1 -1
  179. package/dist/elements/thread.js +1 -1
  180. package/dist/elements/toast.js +1 -1
  181. package/dist/elements/tool.js +1 -1
  182. package/dist/elements/tooltip.js +1 -1
  183. package/dist/elements/voice-input.js +1 -1
  184. package/dist/elements/voice-output.js +1 -1
  185. package/dist/elements.d.ts +589 -167
  186. package/dist/{elements/chunks/ellipsis-CVxqfsDZ.js → ellipsis-DFr4Vqr3.js} +1 -1
  187. package/dist/embed-5s_vU4J0.js +1 -0
  188. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  189. package/dist/{elements/chunks/external-link-DG2sRdQL.js → external-link-u9d85ORQ.js} +1 -1
  190. package/dist/file-text-CX7_x42o.js +1 -0
  191. package/dist/{elements/chunks/file-tree-yGWNK0Js.js → file-tree-D3kBjXI5.js} +1 -1
  192. package/dist/folder-V-YdIL8f.js +1 -0
  193. package/dist/form-BRNUMCN2.js +1 -0
  194. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  195. package/dist/github-light-JYsPkUQd.js +1 -4
  196. package/dist/hover-card-DaeiYrTA.js +1 -0
  197. package/dist/html-CPZ3oZQ7.js +1 -10
  198. package/dist/icon-BpixdZNq.js +1 -0
  199. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  200. package/dist/index.d.ts +3 -1
  201. package/dist/index.js +8473 -6470
  202. package/dist/index.server.js +6495 -4958
  203. package/dist/info-8mFsIjS3.js +1 -0
  204. package/dist/input-d1Yuu-hz.js +1 -0
  205. package/dist/javascript-C25yR2R2.js +1 -6
  206. package/dist/json-DxJze_jm.js +1 -6
  207. package/dist/kai-provider.es.js +40 -40
  208. package/dist/kai.es.js +1 -1
  209. package/dist/kbd-961zMPFB.js +1 -0
  210. package/dist/link-DlZZBGaR.js +1 -0
  211. package/dist/{elements/chunks/link-preview-Cf5KOP-i.js → link-preview-DIIbV2Sd.js} +1 -1
  212. package/dist/loader-YQszHFaW.js +1 -0
  213. package/dist/{elements/chunks/markdown-DbozyYqa.js → markdown-CVk1MPsj.js} +1 -1
  214. package/dist/mcp.es.js +3873 -544
  215. package/dist/message-CwKVEdZg.js +1 -0
  216. package/dist/message-DS8lzHc1.js +1 -0
  217. package/dist/message-circle-Xf8Q-vZp.js +1 -0
  218. package/dist/message-square-Cxnwgkns.js +1 -0
  219. package/dist/{elements/chunks/minimize-2-SegJ_oku.js → minimize-2-UEZmP2QT.js} +1 -1
  220. package/dist/{elements/chunks/model-switcher-D7JrFFHp.js → model-switcher-Kk1jEQ5t.js} +1 -1
  221. package/dist/{elements/chunks/overlay-CV7z2YuX.js → overlay-C1od6LOz.js} +1 -1
  222. package/dist/panel-right-ryET0yyn.js +1 -0
  223. package/dist/paperclip-BDNoZFJP.js +1 -0
  224. package/dist/play-OFIvYXUQ.js +1 -0
  225. package/dist/primitives/card-data-types.d.ts +49 -0
  226. package/dist/primitives/card-routing.d.ts +1 -33
  227. package/dist/primitives/card-validate.d.ts +2 -2
  228. package/dist/primitives/chat-config.d.ts +29 -2
  229. package/dist/primitives/field-mask.d.ts +96 -0
  230. package/dist/primitives/field-semantics.d.ts +36 -0
  231. package/dist/primitives/input-mask.d.ts +46 -0
  232. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  233. package/dist/primitives/use-resize-observer.d.ts +23 -0
  234. package/dist/primitives/use-speech-recognition.d.ts +8 -0
  235. package/dist/primitives/use-text-stream.d.ts +5 -0
  236. package/dist/progress-bar-BvYX0aoy.js +1 -0
  237. package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
  238. package/dist/radio-DcL23cNJ.js +1 -0
  239. package/dist/react/index.d.ts +306 -283
  240. package/dist/react.js +309 -247
  241. package/dist/reasoning-Cackq4Rh.js +1 -0
  242. package/dist/register-impl-C7rENvPK.js +424 -0
  243. package/dist/{elements/chunks/resizable-Dpzjlvp4.js → resizable-Cp9HUYih.js} +1 -1
  244. package/dist/rotate-ccw-0DLRDHH5.js +1 -0
  245. package/dist/rotate-cw-DOB9IGcU.js +1 -0
  246. package/dist/schemas/form.schema.json +21 -1
  247. package/dist/schemas/index.d.ts +1 -1
  248. package/dist/schemas/tool-defs.d.ts +45 -0
  249. package/dist/schemas.js +398 -305
  250. package/dist/scroll-area-emVmWP4K.js +1 -0
  251. package/dist/scroll-button-CeP_gv1x.js +1 -0
  252. package/dist/select-CWmu00tS.js +1 -0
  253. package/dist/{elements/chunks/separator-DH2r_ZSZ.js → separator-CE6vly6z.js} +1 -1
  254. package/dist/{elements/chunks/settings-BFMOd6yB.js → settings-CE-78SvZ.js} +1 -1
  255. package/dist/{elements/chunks/settings-group-B3VZKIcz.js → settings-group-ZFWNxFQR.js} +1 -1
  256. package/dist/{elements/chunks/skeleton-N8BuWYmf.js → skeleton-BebdP4mf.js} +1 -1
  257. package/dist/slider-BAKRYUlz.js +1 -0
  258. package/dist/slots-DaNjX0rc.js +1 -0
  259. package/dist/{solid-BMuDmFo_.js → solid-BE5ui6sy.js} +10672 -8116
  260. package/dist/{solid-CGYNdLRR.js → solid-DFhsce3O.js} +8137 -6056
  261. package/dist/solid.d.ts +15 -0
  262. package/dist/solid.js +248 -235
  263. package/dist/solid.server.js +248 -235
  264. package/dist/source-B0FlguSx.js +1 -0
  265. package/dist/star-CBMZvLuN.js +1 -0
  266. package/dist/state/index.d.ts +5 -1
  267. package/dist/state/mock.d.ts +32 -2
  268. package/dist/state/persistence.d.ts +51 -0
  269. package/dist/state/stream.d.ts +50 -0
  270. package/dist/state/threads.d.ts +71 -0
  271. package/dist/state.js +297 -172
  272. package/dist/store-7eoadydQ.js +1 -0
  273. package/dist/svelte--5p79yCD.js +1 -15
  274. package/dist/switch-CViARZCy.js +1 -0
  275. package/dist/tasks-card-MjCcOhR_.js +1 -0
  276. package/dist/text-shimmer-DKlwRF2G.js +1 -0
  277. package/dist/textarea-3ay2Me2-.js +1 -0
  278. package/dist/theme.tokens.css +82 -13
  279. package/dist/{elements/chunks/thumbs-up-DQe0hvkZ.js → thumbs-up-DQcu2HMO.js} +1 -1
  280. package/dist/toast-store-C6yKOhbk.js +1 -0
  281. package/dist/{elements/chunks/tool-C1YtxwPu.js → tool-BCYNlfKy.js} +1 -1
  282. package/dist/{elements/chunks/tooltip-BqwnFWhn.js → tooltip-05zI4hjs.js} +1 -1
  283. package/dist/trash-2-BR1RxGky.js +1 -0
  284. package/dist/{elements/chunks/triangle-alert-B6or7F5u.js → triangle-alert-DHohDOCy.js} +1 -1
  285. package/dist/tsx-B8rCNbgL.js +1 -6
  286. package/dist/types.d.ts +4 -2
  287. package/dist/typescript-RycA9KXf.js +1 -6
  288. package/dist/ui/checkbox-group.d.ts +83 -0
  289. package/dist/ui/checkbox.d.ts +40 -0
  290. package/dist/ui/dock.d.ts +131 -0
  291. package/dist/ui/hover-card.d.ts +44 -0
  292. package/dist/ui/icon.d.ts +3 -0
  293. package/dist/ui/input.d.ts +48 -2
  294. package/dist/ui/radio.d.ts +85 -0
  295. package/dist/ui/select.d.ts +71 -0
  296. package/dist/ui/slider.d.ts +65 -0
  297. package/dist/ui/switch.d.ts +9 -2
  298. package/dist/upload-BbGW6A_u.js +1 -0
  299. package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
  300. package/dist/use-card-resolution-CJW4grFa.js +1 -0
  301. package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-BUOVgPOB.js} +27 -27
  302. package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CUsUJvFI.js} +28 -28
  303. package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CW2i4bfr.js} +2 -2
  304. package/dist/{elements/chunks/variant-aurora-QXTZPyST.js → variant-aurora-xiMPty8f.js} +2 -2
  305. package/dist/variant-custom-BQtOnmeO.js +1 -0
  306. package/dist/{variant-custom-BhS9wIpU.js → variant-custom-CAm9L-E5.js} +65 -65
  307. package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-CAnzzppW.js} +40 -40
  308. package/dist/variant-custom-D91HjAKz.js +1 -0
  309. package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-Ca1Ls4_y.js} +21 -21
  310. package/dist/{variant-wave-CbVsmgwT.js → variant-wave-CxX39DKd.js} +25 -25
  311. package/dist/{variant-wave-BwzrHD1s.js → variant-wave-DYyzIIZv.js} +2 -2
  312. package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-uY5crU_K.js} +2 -2
  313. package/dist/vue-BmIZj4XD.js +1 -33
  314. package/dist/wire/chunk.d.ts +47 -0
  315. package/dist/wire/diagnostics.d.ts +575 -0
  316. package/dist/wire/encode-probe.d.ts +70 -0
  317. package/dist/wire/encode.d.ts +24 -0
  318. package/dist/wire/index.d.ts +2 -0
  319. package/dist/wire/sse.d.ts +10 -2
  320. package/dist/wire.js +917 -480
  321. package/dist/x-C5m1JFgi.js +1 -0
  322. package/frameworks/react/index.tsx +325 -108
  323. package/llms-full.txt +1354 -82
  324. package/llms.txt +6 -3
  325. package/package.json +39 -6
  326. package/src/agent-tooling/archetypes.ts +41 -9
  327. package/src/agent-tooling/catalog/README.md +676 -0
  328. package/src/agent-tooling/catalog/catalog-types.ts +231 -0
  329. package/src/agent-tooling/catalog/fabrications.ts +96 -0
  330. package/src/agent-tooling/catalog/invariants.ts +284 -0
  331. package/src/agent-tooling/catalog/labs-titles.ts +114 -0
  332. package/src/agent-tooling/catalog/scenarios.ts +87 -0
  333. package/src/agent-tooling/catalog/surfaces.ts +347 -0
  334. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  335. package/src/agent-tooling/construct/cli.ts +131 -0
  336. package/src/agent-tooling/construct/codegen.ts +1353 -0
  337. package/src/agent-tooling/construct/construct.v1.schema.json +250 -0
  338. package/src/agent-tooling/construct/dev.ts +148 -0
  339. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  340. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  341. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +23 -0
  342. package/src/agent-tooling/construct/schema.ts +371 -0
  343. package/src/agent-tooling/integrations/anthropic.ts +7 -1
  344. package/src/agent-tooling/integrations/cloudflare.ts +43 -2
  345. package/src/agent-tooling/integrations/langgraph.ts +7 -1
  346. package/src/agent-tooling/integrations/mastra.ts +7 -1
  347. package/src/agent-tooling/integrations/mock.ts +92 -17
  348. package/src/agent-tooling/integrations/ollama.ts +7 -1
  349. package/src/agent-tooling/integrations/openai.ts +7 -1
  350. package/src/agent-tooling/integrations/openrouter.ts +7 -1
  351. package/src/agent-tooling/integrations/pi.ts +56 -1
  352. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
  353. package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
  354. package/src/agent-tooling/mcp/manifest.ts +100 -1
  355. package/src/agent-tooling/mcp/server.ts +63 -6
  356. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  357. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  358. package/src/agent-tooling/mcp/tools/reference.ts +542 -5
  359. package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
  360. package/src/agent-tooling/mcp/tools/theme.ts +146 -30
  361. package/src/agent-tooling/mcp/validate-args.ts +141 -0
  362. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  363. package/src/agent-tooling/recipes/index.ts +22 -0
  364. package/src/agent-tooling/recipes/types.ts +32 -0
  365. package/src/agent-tooling/registry.ts +24 -7
  366. package/src/agent-tooling/route-emit.ts +26 -2
  367. package/src/components/attachment-types.ts +54 -1
  368. package/src/components/attachments.tsx +169 -22
  369. package/src/components/audio-visualizer/fit-scale.ts +112 -0
  370. package/src/components/audio-visualizer/index.tsx +80 -3
  371. package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
  372. package/src/components/audio-visualizer/variant-bar.tsx +38 -6
  373. package/src/components/audio-visualizer/variant-custom.tsx +8 -2
  374. package/src/components/audio-visualizer/variant-grid.tsx +16 -11
  375. package/src/components/audio-visualizer/variant-radial.tsx +15 -10
  376. package/src/components/audio-visualizer/variant-wave.tsx +7 -2
  377. package/src/components/card-renderer.tsx +39 -8
  378. package/src/components/card.tsx +10 -2
  379. package/src/components/chat-container.tsx +4 -1
  380. package/src/components/chat-scope-picker.tsx +2 -2
  381. package/src/components/chat-thread.tsx +105 -13
  382. package/src/components/choice-card.tsx +102 -37
  383. package/src/components/coachmark.tsx +1 -1
  384. package/src/components/code-block.tsx +81 -5
  385. package/src/components/composer.tsx +89 -8
  386. package/src/components/confirm-card.tsx +20 -4
  387. package/src/components/conversation-item.tsx +138 -4
  388. package/src/components/conversation-list.tsx +230 -15
  389. package/src/components/embed.tsx +4 -0
  390. package/src/components/empty.tsx +14 -3
  391. package/src/components/file-tree.tsx +5 -1
  392. package/src/components/form-widgets.tsx +257 -195
  393. package/src/components/form.tsx +336 -21
  394. package/src/components/loader.tsx +12 -5
  395. package/src/components/message-skills.tsx +1 -1
  396. package/src/components/message.tsx +142 -12
  397. package/src/components/prompt-input.tsx +7 -1
  398. package/src/components/prompt-suggestion.tsx +4 -1
  399. package/src/components/reasoning.tsx +30 -3
  400. package/src/components/response-compare.tsx +2 -2
  401. package/src/components/response-stream.tsx +13 -7
  402. package/src/components/screen.tsx +5 -0
  403. package/src/components/scroll-button.tsx +63 -4
  404. package/src/components/source.tsx +6 -1
  405. package/src/components/tasks-card.tsx +58 -16
  406. package/src/components/thread.tsx +10 -1
  407. package/src/components/toast.tsx +11 -5
  408. package/src/components/tool-types.ts +9 -0
  409. package/src/components/voice-input.tsx +35 -3
  410. package/src/components/voice-output.tsx +46 -6
  411. package/src/components/workspace-shell.tsx +285 -0
  412. package/src/diagnostics/hook.ts +352 -0
  413. package/src/diagnostics/index.ts +82 -0
  414. package/src/diagnostics/report-request.ts +296 -0
  415. package/src/elements/attachments.tsx +9 -8
  416. package/src/elements/audio-visualizer.tsx +48 -4
  417. package/src/elements/chat-workspace.tsx +159 -287
  418. package/src/elements/chat.tsx +31 -10
  419. package/src/elements/checkbox-group.tsx +164 -0
  420. package/src/elements/checkbox.tsx +134 -0
  421. package/src/elements/code-block.tsx +19 -1
  422. package/src/elements/command.tsx +4 -1
  423. package/src/elements/compiled.css +1 -1
  424. package/src/elements/composer.tsx +12 -1
  425. package/src/elements/conversation-item.tsx +120 -0
  426. package/src/elements/conversation-list.tsx +60 -11
  427. package/src/elements/default-input.tsx +5 -5
  428. package/src/elements/define-entry.ts +12 -0
  429. package/src/elements/define.tsx +168 -8
  430. package/src/elements/diagnostic-events.ts +114 -0
  431. package/src/elements/dialog.tsx +47 -0
  432. package/src/elements/disclosure.ts +34 -12
  433. package/src/elements/dock.tsx +215 -0
  434. package/src/elements/dropdown.tsx +123 -0
  435. package/src/elements/element-data-types.ts +40 -0
  436. package/src/elements/element-diagnostics.ts +392 -0
  437. package/src/elements/element-manifest.json +36 -0
  438. package/src/elements/element-meta.json +1483 -560
  439. package/src/elements/element-nonscalar.json +185 -0
  440. package/src/elements/element-types.d.ts +589 -167
  441. package/src/elements/feedback-bar.tsx +5 -0
  442. package/src/elements/icon-names.json +29 -0
  443. package/src/elements/input.tsx +312 -23
  444. package/src/elements/menu.tsx +13 -0
  445. package/src/elements/message.tsx +2 -0
  446. package/src/elements/pane-grid.tsx +111 -0
  447. package/src/elements/prompt-dock.tsx +2 -1
  448. package/src/elements/prompt-input.tsx +16 -10
  449. package/src/elements/radio-group.tsx +129 -0
  450. package/src/elements/register-impl.ts +52 -0
  451. package/src/elements/register.ts +28 -0
  452. package/src/elements/resizable.tsx +52 -25
  453. package/src/elements/scroll-button.tsx +59 -6
  454. package/src/elements/select.tsx +165 -0
  455. package/src/elements/slider.tsx +171 -0
  456. package/src/elements/slots.ts +108 -13
  457. package/src/elements/styles.css +119 -9
  458. package/src/elements/thinking-bar.tsx +16 -7
  459. package/src/elements/thread.tsx +2 -0
  460. package/src/elements/toast.tsx +17 -1
  461. package/src/elements/tool.tsx +26 -5
  462. package/src/elements/voice-input.tsx +11 -2
  463. package/src/elements/voice-output.tsx +16 -3
  464. package/src/index.ts +4 -1
  465. package/src/primitives/card-data-types.ts +49 -0
  466. package/src/primitives/card-host.tsx +4 -2
  467. package/src/primitives/card-routing.ts +7 -56
  468. package/src/primitives/card-schemas/form.schema.json +21 -1
  469. package/src/primitives/card-validate.ts +10 -2
  470. package/src/primitives/chat-config.tsx +33 -6
  471. package/src/primitives/field-mask.ts +256 -0
  472. package/src/primitives/field-semantics.ts +115 -0
  473. package/src/primitives/input-mask.ts +853 -0
  474. package/src/primitives/toast-store.ts +66 -4
  475. package/src/primitives/url-scheme-policy.ts +70 -0
  476. package/src/primitives/use-resize-observer.ts +33 -0
  477. package/src/primitives/use-speech-recognition.ts +12 -1
  478. package/src/primitives/use-stick-to-bottom.ts +34 -2
  479. package/src/primitives/use-text-stream.ts +33 -2
  480. package/src/remote/provider-runtime.ts +6 -3
  481. package/src/schemas/index.ts +1 -0
  482. package/src/schemas/tool-defs.ts +307 -6
  483. package/src/solid.ts +15 -0
  484. package/src/state/index.ts +13 -1
  485. package/src/state/mock.ts +92 -6
  486. package/src/state/persistence.ts +180 -0
  487. package/src/state/stream.ts +71 -4
  488. package/src/state/threads.ts +142 -0
  489. package/src/types.ts +4 -2
  490. package/src/ui/agent-card.tsx +2 -2
  491. package/src/ui/avatar.tsx +1 -1
  492. package/src/ui/checkbox-group.tsx +153 -0
  493. package/src/ui/checkbox.tsx +63 -0
  494. package/src/ui/dialog.tsx +6 -0
  495. package/src/ui/dock.tsx +584 -0
  496. package/src/ui/dropdown.tsx +67 -23
  497. package/src/ui/hover-card.tsx +156 -6
  498. package/src/ui/icon.tsx +53 -0
  499. package/src/ui/input.tsx +275 -16
  500. package/src/ui/kbd.tsx +1 -1
  501. package/src/ui/nav.tsx +1 -1
  502. package/src/ui/pane-group.tsx +2 -2
  503. package/src/ui/radio.tsx +150 -0
  504. package/src/ui/resizable.tsx +11 -0
  505. package/src/ui/select.tsx +168 -0
  506. package/src/ui/slider.tsx +178 -0
  507. package/src/ui/switch.tsx +49 -18
  508. package/src/utils/cn.ts +11 -5
  509. package/src/wire/chunk.ts +47 -0
  510. package/src/wire/consume.ts +185 -26
  511. package/src/wire/diagnostics.ts +727 -0
  512. package/src/wire/encode-probe.ts +214 -0
  513. package/src/wire/encode.ts +267 -14
  514. package/src/wire/formats/anthropic.ts +8 -1
  515. package/src/wire/formats/openai.ts +5 -0
  516. package/src/wire/index.ts +22 -0
  517. package/src/wire/read.ts +250 -6
  518. package/src/wire/sse.ts +16 -3
  519. package/theme.css +84 -13
  520. package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
  521. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
  522. package/dist/elements/chunks/artifact-BSuPYFHY.js +0 -1
  523. package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
  524. package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
  525. package/dist/elements/chunks/badge-TIMiJFKc.js +0 -1
  526. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  527. package/dist/elements/chunks/card-renderer-CSoTL_e1.js +0 -1
  528. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  529. package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
  530. package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
  531. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
  532. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
  533. package/dist/elements/chunks/choice-card-CGMc3kek.js +0 -1
  534. package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
  535. package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
  536. package/dist/elements/chunks/circle-x-mhH7pOK2.js +0 -1
  537. package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
  538. package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
  539. package/dist/elements/chunks/composer-CZZmtYOT.js +0 -69
  540. package/dist/elements/chunks/confirm-card-BWjf2-xo.js +0 -1
  541. package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
  542. package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
  543. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  544. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  545. package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
  546. package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
  547. package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
  548. package/dist/elements/chunks/download-DnCyWFJE.js +0 -1
  549. package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
  550. package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
  551. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  552. package/dist/elements/chunks/file-text-D0J9F6M7.js +0 -1
  553. package/dist/elements/chunks/folder-NV1pi_ud.js +0 -1
  554. package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
  555. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  556. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  557. package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
  558. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  559. package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
  560. package/dist/elements/chunks/info-C8r5Q6cc.js +0 -1
  561. package/dist/elements/chunks/input-DrseEjhp.js +0 -1
  562. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  563. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  564. package/dist/elements/chunks/kbd-OGJDUT-O.js +0 -1
  565. package/dist/elements/chunks/link-BdsfLB7b.js +0 -1
  566. package/dist/elements/chunks/loader-B5QzK_8_.js +0 -1
  567. package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
  568. package/dist/elements/chunks/message-DtXKPqiR.js +0 -1
  569. package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
  570. package/dist/elements/chunks/paperclip-Bg0ITU3b.js +0 -1
  571. package/dist/elements/chunks/progress-bar-D1_rzt6N.js +0 -1
  572. package/dist/elements/chunks/prompt-suggestion-D5Sm8xF1.js +0 -1
  573. package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
  574. package/dist/elements/chunks/rotate-cw-DzVDJOA1.js +0 -1
  575. package/dist/elements/chunks/scroll-area-93A1bbeG.js +0 -1
  576. package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
  577. package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
  578. package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
  579. package/dist/elements/chunks/source-w9V0NnBn.js +0 -1
  580. package/dist/elements/chunks/star-DHvACxtm.js +0 -1
  581. package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
  582. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  583. package/dist/elements/chunks/tasks-card-lPxoodlA.js +0 -1
  584. package/dist/elements/chunks/text-shimmer-BY_rI3s5.js +0 -1
  585. package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
  586. package/dist/elements/chunks/toast-store-VUZZBhzH.js +0 -1
  587. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  588. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  589. package/dist/elements/chunks/use-card-resolution-DJg32jGH.js +0 -1
  590. package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
  591. package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
  592. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  593. package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
  594. package/dist/llms/llms-full.txt +0 -2834
  595. package/dist/llms/llms.txt +0 -156
  596. package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
  597. package/dist/register-impl-DGmGzrEM.js +0 -145
  598. package/dist/ui/stat.d.ts +0 -16
  599. package/dist/variant-custom-MkTNTycd.js +0 -1
  600. package/src/primitives/card-validate-generator.testlib.ts +0 -38
  601. package/src/ui/stat.tsx +0 -41
  602. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  603. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * The construct format, v1 — Zod is the SINGLE SOURCE OF TRUTH.
3
+ *
4
+ * The published JSON Schema (apps/docs/public/schemas/construct/v1.json) and the
5
+ * checked-in construct.v1.schema.json are DERIVED from this object by
6
+ * scripts/gen-construct-schema.mjs (build:api, drift-guarded). Never edit those
7
+ * by hand; never restate an enum from here anywhere else — read it off
8
+ * `ConstructSchema.shape` or the generated artifact.
9
+ *
10
+ * Format rules (spec, binding): vocabulary never logic — no handlers, no
11
+ * expressions; `.strict()` everywhere so an unknown key is a loud rejection,
12
+ * not a silently ignored one. No secrets, no client: `provider` can name a URL
13
+ * and a wire format, nothing else.
14
+ */
15
+ import { z } from 'zod';
16
+ // REPO-INTERNAL ONLY: isSafeUrl is not reachable from the published package
17
+ // (see agent-tooling/catalog/invariants.ts's own note on this), so the
18
+ // emitted App.tsx cannot import it — the sink codegen writes to
19
+ // (launcherIcon -> <img src>) has no guard it can reach at runtime. Enforcing
20
+ // the SAME policy here, at authoring time, is the reachable equivalent: a
21
+ // hostile launcherIcon is rejected before a construct ever validates, so
22
+ // codegen never has an unsafe value to emit. Reuses the kit's one existing
23
+ // URL-sink policy (markdown.tsx's image renderer, artifact.tsx) rather than
24
+ // authoring a second one. Imported from url-scheme-policy.ts, NOT
25
+ // card-routing.ts: this file compiles under tsconfig.mcp.json's Node-only, no-
26
+ // DOM-lib pass (transitively, via mcp/tools/construct.ts), and card-routing.ts
27
+ // pulls in HTMLElement/window/CustomEvent that pass can't see.
28
+ import { isSafeUrl } from '../../primitives/url-scheme-policy';
29
+
30
+ export const CONSTRUCT_SCHEMA_URL = 'https://ui.kitn.ai/schemas/construct/v1.json';
31
+
32
+ /** A valid custom-element tag: lowercase, starts with a letter, contains a hyphen. */
33
+ const TAG_RE = /^[a-z][a-z0-9]*-[a-z0-9-]+$/;
34
+
35
+ const ProviderSchema = z.discriminatedUnion('mode', [
36
+ z.object({ mode: z.literal('mock') }).strict(),
37
+ z
38
+ .object({
39
+ mode: z.literal('endpoint'),
40
+ /** The CONSUMER's chat route. Kit parses, consumer fetches. */
41
+ url: z.string().min(1),
42
+ wire: z.enum(['openai', 'anthropic']),
43
+ })
44
+ .strict(),
45
+ ]);
46
+
47
+ export const ConstructSchema = z
48
+ .object({
49
+ $schema: z.string().optional(),
50
+ /** The emitted tag: <acme-support>. Must satisfy customElements.define. */
51
+ name: z
52
+ .string()
53
+ .regex(TAG_RE, 'must be a valid custom-element tag: lowercase, with a hyphen (e.g. "acme-support")'),
54
+ // Widened progressively: fullscreen/aside/split landed in Task 12, custom in Task 13.
55
+ layout: z.enum(['widget', 'fullscreen', 'aside', 'split', 'custom']),
56
+ provider: ProviderSchema,
57
+ /** Plain (unsigned) identity passthrough — no signing/auth infra (owner
58
+ * ruling, 2026-08-26: signed JWT/HMAC identity is later additive
59
+ * vocabulary, not this field). TOP-LEVEL, not nested under `provider`:
60
+ * local history scoping (`capabilities.history.persistence: 'local'`)
61
+ * needs userId independent of provider mode — nesting it inside the
62
+ * provider union would make it silently inert for `mode: 'mock'` +
63
+ * local history, a real and common combination. Threaded as the
64
+ * `x-kai-user-id` header on every emitted fetch to the CONSUMER's own
65
+ * backend (the endpoint provider's chat POST, history's endpoint
66
+ * GET/PUT), and folded into the localStorage key for `local` history so
67
+ * different users on the same browser profile don't share one thread.
68
+ * Constrained to printable-ASCII (no CR/LF, no non-ISO-8859-1 code
69
+ * points) beyond the usual construct-authored-text escaping: this value
70
+ * reaches an HTTP HEADER VALUE, not just a JS string literal, and
71
+ * `fetch()` throws at RUNTIME on a header value containing CR/LF or
72
+ * outside ISO-8859-1 — a bad userId would surface as an opaque crash in
73
+ * the CONSUMER's generated app, not a construct-validation error. Reject
74
+ * loudly here instead, where the author gets a path + message. */
75
+ userId: z
76
+ .string()
77
+ .min(1)
78
+ .regex(
79
+ /^[\x20-\x7E]+$/,
80
+ 'must be printable ASCII with no line breaks (it becomes an HTTP header value)',
81
+ )
82
+ .optional(),
83
+ theme: z
84
+ .object({
85
+ /** Any CSS color; becomes --kai-color-primary on the host. */
86
+ accent: z.string().optional(),
87
+ mode: z.enum(['light', 'dark', 'system']).default('system'),
88
+ })
89
+ .strict()
90
+ .optional(),
91
+ /** Construct-wide header chrome, valid on every layout (not layout-scoped
92
+ * like `widget`, and not a capability toggle — a header is a construct-
93
+ * wide fact, like `theme`). Rendered inside ChatThread's own built-in
94
+ * header bar; a logo/icon projects through the kit's EXISTING
95
+ * `header-start` named slot (`slots` vocabulary above), not a second
96
+ * image-prop convention here. */
97
+ header: z
98
+ .object({
99
+ /** Rendered in ChatThread's built-in header bar (left side). Construct-
100
+ * authored/untrusted text, like theme.accent/provider.url — JSON.stringify'd
101
+ * at its one emit site, never a raw JSX attribute string. */
102
+ title: z.string().min(1).optional(),
103
+ })
104
+ .strict()
105
+ .optional(),
106
+ /** Greeting shown while the thread is empty (no messages yet) — the
107
+ * proven "welcome screen" pattern (Intercom-class): title + optional
108
+ * description + optional icon above, with `capabilities.starters`'
109
+ * chips and the composer still rendering below (ChatThread's `empty`
110
+ * REPLACE slot only stands in for the empty MESSAGE LIST — see
111
+ * chat-thread.tsx's own doc comment on `empty` — so the chips are
112
+ * never lost). Construct-wide like `header`, not a capability: it's a
113
+ * fact about the empty state, not a toggleable affordance.
114
+ * `title`/`description` are construct-authored/untrusted text, like
115
+ * `header.title`/`theme.accent`/`provider.url` — JSON.stringify'd at
116
+ * their one emit site, never a raw JSX attribute string. `icon` is a
117
+ * URL reaching an `<img src>` sink in emitted code, exactly like
118
+ * `widget.launcherIcon` — same `isSafeUrl` policy, same superRefine
119
+ * shape, below. */
120
+ empty: z
121
+ .object({
122
+ title: z.string().min(1),
123
+ description: z.string().min(1).optional(),
124
+ icon: z.string().min(1).optional(),
125
+ })
126
+ .strict()
127
+ .optional(),
128
+ // Capability vocabulary, widened one field at a time by later tasks.
129
+ capabilities: z
130
+ .object({
131
+ /** Starter prompts shown on the empty thread; clicking one sends it.
132
+ * 1-6 non-empty strings — construct-authored text, escaped like
133
+ * `theme.accent`/`provider.url` at every emit interpolation site. */
134
+ starters: z.array(z.string().min(1)).min(1).max(6).optional(),
135
+ /** Enables the paperclip attach affordance; accept is a non-empty
136
+ * list of media types/globs, e.g. ["image/*", "application/pdf"] —
137
+ * WHETHER stays with the construct author (this field), HOW stays
138
+ * with the kit (ChatThread's own attach/accept props, threaded
139
+ * through by codegen). */
140
+ attachments: z
141
+ .object({
142
+ /** Accept-list of media types/globs, e.g. ["image/*", "application/pdf"]. */
143
+ accept: z.array(z.string().min(1)).min(1),
144
+ })
145
+ .strict()
146
+ .optional(),
147
+ /** Conversation persistence. `none` (default, nothing emitted): the
148
+ * thread lives only in memory for the tab's lifetime. `local`:
149
+ * persisted to this browser's localStorage, keyed by the construct's
150
+ * tag — a mechanism decision (WHERE); what to retain and for how
151
+ * long stays an app decision (component-scope-boundary), so no
152
+ * retention count/quota lands here. `endpoint`: the CONSUMER's own
153
+ * thread route (GET returns ChatMessage[], PUT stores them) —
154
+ * requires `url`; `url` is rejected for any other persistence
155
+ * (superRefine below, both directions loud). */
156
+ history: z
157
+ .object({
158
+ persistence: z.enum(['none', 'local', 'endpoint']),
159
+ /** endpoint persistence only: the CONSUMER's thread routes (GET returns
160
+ * ChatMessage[], PUT stores them). Refined below. */
161
+ url: z.string().min(1).optional(),
162
+ })
163
+ .strict()
164
+ .optional(),
165
+ /** How the model's thinking (reasoning parts) renders. `'full'`
166
+ * (the default when omitted — see codegen.ts's emitReasoningProp)
167
+ * is the collapsible "Thinking" disclosure, shimmering while it
168
+ * streams. `'compact'` shows only a shimmer/typing loader while
169
+ * reasoning streams, with no expandable detail. `'off'` hides
170
+ * reasoning entirely. This is HOW an existing medium fact (the
171
+ * model's thinking) displays — the kit's call — so it maps straight
172
+ * onto ChatThread's own `reasoning` prop; there is no app-layer
173
+ * quota or retention decision hiding in it. Left `.optional()`
174
+ * rather than `.default('full')`, matching every sibling field in
175
+ * this object: a zod `.default()` here would make `reasoning`
176
+ * REQUIRED on the inferred input type (z.infer is the output type),
177
+ * breaking every `capabilities: {...}` object literal in this file
178
+ * and its tests that doesn't mention it. */
179
+ reasoning: z.enum(['full', 'compact', 'off']).optional(),
180
+ /** Seeds the reasoning disclosure open AND keeps it tracking the
181
+ * stream (open while streaming, closes when it settles) — the
182
+ * pre-Task-19f `full` behavior. Default false/absent: the panel
183
+ * starts closed (just the "Thinking" shimmer chip) and only opens
184
+ * on click — the current default (owner ruling, 2026-08-26).
185
+ * Meaningless when `reasoning` is `'compact'` (no expandable
186
+ * content exists) or `'off'` (nothing renders); rejected on both,
187
+ * loudly, below. */
188
+ reasoningOpen: z.boolean().optional(),
189
+ })
190
+ .strict()
191
+ .optional(),
192
+ /** Named generative-UI card definitions the model can emit as tool calls,
193
+ * rendered in the thread. Top-level (not a capability): a card is
194
+ * something the construct's model can DO at any point once a card is
195
+ * declared, not a thread-affordance toggle like starters/attachments/
196
+ * reasoning. Reuses the kit's own card-tool projection end to end
197
+ * (`cardTools`/`toOpenAITools`/`toAnthropicTools`, `@kitn.ai/ui/schemas`)
198
+ * — never a second one authored here. `schema` is validated structurally
199
+ * only; deep card validation (incl. `x-kai-format` mask hints) is the
200
+ * kit's own card contract at render time. */
201
+ cards: z
202
+ .array(
203
+ z
204
+ .object({
205
+ /** Tool-facing card name, e.g. "refund_approval". */
206
+ name: z.string().regex(/^[a-z][a-z0-9_]*$/),
207
+ /** The kit's card schema JSON (incl. x-kai-format mask hints).
208
+ * Validated structurally here; deep card validation is the kit's
209
+ * own card contract at render time. */
210
+ schema: z.record(z.string(), z.unknown()),
211
+ })
212
+ .strict(),
213
+ )
214
+ .min(1)
215
+ .optional(),
216
+ /** Named `<slot>` projection points the emitted web component exposes —
217
+ * the format's ONLY escape hatch (named slots, no code-in-JSON). Each
218
+ * name must be a valid HTML slot-attribute value AND a legible
219
+ * identifier: kebab-case, starting with a letter (the same shape as a
220
+ * CSS custom-ident) — `slot name="Header!"` is rejected, not sanitized,
221
+ * matching the format's loud-rejection discipline everywhere else.
222
+ * 1-8 entries, no duplicates (superRefine below — a regex alone can't
223
+ * see across array entries). `layout: 'custom'` requires at least one
224
+ * declared slot (superRefine below): `custom` IS the slots grain — a
225
+ * `custom` layout with nothing to project into is not meaningfully
226
+ * different from `fullscreen`. */
227
+ slots: z
228
+ .array(z.string().regex(/^[a-z][a-z0-9-]*$/, 'slot names must be kebab-case, starting with a letter'))
229
+ .min(1)
230
+ .max(8)
231
+ .optional(),
232
+ /** Layout-scoped FAB chrome, `layout: 'widget'` only (superRefine below).
233
+ * Purely additive sibling to `layout` (widen-never-restructure) — mirrors
234
+ * Dock's own props verbatim, threaded through by codegen's emitLayoutOpen. */
235
+ widget: z
236
+ .object({
237
+ /** Which corner. Mirrors Dock's own DockPosition enum verbatim — not a
238
+ * new left/right binary. Omitted keeps Dock's own default (bottom-end). */
239
+ position: z.enum(['bottom-end', 'bottom-start', 'top-end', 'top-start']).optional(),
240
+ /** An image URL for the closed-state launcher glyph, replacing Dock's
241
+ * built-in chat-bubble icon. Construct-authored/untrusted text, like
242
+ * theme.accent and provider.url — escaped the same way at its one emit site. */
243
+ launcherIcon: z.string().min(1).optional(),
244
+ /** Seed Dock's open state at mount. Uncontrolled — never steals focus (Dock's
245
+ * own focus contract, dock.tsx). Omitted keeps Dock's own default (closed). */
246
+ defaultOpen: z.boolean().optional(),
247
+ })
248
+ .strict()
249
+ .optional(),
250
+ })
251
+ .strict()
252
+ .superRefine((construct, ctx) => {
253
+ if (construct.slots) {
254
+ const seen = new Set<string>();
255
+ construct.slots.forEach((name, i) => {
256
+ if (seen.has(name)) {
257
+ ctx.addIssue({
258
+ code: z.ZodIssueCode.custom,
259
+ path: ['slots', i],
260
+ message: `duplicate slot name "${name}"`,
261
+ });
262
+ }
263
+ seen.add(name);
264
+ });
265
+ }
266
+ if (construct.layout === 'custom' && (!construct.slots || construct.slots.length === 0)) {
267
+ ctx.addIssue({
268
+ code: z.ZodIssueCode.custom,
269
+ path: ['slots'],
270
+ message: '"custom" layout requires at least one declared slot — custom IS the slots grain',
271
+ });
272
+ }
273
+ if (construct.layout === 'split' && construct.slots) {
274
+ const i = construct.slots.indexOf('pane');
275
+ if (i !== -1) {
276
+ ctx.addIssue({
277
+ code: z.ZodIssueCode.custom,
278
+ path: ['slots', i],
279
+ message:
280
+ '"pane" collides with the "split" layout\'s own fixed <slot name="pane"> — choose a different slot name',
281
+ });
282
+ }
283
+ }
284
+ if (construct.widget && construct.layout !== 'widget') {
285
+ ctx.addIssue({
286
+ code: z.ZodIssueCode.custom,
287
+ path: ['widget'],
288
+ message: '"widget" is only valid on layout: "widget"',
289
+ });
290
+ }
291
+ if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
292
+ ctx.addIssue({
293
+ code: z.ZodIssueCode.custom,
294
+ path: ['widget', 'launcherIcon'],
295
+ message: 'launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes',
296
+ });
297
+ }
298
+ if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
299
+ ctx.addIssue({
300
+ code: z.ZodIssueCode.custom,
301
+ path: ['empty', 'icon'],
302
+ message: 'icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes',
303
+ });
304
+ }
305
+ const reasoning = construct.capabilities?.reasoning;
306
+ const reasoningOpen = construct.capabilities?.reasoningOpen;
307
+ if (reasoningOpen !== undefined && (reasoning === 'compact' || reasoning === 'off')) {
308
+ ctx.addIssue({
309
+ code: z.ZodIssueCode.custom,
310
+ path: ['capabilities', 'reasoningOpen'],
311
+ message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open',
312
+ });
313
+ }
314
+ const history = construct.capabilities?.history;
315
+ if (!history) return;
316
+ if (history.persistence === 'endpoint' && !history.url) {
317
+ ctx.addIssue({
318
+ code: z.ZodIssueCode.custom,
319
+ path: ['capabilities', 'history', 'url'],
320
+ message: '"endpoint" persistence requires a url',
321
+ });
322
+ }
323
+ if (history.persistence !== 'endpoint' && history.url !== undefined) {
324
+ ctx.addIssue({
325
+ code: z.ZodIssueCode.custom,
326
+ path: ['capabilities', 'history', 'url'],
327
+ message: 'url is only valid with "endpoint" persistence',
328
+ });
329
+ }
330
+ });
331
+
332
+ export type Construct = z.infer<typeof ConstructSchema>;
333
+
334
+ export interface ConstructProblem {
335
+ /** Dotted path into the construct, '' for the root. */
336
+ path: string;
337
+ message: string;
338
+ }
339
+
340
+ export type ValidationOutcome =
341
+ | { ok: true; construct: Construct }
342
+ | { ok: false; problems: ConstructProblem[] };
343
+
344
+ /**
345
+ * Validate one construct. The ONLY doorway to codegen: a failure never reaches
346
+ * generation — the problems go back to the author/agent with paths and reasons.
347
+ */
348
+ export function validateConstruct(input: unknown): ValidationOutcome {
349
+ const parsed = ConstructSchema.safeParse(input);
350
+ if (parsed.success) return { ok: true, construct: parsed.data };
351
+ return {
352
+ ok: false,
353
+ problems: parsed.error.issues.flatMap((issue) => {
354
+ // zod's unrecognized-key issue names the keys in its message but paths the
355
+ // OBJECT; surface each unknown key as its own problem so the agent sees
356
+ // exactly which word is not vocabulary.
357
+ if (issue.code === 'unrecognized_keys') {
358
+ return issue.keys.map((key) => ({
359
+ path: [...issue.path.map(String), key].join('.'),
360
+ message: `"${key}" is not construct vocabulary`,
361
+ }));
362
+ }
363
+ return [
364
+ {
365
+ path: issue.path.map(String).join('.'),
366
+ message: issue.message,
367
+ },
368
+ ];
369
+ }),
370
+ };
371
+ }
@@ -317,7 +317,13 @@ function reframeToOpenAISse(body: ReadableStream<Uint8Array>): ReadableStream<Ui
317
317
  async function chatHandler(request: Request): Promise<Response> {
318
318
  // Both model and tools come from the browser. \`tools\` arrives in OpenAI
319
319
  // function form and is converted to this API's shape below.
320
- const { model, messages, tools } = await readChatRequest(request);
320
+ let chatBody: ChatRequestBody;
321
+ try {
322
+ chatBody = await readChatRequest(request);
323
+ } catch (error) {
324
+ return toChatErrorResponse(error);
325
+ }
326
+ const { model, messages, tools } = chatBody;
321
327
  const { system, messages: anthropicMessages } = toAnthropicBody(messages);
322
328
  const anthropicTools = toAnthropicTools(tools);
323
329
 
@@ -24,9 +24,44 @@ import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';
24
24
  */
25
25
  type ChatRequestBody = { messages: OpenAIWireMessage[] };
26
26
 
27
+ class ChatRequestError extends Error {
28
+ constructor(readonly status: number, message: string) { super(message); }
29
+ }
30
+
31
+ /** Narrow the JSON body once, at the edge. A bare GET, a malformed body, or a
32
+ * missing messages array is a ChatRequestError with a status — NEVER an
33
+ * unhandled SyntaxError (findings F-10). */
34
+ async function readChatRequest(request: Request): Promise<ChatRequestBody> {
35
+ if (request.method !== 'POST') {
36
+ throw new ChatRequestError(405, \`Method \${request.method} not allowed — POST /api/chat.\`);
37
+ }
38
+ let parsed: unknown;
39
+ try { parsed = await request.json(); } catch {
40
+ throw new ChatRequestError(400, 'Request body is not valid JSON.');
41
+ }
42
+ const body = parsed as ChatRequestBody;
43
+ if (!Array.isArray(body?.messages)) {
44
+ throw new ChatRequestError(400, 'Request body must carry a messages array.');
45
+ }
46
+ return body;
47
+ }
48
+
49
+ /** Map a guard rejection to the Response its status demands; rethrow anything
50
+ * else — an unexpected error should be loud, not laundered into a 400. */
51
+ function toChatErrorResponse(error: unknown): Response {
52
+ if (error instanceof ChatRequestError) return Response.json({ error: error.message }, { status: error.status });
53
+ throw error;
54
+ }
55
+
27
56
  export default {
28
57
  async fetch(req: Request, env: Env): Promise<Response> {
29
- const { messages } = (await req.json()) as ChatRequestBody;
58
+ let chatBody: ChatRequestBody;
59
+ try {
60
+ chatBody = await readChatRequest(req);
61
+ } catch (error) {
62
+ return toChatErrorResponse(error);
63
+ }
64
+ const { messages } = chatBody;
30
65
 
31
66
  let nativeStream: ReadableStream<Uint8Array>;
32
67
  try {
@@ -101,7 +136,13 @@ export default {
101
136
  },
102
137
  webRoute: `async function chatHandler(request: Request): Promise<Response> {
103
138
  // Proxy Workers AI over its OpenAI-compatible HTTP endpoint, token server-side.
104
- const { messages } = await readChatRequest(request);
139
+ let chatBody: ChatRequestBody;
140
+ try {
141
+ chatBody = await readChatRequest(request);
142
+ } catch (error) {
143
+ return toChatErrorResponse(error);
144
+ }
145
+ const { messages } = chatBody;
105
146
 
106
147
  const upstream = await fetch(
107
148
  \`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
@@ -31,7 +31,13 @@ const agent = createReactAgent({
31
31
 
32
32
  // Stream a compiled LangGraph agent to the browser as OpenAI-format SSE.
33
33
  async function chatHandler(request: Request): Promise<Response> {
34
- const { messages } = await readChatRequest(request);
34
+ let chatBody: ChatRequestBody;
35
+ try {
36
+ chatBody = await readChatRequest(request);
37
+ } catch (error) {
38
+ return toChatErrorResponse(error);
39
+ }
40
+ const { messages } = chatBody;
35
41
 
36
42
  // agent.stream() coerces plain {role, content} objects into BaseMessage
37
43
  // instances itself, including OpenAI-shaped tool_calls, so the wire messages
@@ -28,7 +28,13 @@ const mastra = new MastraClient({ baseUrl: MASTRA_URL });
28
28
 
29
29
  // Proxy a Mastra agent to the browser as OpenAI-format SSE.
30
30
  async function chatHandler(request: Request): Promise<Response> {
31
- const { messages } = await readChatRequest(request);
31
+ let chatBody: ChatRequestBody;
32
+ try {
33
+ chatBody = await readChatRequest(request);
34
+ } catch (error) {
35
+ return toChatErrorResponse(error);
36
+ }
37
+ const { messages } = chatBody;
32
38
 
33
39
  // MastraClient's Agent.stream() takes AI-SDK CoreMessage[], not the OpenAI
34
40
  // wire format: each message has ONE literal role (not a union) and content
@@ -35,9 +35,22 @@ import type { Integration } from '../types';
35
35
  * Swap `integration` for a real provider (openai, anthropic, openrouter, ollama,
36
36
  * …) when ready.
37
37
  *
38
- * It carries an empty `routeTemplates` on purpose: there is no server route to
39
- * emit. `category: 'mock'` keeps it out of the real provider/gateway/framework/
40
- * harness catalogs.
38
+ * THE ROUTE IS OPTIONAL, AND IT EXISTS ANYWAY (rung-2 finding G-04, recurring
39
+ * since rung 1). Two clean-room builders in a row were told "replies come from a
40
+ * local dev endpoint that streams a mocked response" and had to hand-invent the
41
+ * server side, because this was the one integration whose block (2) held prose
42
+ * where every other integration ships code. So `webRoute` below is the mock's
43
+ * server half: the SAME `createMockResponder()` frames, written to the wire by a
44
+ * portable `chatHandler(Request) => Response` that the scaffolder wraps with the
45
+ * identical per-framework adapters a real integration gets (Vite dev middleware
46
+ * for the SPA frameworks, POST exports for next/svelte/tanstack, the
47
+ * Express/Worker/Angular hosts). Nothing about the emitted app REQUIRES it — the
48
+ * front end streams locally and never fetches — which the emitted block states,
49
+ * along with the one-expression swap that points onSubmit at it.
50
+ *
51
+ * `routeTemplates` stays empty: the handler is web-standard, so there is no
52
+ * framework whose declaration it cannot be wrapped in. `category: 'mock'` keeps
53
+ * it out of the real provider/gateway/framework/harness catalogs.
41
54
  */
42
55
  const mock: Integration = {
43
56
  id: 'mock',
@@ -47,26 +60,88 @@ const mock: Integration = {
47
60
  streamFormat: 'native',
48
61
  envVars: [],
49
62
  routeTemplates: {},
63
+ webRoute: `import { createMockResponder } from '@kitn.ai/ui/state';
64
+
65
+ // The kit's own mock responder — no provider, no key, no upstream. MODULE
66
+ // scope, not per-request: the responder owns the cursor into its canned
67
+ // replies, so rebuilding it per turn would answer with the first one forever.
68
+ const mockResponse = createMockResponder();
69
+
70
+ async function chatHandler(request: Request): Promise<Response> {
71
+ // The responder cycles its replies whatever you send, so the prompt is a
72
+ // courtesy — but the body is still read exactly the way a real route reads
73
+ // it, so swapping this handler for a provider's changes nothing upstream of
74
+ // this file.
75
+ let chatBody: ChatRequestBody;
76
+ try {
77
+ chatBody = await readChatRequest(request);
78
+ } catch (error) {
79
+ return toChatErrorResponse(error);
80
+ }
81
+ const { messages } = chatBody;
82
+ const last = [...messages].reverse().find((m) => m.role === 'user');
83
+ const prompt = last && typeof last.content === 'string' ? last.content : '';
84
+
85
+ // createMockResponder() yields COMPLETE OpenAI chat-completions SSE frames —
86
+ // the same strings the front end's local preview streams — so this route only
87
+ // writes them out verbatim. No framing is built here; the browser parses them
88
+ // with readOpenAIStream from @kitn.ai/ui/wire, exactly as it would a real
89
+ // route's response. Every frame is marked as a mock (a ': kai-mock' banner, a
90
+ // _kai_mock field, model 'kai-mock', zero usage).
91
+ const encoder = new TextEncoder();
92
+ let open = true;
93
+ const body = new ReadableStream<Uint8Array>({
94
+ async start(controller) {
95
+ for await (const frame of mockResponse(prompt)) {
96
+ // The browser hangs up when the user asks a new question mid-answer;
97
+ // stop producing frames rather than writing into a cancelled stream.
98
+ if (!open) return;
99
+ controller.enqueue(encoder.encode(frame));
100
+ }
101
+ controller.close();
102
+ },
103
+ cancel() {
104
+ open = false;
105
+ },
106
+ });
107
+
108
+ return new Response(body, {
109
+ status: 200,
110
+ headers: {
111
+ 'Content-Type': 'text/event-stream; charset=utf-8',
112
+ // no-transform stops a proxy buffering the stream into one blob.
113
+ 'Cache-Control': 'no-cache, no-transform',
114
+ Connection: 'keep-alive',
115
+ // A proxy in front of a preview build would otherwise buffer the whole
116
+ // stream and the reply would land all at once instead of streaming.
117
+ 'X-Accel-Buffering': 'no',
118
+ },
119
+ });
120
+ }`,
50
121
  streamMapping:
51
- "No backend and no provider, but a real wire. createMockResponder() from @kitn.ai/ui/state yields canned SSE frames and the scaffold reads them with readOpenAIStream from @kitn.ai/ui/wire, on the same code path a real route's response takes: same reader, same part folding, same abort handling. The frames carry the OpenAI chat-completions shape because the mock stands in for your /api/chat ROUTE, not for a provider (every other integration here re-frames its provider to that shape server-side), so going live replaces ONE expression: mockResponse(value) becomes the awaited response from your own chat route, and nothing else in the handler changes. Nothing here can be mistaken for a real turn: the stream opens with a ': kai-mock' SSE comment, every frame carries a _kai_mock field naming createMockResponder, model reports as 'kai-mock' (no provider serves it), and usage is all zeros.",
122
+ "No backend and no provider, but a real wire. createMockResponder() from @kitn.ai/ui/state yields canned SSE frames and the scaffold reads them with readOpenAIStream from @kitn.ai/ui/wire, on the same code path a real route's response takes: same reader, same part folding, same abort handling. The frames carry the OpenAI chat-completions shape because the mock stands in for your /api/chat ROUTE, not for a provider (every other integration here re-frames its provider to that shape server-side), so going live replaces ONE expression: mockResponse(value) becomes the awaited response from your own chat route, and nothing else in the handler changes. Nothing here can be mistaken for a real turn: the stream opens with a ': kai-mock' SSE comment, every frame carries a _kai_mock field naming createMockResponder, model reports as 'kai-mock' (no provider serves it), and usage is all zeros. Block (2) serves the same frames over HTTP as an OPTIONAL route, so the /api/chat seam can be stood up before any provider exists.",
52
123
  runNote:
53
- 'No backend or API key needed: the reply is generated in the browser and parsed by the same reader a real route feeds. Run the front-end as-is; swap `integration` for a real provider (openai, anthropic, openrouter, ollama) when ready, and the emitted handler differs by one expression.',
124
+ 'No backend or API key needed: the reply is generated in the browser and parsed by the same reader a real route feeds. Run the front-end as-is; swap `integration` for a real provider (openai, anthropic, openrouter, ollama) when ready, and the emitted handler differs by one expression. Block (2) is the OPTIONAL server half: the same mock frames served over HTTP, for standing up the /api/chat seam before any provider exists.',
54
125
  docsSlug: 'integrations/mock',
55
- // Nothing: there is no HTTP request at all. The frames are produced in-process
56
- // by createMockResponder(), so there is no body to forward a model or a tools
57
- // array on, and no route that would read one.
126
+ // Nothing: the front end makes no HTTP request at all, and the optional route
127
+ // reads only `messages` and only as a courtesy prompt the responder is free
128
+ // to ignore. There is no model to pick and no tools array anything would run.
58
129
  forwardsFromClient: [],
59
- // Nothing to install: there is no route to install anything for.
130
+ // Nothing to install: the route's one import is the kit itself, which every
131
+ // scaffold already depends on (registry.test.ts pins deps.npm to the route's
132
+ // imports minus @kitn.ai/ui).
60
133
  deps: { npm: [], pip: [] },
61
- // The other 'frontend-safe' entry, and the only one that is true by absence:
62
- // no routeTemplates, no webRoute, no envVars. There is no request, no upstream
63
- // and no secret, so there is nothing a public bundle could give away. This is
64
- // the one place where "declares nothing" genuinely means safe — which is
65
- // precisely why it still has to SAY so rather than be left blank.
134
+ // The other 'frontend-safe' entry. The route now exists (G-04) but changes
135
+ // nothing here: it holds no credential, sends no auth header and reaches no
136
+ // upstream, so there is still nothing a public bundle could give away. The
137
+ // schema's own refinement re-checks that claim against the route source.
66
138
  keyExposure: 'frontend-safe',
67
- // Nothing at all, and here that is the literal truth rather than a shorthand:
68
- // there is no route, no upstream and no process. This is the "No backend"
69
- // group of create-kai's gateway prompt all by itself.
139
+ // Nothing to supply out of band: no upstream, no process, no runtime. The
140
+ // route is optional the emitted app streams locally without it which is
141
+ // what keeps mock in the "No backend" group of create-kai's gateway prompt
142
+ // (listGatewayGroups derives that from frontend-safe + outOfBand 'none', not
143
+ // from the absence of a route, precisely because this route is one nobody
144
+ // NEEDS).
70
145
  outOfBand: 'none',
71
146
  };
72
147
 
@@ -20,7 +20,13 @@ const ollama: Integration = {
20
20
  // The model is pinned here, not sent by the browser. tools IS forwarded:
21
21
  // it is undefined unless the front end declared any, and JSON.stringify
22
22
  // drops it, so the same handler serves a tool archetype and a plain chat.
23
- const { messages, tools } = await readChatRequest(request);
23
+ let chatBody: ChatRequestBody;
24
+ try {
25
+ chatBody = await readChatRequest(request);
26
+ } catch (error) {
27
+ return toChatErrorResponse(error);
28
+ }
29
+ const { messages, tools } = chatBody;
24
30
 
25
31
  const upstream = await fetch('http://localhost:11434/v1/chat/completions', {
26
32
  method: 'POST',
@@ -14,7 +14,13 @@ const openai: Integration = {
14
14
  // Both model and tools come from the browser. \`tools\` is undefined unless the
15
15
  // front end declared any, and JSON.stringify drops it, so the same handler
16
16
  // serves a tool archetype and a plain chat.
17
- const { model, messages, tools } = await readChatRequest(request);
17
+ let chatBody: ChatRequestBody;
18
+ try {
19
+ chatBody = await readChatRequest(request);
20
+ } catch (error) {
21
+ return toChatErrorResponse(error);
22
+ }
23
+ const { model, messages, tools } = chatBody;
18
24
 
19
25
  const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
20
26
  method: 'POST',
@@ -14,7 +14,13 @@ const openrouter: Integration = {
14
14
  webRoute: `async function chatHandler(request: Request): Promise<Response> {
15
15
  // tools is undefined unless the front end sent one; JSON.stringify drops it,
16
16
  // so the same handler serves a tool archetype and a plain chat.
17
- const { model, messages, tools } = await readChatRequest(request);
17
+ let chatBody: ChatRequestBody;
18
+ try {
19
+ chatBody = await readChatRequest(request);
20
+ } catch (error) {
21
+ return toChatErrorResponse(error);
22
+ }
23
+ const { model, messages, tools } = chatBody;
18
24
 
19
25
  const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
20
26
  method: 'POST',