@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,1075 +0,0 @@
1
- import {
2
- type JSX,
3
- createSignal,
4
- createEffect,
5
- createMemo,
6
- createUniqueId,
7
- on,
8
- onMount,
9
- onCleanup,
10
- Show,
11
- For,
12
- } from 'solid-js';
13
- import { cn } from '../utils/cn';
14
- import { useChatConfig, textClass } from '../primitives/chat-config';
15
- import {
16
- type ComposerDoc,
17
- type EntityRef,
18
- normalizeValue,
19
- serializeToText,
20
- entitiesOf,
21
- docIsEmpty,
22
- } from '../primitives/composer-model';
23
- import {
24
- parseDom,
25
- renderDoc,
26
- ZWSP,
27
- createEntityEl,
28
- createTextWalker,
29
- entityStore,
30
- isEntityEl,
31
- kindGlyph,
32
- } from './composer-dom';
33
- import { activeTriggerFor } from '../primitives/composer-triggers';
34
- import { usePosition, useDismiss, createPresence } from '../ui/overlay';
35
- import { findHighlightMatches, applyHighlights } from './composer-highlight';
36
- import { createHistory } from './composer-history';
37
-
38
- export interface TriggerItem {
39
- id: string;
40
- label: string;
41
- icon?: string;
42
- /** Secondary muted text shown beside the label in the menu (Codex-style). */
43
- description?: string;
44
- /** Section header to group this item under in the menu (e.g. 'Plugins', 'Agents'). */
45
- group?: string;
46
- /** Entity kind for the inserted pill; overrides the trigger's default `kind` so
47
- * one trigger can mix kinds across sections (e.g. agents + plugins under `@`). */
48
- kind?: string;
49
- promptText?: string;
50
- data?: Record<string, unknown>;
51
- }
52
- export interface TriggerDef {
53
- char: string;
54
- kind: string;
55
- items?: TriggerItem[];
56
- }
57
- export type HighlightRule =
58
- | string
59
- | { pattern: string; flags?: string; class?: string };
60
- export interface ComposerChange {
61
- doc: ComposerDoc;
62
- text: string;
63
- entities: EntityRef[];
64
- }
65
- /** Imperative handle exposed via `controllerRef` — surfaces the composer's latent
66
- * capabilities (focus/blur the editable, clear the doc, submit, insert a pill) so
67
- * the `<kai-composer>` facade can forward them as instance methods. */
68
- export interface ComposerController {
69
- focus(options?: FocusOptions): void;
70
- blur(): void;
71
- clear(): void;
72
- send(): void;
73
- insertEntity(entity: EntityRef): void;
74
- }
75
- export interface ComposerProps {
76
- value?: string | ComposerDoc;
77
- placeholder?: string;
78
- disabled?: boolean;
79
- loading?: boolean;
80
- maxHeight?: number | string;
81
- submitOnEnter?: boolean;
82
- triggers?: TriggerDef[];
83
- highlights?: HighlightRule[];
84
- /** Default icon per entity kind (kind → image URL/data-URI), shown on a pill +
85
- * menu item when the item has no `icon` of its own. Overrides the built-in
86
- * glyphs (agent/plugin). Example: `{ agent: '/icons/bot.svg' }`. */
87
- kindIcons?: Record<string, string>;
88
- /** Render WITHOUT the rounded frame/background/padding — just the editable +
89
- * placeholder + menu. For embedding inside another frame (e.g. PromptInput). */
90
- bare?: boolean;
91
- /** Override the editable element's classes (used in `bare` mode to match an
92
- * existing input's exact look). The placeholder mirrors these for alignment. */
93
- editableClass?: string;
94
- /** Accessible name for the editable (role=textbox). Falls back to the
95
- * placeholder, then "Message input". */
96
- ariaLabel?: string;
97
- /** Receive the editable element (e.g. to register it for click-to-focus). */
98
- editableRef?: (el: HTMLDivElement) => void;
99
- /** Receive the imperative controller once mounted. The `<kai-composer>` facade
100
- * forwards these as element methods (focus/blur/clear/send/insertEntity). */
101
- controllerRef?: (controller: ComposerController) => void;
102
- onChange?: (change: ComposerChange) => void;
103
- onSubmit?: (change: ComposerChange) => void;
104
- onTrigger?: (info: { char: string; query: string; rect: DOMRect }) => void;
105
- onTriggerClose?: () => void;
106
- onEntityAdd?: (entity: EntityRef) => void;
107
- onEntityRemove?: (entity: EntityRef) => void;
108
- /** The editable gained focus. `focus`/`blur` are NOT composed, so they don't
109
- * escape the shadow root — this re-exposes them. (keydown/paste/focusin/focusout
110
- * are composed and already reach the host as native events; no wrapper needed.) */
111
- onFocus?: (e: FocusEvent) => void;
112
- /** The editable lost focus. */
113
- onBlur?: (e: FocusEvent) => void;
114
- }
115
-
116
- /**
117
- * Shadow-DOM-aware selection. `document.getSelection()` does NOT expose a
118
- * selection that lives inside an open ShadowRoot in Chromium — it retargets the
119
- * range to the host — so caret math and node insertion would silently operate
120
- * OUTSIDE the editable (pills land in the light DOM, the menu never closes).
121
- * `ShadowRoot.getSelection()` (Chrome) returns the real in-shadow selection. Fall
122
- * back to the document selection for the light DOM / jsdom (no shadow root).
123
- */
124
- function getActiveSelection(node: Node): Selection | null {
125
- const root = node.getRootNode();
126
- if (
127
- typeof ShadowRoot !== 'undefined' &&
128
- root instanceof ShadowRoot &&
129
- typeof (root as unknown as { getSelection?: () => Selection | null }).getSelection === 'function'
130
- ) {
131
- return (root as unknown as { getSelection: () => Selection | null }).getSelection();
132
- }
133
- return node.ownerDocument?.getSelection() ?? null;
134
- }
135
-
136
- /** Compute the full ZWSP-stripped text of all text nodes in the editable. */
137
- function getFullText(root: HTMLElement): string {
138
- let text = '';
139
- const walker = createTextWalker(root);
140
- let node = walker.nextNode() as Text | null;
141
- while (node) {
142
- text += (node.textContent ?? '').split(ZWSP).join('');
143
- node = walker.nextNode() as Text | null;
144
- }
145
- return text;
146
- }
147
-
148
- /**
149
- * Compute the global text offset for the current caret position.
150
- * Walks SHOW_TEXT nodes, strips ZWSP, sums lengths before the caret node.
151
- */
152
- function getCaretTextOffset(root: HTMLElement): number {
153
- const sel = getActiveSelection(root);
154
- if (!sel || sel.rangeCount === 0) return 0;
155
- const range = sel.getRangeAt(0);
156
- if (!range.collapsed) return 0;
157
-
158
- const { startContainer, startOffset } = range;
159
- let offset = 0;
160
- const walker = createTextWalker(root);
161
- let node = walker.nextNode() as Text | null;
162
- while (node) {
163
- if (node === startContainer) {
164
- // Strip ZWSP chars that come before startOffset
165
- const raw = (node.textContent ?? '').slice(0, startOffset);
166
- offset += raw.split(ZWSP).join('').length;
167
- return offset;
168
- }
169
- offset += (node.textContent ?? '').split(ZWSP).join('').length;
170
- node = walker.nextNode() as Text | null;
171
- }
172
- return offset;
173
- }
174
-
175
- /**
176
- * Get the client rect of the caret. Uses a collapsed range if possible.
177
- * If the rect is zero-sized, inserts a temporary measuring span briefly.
178
- */
179
- function getCaretRect(root: HTMLElement): DOMRect | null {
180
- const ownerDoc = root.ownerDocument;
181
- const sel = getActiveSelection(root);
182
- if (!sel || sel.rangeCount === 0) return null;
183
- const range = sel.getRangeAt(0).cloneRange();
184
- range.collapse(true);
185
-
186
- const rects = range.getClientRects();
187
- if (rects.length > 0 && (rects[0].width > 0 || rects[0].height > 0)) {
188
- return rects[0];
189
- }
190
-
191
- // Fallback: insert a measuring span, read its rect, remove it.
192
- const marker = ownerDoc.createElement('span');
193
- marker.textContent = '​';
194
- marker.style.position = 'absolute';
195
- marker.style.visibility = 'hidden';
196
- range.insertNode(marker);
197
- const rect = marker.getBoundingClientRect();
198
- marker.remove();
199
- return rect.width > 0 || rect.height > 0 ? rect : null;
200
- }
201
-
202
- /**
203
- * Place the caret at a stripped-text offset (inverse of getCaretTextOffset).
204
- * Walks pill-skipping text nodes; pills are 0-width in this coordinate space, so
205
- * an offset lands in the surrounding text. Best-effort: falls back to end-of-content.
206
- */
207
- function setCaretToOffset(root: HTMLElement, offset: number): void {
208
- const sel = getActiveSelection(root);
209
- if (!sel) return;
210
- const range = root.ownerDocument.createRange();
211
- const walker = createTextWalker(root);
212
- let remaining = offset;
213
- let node = walker.nextNode() as Text | null;
214
- let placed = false;
215
- while (node) {
216
- const raw = node.textContent ?? '';
217
- const strippedLen = raw.split(ZWSP).join('').length;
218
- if (remaining <= strippedLen) {
219
- // Convert the stripped offset to a raw offset within this node.
220
- let rawOffset = 0;
221
- let seen = 0;
222
- while (rawOffset < raw.length && seen < remaining) {
223
- if (raw[rawOffset] !== ZWSP) seen++;
224
- rawOffset++;
225
- }
226
- // Skip past any ZWSP filler so the caret sits after the pill marker.
227
- while (rawOffset < raw.length && raw[rawOffset] === ZWSP) rawOffset++;
228
- range.setStart(node, rawOffset);
229
- placed = true;
230
- break;
231
- }
232
- remaining -= strippedLen;
233
- node = walker.nextNode() as Text | null;
234
- }
235
- if (!placed) {
236
- range.selectNodeContents(root);
237
- range.collapse(false);
238
- } else {
239
- range.collapse(true);
240
- }
241
- sel.removeAllRanges();
242
- sel.addRange(range);
243
- }
244
-
245
- export function Composer(props: ComposerProps): JSX.Element {
246
- const config = useChatConfig();
247
- let editable!: HTMLDivElement;
248
- // A SIGNAL (not a plain `let`): usePosition's floating ref must be reactive so
249
- // Floating UI recomputes once the menu mounts. A non-reactive ref left the menu
250
- // parked at the page origin (0,0) instead of anchored at the caret.
251
- const [menuRef, setMenuRef] = createSignal<HTMLDivElement>();
252
- const highlightName = `kai-composer-highlight-${createUniqueId()}`;
253
- const [empty, setEmpty] = createSignal(docIsEmpty(normalizeValue(props.value)));
254
-
255
- // --- Atomic pill selection (Claude Code-style: arrow onto a pill selects the
256
- // whole unit; arrow again steps the caret past it; Backspace/Delete removes
257
- // it). The selected pill carries `data-selected` for the highlight. This is
258
- // a caret/visual affordance only — it never mutates the doc. ---
259
- const [selectedPill, setSelectedPill] = createSignal<HTMLElement | null>(null);
260
- const selectPill = (el: HTMLElement) => {
261
- const cur = selectedPill();
262
- if (cur && cur !== el) cur.removeAttribute('data-selected');
263
- el.setAttribute('data-selected', '');
264
- setSelectedPill(el);
265
- };
266
- const clearPillSelection = () => {
267
- const cur = selectedPill();
268
- if (!cur) return;
269
- cur.removeAttribute('data-selected');
270
- setSelectedPill(null);
271
- };
272
-
273
- // --- Undo/redo history (we own it; native contenteditable undo corrupts pills) ---
274
- const history = createHistory({ doc: normalizeValue(props.value), caret: 0 });
275
- let lastEditAt = 0;
276
- const COALESCE_MS = 500;
277
-
278
- // --- Trigger menu state ---
279
- type ActiveTriggerState = {
280
- def: TriggerDef;
281
- query: string;
282
- start: number;
283
- caretRect: DOMRect;
284
- };
285
- const [activeTrigger, setActiveTrigger] = createSignal<ActiveTriggerState | null>(null);
286
- const [selectedIndex, setSelectedIndex] = createSignal(0);
287
-
288
- const filteredItems = createMemo((): TriggerItem[] => {
289
- const t = activeTrigger();
290
- if (!t?.def.items) return [];
291
- // Exclude entities already present in the doc (by effective kind + id) — don't
292
- // offer a skill/agent/plugin the user has already added.
293
- const present = new Set(entitiesOf(parseDom(editable)).map((e) => `${e.kind}:${e.id}`));
294
- const q = t.query.toLowerCase();
295
- return t.def.items.filter((item) => {
296
- const kind = item.kind ?? t.def.kind;
297
- if (present.has(`${kind}:${item.id}`)) return false;
298
- if (q === '') return true;
299
- // Match label or description (Codex filters across both).
300
- return item.label.toLowerCase().includes(q) || (item.description?.toLowerCase().includes(q) ?? false);
301
- });
302
- });
303
-
304
- // Group the filtered items into sections by `group`, preserving first-appearance
305
- // order. Each item keeps its FLAT index so selection/keyboard-nav stays simple.
306
- const groupedItems = createMemo(() => {
307
- const groups: { group: string | undefined; items: { item: TriggerItem; index: number }[] }[] = [];
308
- filteredItems().forEach((item, index) => {
309
- let g = groups.find((x) => x.group === item.group);
310
- if (!g) { g = { group: item.group, items: [] }; groups.push(g); }
311
- g.items.push({ item, index });
312
- });
313
- return groups;
314
- });
315
-
316
- // The built-in menu is open only when there are items left to show (after
317
- // query filtering AND excluding already-added entities). `kai-trigger` still
318
- // fires regardless, so consumers driving a custom menu aren't affected.
319
- const menuOpen = createMemo(() => filteredItems().length > 0);
320
-
321
- // Icon resolution for a menu item: its own icon → the per-kind default.
322
- // (When neither, the menu renders the built-in kind glyph as a fallback.)
323
- const itemKind = (item: TriggerItem) => item.kind ?? activeTrigger()?.def.kind ?? '';
324
- const itemIconSrc = (item: TriggerItem) => item.icon ?? props.kindIcons?.[itemKind(item)];
325
-
326
- // Keep selectedIndex in bounds when filteredItems changes
327
- createEffect(() => {
328
- const max = filteredItems().length;
329
- if (selectedIndex() >= max) setSelectedIndex(Math.max(0, max - 1));
330
- });
331
-
332
- // --- Floating menu positioning ---
333
- // Virtual reference element built from the live caret rect
334
- const virtualRef = createMemo((): HTMLElement | undefined => {
335
- const t = activeTrigger();
336
- if (!t) return undefined;
337
- const rect = t.caretRect;
338
- return {
339
- getBoundingClientRect: () => rect,
340
- } as unknown as HTMLElement;
341
- });
342
-
343
- const { pos, hidden } = usePosition(virtualRef, menuRef, { placement: 'bottom-start', gutter: 4 });
344
- const { present, state, setRef } = createPresence(menuOpen);
345
-
346
- useDismiss({
347
- enabled: menuOpen,
348
- onDismiss: () => closeTrigger(),
349
- refs: () => [editable, menuRef()],
350
- });
351
-
352
- function closeTrigger() {
353
- if (activeTrigger()) {
354
- setActiveTrigger(null);
355
- props.onTriggerClose?.();
356
- }
357
- }
358
-
359
- // --- Core logic ---
360
-
361
- const snapshot = (): ComposerChange => {
362
- const doc = parseDom(editable);
363
- return { doc, text: serializeToText(doc), entities: entitiesOf(doc) };
364
- };
365
-
366
- /** Recompute and register CSS Custom Highlight ranges for keyword rules. No
367
- * rules → apply an empty set, which clears any prior registration. */
368
- function recomputeHighlights() {
369
- const rules = props.highlights;
370
- const matches = rules?.length ? findHighlightMatches(getFullText(editable), rules) : [];
371
- applyHighlights(editable, matches, ZWSP, highlightName);
372
- }
373
-
374
- onMount(() => {
375
- renderDoc(editable, normalizeValue(props.value), editable.ownerDocument, props.kindIcons);
376
- setEmpty(docIsEmpty(parseDom(editable)));
377
- recomputeHighlights();
378
- history.reset({ doc: parseDom(editable), caret: 0 });
379
- });
380
-
381
- // Re-render when the controlled `value` prop changes, but ONLY when the
382
- // editable is NOT focused (don't stomp the caret while the user is typing).
383
- createEffect(on(() => props.value, (v) => {
384
- // Re-render only when the incoming value actually differs from what the DOM
385
- // already shows. This skips the echo of our own onChange (so the caret isn't
386
- // stomped while typing) while still honoring genuine external changes —
387
- // including a clear-after-submit that fires while the editable is focused.
388
- const incoming = serializeToText(normalizeValue(v));
389
- if (incoming === serializeToText(parseDom(editable))) return;
390
- clearPillSelection(); // selected pill node is about to be replaced
391
- renderDoc(editable, normalizeValue(v), editable.ownerDocument, props.kindIcons);
392
- setEmpty(docIsEmpty(parseDom(editable)));
393
- recomputeHighlights();
394
- history.reset({ doc: parseDom(editable), caret: 0 }); // external value = new baseline
395
- }, { defer: true }));
396
-
397
- // Re-decorate when `highlights` changes on its own — e.g. a consumer (or the
398
- // docs <Example>) assigns `value` then `highlights` as separate properties
399
- // after mount. Without this, highlights set after the value-effect ran would
400
- // never apply. `defer` skips the mount run (onMount already decorates).
401
- createEffect(on(() => props.highlights, () => recomputeHighlights(), { defer: true }));
402
-
403
- function updateTriggerState() {
404
- const defs = props.triggers;
405
- if (!defs?.length) {
406
- if (activeTrigger()) closeTrigger();
407
- return;
408
- }
409
- const text = getFullText(editable);
410
- const caret = getCaretTextOffset(editable);
411
- const hit = activeTriggerFor(text, caret, defs);
412
- if (hit) {
413
- const rect = getCaretRect(editable);
414
- if (rect) {
415
- const prev = activeTrigger();
416
- setActiveTrigger({ def: hit.def, query: hit.query, start: hit.start, caretRect: rect });
417
- // Fire onTrigger if char/query changed or first activation
418
- if (!prev || prev.def.char !== hit.def.char || prev.query !== hit.query) {
419
- props.onTrigger?.({ char: hit.def.char, query: hit.query, rect });
420
- }
421
- }
422
- } else {
423
- if (activeTrigger()) closeTrigger();
424
- }
425
- }
426
-
427
- // Update derived state + notify the consumer (no history side effects).
428
- const syncState = (change: ComposerChange) => {
429
- setEmpty(docIsEmpty(change.doc));
430
- props.onChange?.(change);
431
- updateTriggerState();
432
- recomputeHighlights();
433
- };
434
-
435
- // Native input (typing / deleting): sync + record a (time-coalesced) history step.
436
- const handleInput = () => {
437
- clearPillSelection(); // any text edit drops a stale pill highlight
438
- const change = snapshot();
439
- syncState(change);
440
- const now = Date.now();
441
- history.record({ doc: change.doc, caret: getCaretTextOffset(editable) }, now - lastEditAt < COALESCE_MS);
442
- lastEditAt = now;
443
- };
444
-
445
- // Structural edit (pill insert/delete): sync + record a DISTINCT history step.
446
- const commitChange = () => {
447
- const change = snapshot();
448
- syncState(change);
449
- history.record({ doc: change.doc, caret: getCaretTextOffset(editable) }, false);
450
- lastEditAt = 0; // next typing starts its own entry
451
- };
452
-
453
- // Restore a snapshot (undo/redo). Re-renders the doc (pills included) + caret,
454
- // syncs derived state, but does NOT push a new history entry.
455
- const applySnapshot = (snap: { doc: ComposerDoc; caret: number }) => {
456
- clearPillSelection(); // the selected pill node is about to be replaced
457
- renderDoc(editable, snap.doc, editable.ownerDocument, props.kindIcons);
458
- setCaretToOffset(editable, snap.caret);
459
- syncState(snapshot());
460
- lastEditAt = 0;
461
- };
462
- const doUndo = () => { const s = history.undo(); if (s) applySnapshot(s); };
463
- const doRedo = () => { const s = history.redo(); if (s) applySnapshot(s); };
464
-
465
- // Also update trigger state on selectionchange (while focused). These handlers
466
- // double as the consumer-facing focus/blur surface (focus/blur are NOT composed,
467
- // so they don't escape the shadow root natively — we re-expose them as callbacks
468
- // the element turns into kai-focus/kai-blur).
469
- let focused = false;
470
- const handleFocus = (e: FocusEvent) => { focused = true; props.onFocus?.(e); };
471
- const handleBlur = (e: FocusEvent) => { focused = false; clearPillSelection(); props.onBlur?.(e); };
472
-
473
- onMount(() => {
474
- // Captured at SETUP and closed over, never re-resolved as a global inside
475
- // `onCleanup`: cleanup can run after the host removed the DOM globals (a
476
- // `kai-*` release is deferred one microtask past detachment, so an
477
- // environment teardown gets in between), and a bare `document` there throws
478
- // -- from a promise nobody holds, so it lands as an unhandled rejection that
479
- // fails the run while every test passes. That is how this was found.
480
- // See tests/components/teardown-without-dom-globals.test.tsx.
481
- // `ownerDocument` rather than a captured `document`, matching the rest of
482
- // this file and staying correct for a composer inside an iframe.
483
- const doc = editable.ownerDocument;
484
- const onSelectionChange = () => {
485
- if (focused) updateTriggerState();
486
- };
487
- doc.addEventListener('selectionchange', onSelectionChange);
488
- onCleanup(() => doc.removeEventListener('selectionchange', onSelectionChange));
489
- });
490
-
491
- // --- Imperative controller (Pattern C): hand the facade a handle over the
492
- // composer's latent capabilities. `editable`/`snapshot`/`insertEntity` are
493
- // already wired internally — this just surfaces them without changing any
494
- // existing behavior. ---
495
- onMount(() => {
496
- props.controllerRef?.({
497
- focus: (options) => editable.focus(options),
498
- blur: () => editable.blur(),
499
- // Empty the composer to a blank doc + reset the history baseline. Mirrors
500
- // the external-value clear path so derived state syncs and onChange fires
501
- // (which the facade turns into kai-value-change).
502
- clear: () => {
503
- clearPillSelection();
504
- renderDoc(editable, [], editable.ownerDocument, props.kindIcons);
505
- syncState(snapshot());
506
- history.reset({ doc: parseDom(editable), caret: 0 });
507
- lastEditAt = 0;
508
- },
509
- // Submit the current content programmatically — same path as Enter.
510
- send: () => props.onSubmit?.(snapshot()),
511
- // Insert an atomic pill at the caret (no trigger token to delete).
512
- insertEntity: (entity) => insertEntity(entity),
513
- });
514
- });
515
-
516
- /**
517
- * Select an item from the trigger menu.
518
- */
519
- function selectItem(item: TriggerItem) {
520
- const t = activeTrigger();
521
- if (!t) return;
522
- const entity: EntityRef = {
523
- kind: item.kind ?? t.def.kind,
524
- id: item.id,
525
- label: item.label,
526
- icon: item.icon,
527
- promptText: item.promptText,
528
- data: item.data,
529
- };
530
- // Delete the trigger token = the char + query ("/re" → 3) before the caret.
531
- insertEntity(entity, { deleteBefore: 1 + t.query.length });
532
- closeTrigger();
533
- }
534
-
535
- /**
536
- * Insert an entity pill at the caret.
537
- * If `opts.deleteBefore` is given, that many non-ZWSP characters immediately
538
- * before the caret are removed first (the trigger token). The deletion walks
539
- * BACKWARD from the caret within its text run, so it can never cross an earlier
540
- * pill — a global-offset delete used to eat the previous pill, capping the field
541
- * at two pills. After insertion the caret sits just after the new ZWSP marker.
542
- */
543
- const insertEntity = (entity: EntityRef, opts?: { deleteBefore?: number }) => {
544
- const ownerDoc = editable.ownerDocument;
545
- const sel = getActiveSelection(editable);
546
- if (!sel || sel.rangeCount === 0) return;
547
-
548
- // Delete the trigger token: walk BACK from the caret over `deleteBefore`
549
- // non-ZWSP chars to find the token start, then delete start→caret. Walking
550
- // back stays within the caret's text run (stops at a pill), so it never eats
551
- // an earlier pill. Deleting via a range collapses the live selection cleanly.
552
- const caret = sel.getRangeAt(0);
553
- if (opts?.deleteBefore && caret.collapsed) {
554
- let remaining = opts.deleteBefore;
555
- let node: Node | null = caret.startContainer;
556
- let offset = caret.startOffset;
557
- while (remaining > 0 && node && node.nodeType === 3) {
558
- const text = node.textContent ?? '';
559
- while (offset > 0 && remaining > 0) {
560
- offset--;
561
- if (text[offset] !== ZWSP) remaining--;
562
- }
563
- if (remaining > 0) {
564
- const prev: ChildNode | null = node.previousSibling;
565
- if (prev && prev.nodeType === 3) { node = prev; offset = (prev.textContent ?? '').length; }
566
- else break; // a pill (or nothing) precedes — stop, never delete across it
567
- }
568
- }
569
- const del = ownerDoc.createRange();
570
- del.setStart(node as Node, offset);
571
- del.setEnd(caret.startContainer, caret.startOffset);
572
- del.deleteContents();
573
- }
574
-
575
- // Insert at the live caret (re-collapsed at the deletion point by the browser).
576
- const range = sel.getRangeAt(0);
577
- if (!range.collapsed) range.deleteContents();
578
- const pill = createEntityEl(ownerDoc, entity, props.kindIcons);
579
- const zwspNode = ownerDoc.createTextNode(ZWSP);
580
- range.insertNode(zwspNode);
581
- range.insertNode(pill); // inserted before the zwsp → DOM order [pill][zwsp]
582
-
583
- const after = ownerDoc.createRange();
584
- after.setStartAfter(zwspNode);
585
- after.collapse(true);
586
- sel.removeAllRanges();
587
- sel.addRange(after);
588
-
589
- props.onEntityAdd?.(entity);
590
- commitChange();
591
- };
592
-
593
- // A string consisting only of ZWSP markers (an empty string counts — `[].every`
594
- // is true). Used to recognize the zero-width filler that trails a pill.
595
- const onlyZwsp = (s: string | null): boolean =>
596
- (s ?? '').split('').every((ch) => ch === ZWSP);
597
-
598
- // Zero-width filler between a pill and adjacent content: an empty OR ZWSP-only
599
- // text node. Inserting a pill (and then typing) leaves these behind — e.g.
600
- // `[pill][t""][t"<ZWSP> world"]` — so adjacency checks MUST skip them to find
601
- // the pill. (This was the forward/backward asymmetry: a pill's `nextSibling` is
602
- // the pill itself, but its following text node's `previousSibling` is filler.)
603
- const isFiller = (n: Node | null): boolean =>
604
- !!n && n.nodeType === Node.TEXT_NODE && onlyZwsp(n.textContent);
605
-
606
- /** The entity pill immediately BEFORE a collapsed caret (skipping zero-width
607
- * filler), or null. Shared by Backspace and ArrowLeft selection. */
608
- const pillBeforeCaret = (range: Range): HTMLElement | null => {
609
- const { startContainer, startOffset } = range;
610
- if (startContainer === editable) {
611
- let i = startOffset - 1;
612
- while (isFiller(editable.childNodes[i])) i--;
613
- const node = editable.childNodes[i];
614
- return isEntityEl(node) ? (node as HTMLElement) : null;
615
- }
616
- if (startContainer.nodeType === Node.TEXT_NODE) {
617
- // Only treat the caret as "right after a pill" when nothing but filler
618
- // precedes it within this text run.
619
- if (!onlyZwsp((startContainer.textContent ?? '').slice(0, startOffset))) return null;
620
- let prev = startContainer.previousSibling;
621
- while (isFiller(prev)) prev = prev!.previousSibling;
622
- return isEntityEl(prev) ? (prev as HTMLElement) : null;
623
- }
624
- return null;
625
- };
626
-
627
- /** The entity pill immediately AFTER a collapsed caret (skipping zero-width
628
- * filler), or null. Shared by Delete and ArrowRight selection. */
629
- const pillAfterCaret = (range: Range): HTMLElement | null => {
630
- const { startContainer, startOffset } = range;
631
- if (startContainer === editable) {
632
- let i = startOffset;
633
- while (isFiller(editable.childNodes[i])) i++;
634
- const node = editable.childNodes[i];
635
- return isEntityEl(node) ? (node as HTMLElement) : null;
636
- }
637
- if (startContainer.nodeType === Node.TEXT_NODE) {
638
- if (!onlyZwsp((startContainer.textContent ?? '').slice(startOffset))) return null;
639
- let next = startContainer.nextSibling;
640
- while (isFiller(next)) next = next!.nextSibling;
641
- return isEntityEl(next) ? (next as HTMLElement) : null;
642
- }
643
- return null;
644
- };
645
-
646
- /** Remove a pill (and its trailing ZWSP marker), fire onEntityRemove, commit. */
647
- const removePillEl = (pill: HTMLElement) => {
648
- const entity = entityStore.get(pill);
649
- const after = pill.nextSibling;
650
- if (after?.nodeType === Node.TEXT_NODE && onlyZwsp(after.textContent)) after.remove();
651
- pill.remove();
652
- if (entity) props.onEntityRemove?.(entity);
653
- commitChange();
654
- };
655
-
656
- /** Collapse the caret just before a pill. */
657
- const caretBeforePill = (pill: HTMLElement) => {
658
- const sel = getActiveSelection(editable);
659
- if (!sel) return;
660
- const r = editable.ownerDocument.createRange();
661
- r.setStartBefore(pill);
662
- r.collapse(true);
663
- sel.removeAllRanges();
664
- sel.addRange(r);
665
- };
666
- /** Collapse the caret just after a pill (past its trailing zero-width filler,
667
- * so the next character lands outside the pill). */
668
- const caretAfterPill = (pill: HTMLElement) => {
669
- const sel = getActiveSelection(editable);
670
- if (!sel) return;
671
- let node: Node = pill;
672
- while (isFiller(node.nextSibling)) node = node.nextSibling!;
673
- const r = editable.ownerDocument.createRange();
674
- r.setStartAfter(node);
675
- r.collapse(true);
676
- sel.removeAllRanges();
677
- sel.addRange(r);
678
- };
679
-
680
- const handleKeyDown = (e: KeyboardEvent) => {
681
- // --- Undo / redo (we own the history; native contenteditable undo corrupts
682
- // pills, so suppress it and drive our doc-snapshot stack instead) ---
683
- if ((e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z')) {
684
- e.preventDefault();
685
- if (e.shiftKey) doRedo();
686
- else doUndo();
687
- return;
688
- }
689
- if ((e.metaKey || e.ctrlKey) && (e.key === 'y' || e.key === 'Y')) { // Windows redo
690
- e.preventDefault();
691
- doRedo();
692
- return;
693
- }
694
-
695
- // --- Trigger menu keyboard navigation (takes priority) ---
696
- if (menuOpen() && filteredItems().length > 0) {
697
- if (e.key === 'ArrowDown') {
698
- e.preventDefault();
699
- setSelectedIndex((i) => (i + 1) % filteredItems().length);
700
- return;
701
- }
702
- if (e.key === 'ArrowUp') {
703
- e.preventDefault();
704
- setSelectedIndex((i) => (i - 1 + filteredItems().length) % filteredItems().length);
705
- return;
706
- }
707
- // Enter OR Tab selects the highlighted item (Tab is a common picker accept).
708
- if (e.key === 'Enter' || e.key === 'Tab') {
709
- const items = filteredItems();
710
- if (items.length > 0) {
711
- e.preventDefault();
712
- e.stopPropagation();
713
- selectItem(items[selectedIndex()]);
714
- return;
715
- }
716
- }
717
- if (e.key === 'Escape') {
718
- e.preventDefault();
719
- closeTrigger();
720
- return;
721
- }
722
- } else if (activeTrigger() && e.key === 'Escape') {
723
- // No items shown but trigger is active (e.g. only onTrigger fired) — close on Escape
724
- e.preventDefault();
725
- closeTrigger();
726
- return;
727
- }
728
-
729
- // --- A selected pill behaves as one unit (Claude Code-style) ---
730
- const selPill = selectedPill();
731
- if (selPill && selPill.isConnected) {
732
- if (e.key === 'ArrowLeft') { e.preventDefault(); caretBeforePill(selPill); clearPillSelection(); return; }
733
- if (e.key === 'ArrowRight') { e.preventDefault(); caretAfterPill(selPill); clearPillSelection(); return; }
734
- if (e.key === 'Backspace' || e.key === 'Delete') {
735
- e.preventDefault();
736
- const anchor = selPill.previousSibling; // text node the caret should return to
737
- clearPillSelection();
738
- removePillEl(selPill);
739
- const sel = getActiveSelection(editable);
740
- if (sel) {
741
- const r = editable.ownerDocument.createRange();
742
- if (anchor && anchor.isConnected) r.setStartAfter(anchor);
743
- else r.setStart(editable, 0);
744
- r.collapse(true);
745
- sel.removeAllRanges();
746
- sel.addRange(r);
747
- }
748
- return;
749
- }
750
- if (e.key === 'Escape') { e.preventDefault(); clearPillSelection(); return; }
751
- // A bare modifier keypress shouldn't drop the selection.
752
- if (e.key === 'Shift' || e.key === 'Meta' || e.key === 'Control' || e.key === 'Alt') return;
753
- // Any other key (typing, Enter, …): drop the highlight and let the key act
754
- // from just after the pill — then fall through to the normal handlers.
755
- caretAfterPill(selPill);
756
- clearPillSelection();
757
- }
758
-
759
- // --- ArrowLeft/Right INTO an adjacent pill selects it as one unit ---
760
- if (
761
- !selectedPill() &&
762
- (e.key === 'ArrowLeft' || e.key === 'ArrowRight') &&
763
- !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey
764
- ) {
765
- const sel = getActiveSelection(editable);
766
- if (sel && sel.rangeCount > 0) {
767
- const range = sel.getRangeAt(0);
768
- if (range.collapsed) {
769
- const pill = e.key === 'ArrowLeft' ? pillBeforeCaret(range) : pillAfterCaret(range);
770
- if (pill) { e.preventDefault(); selectPill(pill); return; }
771
- }
772
- }
773
- }
774
-
775
- // --- Backspace / Delete: atomically remove pill immediately before/after caret ---
776
- if (e.key === 'Backspace' || e.key === 'Delete') {
777
- const sel = getActiveSelection(editable);
778
- if (!sel || sel.rangeCount === 0) return;
779
-
780
- const range = sel.getRangeAt(0);
781
- if (!range.collapsed) return; // let the browser handle a selection deletion
782
-
783
- const pill = e.key === 'Backspace' ? pillBeforeCaret(range) : pillAfterCaret(range);
784
- if (pill) {
785
- e.preventDefault();
786
- removePillEl(pill);
787
- return;
788
- }
789
- }
790
-
791
- // --- Enter: submit (only when menu is NOT open or no item is highlighted) ---
792
- if (e.key === 'Enter' && !e.shiftKey && props.submitOnEnter !== false) {
793
- e.preventDefault();
794
- if (props.disabled || props.loading) return;
795
- props.onSubmit?.(snapshot());
796
- }
797
- };
798
-
799
- // Close trigger when space is typed (handled via input event — space moves caret
800
- // past whitespace boundary, so updateTriggerState will return null anyway).
801
- // This is covered by updateTriggerState() in handleInput.
802
-
803
- const maxH = () => props.maxHeight ?? 240;
804
- // This element is `contenteditable` with `role="textbox"` — a real tab stop,
805
- // and the primary one in the kit — so it must never be left with NO focus
806
- // affordance. WHICH element draws it depends on who owns the frame:
807
- //
808
- // - Standalone (the `else` branch below renders the rounded shell): the FRAME
809
- // owns the ring via `focus-within:ring-*`, exactly like `PromptInput`, so
810
- // the editable neutralizes its own. The editable is a square, full-bleed
811
- // box inside a `rounded-xl` shell, so an outline on IT painted a sharp
812
- // rectangle hugging the text region inside the rounded border — the bug.
813
- // - Bare: the host owns radius/bg/padding and may draw nothing at all
814
- // (`PromptInput` passes its own `editableClass` and rings its frame), so
815
- // the editable keeps `kai-focus-inset` — a ring inside the border box,
816
- // since an outset one would be clipped by a rounded host shell.
817
- //
818
- // A consumer passing `editableClass` still replaces this wholesale — including
819
- // the `text-start` pin below, so a replacement string owes its own (`PromptInput`,
820
- // the one in-repo caller, carries it).
821
- const editableCls = () =>
822
- props.editableClass ??
823
- cn(
824
- // `text-start` is a PIN, not a style choice: `text-align` inherits, so any
825
- // centered ancestor (`Empty`'s root did exactly this) reached in and centered
826
- // the placeholder AND the typed text. An input control's text alignment is a
827
- // fact about the control, so it states it rather than inheriting it. LOGICAL
828
- // (`start`), not `text-left` — in RTL the correct edge is the right one, and
829
- // pinning the physical value would be a worse bug than the one it fixes.
830
- 'text-foreground min-h-[44px] w-full text-start overflow-y-auto whitespace-pre-wrap break-words',
831
- // Size from the SAME token every other text surface reads (`ChatConfig`'s
832
- // proseSize, default `sm` = the 14px the rest of the kit sets explicitly) —
833
- // `Markdown`, `MessageContent` and `PromptInputTextarea` all go through
834
- // `textClass`. Without it the editable set no size at all and just inherited
835
- // whatever the shadow root's base was (16px in a stock page), so the composer
836
- // read a step larger than every component beside it. Going through the token
837
- // also means `proseSize` moves the composer like it moves `PromptInput`,
838
- // rather than leaving one input deaf to it.
839
- textClass(config.proseSize()),
840
- props.bare
841
- ? 'kai-focus-inset'
842
- : 'outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
843
- );
844
-
845
- const inner = (
846
- <>
847
- {/* Static style for CSS Custom Highlight API decoration.
848
- No-op in browsers that don't support ::highlight(); the selector
849
- is simply unrecognized and dropped. */}
850
- <style>{`
851
- /* MARKED text, not selected text. Reads --color-highlight (overridable via
852
- --kai-color-highlight), a warm <mark> yellow. It used to be a 22% tint of
853
- --color-primary with a #6366f1 indigo fallback — but the kit's
854
- --color-primary is a NEUTRAL (near-black in light, near-white in dark),
855
- so the indigo the fallback implied never appeared and the highlight
856
- rendered plain grey in both themes.
857
-
858
- NO \`prefers-color-scheme\` here, deliberately — the same trap that put
859
- the pill hues below at 2.33:1. The OS scheme and the kit's RESOLVED
860
- theme are different questions, and \`theme="light"\` on a dark-OS machine
861
- is an ordinary configuration; a scheme-keyed value would paint the dark
862
- amber on the light field. The pills answer this by scoping to \`.dark\`,
863
- which is NOT available here: \`::highlight()\` names a document-registered
864
- highlight, so there is no wrapper to hang a descendant selector on.
865
-
866
- It does not need one. \`--color-highlight\` reaches this pseudo through
867
- highlight inheritance from the ORIGINATING element — the text inside the
868
- \`.dark\` wrapper — so the token already answers the resolved theme,
869
- verified by rendering the dark amber under \`theme="dark"\` with only the
870
- LIGHT literal below as a fallback. The literal is therefore reachable
871
- only when no kit stylesheet is loaded at all, and it is deliberately the
872
- light one: an unstyled page is a white page, and guessing from the OS is
873
- exactly the failure above. */
874
- ::highlight(${highlightName}) { background-color: var(--color-highlight, hsl(45 96% 78%)); }
875
- `}</style>
876
- {/* Atomic entity pill styling. Self-contained (currentColor-based) so it
877
- renders correctly without depending on a Tailwind rebuild for the
878
- custom .kai-composer-pill class. */}
879
- <style>{`
880
- .kai-composer-pill {
881
- display: inline-flex; align-items: center; gap: 0.2rem;
882
- /* middle (not baseline): a flex pill's baseline differs with/without an
883
- icon, which misaligns adjacent pills — middle aligns them consistently. */
884
- vertical-align: middle; height: 1.5em;
885
- /* Asymmetric margin: a roomier trailing gap (so typed text isn't flush
886
- after a pill) + a smaller leading gap. */
887
- margin: 0 0.3rem 0 0.1rem;
888
- border-radius: 0.375rem;
889
- font-weight: 500; white-space: nowrap; user-select: none; cursor: default;
890
- box-sizing: border-box; line-height: 1;
891
- }
892
- /* Skills + agents: LIGHT — decorated inline text led by a sigil, NOT a
893
- chip. A distinct colour per kind (overridable); the sigil is dimmer
894
- than the label. (A GUI affords this richer-but-light look; a CLI can't.) */
895
- .kai-composer-pill[data-kind="skill"],
896
- .kai-composer-pill[data-kind="agent"] {
897
- padding: 0 0.1rem; background: transparent;
898
- /* Read like a command, not a title — lowercase the DISPLAY only; the
899
- entity label + emitted payload keep the consumer's original casing. */
900
- text-transform: lowercase;
901
- }
902
- .kai-composer-pill[data-kind="skill"] { color: var(--kai-pill-skill, #2563eb); }
903
- .kai-composer-pill[data-kind="agent"] { color: var(--kai-pill-agent, #7c3aed); }
904
- /* Brighter hues on dark so they don't muddy against a dark field. The
905
- consumer's --kai-pill-* override still wins (it's the var value); only
906
- the FALLBACK shifts per scheme.
907
-
908
- Keyed on the kit's RESOLVED theme (the \`.dark\` wrapper an element
909
- facade puts inside its shadow root, and the \`.dark\` class the docs /
910
- Storybook put on <html>) — NOT \`prefers-color-scheme\`. Those are
911
- different questions: \`theme="light"\` on a machine whose OS is dark is
912
- an ordinary, common configuration, and the media query answered the OS
913
- while the surface under the pill answered the attribute. That painted
914
- the DARK hue (#6ea8fe) on the LIGHT field: 2.33:1, under both the
915
- 4.5:1 text floor (SC 1.4.3) and even 3:1. Matched pairs looked fine,
916
- which is why it survived — never scheme-match a colour whose backdrop
917
- is chosen by the theme attribute. */
918
- .dark .kai-composer-pill[data-kind="skill"] { color: var(--kai-pill-skill, #6ea8fe); }
919
- .dark .kai-composer-pill[data-kind="agent"] { color: var(--kai-pill-agent, #c4a7fc); }
920
- /* Dimmer than the label, but the sigil is what makes a pill read as
921
- \`/skill\` vs \`@agent\`, so it is a meaningful glyph and owes 3:1
922
- (SC 1.4.11). At 0.6 the light-mode composite was 2.52:1. */
923
- .kai-composer-pill-sigil { opacity: 0.8; font-weight: 600; }
924
- /* Plugins (and any other/unknown kind): the richer CHIP — background +
925
- icon — so a composite bundle reads differently at a glance. */
926
- .kai-composer-pill:not([data-kind="skill"]):not([data-kind="agent"]) {
927
- padding: 0 0.4rem;
928
- background-color: color-mix(in srgb, currentColor 10%, transparent);
929
- }
930
- /* Selected (caret arrowed onto the pill): a highlight box, like Claude
931
- Code web. Tinted by the pill's own colour via currentColor. */
932
- .kai-composer-pill[data-selected] {
933
- background-color: color-mix(in srgb, currentColor 18%, transparent);
934
- box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent);
935
- }
936
- /* Fixed height + an icon never taller than the box → iconed and iconless
937
- pills are exactly the same height (no baseline shift in the line). */
938
- .kai-composer-pill-icon { width: 1em; height: 1em; flex-shrink: 0; }
939
- img.kai-composer-pill-icon { border-radius: 9999px; object-fit: cover; }
940
- .kai-composer-pill-glyph { display: inline-flex; align-items: center; justify-content: center; opacity: 0.8; }
941
- .kai-composer-pill-glyph svg { width: 1em; height: 1em; display: block; }
942
- /* The editable is the containing block for the placeholder pseudo-element. */
943
- [data-kai-composer-editable] { position: relative; }
944
- /* While a pill is arrow-selected the pill IS the selection, so hide the text
945
- caret so a blinking cursor doesn't sit beside the highlight box. Returns
946
- the moment the pill selection clears (arrow off, type, click, backspace). */
947
- [data-kai-composer-editable][data-pill-selected] { caret-color: transparent; }
948
- /* Placeholder via pseudo-element — exempt from axe color-contrast like a
949
- native <textarea> placeholder. position:absolute (with auto offsets =
950
- its static text-origin spot) takes it OUT of flow, so the caret sits at
951
- the START of the field instead of after the placeholder text. */
952
- [data-kai-composer-editable].kai-composer-empty::before {
953
- content: attr(data-placeholder);
954
- position: absolute;
955
- color: var(--color-muted-foreground, #9ca3af);
956
- pointer-events: none;
957
- }
958
- `}</style>
959
- {/* Placeholder is a `::before` pseudo-element (see the style block) — it sits
960
- at the text origin automatically (respecting the editable's padding/font),
961
- and, like a native <textarea> placeholder, is exempt from axe color-contrast
962
- (a real text node would fail it at the muted color). */}
963
- <div class="relative">
964
- <div
965
- ref={(el) => { editable = el; props.editableRef?.(el); }}
966
- data-kai-composer-editable
967
- data-pill-selected={selectedPill() ? '' : undefined}
968
- data-placeholder={props.placeholder ?? ''}
969
- contentEditable={props.disabled ? false : ('plaintext-only' as unknown as boolean)}
970
- role="textbox"
971
- aria-multiline="true"
972
- aria-label={props.ariaLabel || props.placeholder || 'Message input'}
973
- onInput={handleInput}
974
- onKeyDown={handleKeyDown}
975
- onMouseDown={clearPillSelection}
976
- onFocus={handleFocus}
977
- onBlur={handleBlur}
978
- // The empty/placeholder class is folded into `class` (NOT a separate
979
- // `classList`) — a reactive `class` string would otherwise clobber a
980
- // classList toggle (Solid sets className wholesale on re-eval).
981
- class={cn(editableCls(), empty() && props.placeholder ? 'kai-composer-empty' : '')}
982
- style={{
983
- 'max-height':
984
- typeof maxH() === 'number' ? `${maxH()}px` : String(maxH()),
985
- }}
986
- />
987
- </div>
988
-
989
- {/* Trigger menu */}
990
- <Show when={present()}>
991
- <div
992
- ref={(el) => { setMenuRef(el); setRef(el); }}
993
- role="listbox"
994
- aria-label="Suggestions"
995
- data-state={state()}
996
- class={cn(
997
- 'absolute z-50 min-w-[240px] max-w-[420px] max-h-[320px] overflow-y-auto rounded-lg bg-card shadow-lg py-1',
998
- 'border border-border',
999
- )}
1000
- style={{
1001
- position: 'fixed',
1002
- left: `${pos().x}px`,
1003
- top: `${pos().y}px`,
1004
- visibility: hidden() ? 'hidden' : 'visible',
1005
- }}
1006
- >
1007
- <For each={groupedItems()}>
1008
- {(section) => (
1009
- <>
1010
- <Show when={section.group}>
1011
- <div class="px-3 pt-2 pb-1 text-xs font-medium text-muted-foreground select-none">{section.group}</div>
1012
- </Show>
1013
- <For each={section.items}>
1014
- {(entry) => (
1015
- <button
1016
- role="option"
1017
- aria-selected={selectedIndex() === entry.index}
1018
- data-index={entry.index}
1019
- class={cn(
1020
- 'w-full flex items-center gap-2 px-3 py-1.5 text-left text-sm transition-colors',
1021
- selectedIndex() === entry.index
1022
- ? 'bg-muted text-foreground'
1023
- : 'text-foreground hover:bg-muted',
1024
- )}
1025
- onMouseEnter={() => setSelectedIndex(entry.index)}
1026
- // Keep focus (and the caret) in the editable so insertion works.
1027
- onMouseDown={(e) => e.preventDefault()}
1028
- onClick={(e) => { e.preventDefault(); selectItem(entry.item); }}
1029
- >
1030
- <Show
1031
- when={itemIconSrc(entry.item)}
1032
- fallback={
1033
- <Show when={kindGlyph(itemKind(entry.item))}>
1034
- {(glyph) => (
1035
- <span class="kai-composer-pill-glyph w-4 h-4 shrink-0" aria-hidden="true" innerHTML={glyph()} />
1036
- )}
1037
- </Show>
1038
- }
1039
- >
1040
- {(src) => <img src={src()} alt="" class="w-4 h-4 rounded object-cover shrink-0" />}
1041
- </Show>
1042
- <span class="font-medium whitespace-nowrap shrink-0">{entry.item.label}</span>
1043
- <Show when={entry.item.description}>
1044
- <span class="text-muted-foreground truncate min-w-0">{entry.item.description}</span>
1045
- </Show>
1046
- </button>
1047
- )}
1048
- </For>
1049
- </>
1050
- )}
1051
- </For>
1052
- </div>
1053
- </Show>
1054
- </>
1055
- );
1056
-
1057
- // Bare mode: no frame (the host owns radius/bg/padding). Standalone: full frame.
1058
- return props.bare ? (
1059
- inner
1060
- ) : (
1061
- <div
1062
- class={cn(
1063
- // Same frame-owns-the-ring pattern as `PromptInput`: the editable inside
1064
- // neutralizes its own outline (see `editableCls`), so this rounded shell
1065
- // draws the focus affordance and the ring follows `rounded-xl` instead of
1066
- // painting a square box around the inner text region.
1067
- 'kai-composer relative rounded-xl bg-surface p-2',
1068
- 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-0',
1069
- props.disabled && 'cursor-not-allowed opacity-60',
1070
- )}
1071
- >
1072
- {inner}
1073
- </div>
1074
- );
1075
- }