@kitn.ai/ui 0.25.1 → 0.26.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 (409) hide show
  1. package/README.md +5 -7
  2. package/dist/components/attachment-types.d.ts +46 -1
  3. package/dist/components/attachments.d.ts +31 -0
  4. package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
  5. package/dist/components/audio-visualizer/index.d.ts +15 -1
  6. package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
  7. package/dist/components/chat-thread.d.ts +3 -41
  8. package/dist/components/code-block.d.ts +22 -0
  9. package/dist/components/conversation-item.d.ts +54 -1
  10. package/dist/components/conversation-list.d.ts +65 -0
  11. package/dist/components/form.d.ts +42 -0
  12. package/dist/components/message.d.ts +7 -0
  13. package/dist/components/toast.d.ts +2 -1
  14. package/dist/components/voice-input.d.ts +8 -0
  15. package/dist/components/voice-output.d.ts +12 -1
  16. package/dist/components/workspace-shell.d.ts +87 -0
  17. package/dist/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
  18. package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
  19. package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
  20. package/dist/custom-elements.json +2931 -2572
  21. package/dist/diagnostics/hook.d.ts +81 -0
  22. package/dist/diagnostics/index.d.ts +7 -0
  23. package/dist/diagnostics/report-request.d.ts +61 -0
  24. package/dist/diagnostics.d.ts +5 -0
  25. package/dist/diagnostics.js +209 -0
  26. package/dist/elements/agent-card.js +1 -1
  27. package/dist/elements/artifact.js +1 -1
  28. package/dist/elements/attachments.js +1 -1
  29. package/dist/elements/audio-visualizer.js +1 -1
  30. package/dist/elements/autoloader.js +1 -1
  31. package/dist/elements/avatar.js +1 -1
  32. package/dist/elements/badge.js +1 -1
  33. package/dist/elements/button.js +1 -1
  34. package/dist/elements/card.js +1 -1
  35. package/dist/elements/cards.js +1 -1
  36. package/dist/elements/chain-of-thought.js +1 -1
  37. package/dist/elements/chat-scope-picker.js +1 -1
  38. package/dist/elements/chat-workspace.js +1 -1
  39. package/dist/elements/chat.js +1 -1
  40. package/dist/elements/checkpoint.js +1 -1
  41. package/dist/elements/choice.js +1 -1
  42. package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
  43. package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
  44. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
  45. package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
  46. package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
  47. package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
  48. package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
  49. package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
  50. package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
  51. package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
  52. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
  53. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
  54. package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
  55. package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
  56. package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
  57. package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
  58. package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
  59. package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
  60. package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
  61. package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
  62. package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
  63. package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
  64. package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
  65. package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
  66. package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
  67. package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
  68. package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
  69. package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
  70. package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
  71. package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
  72. package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
  73. package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
  74. package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
  75. package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
  76. package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
  77. package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
  78. package/dist/elements/chunks/form-CQirdC4W.js +1 -0
  79. package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
  80. package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
  81. package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
  82. package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
  83. package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
  84. package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
  85. package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
  86. package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
  87. package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
  88. package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
  89. package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
  90. package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
  91. package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
  92. package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
  93. package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
  94. package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
  95. package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
  96. package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
  97. package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
  98. package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
  99. package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
  100. package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
  101. package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
  102. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
  103. package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
  104. package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
  105. package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
  106. package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
  107. package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
  108. package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
  109. package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
  110. package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
  111. package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
  112. package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
  113. package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
  114. package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
  115. package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
  116. package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
  117. package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
  118. package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
  119. package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
  120. package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
  121. package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
  122. package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
  123. package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
  124. package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
  125. package/dist/elements/coachmark.js +1 -1
  126. package/dist/elements/code-block.js +1 -1
  127. package/dist/elements/command.js +1 -1
  128. package/dist/elements/compare.js +1 -1
  129. package/dist/elements/composer.js +1 -1
  130. package/dist/elements/confirm-card.js +1 -1
  131. package/dist/elements/context-meter.js +1 -1
  132. package/dist/elements/conversation-item.d.ts +1 -0
  133. package/dist/elements/conversation-item.js +1 -0
  134. package/dist/elements/conversation-list.js +1 -1
  135. package/dist/elements/default-input.d.ts +3 -3
  136. package/dist/elements/define.d.ts +27 -0
  137. package/dist/elements/diagnostic-events.d.ts +74 -0
  138. package/dist/elements/dialog.js +1 -1
  139. package/dist/elements/dock.d.ts +1 -0
  140. package/dist/elements/dock.js +149 -0
  141. package/dist/elements/dropdown.d.ts +1 -0
  142. package/dist/elements/dropdown.js +1 -0
  143. package/dist/elements/editable-label.js +1 -1
  144. package/dist/elements/element-diagnostics.d.ts +81 -0
  145. package/dist/elements/embed.js +1 -1
  146. package/dist/elements/empty.js +1 -1
  147. package/dist/elements/feedback-bar.js +1 -1
  148. package/dist/elements/file-tree.js +1 -1
  149. package/dist/elements/file-upload.js +1 -1
  150. package/dist/elements/form.js +1 -1
  151. package/dist/elements/hover-card.js +1 -1
  152. package/dist/elements/icon.js +1 -1
  153. package/dist/elements/image.js +1 -1
  154. package/dist/elements/input.js +1 -1
  155. package/dist/elements/kbd.js +1 -1
  156. package/dist/elements/link-preview.js +1 -1
  157. package/dist/elements/loader.js +1 -1
  158. package/dist/elements/markdown.js +1 -1
  159. package/dist/elements/menu.js +1 -1
  160. package/dist/elements/message-skills.js +1 -1
  161. package/dist/elements/message.js +1 -1
  162. package/dist/elements/model-switcher.js +1 -1
  163. package/dist/elements/nav.js +1 -1
  164. package/dist/elements/notice.js +1 -1
  165. package/dist/elements/pane-group.js +1 -1
  166. package/dist/elements/pane.js +1 -1
  167. package/dist/elements/popover.js +1 -1
  168. package/dist/elements/progress-bar.js +1 -1
  169. package/dist/elements/prompt-dock.js +1 -1
  170. package/dist/elements/prompt-input.js +1 -1
  171. package/dist/elements/prompt-suggestions.js +1 -1
  172. package/dist/elements/reasoning.js +1 -1
  173. package/dist/elements/register.d.ts +2 -0
  174. package/dist/elements/remote.js +1 -1
  175. package/dist/elements/resizable.js +1 -1
  176. package/dist/elements/response-stream.js +3 -3
  177. package/dist/elements/screen.js +1 -1
  178. package/dist/elements/scroll-area.js +1 -1
  179. package/dist/elements/scroll-button.js +1 -1
  180. package/dist/elements/search.js +1 -1
  181. package/dist/elements/segmented.js +1 -1
  182. package/dist/elements/separator.js +1 -1
  183. package/dist/elements/setting-item.js +1 -1
  184. package/dist/elements/settings-group.js +1 -1
  185. package/dist/elements/skeleton.js +1 -1
  186. package/dist/elements/slots.d.ts +35 -3
  187. package/dist/elements/source.js +1 -1
  188. package/dist/elements/status.js +1 -1
  189. package/dist/elements/switch.js +1 -1
  190. package/dist/elements/tabs.js +1 -1
  191. package/dist/elements/tasks.js +1 -1
  192. package/dist/elements/text-shimmer.js +1 -1
  193. package/dist/elements/thinking-bar.js +1 -1
  194. package/dist/elements/thread.js +1 -1
  195. package/dist/elements/toast.js +1 -1
  196. package/dist/elements/tool.js +1 -1
  197. package/dist/elements/tooltip.js +1 -1
  198. package/dist/elements/voice-input.js +1 -1
  199. package/dist/elements/voice-output.js +1 -1
  200. package/dist/elements.d.ts +232 -148
  201. package/dist/index.d.ts +2 -0
  202. package/dist/index.js +5456 -4931
  203. package/dist/index.server.js +4142 -3669
  204. package/dist/kai-provider.es.js +40 -40
  205. package/dist/kai.es.js +1 -1
  206. package/dist/llms/llms-full.txt +184 -69
  207. package/dist/llms/llms.txt +3 -3
  208. package/dist/mcp.es.js +2669 -546
  209. package/dist/primitives/use-resize-observer.d.ts +23 -0
  210. package/dist/primitives/use-speech-recognition.d.ts +8 -0
  211. package/dist/primitives/use-text-stream.d.ts +5 -0
  212. package/dist/react/index.d.ts +115 -266
  213. package/dist/react.js +247 -221
  214. package/dist/register-impl-DpE7icIb.js +293 -0
  215. package/dist/schemas/index.d.ts +1 -1
  216. package/dist/schemas/tool-defs.d.ts +35 -0
  217. package/dist/schemas.js +403 -310
  218. package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
  219. package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
  220. package/dist/solid.d.ts +5 -0
  221. package/dist/solid.js +210 -203
  222. package/dist/solid.server.js +210 -203
  223. package/dist/state/index.d.ts +4 -0
  224. package/dist/state/persistence.d.ts +51 -0
  225. package/dist/state/stream.d.ts +50 -0
  226. package/dist/state/threads.d.ts +71 -0
  227. package/dist/state.js +269 -169
  228. package/dist/types.d.ts +4 -2
  229. package/dist/ui/dock.d.ts +109 -0
  230. package/dist/ui/hover-card.d.ts +44 -0
  231. package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
  232. package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
  233. package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
  234. package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
  235. package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
  236. package/dist/variant-custom-L16JLLZy.js +1 -0
  237. package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
  238. package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
  239. package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
  240. package/dist/wire/chunk.d.ts +47 -0
  241. package/dist/wire/diagnostics.d.ts +575 -0
  242. package/dist/wire/encode-probe.d.ts +70 -0
  243. package/dist/wire/encode.d.ts +24 -0
  244. package/dist/wire/index.d.ts +2 -0
  245. package/dist/wire/sse.d.ts +10 -2
  246. package/dist/wire.js +917 -480
  247. package/frameworks/react/index.tsx +132 -93
  248. package/llms-full.txt +184 -69
  249. package/llms.txt +3 -3
  250. package/package.json +27 -7
  251. package/src/agent-tooling/archetypes.ts +41 -9
  252. package/src/agent-tooling/catalog/README.md +676 -0
  253. package/src/agent-tooling/catalog/catalog-types.ts +231 -0
  254. package/src/agent-tooling/catalog/fabrications.ts +96 -0
  255. package/src/agent-tooling/catalog/invariants.ts +284 -0
  256. package/src/agent-tooling/catalog/labs-titles.ts +114 -0
  257. package/src/agent-tooling/catalog/scenarios.ts +87 -0
  258. package/src/agent-tooling/catalog/surfaces.ts +275 -0
  259. package/src/agent-tooling/integrations/anthropic.ts +7 -1
  260. package/src/agent-tooling/integrations/cloudflare.ts +43 -2
  261. package/src/agent-tooling/integrations/langgraph.ts +7 -1
  262. package/src/agent-tooling/integrations/mastra.ts +7 -1
  263. package/src/agent-tooling/integrations/mock.ts +92 -17
  264. package/src/agent-tooling/integrations/ollama.ts +7 -1
  265. package/src/agent-tooling/integrations/openai.ts +7 -1
  266. package/src/agent-tooling/integrations/openrouter.ts +7 -1
  267. package/src/agent-tooling/integrations/pi.ts +56 -1
  268. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
  269. package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
  270. package/src/agent-tooling/mcp/manifest.ts +100 -1
  271. package/src/agent-tooling/mcp/server.ts +61 -5
  272. package/src/agent-tooling/mcp/tools/reference.ts +440 -4
  273. package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
  274. package/src/agent-tooling/mcp/tools/theme.ts +146 -30
  275. package/src/agent-tooling/mcp/validate-args.ts +141 -0
  276. package/src/agent-tooling/registry.ts +19 -7
  277. package/src/agent-tooling/route-emit.ts +26 -2
  278. package/src/components/attachment-types.ts +47 -1
  279. package/src/components/attachments.tsx +169 -22
  280. package/src/components/audio-visualizer/fit-scale.ts +112 -0
  281. package/src/components/audio-visualizer/index.tsx +80 -3
  282. package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
  283. package/src/components/audio-visualizer/variant-bar.tsx +38 -6
  284. package/src/components/audio-visualizer/variant-custom.tsx +8 -2
  285. package/src/components/audio-visualizer/variant-grid.tsx +16 -11
  286. package/src/components/audio-visualizer/variant-radial.tsx +15 -10
  287. package/src/components/audio-visualizer/variant-wave.tsx +7 -2
  288. package/src/components/chat-container.tsx +4 -1
  289. package/src/components/chat-thread.tsx +36 -7
  290. package/src/components/code-block.tsx +79 -3
  291. package/src/components/composer.tsx +8 -1
  292. package/src/components/conversation-item.tsx +114 -4
  293. package/src/components/conversation-list.tsx +212 -12
  294. package/src/components/embed.tsx +4 -0
  295. package/src/components/form.tsx +41 -14
  296. package/src/components/message.tsx +75 -5
  297. package/src/components/response-stream.tsx +13 -7
  298. package/src/components/screen.tsx +5 -0
  299. package/src/components/scroll-button.tsx +7 -0
  300. package/src/components/toast.tsx +11 -5
  301. package/src/components/voice-input.tsx +35 -3
  302. package/src/components/voice-output.tsx +46 -6
  303. package/src/components/workspace-shell.tsx +285 -0
  304. package/src/diagnostics/hook.ts +352 -0
  305. package/src/diagnostics/index.ts +82 -0
  306. package/src/diagnostics/report-request.ts +296 -0
  307. package/src/elements/attachments.tsx +6 -7
  308. package/src/elements/audio-visualizer.tsx +48 -4
  309. package/src/elements/chat-workspace.tsx +159 -287
  310. package/src/elements/chat.tsx +12 -9
  311. package/src/elements/code-block.tsx +19 -1
  312. package/src/elements/command.tsx +4 -1
  313. package/src/elements/compiled.css +1 -1
  314. package/src/elements/conversation-item.tsx +80 -0
  315. package/src/elements/conversation-list.tsx +60 -11
  316. package/src/elements/default-input.tsx +5 -5
  317. package/src/elements/define.tsx +168 -8
  318. package/src/elements/diagnostic-events.ts +114 -0
  319. package/src/elements/dialog.tsx +47 -0
  320. package/src/elements/disclosure.ts +34 -12
  321. package/src/elements/dock.tsx +204 -0
  322. package/src/elements/dropdown.tsx +123 -0
  323. package/src/elements/element-diagnostics.ts +392 -0
  324. package/src/elements/element-manifest.json +12 -0
  325. package/src/elements/element-meta.json +672 -439
  326. package/src/elements/element-nonscalar.json +170 -0
  327. package/src/elements/element-types.d.ts +232 -148
  328. package/src/elements/menu.tsx +13 -0
  329. package/src/elements/prompt-dock.tsx +2 -1
  330. package/src/elements/prompt-input.tsx +8 -8
  331. package/src/elements/register-impl.ts +46 -0
  332. package/src/elements/register.ts +28 -0
  333. package/src/elements/resizable.tsx +52 -25
  334. package/src/elements/scroll-button.tsx +5 -0
  335. package/src/elements/slots.ts +107 -12
  336. package/src/elements/styles.css +30 -6
  337. package/src/elements/thinking-bar.tsx +16 -7
  338. package/src/elements/toast.tsx +3 -0
  339. package/src/elements/tool.tsx +26 -5
  340. package/src/elements/voice-input.tsx +11 -2
  341. package/src/elements/voice-output.tsx +16 -3
  342. package/src/index.ts +2 -0
  343. package/src/primitives/use-resize-observer.ts +33 -0
  344. package/src/primitives/use-speech-recognition.ts +12 -1
  345. package/src/primitives/use-stick-to-bottom.ts +34 -2
  346. package/src/primitives/use-text-stream.ts +33 -2
  347. package/src/remote/provider-runtime.ts +6 -3
  348. package/src/schemas/index.ts +1 -0
  349. package/src/schemas/tool-defs.ts +297 -6
  350. package/src/solid.ts +5 -0
  351. package/src/state/index.ts +12 -0
  352. package/src/state/persistence.ts +180 -0
  353. package/src/state/stream.ts +71 -4
  354. package/src/state/threads.ts +142 -0
  355. package/src/types.ts +4 -2
  356. package/src/ui/dialog.tsx +6 -0
  357. package/src/ui/dock.tsx +445 -0
  358. package/src/ui/dropdown.tsx +67 -23
  359. package/src/ui/hover-card.tsx +156 -6
  360. package/src/ui/input.tsx +29 -10
  361. package/src/ui/resizable.tsx +11 -0
  362. package/src/wire/chunk.ts +47 -0
  363. package/src/wire/consume.ts +185 -26
  364. package/src/wire/diagnostics.ts +727 -0
  365. package/src/wire/encode-probe.ts +214 -0
  366. package/src/wire/encode.ts +267 -14
  367. package/src/wire/formats/anthropic.ts +8 -1
  368. package/src/wire/formats/openai.ts +5 -0
  369. package/src/wire/index.ts +22 -0
  370. package/src/wire/read.ts +250 -6
  371. package/src/wire/sse.ts +16 -3
  372. package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
  373. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
  374. package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
  375. package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
  376. package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
  377. package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
  378. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
  379. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
  380. package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
  381. package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
  382. package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
  383. package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
  384. package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
  385. package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
  386. package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
  387. package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
  388. package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
  389. package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
  390. package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
  391. package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
  392. package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
  393. package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
  394. package/dist/elements/chunks/input-DrseEjhp.js +0 -1
  395. package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
  396. package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
  397. package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
  398. package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
  399. package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
  400. package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
  401. package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
  402. package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
  403. package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
  404. package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
  405. package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
  406. package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
  407. package/dist/register-impl-DGmGzrEM.js +0 -145
  408. package/dist/variant-custom-MkTNTycd.js +0 -1
  409. package/src/primitives/card-validate-generator.testlib.ts +0 -38
@@ -27,9 +27,14 @@
27
27
  // `format: "uri"` (link/embed/artifact), `minimum` (tasks/embed/artifact),
28
28
  // `maxItems` (confirm/artifact), `pattern` (embed), `if`/`then` (embed), `oneOf`
29
29
  // (artifact), a free-form object (form) and untyped `payload`/`allowOther` members
30
- // (confirm/choice). Non-strict is therefore not merely the default, it is currently
31
- // the only working mode, and it is also the mode the five-configuration conformance
32
- // spike actually proved cards in.
30
+ // (confirm/choice). Non-strict is therefore the default and the mode the
31
+ // five-configuration conformance spike actually proved cards in but since F-20 it
32
+ // is no longer a raw pass-through either: a root combinator (`anyOf` on artifact,
33
+ // `allOf` on embed) gets HTTP 400 from OpenAI and Anthropic even non-strict, so the
34
+ // non-strict projection relaxes those root combinators LOUDLY (constraint restated in
35
+ // the description, console.warn names the relaxation, registry.validate still
36
+ // enforces at render time). The jsonschema projection stays byte-faithful to the
37
+ // authored schema.
33
38
  //
34
39
  // The alternative was to silently drop the offending keywords. That is rejected for
35
40
  // the reason the plan gives: a dropped `maxItems: 4` means the model emits six
@@ -126,6 +131,42 @@ export interface CardToolOptions<P extends ToolProvider = ToolProvider> {
126
131
  * When it throws it names the card, the path and the keyword.
127
132
  */
128
133
  readonly strict?: boolean;
134
+ /**
135
+ * Narrow the DERIVED tool schema per card type, after projection (F-23).
136
+ *
137
+ * A card's authored schema is deliberately permissive — `files` is optional on an
138
+ * artifact, its items need only a `path` — because the card CONTRACT must accept
139
+ * what a host may legitimately render. A model filling the tool in, though, is
140
+ * often better served by a tighter tool: requiring `code` on every file, or at
141
+ * least one file at all, moves a class of failures from render time (a card the
142
+ * registry rejects) to generation time (the model literally cannot emit one).
143
+ *
144
+ * This narrows the WIRE projection only — the authored schema and
145
+ * `registry.validate()` are untouched, so a hand-built envelope still validates.
146
+ * Applied for every provider, `jsonschema` included, and merged loudly: an
147
+ * unknown path or a required name the node has no property for is a TypeError
148
+ * naming the card, the path and the field, because a tool that can never be
149
+ * satisfied must not ship quietly.
150
+ */
151
+ readonly require?: Readonly<Record<string, readonly CardRequireRule[]>>;
152
+ }
153
+
154
+ /**
155
+ * One narrowing instruction for one node of one card's projected schema.
156
+ *
157
+ * `path` is a dot-path from the envelope-data root through `properties` keys
158
+ * (`''` names the root node itself). On an ARRAY node, `required` narrows
159
+ * `items.required` and `minItems` lands on the array; on an object node,
160
+ * `required` lands on the node's own `required`. Existing requirements merge,
161
+ * never replace.
162
+ */
163
+ export interface CardRequireRule {
164
+ /** Dot-path from the envelope-data root to a schema node, e.g. 'files'. */
165
+ readonly path: string;
166
+ /** Property names to require. On an ARRAY node this narrows `items.required`; on an object node, the node's own `required`. */
167
+ readonly required?: readonly string[];
168
+ /** Sets `minItems` (array nodes only). */
169
+ readonly minItems?: number;
129
170
  }
130
171
 
131
172
  /**
@@ -225,7 +266,7 @@ function formatUnsupported(subset: ProviderSubset, cards: readonly UnsupportedCa
225
266
  lines.push('');
226
267
  lines.push(` subset source: ${subset.source} (read ${subset.checkedOn})`);
227
268
  lines.push(
228
- ' Fix: drop `strict: true`. Non-strict is the default and is the mode the kit\'s cards are proven in; both providers accept a loose schema there and ignore what they do not compile. Rewriting a built-in card schema to fit a strict subset would change what a valid card IS, which is a card-contract change, not a projection setting.',
269
+ ' Fix: drop `strict: true`. Non-strict mode projects a provider-valid schema (root combinators are relaxed with the constraint restated in the description and still enforced by card validation). Rewriting a built-in card schema to fit a strict subset would change what a valid card IS, which is a card-contract change, not a projection setting.',
229
270
  );
230
271
  return lines.join('\n');
231
272
  }
@@ -237,6 +278,227 @@ function formatUnsupported(subset: ProviderSubset, cards: readonly UnsupportedCa
237
278
  const isRecord = (v: unknown): v is Record<string, unknown> =>
238
279
  typeof v === 'object' && v !== null && !Array.isArray(v);
239
280
 
281
+ /**
282
+ * Root combinators the two largest providers refuse on a tool schema's root node
283
+ * (measured: OpenAI and Anthropic 400 before the model runs — findings F-20;
284
+ * DeepSeek's OpenAI-compatible route requires the same workaround). The AUTHORED
285
+ * schemas keep them — card validation keeps every constraint. Only the WIRE
286
+ * projection relaxes, loudly: the constraint is restated in the description the
287
+ * model reads, a console.warn names the relaxation, and a violating envelope
288
+ * still fails registry.validate() at render time with a hard diagnostic.
289
+ */
290
+ const ROOT_RELAXATIONS: Readonly<Record<string, string>> = {
291
+ artifact:
292
+ 'Provide `src` (a preview URL) or `files` — at least one. An envelope with neither is rejected.',
293
+ embed:
294
+ "For provider 'generic', include `url`. For 'youtube'/'vimeo', include `id` or `url`.",
295
+ };
296
+
297
+ /** Strip a banned combinator key from the projected ROOT node only (nested
298
+ * combinators are accepted by every provider measured and stay). Returns the
299
+ * relaxation note to restate in the TOOL description the model reads. Two
300
+ * distinct no-note return paths: `undefined` when NOTHING was banned (the wire
301
+ * projection is faithful, nothing was widened), and GENERIC_NOTE when something
302
+ * WAS banned but no curated copy exists (a custom card) — that path still
303
+ * restates the relaxation, because deleting a combinator without telling the
304
+ * model would be a silent widening. */
305
+ const GENERIC_RELAXATION_NOTE =
306
+ 'The root constraint of this schema was relaxed for this provider; consult the card registry for what a valid payload is.';
307
+
308
+ const BANNED_ROOT_KEYS = ['anyOf', 'allOf', 'oneOf', 'not', 'enum', 'const'] as const;
309
+
310
+ /**
311
+ * The object shape a literal `enum` / `const` VALUE implies.
312
+ *
313
+ * `const: { op: 'ping' }` is a whole-instance constraint, which a provider refuses
314
+ * at the root exactly like a combinator. The instance it admits is an object, so
315
+ * the shape survives the relaxation as `properties: { op: { const: 'ping' } }` with
316
+ * `op` required — nothing widened that the branch did not already allow.
317
+ */
318
+ function branchFromLiteral(value: unknown): Record<string, unknown> | null {
319
+ if (!isRecord(value)) return null;
320
+ const properties: Record<string, unknown> = {};
321
+ for (const [key, member] of Object.entries(value)) properties[key] = { const: member };
322
+ return { type: 'object', properties, required: Object.keys(value) };
323
+ }
324
+
325
+ /** The branch schemas one banned root keyword contributes, or `[]` for one that has none. */
326
+ function branchesOfRootKeyword(keyword: string, value: unknown): Record<string, unknown>[] {
327
+ if (keyword === 'anyOf' || keyword === 'oneOf' || keyword === 'allOf') {
328
+ return Array.isArray(value) ? value.filter(isRecord) : [];
329
+ }
330
+ if (keyword === 'const') return [branchFromLiteral(value)].filter((b): b is Record<string, unknown> => b !== null);
331
+ if (keyword === 'enum') {
332
+ return Array.isArray(value)
333
+ ? value.map(branchFromLiteral).filter((b): b is Record<string, unknown> => b !== null)
334
+ : [];
335
+ }
336
+ // `not` is a NEGATIVE constraint: nothing it says is a property the instance may
337
+ // carry, so there is nothing sound to merge back. It is dropped and restated in
338
+ // the description like every other relaxation.
339
+ return [];
340
+ }
341
+
342
+ /**
343
+ * Fold a root combinator's branches back into the object root that replaces it.
344
+ *
345
+ * The two built-ins showed the shape of the rule and this generalises it rather
346
+ * than special-casing them. `artifact`'s root `anyOf` is `[{required:['src']},
347
+ * {required:['files']}]`: a DISJUNCTION guarantees only what EVERY branch
348
+ * guarantees, so the intersection is empty and `required` stays absent — which is
349
+ * exactly what the artifact projection already did. `embed`'s root `allOf` is two
350
+ * `if`/`then` pairs: a CONJUNCTION guarantees every branch's own top-level
351
+ * `required`, and an `if`/`then` branch declares none, so again nothing is added.
352
+ * A consumer card whose branches carry real `properties` is the case neither
353
+ * built-in reaches, and the one this exists for.
354
+ *
355
+ * Properties are UNIONed (each branch's shape must remain expressible — dropping
356
+ * one is the silent narrowing the whole F-20 fix exists to avoid); a name already
357
+ * on the root keeps the root's definition. `required` is the intersection for
358
+ * `anyOf`/`oneOf`/`enum`/`const` and the union for `allOf`, unioned in turn with
359
+ * whatever the root already required, and filtered to names the merged
360
+ * `properties` actually carries so the tool can never be unsatisfiable.
361
+ */
362
+ function mergeBranchesIntoRoot(parameters: Record<string, unknown>, banned: readonly string[]): void {
363
+ const properties: Record<string, unknown> = isRecord(parameters.properties)
364
+ ? { ...parameters.properties }
365
+ : {};
366
+ const guaranteed = new Set<string>(
367
+ Array.isArray(parameters.required) ? parameters.required.map(String) : [],
368
+ );
369
+
370
+ for (const keyword of banned) {
371
+ const branches = branchesOfRootKeyword(keyword, parameters[keyword]);
372
+ if (branches.length === 0) continue;
373
+
374
+ for (const branch of branches) {
375
+ if (!isRecord(branch.properties)) continue;
376
+ for (const [name, sub] of Object.entries(branch.properties)) {
377
+ if (!(name in properties)) properties[name] = sub;
378
+ }
379
+ }
380
+
381
+ const perBranch = branches.map(
382
+ (branch) => new Set(Array.isArray(branch.required) ? branch.required.map(String) : []),
383
+ );
384
+ if (keyword === 'allOf') {
385
+ for (const set of perBranch) for (const name of set) guaranteed.add(name);
386
+ } else {
387
+ // Disjunction: keep only what every branch requires.
388
+ const [first, ...rest] = perBranch;
389
+ for (const name of first) {
390
+ if (rest.every((set) => set.has(name))) guaranteed.add(name);
391
+ }
392
+ }
393
+ }
394
+
395
+ if (Object.keys(properties).length > 0) parameters.properties = properties;
396
+ const required = [...guaranteed].filter((name) => name in properties);
397
+ if (required.length > 0) parameters.required = required;
398
+ }
399
+
400
+ /**
401
+ * Make the projected ROOT the object shape every provider demands.
402
+ *
403
+ * Deleting a root combinator is only half of F-20. A schema whose root was NOTHING
404
+ * BUT the combinator projects to `{"title":…,"description":…}` once it is gone —
405
+ * accepted by no provider (measured: OpenAI 400, "schema must be a JSON Schema of
406
+ * type \"object\"") and unfillable by any model. Every built-in declares
407
+ * `type: "object"` at its root, so nothing in the suite reached this until a
408
+ * consumer card did.
409
+ *
410
+ * A root that declares a NON-object type cannot be a tool schema at all, and
411
+ * asserting `type: "object"` over it would be a lie about the card. That refuses,
412
+ * naming the card.
413
+ */
414
+ function ensureObjectRoot(parameters: Record<string, unknown>, cardType: string): void {
415
+ const declared = parameters.type;
416
+ if (declared !== undefined && declared !== 'object') {
417
+ throw new TypeError(
418
+ `cardTools: the '${cardType}' card schema declares root type '${String(declared)}', but a provider tool ` +
419
+ `schema root must be an object (OpenAI and Anthropic reject anything else with HTTP 400). ` +
420
+ `Wrap the payload in an object, or project this card with provider 'jsonschema'.`,
421
+ );
422
+ }
423
+ parameters.type = 'object';
424
+ }
425
+
426
+ function relaxRootCombinators(parameters: Record<string, unknown>, cardType: string): string | undefined {
427
+ const note = ROOT_RELAXATIONS[cardType];
428
+ const banned = BANNED_ROOT_KEYS.filter((k) => k in parameters);
429
+ // The object-root guarantee holds for EVERY provider projection, banned keys or
430
+ // not: it is what the provider requires, not what the relaxation broke.
431
+ ensureObjectRoot(parameters, cardType);
432
+ if (banned.length === 0) return undefined;
433
+ mergeBranchesIntoRoot(parameters, banned);
434
+ for (const k of banned) delete parameters[k];
435
+ // eslint-disable-next-line no-console
436
+ console.warn(
437
+ `[kai-card-tools] relaxed root ${banned.join('/')} on the projected '${toolNameForCardType(cardType)}' tool schema ` +
438
+ `for this provider (it would be refused with HTTP 400); the constraint now lives in the ` +
439
+ `description and is still enforced by card validation.`,
440
+ );
441
+ return note ?? GENERIC_RELAXATION_NOTE;
442
+ }
443
+
444
+ /**
445
+ * Apply one card's `require` rules to its PROJECTED parameters (F-23).
446
+ *
447
+ * Runs after `project()` and `relaxRootCombinators()`, on the copy the projection
448
+ * built — never on the authored schema, which `registry.validate()` keeps reading.
449
+ * Merges into whatever `required` already exists rather than replacing it, so a
450
+ * rule can only ever NARROW what the model may emit, never widen it.
451
+ */
452
+ function applyRequire(
453
+ parameters: Record<string, unknown>,
454
+ cardType: string,
455
+ rules: readonly CardRequireRule[],
456
+ ): void {
457
+ for (const rule of rules) {
458
+ // Resolve the dot-path through `properties` keys. `''` names the root node;
459
+ // every other segment must name a property of the node reached so far.
460
+ let node: Record<string, unknown> = parameters;
461
+ if (rule.path !== '') {
462
+ for (const segment of rule.path.split('.')) {
463
+ const props = isRecord(node.properties) ? node.properties : undefined;
464
+ const next = props?.[segment];
465
+ if (!isRecord(next)) {
466
+ throw new TypeError(
467
+ `cardTools: require on '${cardType}': path '${rule.path}' does not resolve in the schema`,
468
+ );
469
+ }
470
+ node = next;
471
+ }
472
+ }
473
+
474
+ const isArrayNode = node.type === 'array' || (node.type === undefined && 'items' in node);
475
+ if (isArrayNode && rule.minItems !== undefined) node.minItems = rule.minItems;
476
+ if (!isArrayNode && rule.minItems !== undefined) {
477
+ throw new TypeError(
478
+ `cardTools: minItems at require path '${rule.path}' applies to array nodes only, ` +
479
+ `but '${rule.path}' in the '${cardType}' schema is not one`,
480
+ );
481
+ }
482
+
483
+ if (rule.required !== undefined) {
484
+ // On an array node the requirement lives on the ITEMS (each element must carry
485
+ // the field), not on the array itself.
486
+ const target = isArrayNode && isRecord(node.items) ? node.items : node;
487
+ const props = isRecord(target.properties) ? target.properties : undefined;
488
+ for (const field of rule.required) {
489
+ if (!props || !(field in props)) {
490
+ throw new TypeError(
491
+ `cardTools: require on '${cardType}' at path '${rule.path}': '${field}' is not a property of that node — ` +
492
+ `a required name with no property would make the tool unsatisfiable`,
493
+ );
494
+ }
495
+ }
496
+ const existing = Array.isArray(target.required) ? target.required.map(String) : [];
497
+ target.required = [...new Set([...existing, ...rule.required])];
498
+ }
499
+ }
500
+ }
501
+
240
502
  /**
241
503
  * Keywords stripped from every projection, strict or not.
242
504
  *
@@ -430,11 +692,40 @@ export function cardTools(a: CardToolInput | CardToolOptions, b?: CardToolOption
430
692
  const defs: ToolDef[] = [];
431
693
  const failures: UnsupportedCardTool[] = [];
432
694
 
695
+ // A `require` key naming a card the call does not carry is a typo, and a typo here
696
+ // would otherwise narrow NOTHING while looking like it narrowed something — the
697
+ // same decide-loudly class as an unknown dot-path below.
698
+ if (opts.require !== undefined) {
699
+ for (const key of Object.keys(opts.require)) {
700
+ if (!(key in schemas)) {
701
+ throw new TypeError(
702
+ `cardTools: require names card type '${key}', which this call does not carry ` +
703
+ `(carries: ${Object.keys(schemas).join(', ') || 'none'})`,
704
+ );
705
+ }
706
+ }
707
+ }
708
+
433
709
  for (const [cardType, schema] of Object.entries(schemas)) {
434
710
  const name = toolNameForCardType(cardType);
435
711
  const description = describe(cardType, schema, descriptions);
436
712
  const parameters = project(schema, projectOptions) as ToolParameters;
437
713
 
714
+ // F-20: non-strict only. Strict mode already refuses these cards with the full
715
+ // subset check, so the relaxation must never mask it. The note is restated in the
716
+ // TOOL description — the envelope field the model reads when choosing the tool.
717
+ const relaxedNote =
718
+ subset === null && opts.provider !== 'jsonschema'
719
+ ? relaxRootCombinators(parameters as Record<string, unknown>, cardType)
720
+ : undefined;
721
+ const wireDescription = relaxedNote ? `${description} ${relaxedNote}` : description;
722
+
723
+ // F-23: every provider, jsonschema included — a consumer handing the bare schema
724
+ // to the Vercel AI SDK narrows their tool the same way. Runs BEFORE the strict
725
+ // subset check so that check judges the schema that actually ships.
726
+ const requireRules = opts.require?.[cardType];
727
+ if (requireRules !== undefined) applyRequire(parameters as Record<string, unknown>, cardType, requireRules);
728
+
438
729
  if (subset) {
439
730
  const violations = checkProviderSubset(parameters, subset);
440
731
  if (violations.length > 0) {
@@ -444,9 +735,9 @@ export function cardTools(a: CardToolInput | CardToolOptions, b?: CardToolOption
444
735
  }
445
736
 
446
737
  if (opts.provider === 'openai') {
447
- defs.push({ type: 'function', function: { name, description, parameters, ...(strict ? { strict: true as const } : {}) } });
738
+ defs.push({ type: 'function', function: { name, description: wireDescription, parameters, ...(strict ? { strict: true as const } : {}) } });
448
739
  } else if (opts.provider === 'anthropic') {
449
- defs.push({ name, description, input_schema: parameters, ...(strict ? { strict: true as const } : {}) });
740
+ defs.push({ name, description: wireDescription, input_schema: parameters, ...(strict ? { strict: true as const } : {}) });
450
741
  } else {
451
742
  defs.push({ name, description, schema: parameters });
452
743
  }
package/src/solid.ts CHANGED
@@ -69,6 +69,8 @@ export { EditableLabel } from './ui/editable-label';
69
69
  export type { EditableLabelProps } from './ui/editable-label';
70
70
  export { Dialog } from './ui/dialog';
71
71
  export type { DialogProps, DialogController } from './ui/dialog';
72
+ export { Dock, DockLauncherGlyph, DockCloseGlyph } from './ui/dock';
73
+ export type { DockProps, DockController, DockPosition, DockFocusOnOpen } from './ui/dock';
72
74
  export { Popover } from './ui/popover';
73
75
  export type { PopoverProps, PopoverController } from './ui/popover';
74
76
  export { Nav } from './ui/nav';
@@ -148,3 +150,6 @@ export type {
148
150
  AudioVisualizerProps, VisualizerVariant, ShaderSpec, ShaderVariantProps,
149
151
  } from './components/audio-visualizer';
150
152
  export type { VisualizerSize } from './components/audio-visualizer/sizes';
153
+ export { WorkspaceShell, type WorkspaceShellProps, type WorkspaceShellController, type WorkspaceAsideSide, type WorkspaceAsideToggleDetail, type WorkspaceAsideResizeDetail } from './components/workspace-shell';
154
+ export { createConversationItemsController, readConversationItemId } from './components/conversation-list';
155
+ export type { ConversationItemsController, ConversationItemsControllerOptions } from './components/conversation-list';
@@ -7,6 +7,18 @@ export type { SetMessages, AssistantStream } from './stream';
7
7
  export { appendTextPart, appendReasoningPart, upsertToolPart, upsertCardPart, fingerprint } from './parts';
8
8
  export type { ReasoningOpts } from './parts';
9
9
 
10
+ // Multi-thread mechanics (the workspace recast, spec §3b): the id-bound
11
+ // SetMessages fold + the in-flight-turn bookkeeping. The consumer's record
12
+ // shape and every persistence/retention decision stay outside — see ./threads.
13
+ export { updateThreadMessages, bindThreadMessages, createThreadSessions } from './threads';
14
+ export type { ThreadLike, SetThreads, BindThreadOptions, ThreadSessions } from './threads';
15
+
16
+ // The persistence SEAM, not persistence: the stored-thread validator (F-18,
17
+ // variant list derived from the MessagePart union) and the debounce/flush
18
+ // shape. No storage backend, no default delay — policy is the consumer's.
19
+ export { parseStoredThread, createSaveScheduler } from './persistence';
20
+ export type { ParsedThread, DroppedStored, SaveScheduler, SaveSchedulerOptions } from './persistence';
21
+
10
22
  // The zero-config mock. Produces SSE frames rather than folding parts directly,
11
23
  // so the no-backend preview runs through the SAME parser a real provider does —
12
24
  // and is deliberately impossible to mistake for one. See ./mock.
@@ -0,0 +1,180 @@
1
+ // src/state/persistence.ts — the mechanical halves of the persistence story.
2
+ //
3
+ // The boundary (spec §1, CLAUDE.md): persistence POLICY — what is stored,
4
+ // where, for how long, under what quota — is the app's. What lives here is
5
+ // only the mechanics every such app re-derives by hand: validating a stored
6
+ // thread back into `ChatMessage[]` (F-18), and the debounce/flush shape that
7
+ // keeps a per-token stream from hitting storage per token. No storage call,
8
+ // no fetch, no DOM; the save fn, the delay, and the reaction to a drop are
9
+ // all the consumer's.
10
+ import type { ChatMessage, FeedbackVote, MessagePart } from '../elements/chat-types';
11
+
12
+ /** One record the parse refused, and why. Deciding loudly is the caller's
13
+ * verb (warn, reject the whole record, count it); this is the information
14
+ * that a quiet version would withhold. */
15
+ export interface DroppedStored {
16
+ at: 'thread' | 'message' | 'part';
17
+ reason: string;
18
+ value: unknown;
19
+ }
20
+
21
+ export interface ParsedThread {
22
+ messages: ChatMessage[];
23
+ /** Empty on a clean parse. Never silently shorter output than input. */
24
+ dropped: DroppedStored[];
25
+ }
26
+
27
+ function isRecord(v: unknown): v is Record<string, unknown> {
28
+ return typeof v === 'object' && v !== null;
29
+ }
30
+
31
+ /**
32
+ * One payload check per `MessagePart` variant. The key set is DERIVED from the
33
+ * union in `src/elements/chat-types.ts` at compile time: `satisfies
34
+ * Record<MessagePart['type'], …>` makes a missing key (a 7th variant landed)
35
+ * and a stale key (a variant renamed away) each a type error in THIS file —
36
+ * the same union `lint:silent-drops` and `verify:scaffold` read, so the lists
37
+ * cannot disagree. The behavioral cross-check lives in ./persistence.test.ts,
38
+ * which re-reads the union by a deliberately different method.
39
+ *
40
+ * These are shape checks, not a trust boundary for rendering: a rehydrated
41
+ * part is model output twice over (it was model output before it was
42
+ * hand-editable storage), and it stays safe to draw because the elements are
43
+ * what draw it. What this buys is that a truncated or half-written record
44
+ * reaches the element as a well-formed thread minus the broken piece, instead
45
+ * of as `parts: undefined` taking the whole app down on first render.
46
+ */
47
+ const PART_VALIDATORS = {
48
+ text: (v) => typeof v.text === 'string',
49
+ reasoning: (v) => typeof v.text === 'string',
50
+ tool: (v) => isRecord(v.tool),
51
+ card: (v) => isRecord(v.envelope) && typeof v.envelope.type === 'string' && typeof v.envelope.id === 'string',
52
+ source: (v) => isRecord(v.source),
53
+ file: (v) => isRecord(v.attachment),
54
+ } satisfies Record<MessagePart['type'], (v: Record<string, unknown>) => boolean>;
55
+
56
+ function parsePart(v: unknown, dropped: DroppedStored[]): MessagePart | null {
57
+ if (!isRecord(v) || typeof v.type !== 'string') {
58
+ dropped.push({ at: 'part', reason: 'not a part record', value: v });
59
+ return null;
60
+ }
61
+ const validate = (PART_VALIDATORS as Record<string, (v: Record<string, unknown>) => boolean>)[v.type];
62
+ if (!validate) {
63
+ // An unknown `type` is dropped rather than passed on: every renderer and
64
+ // encoder switches on it and has no branch for a made-up one.
65
+ dropped.push({ at: 'part', reason: `unknown part type "${v.type}"`, value: v });
66
+ return null;
67
+ }
68
+ if (!validate(v)) {
69
+ dropped.push({ at: 'part', reason: `missing or invalid payload for "${v.type}" part`, value: v });
70
+ return null;
71
+ }
72
+ // Verbatim, not rebuilt: optional fields the variants carry (`raw`,
73
+ // `signature`, `index`, …) are load-bearing on the wire re-encode, so the
74
+ // validated record passes through whole.
75
+ return v as unknown as MessagePart;
76
+ }
77
+
78
+ function parseMessage(v: unknown, dropped: DroppedStored[]): ChatMessage | null {
79
+ if (!isRecord(v) || typeof v.id !== 'string' || (v.role !== 'user' && v.role !== 'assistant') || !Array.isArray(v.parts)) {
80
+ dropped.push({ at: 'message', reason: 'not a message record (id/role/parts)', value: v });
81
+ return null;
82
+ }
83
+ const parts = v.parts
84
+ .map((p) => parsePart(p, dropped))
85
+ .filter((p): p is MessagePart => p !== null);
86
+ if (parts.length === 0) {
87
+ // A message whose every part was unreadable is an empty bubble, and the
88
+ // per-part entries above already say what was lost.
89
+ dropped.push({ at: 'message', reason: 'no readable parts survived', value: v });
90
+ return null;
91
+ }
92
+ const message: ChatMessage = { id: v.id, role: v.role, parts };
93
+ if (v.feedback === 'like' || v.feedback === 'dislike') message.feedback = v.feedback as FeedbackVote;
94
+ if (isRecord(v.avatar)) message.avatar = v.avatar;
95
+ if (Array.isArray(v.actions)) message.actions = v.actions as ChatMessage['actions'];
96
+ return message;
97
+ }
98
+
99
+ /**
100
+ * Validate a stored thread — the JSON-parsed value of one thread's messages
101
+ * array — back into `ChatMessage[]`. Never throws: unreadable records are
102
+ * DROPPED and reported in `dropped`, so a truncated write loses one record,
103
+ * not every conversation. What to DO about a drop (warn, discard the whole
104
+ * record, telemetry) is the caller's policy.
105
+ */
106
+ export function parseStoredThread(value: unknown): ParsedThread {
107
+ const dropped: DroppedStored[] = [];
108
+ if (!Array.isArray(value)) {
109
+ return { messages: [], dropped: [{ at: 'thread', reason: 'stored thread is not an array', value }] };
110
+ }
111
+ const messages = value
112
+ .map((m) => parseMessage(m, dropped))
113
+ .filter((m): m is ChatMessage => m !== null);
114
+ return { messages, dropped };
115
+ }
116
+
117
+ export interface SaveSchedulerOptions {
118
+ /** The quiet period before a scheduled snapshot is written. This is policy
119
+ * and therefore REQUIRED — the kit ships no default debounce. */
120
+ delayMs: number;
121
+ /** The scheduler the seam runs on. Defaults to the global timers; inject
122
+ * your own for tests or a non-timer scheduler. */
123
+ setTimeout?: (fn: () => void, ms: number) => unknown;
124
+ clearTimeout?: (handle: unknown) => void;
125
+ }
126
+
127
+ export interface SaveScheduler<T> {
128
+ /** Remember `snapshot` as the latest state and (re)start the quiet-period
129
+ * timer. Call it on every state change; only the last snapshot inside the
130
+ * quiet period is written. */
131
+ schedule(snapshot: T): void;
132
+ /** Write the pending snapshot NOW and disarm the timer — the
133
+ * `beforeunload`/`visibilitychange` story, so a tab closed mid-stream still
134
+ * persists. No-op when nothing is pending: the last write already happened. */
135
+ flush(): void;
136
+ /** Drop the pending write unsaved (e.g. unmount-without-persist). */
137
+ cancel(): void;
138
+ pending(): boolean;
139
+ }
140
+
141
+ /**
142
+ * The debounce/flush seam: a streaming reply lands a state update per token,
143
+ * and without this shape so would the consumer's storage backend. The kit owns
144
+ * only the timer dance; `save` (WHAT and WHERE, including its own try/catch
145
+ * policy for quota errors) and `delayMs` (HOW LONG) are the consumer's.
146
+ */
147
+ export function createSaveScheduler<T>(save: (snapshot: T) => void, opts: SaveSchedulerOptions): SaveScheduler<T> {
148
+ const setTimer = opts.setTimeout ?? ((fn: () => void, ms: number) => setTimeout(fn, ms));
149
+ const clearTimer = opts.clearTimeout ?? ((handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>));
150
+
151
+ let latest: T | undefined;
152
+ let armed = false;
153
+ let handle: unknown;
154
+
155
+ const disarm = () => {
156
+ if (!armed) return;
157
+ clearTimer(handle);
158
+ armed = false;
159
+ };
160
+ const write = () => {
161
+ armed = false;
162
+ save(latest as T);
163
+ };
164
+
165
+ return {
166
+ schedule(snapshot) {
167
+ latest = snapshot;
168
+ disarm();
169
+ handle = setTimer(write, opts.delayMs);
170
+ armed = true;
171
+ },
172
+ flush() {
173
+ if (!armed) return;
174
+ disarm();
175
+ save(latest as T);
176
+ },
177
+ cancel: disarm,
178
+ pending: () => armed,
179
+ };
180
+ }
@@ -116,6 +116,56 @@ export interface AssistantStream {
116
116
  * assignability already holds. Measurements and the trade: boundary point 2. */
117
117
  addFile(attachment: AttachmentData): AssistantStream;
118
118
  done(): void;
119
+ /** Settles the turn as FAILED and puts `reason` where the reader can find it.
120
+ *
121
+ * WHERE THE REASON LANDS, in order:
122
+ * 1. every tool part that has NOT produced a result flips to `output-error`
123
+ * with `errorText: reason`, so no panel spins forever. A tool ALREADY in
124
+ * `output-error` with its own non-empty `errorText` is left exactly as it
125
+ * is: "search index offline" is the answer to what went wrong and
126
+ * "Connection lost." is the generic outer symptom, so overwriting the
127
+ * specific with the generic loses the only actionable thing on the
128
+ * message. It still counts as carrying the failure. A tool in
129
+ * `output-error` with NO text does get filled in — an error panel with
130
+ * nothing in it says no more than a blank bubble does.
131
+ * 2. if no part was able to carry it, the reason is APPENDED as its own text
132
+ * part.
133
+ *
134
+ * Never both — a turn does not report the same failure twice.
135
+ *
136
+ * "Where the reader can FIND it" is deliberate, and rule 1 and rule 2 are not
137
+ * equally loud. Rule 2 is text in the thread: read without any interaction.
138
+ * Rule 1 is a tool panel, which renders COLLAPSED — the header shows an error
139
+ * icon and a badge, so the failure is unmissable, but the `errorText` itself
140
+ * is one click away. That is the right default for a failed tool inside an
141
+ * otherwise readable answer, and it is why rule 2 exists rather than always
142
+ * stamping the panel and calling it reported.
143
+ *
144
+ * Rule 2 is the whole point and it used to be missing. `abort` only ever did
145
+ * rule 1, so a TEXT-ONLY turn — every turn of a text-only support widget —
146
+ * had nothing to stamp: the string was discarded and a failed request
147
+ * rendered an EMPTY assistant bubble, while the consumer that passed a
148
+ * perfectly good sentence believed it had reported the failure. That is the
149
+ * repo's "decide loudly" rule broken on the one path where being quiet is
150
+ * worst, and it shipped because the discard is invisible from the call site.
151
+ *
152
+ * It is a NEW part, never a merge onto the trailing text: gluing "Connection
153
+ * lost." onto the model's half-finished sentence reads as the model saying
154
+ * it. Text that already streamed stays exactly where it is.
155
+ *
156
+ * The reason is TRIMMED, and `abort()` with no reason — or one that is empty
157
+ * or all whitespace — appends nothing: there is no reason to discard, and the
158
+ * kit will not invent copy the consumer did not write. Rule 1 still runs,
159
+ * with `errorText: undefined`. Whitespace is not a pedantic case here: the
160
+ * scaffold hands over `err.message`, and an `Error` is free to carry `''`,
161
+ * which would otherwise render an INVISIBLE text part — a blank bubble that
162
+ * also claims to have said something.
163
+ *
164
+ * Settled is settled: after `done()` or a first `abort()`, this is a no-op
165
+ * like every other mutator, so the reason cannot be appended twice.
166
+ *
167
+ * The reason is CONSUMER-facing text and is rendered as markdown like any
168
+ * other text part. Pass a sentence a visitor can read, not a stack trace. */
119
169
  abort(reason?: string): void;
120
170
  }
121
171
 
@@ -151,10 +201,27 @@ export function createAssistantStream(
151
201
  addFile(attachment) { mutate((p) => [...p, { type: 'file', attachment }]); return stream; },
152
202
  done() { settled = true; },
153
203
  abort(reason) {
154
- mutate((p) => p.map((part) =>
155
- part.type === 'tool' && part.tool.state !== 'output-available'
156
- ? { ...part, tool: { ...part.tool, state: 'output-error' as const, errorText: reason } }
157
- : part));
204
+ // Whitespace-only is the same nothing as empty. `err.message` on a thrown
205
+ // Error is free to be either, and appending it would render an INVISIBLE
206
+ // text part -- a blank bubble that also claims to have said something.
207
+ const text = reason?.trim() || undefined;
208
+ mutate((p) => {
209
+ let carried = false;
210
+ const next = p.map((part) => {
211
+ if (part.type !== 'tool' || part.tool.state === 'output-available') return part;
212
+ // Already showing its own, more specific failure: keep it, and count
213
+ // it as carrying. See the note above `abort`.
214
+ if (part.tool.state === 'output-error' && part.tool.errorText?.trim()) {
215
+ carried = true;
216
+ return part;
217
+ }
218
+ carried = true;
219
+ return { ...part, tool: { ...part.tool, state: 'output-error' as const, errorText: text } };
220
+ });
221
+ // The reason has to land SOMEWHERE. See the note above `abort`.
222
+ if (carried || !text) return next;
223
+ return [...next, { type: 'text', text }];
224
+ });
158
225
  settled = true;
159
226
  },
160
227
  };