@kitn.ai/ui 0.28.0 → 0.30.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 (615) hide show
  1. package/README.md +11 -7
  2. package/dist/{Icon-DObhsRtm.js → Icon-BzBET3E-.js} +1 -1
  3. package/dist/{action-icons-CcgNN2oT.js → action-icons-DaW2pU6m.js} +1 -1
  4. package/dist/agent-tooling/construct/public.d.ts +15 -0
  5. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  6. package/dist/arrow-down-D783iMJj.js +1 -0
  7. package/dist/arrow-left-0VLM5kyg.js +1 -0
  8. package/dist/artifact-DGjzHYUT.js +1 -0
  9. package/dist/{attachments-Qr4kg-2b.js → attachments-YvXHlg88.js} +1 -1
  10. package/dist/{audio-visualizer-DToYdfSc.js → audio-visualizer-9lz5IHsL.js} +1 -1
  11. package/dist/{badge-CdEu_jn2.js → badge-Dh_vp8A5.js} +1 -1
  12. package/dist/{bell-DQAAOpDz.js → bell-CT7_WUwS.js} +1 -1
  13. package/dist/{button-DkuOnGLy.js → button-C_3te6fm.js} +1 -1
  14. package/dist/{card-renderer-Jx70m6_l.js → card-renderer-DZVaGH2e.js} +1 -1
  15. package/dist/check-CiEOVnEN.js +1 -0
  16. package/dist/{checkbox-D7iRB3nL.js → checkbox-Bu2T2e0U.js} +1 -1
  17. package/dist/{checkbox-group-B2_rpoPM.js → checkbox-group-BiryEXPQ.js} +1 -1
  18. package/dist/chevron-down-CSobuZRG.js +1 -0
  19. package/dist/chevron-right-DR9ZAP-2.js +1 -0
  20. package/dist/{choice-card-m0BYqqOK.js → choice-card-CM6IjkmL.js} +1 -1
  21. package/dist/circle-BC66EYF7.js +1 -0
  22. package/dist/circle-check-BqmahW0O.js +1 -0
  23. package/dist/{circle-x-BPG0iPst.js → circle-x-Bk-dA5Wf.js} +1 -1
  24. package/dist/{code-block-ClMLKb7C.js → code-block-B26t-Q7q.js} +1 -1
  25. package/dist/{collapsible-B4DlWicL.js → collapsible-Bhc636Cf.js} +1 -1
  26. package/dist/components/builder-composer-triggers.d.ts +62 -0
  27. package/dist/components/builder-layout.d.ts +54 -0
  28. package/dist/components/builder-message-actions.d.ts +72 -0
  29. package/dist/components/builder-panel.d.ts +189 -0
  30. package/dist/components/builder-preview.d.ts +11 -0
  31. package/dist/components/builder-shell-controls.d.ts +69 -0
  32. package/dist/components/builder-skeleton.d.ts +58 -0
  33. package/dist/components/builder-start.d.ts +75 -0
  34. package/dist/components/builder-workspace-variants.d.ts +23 -0
  35. package/dist/components/captions.d.ts +66 -0
  36. package/dist/{composer-sQ0HcvyG.js → composer-DMI85B6j.js} +1 -1
  37. package/dist/{confirm-card-rQWDQsnC.js → confirm-card-B0YIAwXp.js} +1 -1
  38. package/dist/construct-cli.es.js +3 -3
  39. package/dist/construct.d.ts +5 -0
  40. package/dist/construct.js +327 -0
  41. package/dist/{context-0kg4ZX1J.js → context-DWgfhqWp.js} +1 -1
  42. package/dist/controllable-BagRpyHv.js +1 -0
  43. package/dist/{conversation-item-BxwQLaFJ.js → conversation-item-BsOsVGmT.js} +1 -1
  44. package/dist/{conversation-list-BCfGN1JU.js → conversation-list-aX3Vtc7L.js} +1 -1
  45. package/dist/{copy-n9u9rqpi.js → copy-BmEwsNhJ.js} +1 -1
  46. package/dist/core-C8fzo39E.js +1 -12
  47. package/dist/{create-tween-uPkArjIG.js → create-tween-CNOAU3Fx.js} +1 -1
  48. package/dist/{create-tween-C-q-JCM-.js → create-tween-WfqiENls.js} +1 -1
  49. package/dist/{create-tween-CRrJXxBd.js → create-tween-wlFtW9Kh.js} +1 -1
  50. package/dist/{create-tween-DZ3RRDHD.js → create-tween-zt79ba32.js} +1 -1
  51. package/dist/{default-input-CaMrJkgH.js → default-input-DMV5Eaze.js} +1 -1
  52. package/dist/define-CI0y3eE4.js +1 -0
  53. package/dist/define.js +40 -40
  54. package/dist/define.server.js +45 -45
  55. package/dist/{disclosure-WUImBbDv.js → disclosure-NnACBxsC.js} +1 -1
  56. package/dist/{dropdown-wZVeJOMd.js → dropdown-DT7G0RGR.js} +1 -1
  57. package/dist/elements/agent-card.js +1 -1
  58. package/dist/elements/artifact.js +1 -1
  59. package/dist/elements/attachments.js +1 -1
  60. package/dist/elements/audio-visualizer.js +1 -1
  61. package/dist/elements/avatar.js +1 -1
  62. package/dist/elements/badge.js +1 -1
  63. package/dist/elements/button.js +1 -1
  64. package/dist/elements/card.js +1 -1
  65. package/dist/elements/cards.js +1 -1
  66. package/dist/elements/chain-of-thought.js +1 -1
  67. package/dist/elements/chat-scope-picker.js +1 -1
  68. package/dist/elements/chat-workspace.js +1 -1
  69. package/dist/elements/chat.js +1 -1
  70. package/dist/elements/checkbox-group.js +1 -1
  71. package/dist/elements/checkbox.js +1 -1
  72. package/dist/elements/checkpoint.js +1 -1
  73. package/dist/elements/choice.js +1 -1
  74. package/dist/elements/coachmark.js +1 -1
  75. package/dist/elements/code-block.js +1 -1
  76. package/dist/elements/command.js +1 -1
  77. package/dist/elements/compare.js +1 -1
  78. package/dist/elements/composer.js +1 -1
  79. package/dist/elements/confirm-card.js +1 -1
  80. package/dist/elements/context-meter.js +1 -1
  81. package/dist/elements/conversation-item.js +1 -1
  82. package/dist/elements/conversation-list.js +1 -1
  83. package/dist/elements/dialog.js +1 -1
  84. package/dist/elements/dock.js +1 -1
  85. package/dist/elements/dropdown.js +1 -1
  86. package/dist/elements/editable-label.js +1 -1
  87. package/dist/elements/embed.js +1 -1
  88. package/dist/elements/empty.js +1 -1
  89. package/dist/elements/feedback-bar.js +1 -1
  90. package/dist/elements/file-tree.js +1 -1
  91. package/dist/elements/file-upload.js +1 -1
  92. package/dist/elements/form.js +1 -1
  93. package/dist/elements/hover-card.js +1 -1
  94. package/dist/elements/icon.js +1 -1
  95. package/dist/elements/image.js +1 -1
  96. package/dist/elements/input.js +1 -1
  97. package/dist/elements/kbd.js +1 -1
  98. package/dist/elements/link-preview.js +1 -1
  99. package/dist/elements/loader.js +1 -1
  100. package/dist/elements/markdown.js +1 -1
  101. package/dist/elements/menu.js +1 -1
  102. package/dist/elements/message-skills.js +1 -1
  103. package/dist/elements/message.js +1 -1
  104. package/dist/elements/model-switcher.js +1 -1
  105. package/dist/elements/nav.js +1 -1
  106. package/dist/elements/notice.js +1 -1
  107. package/dist/elements/pane-grid.js +1 -1
  108. package/dist/elements/pane-group.js +1 -1
  109. package/dist/elements/pane.js +1 -1
  110. package/dist/elements/popover.js +1 -1
  111. package/dist/elements/progress-bar.js +1 -1
  112. package/dist/elements/prompt-dock.js +1 -1
  113. package/dist/elements/prompt-input.js +1 -1
  114. package/dist/elements/prompt-suggestions.js +1 -1
  115. package/dist/elements/radio-group.js +1 -1
  116. package/dist/elements/reasoning.js +1 -1
  117. package/dist/elements/remote.js +1 -1
  118. package/dist/elements/resizable.js +1 -1
  119. package/dist/elements/response-stream.js +1 -1
  120. package/dist/elements/screen.js +1 -1
  121. package/dist/elements/scroll-area.js +1 -1
  122. package/dist/elements/scroll-button.js +1 -1
  123. package/dist/elements/search.js +1 -1
  124. package/dist/elements/segmented.js +1 -1
  125. package/dist/elements/select.js +1 -1
  126. package/dist/elements/separator.js +1 -1
  127. package/dist/elements/setting-item.js +1 -1
  128. package/dist/elements/settings-group.js +1 -1
  129. package/dist/elements/skeleton.js +1 -1
  130. package/dist/elements/slider.js +1 -1
  131. package/dist/elements/source.js +1 -1
  132. package/dist/elements/status.js +1 -1
  133. package/dist/elements/switch.js +1 -1
  134. package/dist/elements/tabs.js +1 -1
  135. package/dist/elements/tasks.js +1 -1
  136. package/dist/elements/text-shimmer.js +1 -1
  137. package/dist/elements/thinking-bar.js +1 -1
  138. package/dist/elements/thread.js +1 -1
  139. package/dist/elements/toast.js +1 -1
  140. package/dist/elements/tool.js +1 -1
  141. package/dist/elements/tooltip.js +1 -1
  142. package/dist/elements/voice-input.js +1 -1
  143. package/dist/elements/voice-output.js +1 -1
  144. package/dist/{ellipsis-CbOpJnU3.js → ellipsis-6pJ7SxYT.js} +1 -1
  145. package/dist/{embed-CrfjkbUG.js → embed-BMje-nxj.js} +1 -1
  146. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  147. package/dist/{external-link-ByXAwBBQ.js → external-link-Beh9DKeR.js} +1 -1
  148. package/dist/{file-text-DMib2qwt.js → file-text-D1GPfZ8Q.js} +1 -1
  149. package/dist/{file-tree-CJqM2aP7.js → file-tree-Dwj2lqg-.js} +1 -1
  150. package/dist/{folder-_BAb929C.js → folder-b6D8x1DY.js} +1 -1
  151. package/dist/{form-DSZgZQZk.js → form-DWgdxDXu.js} +1 -1
  152. package/dist/{hover-card-BQbul9eq.js → hover-card-CWRkqm40.js} +1 -1
  153. package/dist/{icon-CdcfRhYI.js → icon-CL0jdhsf.js} +1 -1
  154. package/dist/index.js +1741 -1725
  155. package/dist/index.server.js +1582 -1566
  156. package/dist/{info-DsFjGuI_.js → info-0cOUherQ.js} +1 -1
  157. package/dist/{input-DKbmkMos.js → input-BH6Ru91Q.js} +1 -1
  158. package/dist/kai.es.js +1 -1
  159. package/dist/{kbd-PvdGhX4s.js → kbd-DRAnVtNX.js} +1 -1
  160. package/dist/{link-zF8PzWfF.js → link-Dlr0vssB.js} +1 -1
  161. package/dist/{link-preview-DbM1866N.js → link-preview-DODgRyV9.js} +1 -1
  162. package/dist/loader-KvJB6NrT.js +1 -0
  163. package/dist/{markdown-LMvmAyBk.js → markdown-DX9tuJVj.js} +1 -1
  164. package/dist/mcp.es.js +9 -5
  165. package/dist/{message-BYZECyek.js → message-BqQ7Ot5q.js} +1 -1
  166. package/dist/{message-BSsgvHvf.js → message-DkaGfBd8.js} +1 -1
  167. package/dist/message-circle-BPpp5QKv.js +1 -0
  168. package/dist/message-square-BLbsd9BJ.js +1 -0
  169. package/dist/{minimize-2-BQRoBV1V.js → minimize-2-CyOHJwcN.js} +1 -1
  170. package/dist/{model-switcher-CMpCOYcB.js → model-switcher-0hrKDiK2.js} +1 -1
  171. package/dist/{overlay-Cb-MkyDa.js → overlay-DBMFXie6.js} +1 -1
  172. package/dist/{panel-right-i8warFxT.js → panel-right-t8SWJL6e.js} +1 -1
  173. package/dist/{paperclip-ByY3UVO6.js → paperclip-CdzuOjbU.js} +1 -1
  174. package/dist/play-BXy1u7rW.js +1 -0
  175. package/dist/primitives/use-auto-resize.d.ts +11 -0
  176. package/dist/{progress-bar-jVAHvMUf.js → progress-bar-CiBtxMWT.js} +1 -1
  177. package/dist/{prompt-suggestion-CD-qtNgC.js → prompt-suggestion-I_DHP21i.js} +1 -1
  178. package/dist/{radio-BsoBMhkF.js → radio-C_p3n39l.js} +1 -1
  179. package/dist/{reasoning-4kBPtlTx.js → reasoning-BDwy_tIe.js} +1 -1
  180. package/dist/register-impl-DvEnhIxx.js +424 -0
  181. package/dist/{resizable-uPmY9VY-.js → resizable-SYC-fStM.js} +1 -1
  182. package/dist/{rotate-ccw-B2xn93py.js → rotate-ccw-B1nwmwuY.js} +1 -1
  183. package/dist/{rotate-cw-CMVLoCyH.js → rotate-cw-CLAnBcm1.js} +1 -1
  184. package/dist/{scroll-area-BqLUo8ZZ.js → scroll-area-zRyXoLhI.js} +1 -1
  185. package/dist/{scroll-button-DslXiJyL.js → scroll-button-jzrbYCG9.js} +1 -1
  186. package/dist/{select-2cLRtPGT.js → select-DM-0nEe0.js} +1 -1
  187. package/dist/{separator-BWQtNJcM.js → separator-BToOOoOt.js} +1 -1
  188. package/dist/{settings-BBR63jLg.js → settings-PaGo_S4U.js} +1 -1
  189. package/dist/{settings-group-B2ciy-Iw.js → settings-group-BFXGIfXq.js} +1 -1
  190. package/dist/{skeleton-CkHI-HgC.js → skeleton-DYpmFnnm.js} +1 -1
  191. package/dist/{slider-DDR9U-i0.js → slider-ebutAmkQ.js} +1 -1
  192. package/dist/{solid-DruVOXnf.js → solid-15v_3WVL.js} +3563 -3412
  193. package/dist/{solid-BzCFdL25.js → solid-5P-otgdi.js} +4502 -4296
  194. package/dist/solid.d.ts +6 -0
  195. package/dist/solid.js +259 -254
  196. package/dist/solid.server.js +259 -254
  197. package/dist/{source-GEnFF528.js → source-c1gpRl9h.js} +1 -1
  198. package/dist/{star-CilHq8GP.js → star-pcV_3LqF.js} +1 -1
  199. package/dist/{store-Ciyk6eZG.js → store-CSgbFFCg.js} +1 -1
  200. package/dist/{switch-KkeQIbuH.js → switch-Bh2OdLV2.js} +1 -1
  201. package/dist/{tasks-card-BrN4bggJ.js → tasks-card-CbqQAn2S.js} +1 -1
  202. package/dist/text-shimmer-BSzgEnDH.js +1 -0
  203. package/dist/textarea-DrYNt_0z.js +1 -0
  204. package/dist/theme.tokens.css +22 -21
  205. package/dist/{thumbs-up-CsT9Wzb9.js → thumbs-up-BL3-NenK.js} +1 -1
  206. package/dist/{toast-store-tgK6MuMv.js → toast-store-DkOlYczt.js} +1 -1
  207. package/dist/{tool-DzUjIBT8.js → tool-CeIWUVUe.js} +1 -1
  208. package/dist/{tooltip-Bkkg_4t0.js → tooltip-GA-nmg06.js} +1 -1
  209. package/dist/{trash-2-jBKxjGGQ.js → trash-2-CT66QWlm.js} +1 -1
  210. package/dist/{triangle-alert-DlQ68ppf.js → triangle-alert-CeHLWxhD.js} +1 -1
  211. package/dist/ui/color-field.d.ts +48 -0
  212. package/dist/ui/textarea.d.ts +4 -0
  213. package/dist/ui/toggle-chip.d.ts +40 -0
  214. package/dist/{upload-TcAHXv9W.js → upload-9Csb0F0J.js} +1 -1
  215. package/dist/{use-card-resolution-DaZqAdWf.js → use-card-resolution-Zg0a08Z-.js} +1 -1
  216. package/dist/{variant-aurora-DbW_OWL2.js → variant-aurora-4VBQHIKV.js} +1 -1
  217. package/dist/{variant-aurora-CNmfHhcx.js → variant-aurora-DBeCAQNk.js} +2 -2
  218. package/dist/{variant-aurora-D6GsdTPA.js → variant-aurora-FzHVXHby.js} +2 -2
  219. package/dist/{variant-aurora-BhLuYxJR.js → variant-aurora-LE3fqxgw.js} +1 -1
  220. package/dist/{variant-custom-BvYHR62b.js → variant-custom-AFRGcOBS.js} +1 -1
  221. package/dist/{variant-custom-DV2x254s.js → variant-custom-BQkNuPpE.js} +2 -2
  222. package/dist/{variant-custom-BRvaNHSi.js → variant-custom-DI5R0rln.js} +1 -1
  223. package/dist/{variant-custom-DMTrEO5w.js → variant-custom-WWnrrRHS.js} +2 -2
  224. package/dist/{variant-wave-CyBueXj9.js → variant-wave-DUCbH5oI.js} +2 -2
  225. package/dist/{variant-wave-BNMGrDfj.js → variant-wave-DuQV7DhQ.js} +2 -2
  226. package/dist/{variant-wave-BpDd8t3Z.js → variant-wave-nSI2sfII.js} +1 -1
  227. package/dist/{variant-wave-sp-qwsdI.js → variant-wave-wTP5v_jU.js} +1 -1
  228. package/dist/x-eiO9V6J6.js +1 -0
  229. package/{src/elements/styles.css → kit-base.css} +27 -44
  230. package/package.json +32 -17
  231. package/solid.css +33 -0
  232. package/theme.css +39 -23
  233. package/bin/route.test.js +0 -58
  234. package/dist/arrow-down-CjRU3JgV.js +0 -1
  235. package/dist/arrow-left-Bnpb1UEW.js +0 -1
  236. package/dist/artifact-Bwt7OgcR.js +0 -1
  237. package/dist/check-njkVdZu2.js +0 -1
  238. package/dist/chevron-down-B72s6PXR.js +0 -1
  239. package/dist/chevron-right-BatuHzUm.js +0 -1
  240. package/dist/circle-check-B62pODB3.js +0 -1
  241. package/dist/circle-zx0ByzY9.js +0 -1
  242. package/dist/controllable-CnGpMF6r.js +0 -1
  243. package/dist/define-By-uL588.js +0 -1
  244. package/dist/loader-BM4CEz4G.js +0 -1
  245. package/dist/message-circle-DruwObfX.js +0 -1
  246. package/dist/message-square-BG6q6mlm.js +0 -1
  247. package/dist/play-BvKKq3fX.js +0 -1
  248. package/dist/register-impl-B1hLkrI8.js +0 -424
  249. package/dist/text-shimmer-Dcj1MHuq.js +0 -1
  250. package/dist/textarea-Dpvbngbg.js +0 -1
  251. package/dist/x-Z5UntkIJ.js +0 -1
  252. package/src/agent-tooling/README.md +0 -192
  253. package/src/agent-tooling/archetypes.ts +0 -205
  254. package/src/agent-tooling/catalog/README.md +0 -676
  255. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  256. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  257. package/src/agent-tooling/catalog/invariants.ts +0 -284
  258. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  259. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  260. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  261. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  262. package/src/agent-tooling/construct/cli.ts +0 -147
  263. package/src/agent-tooling/construct/codegen.ts +0 -1635
  264. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  265. package/src/agent-tooling/construct/dev.ts +0 -148
  266. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  267. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  268. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  269. package/src/agent-tooling/construct/schema.ts +0 -452
  270. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  271. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  272. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  273. package/src/agent-tooling/integrations/mastra.ts +0 -182
  274. package/src/agent-tooling/integrations/mock.ts +0 -148
  275. package/src/agent-tooling/integrations/ollama.ts +0 -97
  276. package/src/agent-tooling/integrations/openai.ts +0 -95
  277. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  278. package/src/agent-tooling/integrations/pi.ts +0 -176
  279. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  280. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  281. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  282. package/src/agent-tooling/mcp/manifest.ts +0 -422
  283. package/src/agent-tooling/mcp/server.ts +0 -116
  284. package/src/agent-tooling/mcp/stdio.ts +0 -18
  285. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  286. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  287. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  288. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  289. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  290. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  291. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  292. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  293. package/src/agent-tooling/recipes/index.ts +0 -22
  294. package/src/agent-tooling/recipes/types.ts +0 -32
  295. package/src/agent-tooling/registry.ts +0 -132
  296. package/src/agent-tooling/route-emit.ts +0 -305
  297. package/src/agent-tooling/types.ts +0 -390
  298. package/src/components/artifact-card.tsx +0 -120
  299. package/src/components/artifact.tsx +0 -914
  300. package/src/components/attachment-types.ts +0 -66
  301. package/src/components/attachments.tsx +0 -536
  302. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  303. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  304. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  305. package/src/components/audio-visualizer/index.tsx +0 -528
  306. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  307. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  308. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  309. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  310. package/src/components/audio-visualizer/sizes.ts +0 -54
  311. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  312. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  313. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  314. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  315. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  316. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  317. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  318. package/src/components/card-fallback.tsx +0 -60
  319. package/src/components/card-renderer.tsx +0 -220
  320. package/src/components/card.tsx +0 -212
  321. package/src/components/chain-of-thought.tsx +0 -348
  322. package/src/components/chat-container.tsx +0 -89
  323. package/src/components/chat-scope-picker.tsx +0 -57
  324. package/src/components/chat-thread.tsx +0 -1014
  325. package/src/components/checkpoint.tsx +0 -88
  326. package/src/components/choice-card.tsx +0 -743
  327. package/src/components/coachmark.tsx +0 -249
  328. package/src/components/code-block.tsx +0 -181
  329. package/src/components/composer-dom.ts +0 -159
  330. package/src/components/composer-highlight.ts +0 -242
  331. package/src/components/composer-history.ts +0 -82
  332. package/src/components/composer.tsx +0 -1075
  333. package/src/components/confirm-card.tsx +0 -402
  334. package/src/components/context.tsx +0 -377
  335. package/src/components/conversation-item.tsx +0 -241
  336. package/src/components/conversation-list.tsx +0 -429
  337. package/src/components/conversation-panel.tsx +0 -118
  338. package/src/components/dismissed-stub.tsx +0 -80
  339. package/src/components/embed.tsx +0 -200
  340. package/src/components/empty.tsx +0 -177
  341. package/src/components/feedback-bar.tsx +0 -166
  342. package/src/components/file-tree.tsx +0 -530
  343. package/src/components/file-upload.tsx +0 -167
  344. package/src/components/form-widgets.tsx +0 -525
  345. package/src/components/form.tsx +0 -1296
  346. package/src/components/home-panel.tsx +0 -159
  347. package/src/components/image.tsx +0 -65
  348. package/src/components/link-preview.tsx +0 -195
  349. package/src/components/loader.tsx +0 -367
  350. package/src/components/markdown.tsx +0 -162
  351. package/src/components/message-skills.tsx +0 -40
  352. package/src/components/message.tsx +0 -765
  353. package/src/components/model-switcher.tsx +0 -132
  354. package/src/components/prompt-input.tsx +0 -188
  355. package/src/components/prompt-suggestion.tsx +0 -150
  356. package/src/components/reasoning.tsx +0 -293
  357. package/src/components/response-compare-types.ts +0 -152
  358. package/src/components/response-compare.tsx +0 -455
  359. package/src/components/response-stream.tsx +0 -109
  360. package/src/components/screen.tsx +0 -181
  361. package/src/components/scroll-button.tsx +0 -93
  362. package/src/components/source.tsx +0 -192
  363. package/src/components/tasks-card.tsx +0 -801
  364. package/src/components/text-shimmer.tsx +0 -37
  365. package/src/components/thinking-bar.tsx +0 -50
  366. package/src/components/thread.tsx +0 -228
  367. package/src/components/toast.tsx +0 -503
  368. package/src/components/tool-classify.ts +0 -26
  369. package/src/components/tool-types.ts +0 -49
  370. package/src/components/tool.tsx +0 -190
  371. package/src/components/use-card-resolution.ts +0 -44
  372. package/src/components/voice-input.tsx +0 -222
  373. package/src/components/voice-output.tsx +0 -242
  374. package/src/components/widget-tab-bar.tsx +0 -73
  375. package/src/components/workspace-shell.tsx +0 -285
  376. package/src/diagnostics/hook.ts +0 -352
  377. package/src/diagnostics/index.ts +0 -82
  378. package/src/diagnostics/report-request.ts +0 -296
  379. package/src/elements/agent-card.tsx +0 -67
  380. package/src/elements/artifact.tsx +0 -194
  381. package/src/elements/attachments.tsx +0 -140
  382. package/src/elements/audio-visualizer.tsx +0 -227
  383. package/src/elements/autoloader.ts +0 -89
  384. package/src/elements/avatar.tsx +0 -33
  385. package/src/elements/badge.tsx +0 -32
  386. package/src/elements/button.tsx +0 -149
  387. package/src/elements/card.tsx +0 -133
  388. package/src/elements/cards.tsx +0 -266
  389. package/src/elements/chain-of-thought.tsx +0 -141
  390. package/src/elements/chat-scope-picker.tsx +0 -65
  391. package/src/elements/chat-types.ts +0 -102
  392. package/src/elements/chat-workspace.tsx +0 -188
  393. package/src/elements/chat.tsx +0 -304
  394. package/src/elements/checkbox-group.tsx +0 -164
  395. package/src/elements/checkbox.tsx +0 -134
  396. package/src/elements/checkpoint.tsx +0 -47
  397. package/src/elements/choice.tsx +0 -100
  398. package/src/elements/coachmark.tsx +0 -90
  399. package/src/elements/code-block.tsx +0 -60
  400. package/src/elements/command.tsx +0 -197
  401. package/src/elements/compare.tsx +0 -111
  402. package/src/elements/compiled.css +0 -2
  403. package/src/elements/composer.tsx +0 -162
  404. package/src/elements/confirm-card.tsx +0 -76
  405. package/src/elements/context-meter.tsx +0 -107
  406. package/src/elements/conversation-item.tsx +0 -120
  407. package/src/elements/conversation-list.tsx +0 -203
  408. package/src/elements/css.ts +0 -5
  409. package/src/elements/default-input.tsx +0 -392
  410. package/src/elements/define-entry.ts +0 -12
  411. package/src/elements/define.tsx +0 -563
  412. package/src/elements/diagnostic-events.ts +0 -114
  413. package/src/elements/dialog.tsx +0 -145
  414. package/src/elements/disclosure.ts +0 -95
  415. package/src/elements/dock.tsx +0 -215
  416. package/src/elements/dropdown.tsx +0 -123
  417. package/src/elements/editable-label.tsx +0 -140
  418. package/src/elements/element-data-types.ts +0 -152
  419. package/src/elements/element-diagnostics.ts +0 -392
  420. package/src/elements/element-manifest.json +0 -354
  421. package/src/elements/element-nonscalar.json +0 -187
  422. package/src/elements/element-types.d.ts +0 -4353
  423. package/src/elements/embed.tsx +0 -35
  424. package/src/elements/empty.tsx +0 -29
  425. package/src/elements/feedback-bar.tsx +0 -66
  426. package/src/elements/file-tree.tsx +0 -62
  427. package/src/elements/file-upload.tsx +0 -44
  428. package/src/elements/form.tsx +0 -94
  429. package/src/elements/hover-card.tsx +0 -80
  430. package/src/elements/icon.tsx +0 -41
  431. package/src/elements/image.tsx +0 -32
  432. package/src/elements/input.tsx +0 -492
  433. package/src/elements/kbd.tsx +0 -45
  434. package/src/elements/link-preview.tsx +0 -34
  435. package/src/elements/loader.tsx +0 -25
  436. package/src/elements/markdown.tsx +0 -38
  437. package/src/elements/menu.tsx +0 -241
  438. package/src/elements/message-skills.tsx +0 -83
  439. package/src/elements/message.tsx +0 -361
  440. package/src/elements/model-switcher.tsx +0 -121
  441. package/src/elements/nav.tsx +0 -87
  442. package/src/elements/notice.tsx +0 -53
  443. package/src/elements/pane-grid.tsx +0 -111
  444. package/src/elements/pane-group.tsx +0 -119
  445. package/src/elements/pane.tsx +0 -129
  446. package/src/elements/popover.tsx +0 -81
  447. package/src/elements/progress-bar.tsx +0 -39
  448. package/src/elements/prompt-dock.tsx +0 -82
  449. package/src/elements/prompt-input.tsx +0 -240
  450. package/src/elements/prompt-suggestions.tsx +0 -113
  451. package/src/elements/radio-group.tsx +0 -129
  452. package/src/elements/reasoning.tsx +0 -71
  453. package/src/elements/register-impl.ts +0 -148
  454. package/src/elements/register.ts +0 -76
  455. package/src/elements/remote.tsx +0 -225
  456. package/src/elements/resizable.globals.d.ts +0 -36
  457. package/src/elements/resizable.tsx +0 -704
  458. package/src/elements/response-stream.tsx +0 -40
  459. package/src/elements/screen.tsx +0 -90
  460. package/src/elements/scroll-area.tsx +0 -33
  461. package/src/elements/scroll-button.tsx +0 -179
  462. package/src/elements/search.tsx +0 -185
  463. package/src/elements/segmented.tsx +0 -129
  464. package/src/elements/select.tsx +0 -165
  465. package/src/elements/separator.tsx +0 -36
  466. package/src/elements/setting-item.tsx +0 -50
  467. package/src/elements/settings-group.tsx +0 -37
  468. package/src/elements/skeleton.tsx +0 -45
  469. package/src/elements/slider.tsx +0 -171
  470. package/src/elements/slot-text.ts +0 -73
  471. package/src/elements/slots.ts +0 -787
  472. package/src/elements/source.tsx +0 -126
  473. package/src/elements/status.tsx +0 -42
  474. package/src/elements/switch.tsx +0 -118
  475. package/src/elements/tabs.tsx +0 -95
  476. package/src/elements/tasks.tsx +0 -103
  477. package/src/elements/text-shimmer.tsx +0 -28
  478. package/src/elements/thinking-bar.tsx +0 -43
  479. package/src/elements/thread.tsx +0 -139
  480. package/src/elements/toast.tsx +0 -106
  481. package/src/elements/tool.tsx +0 -78
  482. package/src/elements/tooltip.tsx +0 -85
  483. package/src/elements/validate-messages.ts +0 -76
  484. package/src/elements/voice-input.tsx +0 -100
  485. package/src/elements/voice-output.tsx +0 -82
  486. package/src/index.ts +0 -430
  487. package/src/primitives/audio-bands.ts +0 -174
  488. package/src/primitives/card-component-types.ts +0 -63
  489. package/src/primitives/card-contract.ts +0 -80
  490. package/src/primitives/card-data-types.ts +0 -361
  491. package/src/primitives/card-host.tsx +0 -37
  492. package/src/primitives/card-recovery.ts +0 -160
  493. package/src/primitives/card-registry.tsx +0 -78
  494. package/src/primitives/card-resolution.ts +0 -36
  495. package/src/primitives/card-routing.ts +0 -85
  496. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  497. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  498. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  499. package/src/primitives/card-schemas/choice.schema.json +0 -75
  500. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  501. package/src/primitives/card-schemas/embed.schema.json +0 -63
  502. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  503. package/src/primitives/card-schemas/form.schema.json +0 -53
  504. package/src/primitives/card-schemas/link.schema.json +0 -54
  505. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  506. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  507. package/src/primitives/card-tags.ts +0 -61
  508. package/src/primitives/card-validate-cards.ts +0 -255
  509. package/src/primitives/card-validate-schemas.ts +0 -58
  510. package/src/primitives/card-validate.ts +0 -131
  511. package/src/primitives/chat-config.tsx +0 -103
  512. package/src/primitives/composer-model.ts +0 -49
  513. package/src/primitives/composer-triggers.ts +0 -35
  514. package/src/primitives/controllable.ts +0 -21
  515. package/src/primitives/conversation-store.ts +0 -248
  516. package/src/primitives/create-kai-chat.ts +0 -66
  517. package/src/primitives/create-tween.ts +0 -266
  518. package/src/primitives/embed-providers.ts +0 -254
  519. package/src/primitives/field-mask.ts +0 -256
  520. package/src/primitives/field-semantics.ts +0 -115
  521. package/src/primitives/highlighter.ts +0 -157
  522. package/src/primitives/input-mask.ts +0 -853
  523. package/src/primitives/link-preview.ts +0 -87
  524. package/src/primitives/message-feedback.ts +0 -124
  525. package/src/primitives/pdf-preview.ts +0 -121
  526. package/src/primitives/toast-store.ts +0 -338
  527. package/src/primitives/url-scheme-policy.ts +0 -70
  528. package/src/primitives/use-audio-analysis.ts +0 -340
  529. package/src/primitives/use-auto-resize.ts +0 -31
  530. package/src/primitives/use-resize-observer.ts +0 -45
  531. package/src/primitives/use-sequencer.ts +0 -72
  532. package/src/primitives/use-speech-recognition.ts +0 -146
  533. package/src/primitives/use-stick-to-bottom.ts +0 -75
  534. package/src/primitives/use-text-stream.ts +0 -143
  535. package/src/primitives/use-voice-recorder.ts +0 -62
  536. package/src/primitives/visualizer-sequences.ts +0 -350
  537. package/src/remote/host-embed.ts +0 -345
  538. package/src/remote/index.ts +0 -2
  539. package/src/remote/origin.ts +0 -30
  540. package/src/remote/provider-runtime.ts +0 -262
  541. package/src/remote/provider.ts +0 -2
  542. package/src/remote/validate.ts +0 -22
  543. package/src/remote/version.ts +0 -12
  544. package/src/remote/wire.ts +0 -48
  545. package/src/schemas/from-tool-call.ts +0 -151
  546. package/src/schemas/index.ts +0 -341
  547. package/src/schemas/provider-subsets.ts +0 -538
  548. package/src/schemas/registry.ts +0 -355
  549. package/src/schemas/tool-defs.ts +0 -793
  550. package/src/solid.ts +0 -173
  551. package/src/state/index.ts +0 -44
  552. package/src/state/messages.ts +0 -53
  553. package/src/state/mock.ts +0 -297
  554. package/src/state/parts.ts +0 -295
  555. package/src/state/persistence.ts +0 -180
  556. package/src/state/stream.ts +0 -246
  557. package/src/state/suggestions.ts +0 -9
  558. package/src/state/threads.ts +0 -142
  559. package/src/test-utils/fake-clock.ts +0 -88
  560. package/src/types.ts +0 -95
  561. package/src/ui/action-icons.ts +0 -53
  562. package/src/ui/agent-card.tsx +0 -218
  563. package/src/ui/avatar.tsx +0 -23
  564. package/src/ui/badge.tsx +0 -21
  565. package/src/ui/button.tsx +0 -49
  566. package/src/ui/card.tsx +0 -245
  567. package/src/ui/checkbox-group.tsx +0 -153
  568. package/src/ui/checkbox.tsx +0 -63
  569. package/src/ui/collapsible.tsx +0 -163
  570. package/src/ui/command.tsx +0 -103
  571. package/src/ui/dialog.tsx +0 -223
  572. package/src/ui/dock.tsx +0 -630
  573. package/src/ui/dropdown.tsx +0 -636
  574. package/src/ui/editable-label.tsx +0 -112
  575. package/src/ui/hover-card.tsx +0 -345
  576. package/src/ui/icon.tsx +0 -167
  577. package/src/ui/input.tsx +0 -391
  578. package/src/ui/kbd.tsx +0 -118
  579. package/src/ui/nav.tsx +0 -282
  580. package/src/ui/notice.tsx +0 -84
  581. package/src/ui/overlay.tsx +0 -277
  582. package/src/ui/pane-grid.tsx +0 -117
  583. package/src/ui/pane-group.tsx +0 -291
  584. package/src/ui/pane.tsx +0 -193
  585. package/src/ui/popover.tsx +0 -106
  586. package/src/ui/progress-bar.tsx +0 -83
  587. package/src/ui/prompt-dock.tsx +0 -145
  588. package/src/ui/radio.tsx +0 -150
  589. package/src/ui/resizable.tsx +0 -693
  590. package/src/ui/scroll-area.tsx +0 -26
  591. package/src/ui/segmented.tsx +0 -109
  592. package/src/ui/select.tsx +0 -168
  593. package/src/ui/separator.tsx +0 -10
  594. package/src/ui/settings-group.tsx +0 -67
  595. package/src/ui/skeleton.tsx +0 -73
  596. package/src/ui/slider.tsx +0 -178
  597. package/src/ui/status.tsx +0 -48
  598. package/src/ui/switch.tsx +0 -119
  599. package/src/ui/tabs.tsx +0 -168
  600. package/src/ui/textarea.tsx +0 -21
  601. package/src/ui/tooltip.tsx +0 -118
  602. package/src/utils/cn.ts +0 -30
  603. package/src/wire/chunk.ts +0 -248
  604. package/src/wire/consume.ts +0 -599
  605. package/src/wire/diagnostics.ts +0 -727
  606. package/src/wire/encode-probe.ts +0 -214
  607. package/src/wire/encode.ts +0 -947
  608. package/src/wire/files.ts +0 -342
  609. package/src/wire/formats/anthropic.ts +0 -274
  610. package/src/wire/formats/openai.ts +0 -228
  611. package/src/wire/index.ts +0 -95
  612. package/src/wire/media-types.ts +0 -344
  613. package/src/wire/read.ts +0 -396
  614. package/src/wire/sink-helpers.ts +0 -50
  615. package/src/wire/sse.ts +0 -153
@@ -1,1014 +0,0 @@
1
- import { createSignal, createEffect, createComputed, createMemo, For, Show, Switch, Match, onMount, untrack } from 'solid-js';
2
- import { ChatConfig, useChatConfig } from '../primitives/chat-config';
3
- import { type ComposerDoc, normalizeValue, serializeToText } from '../primitives/composer-model';
4
- import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container';
5
- import { Message, MessageAvatar, MessageBody } from './message';
6
- import { type AttachmentData } from './attachments';
7
- import { createMessageFeedback, type MessageActionDetail } from '../primitives/message-feedback';
8
- import { ModelSwitcher } from './model-switcher';
9
- import { ScrollButton } from './scroll-button';
10
- import {
11
- Context, ContextTrigger, ContextContent, ContextContentHeader,
12
- ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage,
13
- } from './context';
14
- import { DefaultPromptInput, type RejectedAttachment } from '../elements/default-input';
15
- import type { MediaTypeFilter } from '../wire/media-types';
16
- import type { TriggerDef } from './composer';
17
- import type { ChatMessage } from '../elements/chat-types';
18
- import type { ProseSize } from '../primitives/chat-config';
19
- import type { ModelOption } from '../types';
20
- import type { CardComponentMap } from '../primitives/card-registry';
21
- import type { CardSchemaMap } from './card-renderer';
22
- import type { JSX } from 'solid-js';
23
- import type { ConversationStore } from '../primitives/conversation-store';
24
- import { byRecency } from '../primitives/conversation-store';
25
- import { ConversationPanel } from './conversation-panel';
26
- import { isConversationUnread } from './conversation-item';
27
- import type { ConversationSummary } from '../types';
28
- import { MessagesSquare, ArrowLeft } from 'lucide-solid';
29
- import { Button } from '../ui/button';
30
- import { HomePanel } from './home-panel';
31
- import { WidgetTabBar } from './widget-tab-bar';
32
- import type { HomeConfig, HomeLinkEntry } from '../types';
33
-
34
- export interface ChatThreadContextUsage {
35
- usedTokens: number;
36
- maxTokens: number;
37
- inputTokens?: number;
38
- outputTokens?: number;
39
- estimatedCost?: number;
40
- }
41
-
42
- export interface ChatThreadProps {
43
- /** Extra classes for the thread root (e.g. `h-full`). */
44
- class?: string;
45
- /** The full message thread to render, newest last. Each entry carries its role,
46
- * ordered `parts`, and optional actions/avatar/feedback. Set as a JS property
47
- * (`el.messages = [...]`). */
48
- messages: ChatMessage[];
49
- /** Add/override card type -> component entries, forwarded to `CardRenderer`
50
- * for `card` parts. */
51
- cardTypes?: CardComponentMap;
52
- /** JSON Schemas for the card types this app renders, keyed by envelope type,
53
- * forwarded to `CardRenderer` for `card` parts. The companion of `cardTypes`:
54
- * that says what DRAWS a card, this says what a VALID one looks like.
55
- * `createCardRegistry(...).validationSchemas` is exactly this shape. Without it
56
- * the kit checks its own seven built-ins and leaves your own card type
57
- * unvalidated. A schema here WINS over a built-in of the same name. */
58
- cardSchemas?: CardSchemaMap;
59
- /** The custom-element host node to emit card events off when no `CardProvider`
60
- * is present, forwarded through `MessageBody` to `CardRenderer`. The
61
- * element facades pass their own host element so card events leave as the
62
- * bubbling `kai-card` CustomEvent. */
63
- cardHostElement?: HTMLElement;
64
- /** Value of the input. A **string** is controlled (the host owns the text and
65
- * updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that
66
- * pre-populates pills; the user then edits freely. Leave unset for uncontrolled. */
67
- value?: string | ComposerDoc;
68
- /** Placeholder text shown in the empty input. */
69
- placeholder?: string;
70
- /** When true, shows the loading/streaming state and disables submit (use while
71
- * awaiting the assistant's reply). */
72
- loading?: boolean;
73
- /** Starter prompts shown above the input when the thread is empty. Clicking one
74
- * follows `suggestionMode`. Set as a JS property. */
75
- suggestions?: string[];
76
- /** What clicking a suggestion does: `'submit'` (default) sends it immediately
77
- * as if typed and submitted; `'fill'` just places it in the input. */
78
- suggestionMode?: 'submit' | 'fill';
79
- /** Keep suggestions visible after the conversation starts. By default
80
- * suggestions are conversation starters and hide once `messages` is
81
- * non-empty; set this to keep them always shown. Default false. */
82
- persistSuggestions?: boolean;
83
- /** Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`).
84
- * Defaults to `'sm'`. */
85
- proseSize?: ProseSize;
86
- /** Shiki theme name for syntax-highlighted code blocks (e.g.
87
- * `'github-dark-dimmed'`). */
88
- codeTheme?: string;
89
- /** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
90
- * `<pre>` blocks (lighter, no highlighter load). Default true. */
91
- codeHighlight?: boolean;
92
- /** How `reasoning` parts render across the thread. `'full'` (default) is the
93
- * current collapsible-disclosure behavior; `'compact'` shows only a shimmer
94
- * loader while a reasoning part streams and nothing once it settles (no
95
- * expandable detail); `'off'` renders reasoning parts not at all. Forwarded
96
- * to every `MessageBody` as `reasoningMode`. */
97
- reasoning?: 'full' | 'compact' | 'off';
98
- /** Seeds the reasoning disclosure open AND keeps it tracking the stream
99
- * (open while streaming, closes when it settles): the pre-Task-19f `full`
100
- * behavior. Default false/absent: the panel starts closed (just the
101
- * "Thinking" shimmer chip) and only opens on click, the current default
102
- * (owner ruling, 2026-08-26). Meaningless when `reasoning` is `'compact'`
103
- * or `'off'`. Forwarded to every `MessageBody` as `reasoningDefaultOpen`. */
104
- reasoningOpen?: boolean;
105
- /** Optional header title shown on the left of the header. */
106
- chatTitle?: string;
107
- /** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
108
- * header and a `kai-model-change` event fires on selection. */
109
- models?: ModelOption[];
110
- /** The currently selected model id (pairs with `models`). */
111
- currentModel?: string;
112
- /** Optional context-window token usage. When set, a Context token meter is
113
- * shown in the header. */
114
- context?: ChatThreadContextUsage;
115
- /** Show the scroll-to-bottom button inside the scroll area. Default true. */
116
- scrollButton?: boolean;
117
- /** Whether the host has `slot="header-start"` content (left of the title). Set
118
- * by the `<kai-chat>` facade so a custom control forces the header open. */
119
- headerStart?: boolean;
120
- /** Whether the host has `slot="header-end"` content (right of the controls). */
121
- headerEnd?: boolean;
122
- /** Extra content rendered in the header-end region, AFTER `slot="header-end"`.
123
- * This is a JSX escape hatch for a caller composing `ChatThread` directly as a
124
- * Solid component (no shadow-DOM host, so there is no light-DOM node to slot) —
125
- * the `kai-dock`-docked construct widget is the motivating case: it needs its
126
- * own close affordance to sit IN the header row, sharing it with the title
127
- * instead of floating as a second control with no visible relationship to the
128
- * chat surface. Renders alongside the named slot rather than replacing it, so a
129
- * real `slot="header-end"` consumer and this prop can both be present. Counts
130
- * toward `showHeader()` the same as `headerEnd`, so a construct with no title
131
- * and only this content still gets a header row to sit in. */
132
- headerEndContent?: JSX.Element;
133
- /** Turns on the prior-conversations list: a list-toggle button appears in
134
- * the header row and the panel gains a second, list, view (C-1 — one
135
- * panel, two states, never a persistent sidebar). Off by default, same
136
- * convention as every other capability in this file. Requires BOTH
137
- * `store` AND `onConversationLoad` — the second is the only path a caller
138
- * has to actually receive a loaded conversation's messages back (this
139
- * component never mutates `props.messages` itself). Set with either
140
- * missing, the feature decides loudly (one `console.error` on mount) and
141
- * stays visually off rather than throwing or silently going inert: a
142
- * `store` with no `onConversationLoad` would otherwise make row-select/
143
- * new/restore fire and do nothing visible, and mount's own auto-restore
144
- * would still stamp an active conversation id that the save effect could
145
- * then clobber with whatever `props.messages` the caller drives in next. */
146
- conversations?: boolean;
147
- /** The adapter this thread persists through when `conversations` is on:
148
- * `list()` on mount and on every list-view open, `load(id)` on row select,
149
- * `save(id, messages)` on every message-array change for the active
150
- * conversation. A kit-owned INTERFACE (C-3) — the dev owns invocation,
151
- * transport, auth and retention entirely; `localStorageStore`/`fetchStore`
152
- * (`@kitn.ai/ui`'s `primitives/conversation-store`) are the shipped
153
- * built-ins. Set as a JS property; never expressible as an attribute (an
154
- * adapter is a live object of functions, not scalar data). */
155
- store?: ConversationStore;
156
- /** Fires whenever `load(id)` resolves and this thread's `messages` are
157
- * about to be replaced with that conversation's history — the hook a
158
- * caller uses to actually own and re-render `messages` (this component
159
- * does not mutate `props.messages` itself; C-8 keeps the state machine
160
- * here but the message ARRAY stays the caller's own state, matching every
161
- * other prop in this file). The second argument is the conversation's id
162
- * — `undefined` for the "new conversation" case (C-6: no id exists until
163
- * the first message mints one). Required whenever `conversations` is on;
164
- * see that prop's own doc for the guard this component runs without it. */
165
- onConversationLoad?: (messages: ChatMessage[], id?: string) => void;
166
- /** Whether the surrounding chrome that HOSTS this thread is currently
167
- * VISIBLE to the visitor — e.g. a docked widget's open/closed state.
168
- * `ChatThread` has no knowledge of whatever hosts it (a `Dock`, a plain
169
- * page, anything — same boundary `closeConversationsList` documents), so
170
- * this is the seam: a host that can hide itself sets it, everyone else
171
- * leaves it unset.
172
- *
173
- * Meaningful only when `conversations` is on, where it's the third leg of
174
- * "seen" (owner round, 2026-08-26 — unread indicators): the active
175
- * conversation counts as seen, and gets `store.markRead` called for it,
176
- * only while it's ALSO the active conversation AND the chat view (not the
177
- * list) is showing AND this is true. Undeclared/`true` (the default) means
178
- * "always visible" — correct for every layout with no show/hide concept at
179
- * all (fullscreen/aside/split/custom) and for any widget consumer that
180
- * doesn't wire it, which just means unread never distinguishes "closed"
181
- * from "open" for them (a smaller inaccuracy than the alternative: without
182
- * this leg, a message arriving to the active conversation while the
183
- * widget is actually closed would get silently marked read behind the
184
- * visitor's back, purely because it happened to be the active id). */
185
- hostOpen?: boolean;
186
- /** Fires whenever "is any OTHER conversation (not the active one) unread"
187
- * changes — the value this thread already renders as a dot on its own
188
- * header toggle, reported outward so a sibling control with no view into
189
- * `ChatThread`'s internal conversation-summary state (a `Dock`'s own
190
- * `unread` prop, say) can mirror it. Only meaningful with `conversations`
191
- * on; never fires otherwise. */
192
- onUnreadChange?: (unread: boolean) => void;
193
- /** Turns on the widget home screen (Intercom-pattern, H-1/H-2): the panel
194
- * boots into a `home` view — greeting, most-recent-conversation card, a
195
- * "new conversation" CTA, and host-defined links — with a Home/Messages
196
- * tab bar beneath the content area. The prior-conversations list moves
197
- * from the header toggle onto the Messages tab (H-2); a drilled-into chat
198
- * (from a list row, the recent card, or "new conversation") hides the tab
199
- * bar and shows a back arrow in the header instead. Off by default: unset,
200
- * the widget renders byte-for-byte as it does today. */
201
- home?: HomeConfig;
202
- /** Fires when a `home.links` entry with no `href` is activated (an
203
- * `href`-bearing entry navigates as a real anchor instead — see
204
- * `HomePanelProps.onLink`). Meaningful only when `home` is set. */
205
- onHomeLink?: (entry: HomeLinkEntry) => void;
206
- // ── Composition slots ─────────────────────────────────────────────────────
207
- // Each flag below is set by the `<kai-chat>` facade when matching light-DOM
208
- // `slot="…"` content is projected, and gates one composition slot. Two kinds:
209
- // • INJECT — additive: project YOUR markup into a region (sidebar, footer,
210
- // composer-actions, header-start/-end).
211
- // • REPLACE — substitutive: your markup stands in for a whole region
212
- // (header, empty, composer). A replaced region's projected
213
- // content owns its own data/events — a slotted (light-DOM) node
214
- // can't read this component's reactive state. That boundary is
215
- // the whole reason `messages` stays a data prop, not a slot.
216
- /** REPLACE: full custom header in place of the built-in title/model/context bar. */
217
- headerFull?: boolean;
218
- /** INJECT: left sidebar column (e.g. a conversation list / your own nav). */
219
- sidebar?: boolean;
220
- /** REPLACE: custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). */
221
- empty?: boolean;
222
- /** REPLACE, JSX form: the empty-state content itself, for a caller composing
223
- * `ChatThread` directly as a Solid component rather than through the `<kai-chat>`
224
- * shadow-DOM boundary that `empty`/`slot="empty"` targets. Renders INSIDE this
225
- * component's own tree — so a caller passing the kit's own `<Empty>` composition
226
- * (`components/empty.tsx`) gets it fully styled by the adopted stylesheet, unlike
227
- * `slot="empty"`: that slot only ever receives LIGHT-DOM children of the shadow
228
- * HOST, and light-DOM nodes are outside the shadow root's adopted stylesheets, so
229
- * Tailwind-utility-class content projected there renders bare. Takes priority
230
- * over `empty`/`slot="empty"` when both are set — the two are alternate delivery
231
- * mechanisms for the same region, not additive like `headerEndContent`. */
232
- emptyContent?: JSX.Element;
233
- /** REPLACE: full custom composer in place of the built-in prompt input. The
234
- * projected content wires its own submit (the data-flow boundary). */
235
- composer?: boolean;
236
- /** INJECT: accessory row just above the composer (e.g. extra actions). */
237
- composerActions?: boolean;
238
- /** INJECT: footer row below the composer (disclaimers, token meter, …). */
239
- footer?: boolean;
240
- /** Which attachment media types the user may stage, in HTML `accept` syntax
241
- * (`'image/*,application/pdf'`). Omitted means no filter. Narrowed by what the
242
- * encoders can actually send — the same string, and the same resolver, as
243
- * `toOpenAIMessages(msgs, { accept })`. */
244
- accept?: MediaTypeFilter;
245
- /** Files the composer refused because `accept` excluded them. */
246
- onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
247
- /** When `false`, hides the built-in paperclip attach button. Defaults to
248
- * `true` (undeclared keeps today's behavior: attach visible), matching
249
- * `DefaultPromptInput`'s own default: only an explicit `false` hides it. */
250
- attach?: boolean;
251
- /** Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. */
252
- webSearch?: boolean;
253
- /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
254
- voice?: boolean;
255
- /** Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored
256
- * menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a
257
- * JS property; forwarded to the input. */
258
- triggers?: TriggerDef[];
259
- /** Default icon per entity kind (kind → image src) for pills/menu items. */
260
- kindIcons?: Record<string, string>;
261
- /** Whether each message's action bar is always visible (`'always'`, default)
262
- * or only revealed on hover of that message row (`'hover'`). */
263
- actionsReveal?: 'always' | 'hover';
264
- // callbacks (the facade maps these to dispatch())
265
- onValueChange?: (value: string) => void;
266
- onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void;
267
- onAttachmentsChange?: (attachments: AttachmentData[]) => void;
268
- onSuggestionClick?: (value: string) => void;
269
- onModelChange?: (modelId: string) => void;
270
- onMessageAction?: (detail: MessageActionDetail) => void;
271
- onWebSearch?: () => void;
272
- onVoice?: () => void;
273
- /** Receive the imperative controller once mounted. The kai-chat facade forwards
274
- * these as element methods (focus/clear/send/scrollToBottom). */
275
- controllerRef?: (controller: ChatThreadController) => void;
276
- }
277
-
278
- /** Imperative handle exposed via `controllerRef` — the input half of the chat's
279
- * interaction surface, forwarded onto `<kai-chat>` as instance methods. */
280
- export interface ChatThreadController {
281
- focus(options?: FocusOptions): void;
282
- clear(): void;
283
- send(): void;
284
- scrollToBottom(behavior?: ScrollBehavior): void;
285
- /** Force the widget back to its default landing view — `'home'` when the
286
- * `home` prop is set (H-5), `'chat'` otherwise (a no-op if already there,
287
- * or if neither `home` nor `conversations` is on). ChatThread has no knowledge of
288
- * whatever chrome hosts it — a `Dock`, a plain page, anything — so it
289
- * cannot know when that host closes. The seam is this one imperative
290
- * call: a host that can go from visible to hidden and back (the `kai-dock`
291
- * widget being the motivating case) calls it on every hide, so the NEXT
292
- * open always lands on the chat view rather than wherever the list was
293
- * left (owner: reopening the widget should show the default screen, not
294
- * a stale list view). `Dock`'s own `onOpenChange` already fires on every
295
- * close path — header X, the launcher toggle, and Escape — so a single
296
- * `onOpenChange={(open) => !open && controller.closeConversationsList()}`
297
- * at the call site covers all three with no per-path wiring. */
298
- closeConversationsList(): void;
299
- }
300
-
301
- /**
302
- * THE MESSAGE LIST'S KEY, and the reason it is not the message object. This is
303
- * the canonical note; `thread.tsx` keys its identical list the same way and
304
- * points here.
305
- *
306
- * `<For>` is REFERENCE-keyed, and a streaming assistant message gets a brand
307
- * new object identity on every delta — `createAssistantStream` rebuilds it as
308
- * `{ ...prev[i], parts: next }` because a new reference IS the re-render
309
- * signal. Keyed on the objects, every chunk therefore looks like an entirely
310
- * new list: the whole message row is torn down and rebuilt, and everything the
311
- * user did inside it dies with it. Expanding a tool or reasoning panel
312
- * mid-stream did nothing at all — the disclosure opened and was discarded
313
- * microseconds later by the next token. (`MessageBody` keys its PARTS by
314
- * position for exactly this reason; that fix was dead while its parent kept
315
- * destroying the subtree above it.)
316
- *
317
- * So key on `message.id`, which is stable across the object churn: `<For>` over
318
- * the id array diffs by string value, a delta produces an identical id list,
319
- * and no row moves. The row then reads its message through `messages[i()]` —
320
- * `<For>`'s index accessor, which it keeps current across inserts, removals and
321
- * moves — so the CONTENT keeps updating through accessors while the DOM stays
322
- * put.
323
- *
324
- * WHY NOT `<Index>`, the other way to survive the churn: `<Index>` keys by
325
- * POSITION, so a row's local state (an open panel, a half-filled form card)
326
- * belongs to the slot rather than to the message. Appends and tail truncations
327
- * (regenerate/edit) are fine, but prepending older turns — load-earlier-history,
328
- * which every real chat grows — shifts every row, and each open disclosure
329
- * stays behind with the wrong message. Position IS a valid key inside a
330
- * message, where the folds only ever append or patch a part in place; it is not
331
- * one for the message list, where the host owns the array and splices it.
332
- * Both approaches require the row to read through accessors — that is inherent
333
- * in keeping a row mounted while its object is replaced, not a cost unique to
334
- * either.
335
- *
336
- * Contract: `id` must be unique per message. It already had to be — the
337
- * feedback/copy state above this list is keyed by it.
338
- */
339
- /** How an ASSISTANT row aligns its parts across the column (K-D11).
340
- *
341
- * `stretch`, not `start`. An assistant turn is a `flex flex-col` box, so under
342
- * `items-start` every part is a flex item with a fit-content cross size —
343
- * `min(max-content, column)`. Prose is wider than the column so a text bubble
344
- * looked right, and a generative-UI card was as wide as its widest button: the
345
- * ops-console parameters form measured 285px inside a 768px column while the
346
- * approval card beside it filled all 768. A card in a chat thread filling its
347
- * column is a fact about the medium, and no consumer can reach it — the card
348
- * element is created inside `<kai-chat>`'s shadow root.
349
- *
350
- * Stretch rather than `w-full` on the cards, for two reasons: it is one lever
351
- * instead of one per card surface (the Solid `Card` root AND every `kai-*`
352
- * card host, whose shadow wrapper is `display: contents`), and stretching only
353
- * applies where the cross size is `auto`, so a part that states its own width
354
- * is untouched — `Attachments variant="grid"` stays `w-fit`. `Tool` and
355
- * `Reasoning` already asked for `w-full` explicitly; this is the same
356
- * intention, applied once.
357
- *
358
- * USER rows keep `items-end`: their bubble is `max-w-[85%]` and right aligned,
359
- * and stretching it would break both.
360
- */
361
- const ASSISTANT_ALIGN = 'items-stretch';
362
-
363
- export function ChatThread(props: ChatThreadProps) {
364
- const outer = useChatConfig();
365
- const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
366
- const messageKeys = createMemo(() => props.messages.map((m) => m.id));
367
- // Feedback (copy + vote) state lives ABOVE the per-message <For>, so streaming
368
- // re-renders (a fresh `messages` array ref per chunk) don't wipe it.
369
- // The copy/feedback toasts scope to the chat (this thread's root) so they appear
370
- // in-chat rather than at the page top.
371
- let rootEl: HTMLElement | undefined;
372
- const feedback = createMessageFeedback({
373
- emit: (detail) => props.onMessageAction?.(detail),
374
- target: () => rootEl,
375
- });
376
- const [internal, setInternal] = createSignal<string | ComposerDoc>(props.value ?? '');
377
- const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
378
- // ── Conversations (C-1..C-9) ────────────────────────────────────────────
379
- // ── Home screen (H-1..H-6, Task 3) ──────────────────────────────────────
380
- // `home` set adds a THIRD view (the list moves from the header toggle onto
381
- // the Messages tab — H-2) and boots into it instead of 'chat'. `chatEntry`
382
- // tracks whether the current 'chat' view is a DRILLED chat (entered from
383
- // home/list — hides the tab bar, shows a back arrow) or a ROOT chat (the
384
- // Messages tab with conversations off — tab bar stays, no back arrow).
385
- const homeEnabled = () => props.home != null;
386
- type WidgetView = 'home' | 'chat' | 'list';
387
- const [view, setView] = createSignal<WidgetView>(props.home != null ? 'home' : 'chat');
388
- const [chatEntry, setChatEntry] = createSignal<'home' | 'list' | null>(null);
389
- const tabBarVisible = () => homeEnabled() && (view() !== 'chat' || chatEntry() === null);
390
- const activeTab = (): 'home' | 'messages' => (view() === 'home' ? 'home' : 'messages');
391
- // The initial `view` signal value freezes the home-landing decision at
392
- // MOUNT — but the `kai-` contract has consumers set object props (like
393
- // `home`) as JS properties AFTER the element is appended/upgraded (the
394
- // React wrapper's `useLayoutEffect` runs post-mount by construction), so
395
- // `props.home` is routinely still `undefined` on this component's first
396
- // render. Without this, `<kai-chat>` then `el.home = {...}` renders a
397
- // tab bar (props.home is set by the time JSX reads it reactively) but
398
- // stays on `view() === 'chat'` forever — H-5 violated on a primary
399
- // consumer path. React to the RISING edge instead of relying on the
400
- // initial value: `home` turning on from off lands on 'home', but only
401
- // from the untouched default ('chat' with no drilled entry) — a late
402
- // toggle must never yank a visitor OUT of a chat they're already in
403
- // (root or drilled). `{ defer: true }` skips the run this effect's own
404
- // creation would otherwise trigger.
405
- // Falling edge (symmetric): `home` turning OFF while sitting on the
406
- // 'home' view leaves a default-configured HomePanel with no tab bar to
407
- // navigate away from — reset to 'chat', the only view a home-less widget
408
- // has.
409
- //
410
- // NOT `on(homeEnabled, fn, { defer: true })`: Solid's `on()` skips calling
411
- // `fn` on its first real invocation but does NOT capture the deferred
412
- // read as `prevInput` — the first non-deferred call always sees `prev ===
413
- // undefined` (confirmed against `solid-js/dist/solid.js`'s `on()`, not
414
- // just inferred), so `!isOn && wasOn` can never be true and the falling
415
- // edge silently never fires. Track the previous value by hand instead: a
416
- // closure variable seeded with `homeEnabled()`'s value BEFORE the
417
- // computed exists (so the computed's own first run sees `isOn === wasOn`
418
- // and no-ops, the same "skip the initial run" behavior `defer` was for)
419
- // and updated at the end of every run.
420
- let wasHomeEnabled = untrack(homeEnabled);
421
- createComputed(() => {
422
- const isOn = homeEnabled();
423
- const wasOn = wasHomeEnabled;
424
- if (isOn !== wasOn) {
425
- if (isOn && view() === 'chat' && chatEntry() === null) setView('home');
426
- else if (!isOn && view() === 'home') setView('chat');
427
- }
428
- wasHomeEnabled = isOn;
429
- });
430
- const [conversationSummaries, setConversationSummaries] = createSignal<ConversationSummary[]>([]);
431
- const [activeConversationId, setActiveConversationId] = createSignal<string | undefined>(undefined);
432
- const conversationsReady = () => props.conversations === true && props.store != null && props.onConversationLoad != null;
433
- // Set right before a load/restore hands its messages to the caller via
434
- // `onConversationLoad`, and consumed (read-then-cleared) by the very next
435
- // run of the save effect below. Plain closure variable, not a signal: it
436
- // gates a single effect run rather than driving any render, so it needs no
437
- // reactivity of its own. See the save effect's own comment for why this
438
- // exists (IMPORTANT-1, 2026-08-26 final review).
439
- let suppressNextSave = false;
440
-
441
- onMount(() => {
442
- if (props.conversations && !props.store) {
443
- console.error('ChatThread: `conversations` is true but no `store` was provided — the conversations feature needs a ConversationStore to persist to. Staying in chat-only mode.');
444
- } else if (props.conversations && props.store && !props.onConversationLoad) {
445
- console.error('ChatThread: `conversations` is true but no `onConversationLoad` handler was provided — row-select, "new conversation", and mount auto-restore would have nowhere to deliver the loaded messages, leaving row-tap/new/restore inert (and mount\'s auto-restore would still stamp an active conversation id the save effect could then clobber). Staying in chat-only mode.');
446
- }
447
- });
448
-
449
- const refreshConversations = async () => {
450
- if (!conversationsReady()) return;
451
- try {
452
- // Fresh array reference every refresh (reactivity contract) — the
453
- // adapter already returns a new array per call, so no extra clone is
454
- // needed here; the signal write is what notifies <ConversationPanel>.
455
- setConversationSummaries(await props.store!.list());
456
- } catch (err) {
457
- // Note this can run in the BACKGROUND, not just from `openList()`: the
458
- // save effect below calls this after every save() to keep the badge
459
- // cache fresh. Only bail OUT of the list view here — a transient
460
- // list() blip mid-drilled-chat (or on the root/home views) must stay a
461
- // harmless no-op, not teleport the visitor off whatever they're doing;
462
- // the list view itself has nothing to show without a summaries array,
463
- // so THAT is the one view this failure has to evacuate.
464
- console.warn('ChatThread: conversations list() failed; staying in chat-only mode.', err);
465
- if (view() === 'list') {
466
- setChatEntry(null);
467
- setView(homeEnabled() ? 'home' : 'chat');
468
- }
469
- }
470
- };
471
-
472
- const openList = () => { setView('list'); void refreshConversations(); };
473
- // Messages-tab entry point (H-2): the list moved off the header toggle onto
474
- // this tab, so it routes through 'list' when the store is wired and 'chat'
475
- // (the root chat, ambiguity 1 — no back arrow, `chatEntry` stays null)
476
- // otherwise.
477
- const openMessagesTab = () => {
478
- setChatEntry(null);
479
- if (conversationsReady()) { setView('list'); void refreshConversations(); }
480
- else setView('chat');
481
- };
482
-
483
- const selectConversation = async (id: string) => {
484
- if (!conversationsReady()) return;
485
- try {
486
- const messages = await props.store!.load(id);
487
- setActiveConversationId(id);
488
- setView('chat');
489
- // IMPORTANT-1 (2026-08-26 final review): the caller is about to hand
490
- // this same content straight back in as a new `props.messages`
491
- // reference (the reactivity contract requires a fresh array on every
492
- // change, load included). Without this flag the save effect below
493
- // reads that as "the thread changed," stamps a fresh `updatedAt`, and
494
- // calls `store.save()` — phantom-unreading a fully-read conversation
495
- // on every reload and, for `fetchStore`, firing a full-thread PUT on
496
- // every load for no actual change. Set BEFORE the callback so it's
497
- // armed no matter how synchronously the caller re-renders.
498
- suppressNextSave = true;
499
- // A genuinely new array (never a mutated one) — the reactivity
500
- // contract's array-reference rule, satisfied at the caller boundary.
501
- props.onConversationLoad?.([...messages], id);
502
- } catch (err) {
503
- console.warn(`ChatThread: conversations load(${id}) failed.`, err);
504
- // Defensive: a failed load must not leave a stale drilled-chat
505
- // back-target around from whatever set it before calling this.
506
- setChatEntry(null);
507
- }
508
- };
509
-
510
- const startNewConversation = () => {
511
- // C-6: lazy id — no id, no save, until the first message. Clearing the
512
- // active id and the message list (via onConversationLoad) is enough; the
513
- // id itself is minted the first time the save-on-change effect below
514
- // sees a non-empty thread with no active id. No `suppressNextSave` here:
515
- // an empty thread already short-circuits the save effect on its own
516
- // (`messages.length === 0`), so there is nothing to suppress.
517
- setActiveConversationId(undefined);
518
- setView('chat');
519
- props.onConversationLoad?.([], undefined);
520
- };
521
-
522
- createEffect(() => {
523
- if (!conversationsReady()) return;
524
- const messages = props.messages;
525
- if (messages.length === 0) return; // C-6: nothing persists until the first message
526
- // IMPORTANT-1: skip exactly the one save that is this same effect firing
527
- // on the `props.messages` bounce a load/restore itself caused — see
528
- // `suppressNextSave`'s own comment at `selectConversation`. Consumed
529
- // (cleared) unconditionally so only that one run is skipped; a genuine
530
- // edit right after a load still saves normally.
531
- if (suppressNextSave) { suppressNextSave = false; return; }
532
- // `untrack` the read: this effect also WRITES activeConversationId (minting
533
- // the lazy id below), so a tracked read makes the effect depend on a signal
534
- // it sets itself — the write re-triggers the same run and store.save() fires
535
- // twice with identical args for a conversation's first message. Reading it
536
- // untracked keeps the dependency set to `conversationsReady`/`props.messages`
537
- // only; the mint-then-set below still updates the signal (for the toggle
538
- // button/list UI) without feeding back into this effect's own re-run.
539
- let id = untrack(activeConversationId);
540
- if (!id) {
541
- id = crypto.randomUUID();
542
- setActiveConversationId(id);
543
- }
544
- props.store!.save(id, messages)
545
- // Re-fetch the summary list after every save (not just on mount/list-open):
546
- // `anyUnread`/the badge read `conversationSummaries()`, a CACHE that
547
- // otherwise only refreshes on those two paths, so a message arriving on
548
- // the active conversation while the host is closed (hostOpen === false —
549
- // `seenNow` below is false, so `markRead` correctly does NOT fire) would
550
- // update the persisted record's `updatedAt` but leave the cached summary
551
- // stale, and the badge would never appear until the visitor happened to
552
- // open the list. `refreshConversations` already no-ops safely if this
553
- // component unmounts mid-flight (`conversationsReady` guard) and handles
554
- // its own list() failure (console.warn, degrades to chat-only) — no new
555
- // error path here.
556
- .then(() => refreshConversations())
557
- .catch((err) => {
558
- // Decide loudly (spec's degradation section): the thread stays usable,
559
- // the failure is surfaced, never a silent no-op.
560
- console.error(`ChatThread: conversations save(${id}) failed; the conversation is not persisted for this change.`, err);
561
- });
562
- });
563
-
564
- // "Seen" (owner round, 2026-08-26, revised in the follow-up review): the
565
- // active conversation, AND the chat view (not the list) is showing, AND
566
- // the host is open (`hostOpen`, defaulting true — see that prop's doc).
567
- // ONE memo, shared by both consumers below, so they cannot drift: the
568
- // markRead effect gates on it directly, and `anyUnread` uses it to decide
569
- // whether the active conversation is still exempt from the "unread"
570
- // badge. Before this fix the two were separately-written conditions that
571
- // happened to look alike; `anyUnread` excluded the active id
572
- // UNCONDITIONALLY, so a message arriving on the active conversation while
573
- // the host was CLOSED (hostOpen === false) — the canonical "agent replied
574
- // while your box was shut" case — was dropped from the badge too, even
575
- // though it was never marked read (markRead's own gate correctly skips it
576
- // while closed). The active id is only a legitimate exclusion when it is
577
- // ALSO currently being marked seen.
578
- const seenNow = createMemo(
579
- () => activeConversationId() !== undefined && view() === 'chat' && props.hostOpen !== false,
580
- );
581
-
582
- // Every dependency read here is TRACKED — unlike the save effect above,
583
- // nothing in this effect writes any of them, so there's no feedback loop
584
- // to guard against with `untrack`. Re-running on every `props.messages`
585
- // change is exactly "on new messages arriving while seen holds";
586
- // re-running on `activeConversationId`/`view`/`hostOpen` changes is "on
587
- // select/restore/host-visibility-change" (the first two resolve through
588
- // `selectConversation`, which sets both).
589
- createEffect(() => {
590
- if (!conversationsReady()) return;
591
- const id = activeConversationId();
592
- const seen = seenNow();
593
- void props.messages; // tracked on purpose: see the comment above
594
- if (!seen || id === undefined) return;
595
- props.store!.markRead?.(id).catch((err) => {
596
- // Decide loudly, matching the save effect above — this degrades to
597
- // "unread never clears for this conversation," not a crash.
598
- console.error(`ChatThread: conversations markRead(${id}) failed.`, err);
599
- });
600
- });
601
-
602
- // The header toggle's own dot (rendered below) AND the value reported
603
- // outward via onUnreadChange are the SAME computation — a Dock (or any
604
- // other sibling control with no view into this thread's internal
605
- // conversation-summary state) mirrors it through the callback rather than
606
- // reaching in. Excludes the active conversation by id — but ONLY while
607
- // `seenNow()` holds. When it doesn't (list view open, or the host is
608
- // closed), the active conversation is exactly as unread-eligible as any
609
- // other row: nothing has marked it seen, so a message landing on it must
610
- // still surface here. `undefined` never matches a real conversation id, so
611
- // the exclusion is simply inert (matches nothing) while not seen, rather
612
- // than needing a second branch. Also excludes the active id (not by its
613
- // possibly-stale lastReadAt) so switching `activeConversationId` — e.g.
614
- // selecting a previously-unread row — clears that row's contribution to
615
- // this badge immediately, without waiting on a fresh list() round-trip.
616
- const anyUnread = createMemo(() =>
617
- conversationSummaries().some(
618
- (c) => c.id !== (seenNow() ? activeConversationId() : undefined) && isConversationUnread(c),
619
- ),
620
- );
621
- createEffect(() => props.onUnreadChange?.(anyUnread()));
622
-
623
- // Visitor continuity (C-7's whole point): a plain-history construct
624
- // auto-restored the visitor's thread on mount, so upgrading to
625
- // `conversations` must not regress that — their most recent conversation
626
- // (migrated legacy thread included) has to reappear without an extra tap
627
- // into the list. Guarded three ways: only when `list()` actually returned
628
- // something (a failed list() already degrades to chat-only inside
629
- // `refreshConversations`, and an empty store is a brand-new visitor who
630
- // gets the empty-chat welcome screen); only when nothing is active yet
631
- // (never fights `startNewConversation`/a prior `selectConversation`); and
632
- // only when `props.messages` is still empty (a parent that seeded its own
633
- // thread owns that choice — never clobber it). Reuses `selectConversation`
634
- // so the fresh-array/`onConversationLoad` contract is the same single path
635
- // as an explicit row click, and stays in `view() === 'chat'` throughout
636
- // (this never opens the list). One more guard: `view()` is re-checked
637
- // right before selecting, still 'chat' — mount's own list() and an
638
- // impatient caller's own `openList()`/manual navigation racing it (both
639
- // just microtask chains over the same synchronous localStorage) can
640
- // interleave, and `selectConversation` unconditionally snaps `view` back
641
- // to 'chat'; without this a visitor who opened the list before this
642
- // resolved would get yanked back out of it.
643
- onMount(() => {
644
- if (!conversationsReady()) return;
645
- void (async () => {
646
- await refreshConversations();
647
- if (untrack(activeConversationId) !== undefined) return;
648
- if (props.messages.length !== 0) return;
649
- if (view() !== 'chat') return;
650
- const summaries = conversationSummaries();
651
- if (summaries.length === 0) return;
652
- // Most-recently-updated first. `list()`'s own ordering isn't a
653
- // contract (`localStorageStore` returns insertion order, not recency),
654
- // so sort defensively by `updatedAt` rather than trusting index [0];
655
- // an unparsable/missing date sorts last rather than throwing.
656
- const newest = [...summaries].sort(byRecency)[0];
657
- await selectConversation(newest.id);
658
- })();
659
- });
660
- // A string `value` is controlled; a ComposerDoc `value` is a one-time seed that
661
- // lives in `internal` so the user's (string) edits replace it without a fight.
662
- const current = (): string | ComposerDoc =>
663
- typeof props.value === 'string' ? props.value : internal();
664
- createEffect(() => {
665
- const v = props.value;
666
- if (v != null && typeof v !== 'string') setInternal(v);
667
- });
668
- const handleChange = (v: string) => { setInternal(v); props.onValueChange?.(v); };
669
- // After a send, reset the composer. Clear the internal draft ONLY when the value is
670
- // uncontrolled (props.value === undefined) — a controlled host owns its own value and
671
- // clears it itself. This lets the batteries-included hooks (useKaiChat/createKaiChat),
672
- // whose `bind` does not control `value`, get a clean composer after each submit.
673
- const afterSubmit = () => { setAttachments([]); if (props.value === undefined) setInternal(''); };
674
- const handleSubmit = () => { props.onSubmit?.({ value: serializeToText(normalizeValue(current())), attachments: attachments() }); afterSubmit(); };
675
- const handleSuggestionClick = (v: string) => {
676
- if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
677
- else { props.onSubmit?.({ value: v, attachments: attachments() }); afterSubmit(); }
678
- };
679
- const showHeader = () => !!(
680
- props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd
681
- || props.headerEndContent || conversationsReady()
682
- // The back arrow on a drilled chat needs the header row even with no
683
- // title/models/context/store — a home-only construct (no `conversations`)
684
- // whose "new conversation" card drills into chat still needs somewhere
685
- // to put it.
686
- || (homeEnabled() && view() === 'chat' && chatEntry() !== null)
687
- );
688
- // Recent-conversation card (H-1): only when explicitly opted into
689
- // (`home.recentConversation === true`), summaries are actually hydrated,
690
- // and at least one exists — the newest by the shared recency rule (#335).
691
- const recentSummary = createMemo(() => {
692
- if (!homeEnabled() || props.home?.recentConversation !== true || !conversationsReady()) return undefined;
693
- const summaries = conversationSummaries();
694
- return summaries.length ? [...summaries].sort(byRecency)[0] : undefined;
695
- });
696
- // Suggestions are conversation starters: show only on an empty thread unless
697
- // the host opts into persisting them.
698
- const visibleSuggestions = () =>
699
- props.persistSuggestions || props.messages.length === 0 ? props.suggestions : undefined;
700
- const showScrollButton = () => props.scrollButton !== false;
701
-
702
- // Hand the imperative controller to the facade once mounted (rootEl is set).
703
- onMount(() => {
704
- props.controllerRef?.({
705
- focus: (options) =>
706
- rootEl
707
- ?.querySelector<HTMLElement>('[contenteditable]:not([contenteditable="false"]), textarea')
708
- ?.focus(options),
709
- clear: () => { setInternal(''); setAttachments([]); props.onValueChange?.(''); },
710
- send: () => handleSubmit(),
711
- scrollToBottom: (behavior) => {
712
- const vp = rootEl?.querySelector<HTMLElement>('.overflow-y-auto');
713
- vp?.scrollTo({ top: vp.scrollHeight, behavior: behavior ?? 'smooth' });
714
- },
715
- closeConversationsList: () => { setChatEntry(null); setView(homeEnabled() ? 'home' : 'chat'); },
716
- });
717
- });
718
-
719
- return (
720
- <ChatConfig proseSize={props.proseSize} codeTheme={props.codeTheme} codeHighlight={props.codeHighlight !== false} portalMount={outer.portalMount()}>
721
- {/* The root is a ROW so a `sidebar` slot can sit beside the main column.
722
- With no sidebar projected it collapses to the original column. */}
723
- <div ref={(e) => (rootEl = e as HTMLElement)} class={`flex h-full bg-background ${props.class ?? ''}`}>
724
- <Show when={props.sidebar}>
725
- <aside part="sidebar" class="flex w-64 shrink-0 flex-col overflow-hidden border-r border-border">
726
- <slot name="sidebar" />
727
- </aside>
728
- </Show>
729
- <div class="flex h-full min-w-0 flex-1 flex-col">
730
- {/* Header: a full `header` slot REPLACES the built-in bar; otherwise the
731
- built-in header renders, itself carrying the header-start/header-end
732
- INJECT slots. */}
733
- <Show
734
- when={props.headerFull}
735
- fallback={
736
- <Show when={showHeader()}>
737
- <header part="header-bar" class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
738
- <div class="flex items-center gap-2">
739
- {/* Consumer-injected leading controls (sidebar-toggle, compose, a
740
- popover title-button). Projects light-DOM `slot="header-start"`
741
- children of <kai-chat>; inert outside a shadow root. */}
742
- <slot name="header-start" />
743
- <Show when={props.chatTitle}>
744
- <div class="text-sm font-semibold text-foreground">{props.chatTitle}</div>
745
- </Show>
746
- </div>
747
- <div class="flex items-center gap-2">
748
- <Show when={props.models}>
749
- <ModelSwitcher
750
- models={props.models!}
751
- currentModelId={props.currentModel ?? props.models![0]?.id ?? ''}
752
- onModelChange={(modelId) => props.onModelChange?.(modelId)}
753
- />
754
- </Show>
755
- <Show when={props.context}>
756
- <Context
757
- usedTokens={props.context!.usedTokens} maxTokens={props.context!.maxTokens}
758
- inputTokens={props.context!.inputTokens} outputTokens={props.context!.outputTokens}
759
- estimatedCost={props.context!.estimatedCost}
760
- >
761
- <ContextTrigger />
762
- <ContextContent>
763
- <ContextContentHeader />
764
- <ContextContentBody><div class="space-y-1.5"><ContextInputUsage /><ContextOutputUsage /></div></ContextContentBody>
765
- <ContextContentFooter />
766
- </ContextContent>
767
- </Context>
768
- </Show>
769
- {/* A chat-bubble glyph, not the "menu that does nothing" the retrofit
770
- shipped (owner feedback: it "looks like nothing and isn't even
771
- the same size as the X"). Same `Button variant="ghost"
772
- size="icon-sm"` component AND the same 24px icon size as the
773
- widget's own close X (`codegen.ts`'s `headerEndContent` button,
774
- which renders `DockCloseGlyph` = `<X size={24} />`) — identical
775
- hit-area and optical weight, so the two read as siblings.
776
- Swaps to a back arrow while the list is open, returning to chat. */}
777
- {/* Back arrow for a DRILLED chat (H-5): entered from home, the
778
- recent card, or a list row/new-conversation pill while `home`
779
- is set. Returns to whichever surface it was entered from. */}
780
- <Show when={homeEnabled() && view() === 'chat' && chatEntry() !== null}>
781
- <Button
782
- variant="ghost"
783
- size="icon-sm"
784
- data-kai-home-back
785
- aria-label="Back"
786
- onClick={() => { const target = chatEntry()!; setChatEntry(null); setView(target); }}
787
- >
788
- <ArrowLeft size={24} aria-hidden="true" />
789
- </Button>
790
- </Show>
791
- {/* H-2: with `home` set, the prior-conversations list moved off this
792
- header toggle onto the Messages tab — the toggle no longer renders
793
- at all (H-3). */}
794
- <Show when={props.conversations && props.store && !homeEnabled()}>
795
- <Button
796
- variant="ghost"
797
- size="icon-sm"
798
- class="relative"
799
- data-kai-conversations-toggle
800
- // `aria-label` WINS over any descendant text for a button's
801
- // accessible name, so "unread" has to be folded in here rather
802
- // than in a sr-only sibling span (which AT would never reach).
803
- aria-label={
804
- view() === 'list'
805
- ? 'Back to chat'
806
- : anyUnread() && view() === 'chat'
807
- ? 'Conversations (unread)'
808
- : 'Conversations'
809
- }
810
- onClick={() => (view() === 'list' ? setView('chat') : openList())}
811
- >
812
- <Show when={view() === 'list'} fallback={<MessagesSquare size={24} aria-hidden="true" />}>
813
- <ArrowLeft size={24} aria-hidden="true" />
814
- </Show>
815
- {/* Unread badge (owner round, 2026-08-26): ANY conversation other
816
- than the active one is unread. Only over the chat-bubble glyph
817
- — once the list is open showing the back arrow, the visitor is
818
- already looking at the rows themselves, each carrying its own
819
- dot (ConversationPanel), so a second badge on the arrow would be
820
- redundant chrome. */}
821
- <Show when={anyUnread() && view() === 'chat'}>
822
- <span data-kai-conversations-unread aria-hidden="true" class="absolute right-0.5 top-0.5 size-1.5 rounded-full bg-unread" />
823
- </Show>
824
- </Button>
825
- </Show>
826
- {/* Consumer-injected trailing controls (share, settings, …).
827
- Projects light-DOM `slot="header-end"` children of <kai-chat>. */}
828
- <slot name="header-end" />
829
- {/* JSX escape hatch for a Solid-composed caller with no shadow-DOM
830
- host to slot into (see the prop doc) — a docked widget's own
831
- close affordance is the motivating case, sharing this row with
832
- the title instead of floating as an unrelated second control. */}
833
- {props.headerEndContent}
834
- </div>
835
- </header>
836
- </Show>
837
- }
838
- >
839
- <header part="header" class="shrink-0"><slot name="header" /></header>
840
- </Show>
841
- <div class="relative flex-1 overflow-hidden">
842
- <Switch
843
- fallback={
844
- <ChatContainer class="h-full px-4 py-3">
845
- <ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
846
- {/* REPLACE — custom empty-state content, shown only while the thread is
847
- empty. The component still owns WHEN it shows (data state); the
848
- consumer owns WHAT it looks like. `emptyContent` (JSX, rendered
849
- in-tree and fully styled) wins over `empty`/`slot="empty"`
850
- (light-DOM projection) when both are set — see the prop doc. */}
851
- <Show when={(props.empty || props.emptyContent) && props.messages.length === 0}>
852
- <Show when={props.emptyContent} fallback={<slot name="empty" />}>
853
- {props.emptyContent}
854
- </Show>
855
- </Show>
856
- {/* Keyed by message id (see the note above this component), so a
857
- streaming delta updates the row instead of replacing it. */}
858
- <For each={messageKeys()}>
859
- {(_id, i) => (
860
- // The row reads its message through <For>'s index accessor,
861
- // never through a captured value: the row outlives the delta
862
- // that replaced its object, so every read below has to go
863
- // through `m()` for the new content to land. <Show> supplies
864
- // the non-null accessor and covers the frame where a removal
865
- // has shortened the array.
866
- <Show when={props.messages[i()]}>
867
- {(m) => {
868
- const body = (
869
- <MessageBody
870
- parts={m().parts}
871
- /* F-21: streaming-ness = the thread's ONE existing
872
- loading signal + being the last message and an
873
- assistant turn. No second streaming source. The
874
- reasoning disclosure no longer auto-opens on it
875
- by default (Task 19f, owner ruling 2026-08-26) —
876
- only the trigger's shimmer reflects streaming
877
- unless `reasoningOpen` opts back in. */
878
- isStreaming={props.loading === true && m().role === 'assistant' && i() === props.messages.length - 1}
879
- reasoningMode={props.reasoning}
880
- reasoningDefaultOpen={props.reasoningOpen}
881
- cardTypes={props.cardTypes}
882
- cardSchemas={props.cardSchemas}
883
- cardHostElement={props.cardHostElement}
884
- isUser={m().role === 'user'}
885
- markdown={m().role === 'assistant'}
886
- actions={m().actions}
887
- actionsReveal={reveal()}
888
- activeFeedback={feedback.resolveFeedback(m())}
889
- copied={feedback.isCopied(m().id)}
890
- onAction={(action) => feedback.handleAction(m(), action)}
891
- />
892
- );
893
- const rowGroup = () => (reveal() === 'hover' ? 'group ' : '');
894
- return (
895
- // `role` is the SPEAKER, forwarded on BOTH branches —
896
- // see the same note in thread.tsx. `Message` turns it
897
- // into `role="article"` + an `aria-label`; without it the
898
- // row is a bare div chromium prunes from the
899
- // accessibility tree as "uninteresting".
900
- <Show
901
- when={m().avatar}
902
- fallback={
903
- <Message role={m().role} class={`${rowGroup()}${m().role === 'user' ? 'flex-col items-end' : `flex-col ${ASSISTANT_ALIGN}`}`}>
904
- {body}
905
- </Message>
906
- }
907
- >
908
- {(av) => (
909
- <Message role={m().role} class={rowGroup()}>
910
- <MessageAvatar src={av().src ?? ''} alt={av().alt ?? ''} fallback={av().fallback} />
911
- <div class={`flex min-w-0 flex-1 flex-col ${m().role === 'user' ? 'items-end' : ASSISTANT_ALIGN}`}>
912
- {body}
913
- </div>
914
- </Message>
915
- )}
916
- </Show>
917
- );
918
- }}
919
- </Show>
920
- )}
921
- </For>
922
- <ChatContainerScrollAnchor />
923
- </ChatContainerContent>
924
- <Show when={showScrollButton()}>
925
- <div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
926
- {/* The button now owns its elevation (kai-elevation); a `shadow-sm`
927
- here would set box-shadow a second time and the winner would
928
- be stylesheet order, not this call site. */}
929
- <ScrollButton />
930
- </div>
931
- </Show>
932
- </ChatContainer>
933
- }
934
- >
935
- <Match when={view() === 'home'}>
936
- <HomePanel
937
- greeting={props.home?.greeting}
938
- recent={recentSummary()}
939
- newChatLabel={props.home?.newConversation?.label}
940
- links={props.home?.links}
941
- onSelectRecent={(id) => { setChatEntry('home'); void selectConversation(id); }}
942
- onNewChat={() => { setChatEntry('home'); startNewConversation(); }}
943
- onLink={(entry) => props.onHomeLink?.(entry)}
944
- />
945
- </Match>
946
- <Match when={view() === 'list' && conversationsReady()}>
947
- <ConversationPanel
948
- conversations={conversationSummaries()}
949
- activeId={activeConversationId()}
950
- onSelect={(id) => { setChatEntry(homeEnabled() ? 'list' : null); void selectConversation(id); }}
951
- onNewChat={() => { setChatEntry(homeEnabled() ? 'list' : null); startNewConversation(); }}
952
- />
953
- </Match>
954
- </Switch>
955
- </div>
956
- {/* The list view TAKES OVER the full content area (owner: "if we are
957
- looking at the conversations, i don't think i would see the
958
- suggestions nor the prompt input... the convo list would be taking
959
- over the full content area"). The retrofit only swapped the thread;
960
- this hides the composer-actions row, the composer itself and the
961
- footer too — nothing below the header renders except the panel. */}
962
- <Show when={view() === 'chat'}>
963
- {/* INJECT — accessory row above the composer (extra actions/toolbar). */}
964
- <Show when={props.composerActions}>
965
- <div class="shrink-0 px-4">
966
- <div class="mx-auto flex max-w-3xl items-center gap-2 pb-2"><slot name="composer-actions" /></div>
967
- </div>
968
- </Show>
969
- <div class="shrink-0 px-4 pb-4">
970
- <div class="mx-auto max-w-3xl">
971
- {/* REPLACE — a full `composer` slot stands in for the built-in input.
972
- The slotted content owns its own submit/loading wiring. */}
973
- <Show
974
- when={props.composer}
975
- fallback={
976
- <DefaultPromptInput
977
- value={current()} placeholder={props.placeholder} loading={props.loading === true}
978
- suggestions={visibleSuggestions()} attachments={attachments()}
979
- accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
980
- attach={props.attach} webSearch={props.webSearch === true} voice={props.voice === true}
981
- triggers={props.triggers} kindIcons={props.kindIcons}
982
- onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
983
- onAttachmentsChange={(a) => { setAttachments(a); props.onAttachmentsChange?.(a); }}
984
- onWebSearch={() => props.onWebSearch?.()} onVoice={() => props.onVoice?.()}
985
- />
986
- }
987
- >
988
- <slot name="composer" />
989
- </Show>
990
- </div>
991
- </div>
992
- {/* INJECT: footer row below the composer. */}
993
- <Show when={props.footer}>
994
- <div part="footer" class="shrink-0 px-4 pb-3">
995
- <div class="mx-auto max-w-3xl text-center text-xs text-muted-foreground"><slot name="footer" /></div>
996
- </div>
997
- </Show>
998
- </Show>
999
- {/* Home/Messages tab bar (H-2/H-6): shown on the 'home' and 'list'
1000
- views and on the ROOT chat (Messages tab, conversations off —
1001
- ambiguity 1), hidden on a DRILLED chat so the back arrow above
1002
- is the only way back. */}
1003
- <Show when={tabBarVisible()}>
1004
- <WidgetTabBar
1005
- active={activeTab()}
1006
- onChange={(tab) => (tab === 'home' ? setView('home') : openMessagesTab())}
1007
- unread={anyUnread()}
1008
- />
1009
- </Show>
1010
- </div>
1011
- </div>
1012
- </ChatConfig>
1013
- );
1014
- }