@kitn.ai/ui 0.15.1 → 0.17.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 (333) hide show
  1. package/README.md +37 -13
  2. package/bin/mcp.js +22 -0
  3. package/dist/bash-InADTalH.js +6 -1
  4. package/dist/components/artifact.d.ts +58 -0
  5. package/dist/components/attachments.d.ts +67 -0
  6. package/dist/components/card-fallback.d.ts +10 -0
  7. package/dist/components/card-renderer.d.ts +13 -0
  8. package/dist/components/card.d.ts +28 -0
  9. package/dist/components/chain-of-thought.d.ts +29 -0
  10. package/dist/components/chat-container.d.ts +19 -0
  11. package/dist/components/chat-scope-picker.d.ts +9 -0
  12. package/dist/components/chat-thread.d.ts +91 -0
  13. package/dist/components/checkpoint.d.ts +20 -0
  14. package/dist/components/choice-card.d.ts +78 -0
  15. package/dist/components/code-block.d.ts +16 -0
  16. package/dist/components/confirm-card.d.ts +57 -0
  17. package/dist/components/context.d.ts +65 -0
  18. package/dist/components/conversation-item.d.ts +8 -0
  19. package/dist/components/conversation-list.d.ts +11 -0
  20. package/dist/components/dismissed-stub.d.ts +22 -0
  21. package/dist/components/embed.d.ts +21 -0
  22. package/dist/components/empty.d.ts +43 -0
  23. package/dist/components/feedback-bar.d.ts +45 -0
  24. package/dist/components/file-tree.d.ts +57 -0
  25. package/dist/components/file-upload.d.ts +16 -0
  26. package/dist/components/form-widgets.d.ts +31 -0
  27. package/dist/components/form.d.ts +103 -0
  28. package/dist/components/image.d.ts +12 -0
  29. package/dist/components/link-preview.d.ts +23 -0
  30. package/dist/components/loader.d.ts +60 -0
  31. package/dist/components/markdown.d.ts +8 -0
  32. package/dist/components/message-skills.d.ts +15 -0
  33. package/dist/components/message.d.ts +111 -0
  34. package/dist/components/model-switcher.d.ts +8 -0
  35. package/dist/components/prompt-input.d.ts +38 -0
  36. package/dist/components/prompt-suggestion.d.ts +13 -0
  37. package/dist/components/reasoning.d.ts +20 -0
  38. package/dist/components/response-compare-types.d.ts +66 -0
  39. package/dist/components/response-compare.d.ts +51 -0
  40. package/dist/components/response-stream.d.ts +14 -0
  41. package/dist/components/scroll-button.d.ts +7 -0
  42. package/dist/components/slash-command.d.ts +15 -0
  43. package/dist/components/source.d.ts +23 -0
  44. package/dist/components/tasks-card.d.ts +72 -0
  45. package/dist/components/text-shimmer.d.ts +9 -0
  46. package/dist/components/thinking-bar.d.ts +10 -0
  47. package/dist/components/toast.d.ts +48 -0
  48. package/dist/components/tool.d.ts +16 -0
  49. package/dist/components/use-card-resolution.d.ts +16 -0
  50. package/dist/components/voice-input.d.ts +7 -0
  51. package/dist/core-AYMC6_lb.js +5874 -12
  52. package/dist/css-M7EaDHN_.js +6 -1
  53. package/dist/custom-elements.json +260 -11
  54. package/dist/elements/artifact.d.ts +1 -0
  55. package/dist/elements/artifact.js +1 -0
  56. package/dist/elements/attachments.d.ts +1 -0
  57. package/dist/elements/attachments.js +1 -0
  58. package/dist/elements/autoloader.d.ts +2 -0
  59. package/dist/elements/autoloader.js +1 -0
  60. package/dist/elements/card.d.ts +1 -0
  61. package/dist/elements/card.js +1 -0
  62. package/dist/elements/cards.d.ts +1 -0
  63. package/dist/elements/cards.js +1 -0
  64. package/dist/elements/chain-of-thought.d.ts +1 -0
  65. package/dist/elements/chain-of-thought.js +1 -0
  66. package/dist/elements/chat-scope-picker.d.ts +1 -0
  67. package/dist/elements/chat-scope-picker.js +1 -0
  68. package/dist/elements/chat-workspace.d.ts +1 -0
  69. package/dist/elements/chat-workspace.js +1 -0
  70. package/dist/elements/chat.d.ts +1 -0
  71. package/dist/elements/chat.js +1 -0
  72. package/dist/elements/checkpoint.d.ts +1 -0
  73. package/dist/elements/checkpoint.js +1 -0
  74. package/dist/elements/choice.d.ts +1 -0
  75. package/dist/elements/choice.js +1 -0
  76. package/dist/elements/chunks/Icon-D_Zmwmwh.js +1 -0
  77. package/dist/elements/chunks/action-icons-PVfIvcuG.js +1 -0
  78. package/dist/elements/chunks/attachments-Dp6vqtrc.js +1 -0
  79. package/dist/elements/chunks/bash-InADTalH.js +1 -0
  80. package/dist/elements/chunks/button-CKyYD9G4.js +1 -0
  81. package/dist/elements/chunks/card-Cjho-PYk.js +1 -0
  82. package/dist/elements/chunks/card-routing-C986KuUA.js +1 -0
  83. package/dist/elements/chunks/chat-thread-RT808iPf.js +1 -0
  84. package/dist/elements/chunks/check-CDYaMYrM.js +1 -0
  85. package/dist/elements/chunks/chevron-down-3SBiq8HF.js +1 -0
  86. package/dist/elements/chunks/chevron-right-k2UFlIVK.js +1 -0
  87. package/dist/elements/chunks/cn-Dogu6PbV.js +1 -0
  88. package/dist/elements/chunks/code-block-BYUmfFSd.js +1 -0
  89. package/dist/elements/chunks/collapsible-BiNwdOn6.js +1 -0
  90. package/dist/elements/chunks/context-18g6oi4J.js +1 -0
  91. package/dist/elements/chunks/conversation-list-D9Y0NVdo.js +1 -0
  92. package/dist/elements/chunks/core-AYMC6_lb.js +12 -0
  93. package/dist/elements/chunks/css-M7EaDHN_.js +1 -0
  94. package/dist/elements/chunks/default-input-CSAoYYMd.js +1 -0
  95. package/dist/elements/chunks/download-D8K0Je8r.js +1 -0
  96. package/dist/elements/chunks/dropdown-bUs2iTia.js +1 -0
  97. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +141 -0
  98. package/dist/elements/chunks/external-link-6mA0wxab.js +1 -0
  99. package/dist/elements/chunks/file-text-DnaYk3Dm.js +1 -0
  100. package/dist/elements/chunks/file-tree-CgtRLlxK.js +1 -0
  101. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +1 -0
  102. package/dist/elements/chunks/github-light-JYsPkUQd.js +1 -0
  103. package/dist/elements/chunks/hover-card-CIGw62gP.js +1 -0
  104. package/dist/elements/chunks/html-CPZ3oZQ7.js +1 -0
  105. package/dist/elements/chunks/index-CTBEJeo6.js +1 -0
  106. package/dist/elements/chunks/javascript-C25yR2R2.js +1 -0
  107. package/dist/elements/chunks/json-DxJze_jm.js +1 -0
  108. package/dist/elements/chunks/link-BlGSeoPy.js +1 -0
  109. package/dist/elements/chunks/loader-DvhR4qa0.js +1 -0
  110. package/dist/elements/chunks/markdown-CMTGx9r8.js +64 -0
  111. package/dist/elements/chunks/message-CDZwXjhL.js +1 -0
  112. package/dist/elements/chunks/message-feedback-CYx81QiA.js +1 -0
  113. package/dist/elements/chunks/model-switcher-O_tUhoOT.js +1 -0
  114. package/dist/elements/chunks/overlay-DydXYMQW.js +1 -0
  115. package/dist/elements/chunks/prompt-suggestion-ldlWV5kd.js +1 -0
  116. package/dist/elements/chunks/reasoning-2PUnHlmr.js +1 -0
  117. package/dist/elements/chunks/resizable-B5WNbSeb.js +1 -0
  118. package/dist/elements/chunks/star-t9tI-P0q.js +1 -0
  119. package/dist/elements/chunks/store-C6rIooZz.js +1 -0
  120. package/dist/elements/chunks/svelte--5p79yCD.js +1 -0
  121. package/dist/elements/chunks/text-shimmer-uh9V_KUh.js +1 -0
  122. package/dist/elements/chunks/textarea-3nm6Ia9U.js +1 -0
  123. package/dist/elements/chunks/thumbs-up-BBtguGld.js +1 -0
  124. package/dist/elements/chunks/toast-store-D8cL3fWl.js +1 -0
  125. package/dist/elements/chunks/tool-GJ3vwp1U.js +1 -0
  126. package/dist/elements/chunks/tooltip-DwKAz4XR.js +1 -0
  127. package/dist/elements/chunks/triangle-alert-Ct1zt2JL.js +1 -0
  128. package/dist/elements/chunks/tsx-B8rCNbgL.js +1 -0
  129. package/dist/elements/chunks/typescript-RycA9KXf.js +1 -0
  130. package/dist/elements/chunks/use-card-resolution-D-_0Alzy.js +1 -0
  131. package/dist/elements/chunks/video-mVYiM99-.js +1 -0
  132. package/dist/elements/chunks/vue-BmIZj4XD.js +1 -0
  133. package/dist/elements/chunks/x-CzxL27Ch.js +1 -0
  134. package/dist/elements/code-block.d.ts +1 -0
  135. package/dist/elements/code-block.js +1 -0
  136. package/dist/elements/compare.d.ts +1 -0
  137. package/dist/elements/compare.js +1 -0
  138. package/dist/elements/confirm-card.d.ts +1 -0
  139. package/dist/elements/confirm-card.js +1 -0
  140. package/dist/elements/context-meter.d.ts +1 -0
  141. package/dist/elements/context-meter.js +1 -0
  142. package/dist/elements/conversation-list.d.ts +1 -0
  143. package/dist/elements/conversation-list.js +1 -0
  144. package/dist/elements/embed.d.ts +1 -0
  145. package/dist/elements/embed.js +1 -0
  146. package/dist/elements/empty.d.ts +1 -0
  147. package/dist/elements/empty.js +1 -0
  148. package/dist/elements/feedback-bar.d.ts +1 -0
  149. package/dist/elements/feedback-bar.js +1 -0
  150. package/dist/elements/file-tree.d.ts +1 -0
  151. package/dist/elements/file-tree.js +1 -0
  152. package/dist/elements/file-upload.d.ts +1 -0
  153. package/dist/elements/file-upload.js +1 -0
  154. package/dist/elements/form.d.ts +1 -0
  155. package/dist/elements/form.js +1 -0
  156. package/dist/elements/image.d.ts +1 -0
  157. package/dist/elements/image.js +1 -0
  158. package/dist/elements/link-preview.d.ts +1 -0
  159. package/dist/elements/link-preview.js +1 -0
  160. package/dist/elements/loader.d.ts +1 -0
  161. package/dist/elements/loader.js +1 -0
  162. package/dist/elements/markdown.d.ts +1 -0
  163. package/dist/elements/markdown.js +1 -0
  164. package/dist/elements/message-skills.d.ts +1 -0
  165. package/dist/elements/message-skills.js +1 -0
  166. package/dist/elements/message.d.ts +1 -0
  167. package/dist/elements/message.js +1 -0
  168. package/dist/elements/model-switcher.d.ts +1 -0
  169. package/dist/elements/model-switcher.js +1 -0
  170. package/dist/elements/popover.d.ts +1 -0
  171. package/dist/elements/popover.js +1 -0
  172. package/dist/elements/prompt-input.d.ts +1 -0
  173. package/dist/elements/prompt-input.js +1 -0
  174. package/dist/elements/prompt-suggestions.d.ts +1 -0
  175. package/dist/elements/prompt-suggestions.js +1 -0
  176. package/dist/elements/reasoning.d.ts +1 -0
  177. package/dist/elements/reasoning.js +1 -0
  178. package/dist/elements/resizable.d.ts +1 -0
  179. package/dist/elements/resizable.js +1 -0
  180. package/dist/elements/response-stream.d.ts +1 -0
  181. package/dist/elements/response-stream.js +14 -0
  182. package/dist/elements/scroll-button.d.ts +1 -0
  183. package/dist/elements/scroll-button.js +1 -0
  184. package/dist/elements/source.d.ts +1 -0
  185. package/dist/elements/source.js +1 -0
  186. package/dist/elements/switch.d.ts +1 -0
  187. package/dist/elements/switch.js +1 -0
  188. package/dist/elements/tasks.d.ts +1 -0
  189. package/dist/elements/tasks.js +1 -0
  190. package/dist/elements/text-shimmer.d.ts +1 -0
  191. package/dist/elements/text-shimmer.js +1 -0
  192. package/dist/elements/thinking-bar.d.ts +1 -0
  193. package/dist/elements/thinking-bar.js +1 -0
  194. package/dist/elements/toast.d.ts +1 -0
  195. package/dist/elements/toast.js +1 -0
  196. package/dist/elements/tool.d.ts +1 -0
  197. package/dist/elements/tool.js +1 -0
  198. package/dist/elements/voice-input.d.ts +1 -0
  199. package/dist/elements/voice-input.js +1 -0
  200. package/dist/elements.d.ts +768 -0
  201. package/dist/engine-javascript-vq0WuIJl.js +2516 -14
  202. package/dist/github-dark-dimmed-DUshB20C.js +4 -1
  203. package/dist/github-light-JYsPkUQd.js +4 -1
  204. package/dist/html-CPZ3oZQ7.js +10 -1
  205. package/dist/index.d.ts +124 -2
  206. package/dist/index.js +15593 -0
  207. package/dist/javascript-C25yR2R2.js +6 -1
  208. package/dist/json-DxJze_jm.js +6 -1
  209. package/dist/kitn-chat.es.js +1 -77
  210. package/dist/llms/llms-full.txt +57 -12
  211. package/dist/llms/llms.txt +3 -3
  212. package/dist/mcp.es.js +2296 -0
  213. package/dist/primitives/card-contract.d.ts +119 -0
  214. package/dist/primitives/card-host.d.ts +12 -0
  215. package/dist/primitives/card-recovery.d.ts +55 -0
  216. package/dist/primitives/card-registry.d.ts +17 -0
  217. package/dist/primitives/card-resolution.d.ts +8 -0
  218. package/dist/primitives/card-routing.d.ts +12 -0
  219. package/dist/primitives/card-validate.d.ts +25 -0
  220. package/dist/primitives/chat-config.d.ts +31 -0
  221. package/dist/primitives/controllable.d.ts +13 -0
  222. package/dist/primitives/embed-providers.d.ts +65 -0
  223. package/dist/primitives/highlighter.d.ts +23 -0
  224. package/dist/primitives/link-preview.d.ts +47 -0
  225. package/dist/primitives/message-feedback.d.ts +35 -0
  226. package/dist/primitives/pdf-preview.d.ts +50 -0
  227. package/dist/primitives/toast-store.d.ts +88 -0
  228. package/dist/primitives/use-auto-resize.d.ts +8 -0
  229. package/dist/primitives/use-resize-observer.d.ts +1 -0
  230. package/dist/primitives/use-stick-to-bottom.d.ts +5 -0
  231. package/dist/primitives/use-text-stream.d.ts +19 -0
  232. package/dist/primitives/use-voice-recorder.d.ts +9 -0
  233. package/dist/react/index.d.ts +1198 -0
  234. package/dist/react/runtime.d.ts +15 -0
  235. package/dist/react.js +275 -0
  236. package/dist/register-JyTglR6m.js +1 -0
  237. package/dist/register-impl-C6wlSsts.js +77 -0
  238. package/dist/remote/host-embed.d.ts +33 -0
  239. package/dist/remote/index.d.ts +2 -0
  240. package/dist/remote/origin.d.ts +5 -0
  241. package/dist/remote/provider-runtime.d.ts +27 -0
  242. package/dist/remote/provider.d.ts +2 -0
  243. package/dist/remote/validate.d.ts +4 -0
  244. package/dist/remote/version.d.ts +4 -0
  245. package/dist/remote/wire.d.ts +53 -0
  246. package/dist/schemas/choice.schema.json +5 -0
  247. package/dist/schemas/tasks.schema.json +5 -0
  248. package/dist/svelte--5p79yCD.js +15 -1
  249. package/dist/tsx-B8rCNbgL.js +6 -1
  250. package/dist/types.d.ts +42 -0
  251. package/dist/typescript-RycA9KXf.js +6 -1
  252. package/dist/ui/action-icons.d.ts +11 -0
  253. package/dist/ui/avatar.d.ts +8 -0
  254. package/dist/ui/badge.d.ts +9 -0
  255. package/dist/ui/button.d.ts +10 -0
  256. package/dist/ui/collapsible.d.ts +27 -0
  257. package/dist/ui/dropdown.d.ts +20 -0
  258. package/dist/ui/hover-card.d.ts +28 -0
  259. package/dist/ui/overlay.d.ts +67 -0
  260. package/dist/ui/popover.d.ts +32 -0
  261. package/dist/ui/resizable.d.ts +76 -0
  262. package/dist/ui/scroll-area.d.ts +5 -0
  263. package/dist/ui/separator.d.ts +5 -0
  264. package/dist/ui/skeleton.d.ts +5 -0
  265. package/dist/ui/switch.d.ts +19 -0
  266. package/dist/ui/textarea.d.ts +6 -0
  267. package/dist/ui/tooltip.d.ts +9 -0
  268. package/dist/utils/cn.d.ts +2 -0
  269. package/dist/vue-BmIZj4XD.js +33 -1
  270. package/frameworks/react/index.tsx +67 -11
  271. package/frameworks/react/runtime.tsx +16 -2
  272. package/llms-full.txt +57 -12
  273. package/llms.txt +3 -3
  274. package/package.json +36 -14
  275. package/src/agent-tooling/archetypes.ts +46 -0
  276. package/src/agent-tooling/integrations/cloudflare.ts +88 -0
  277. package/src/agent-tooling/integrations/langgraph.ts +60 -0
  278. package/src/agent-tooling/integrations/mastra.ts +27 -0
  279. package/src/agent-tooling/integrations/mock.ts +34 -0
  280. package/src/agent-tooling/integrations/ollama.ts +47 -0
  281. package/src/agent-tooling/integrations/openrouter.ts +26 -0
  282. package/src/agent-tooling/integrations/pi.ts +40 -0
  283. package/src/agent-tooling/integrations/pydantic-ai.ts +52 -0
  284. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +44 -0
  285. package/src/agent-tooling/mcp/manifest.ts +128 -0
  286. package/src/agent-tooling/mcp/server.ts +59 -0
  287. package/src/agent-tooling/mcp/stdio.ts +18 -0
  288. package/src/agent-tooling/mcp/tools/debug.ts +489 -0
  289. package/src/agent-tooling/mcp/tools/reference.ts +132 -0
  290. package/src/agent-tooling/mcp/tools/scaffold.ts +1759 -0
  291. package/src/agent-tooling/mcp/tools/theme.ts +340 -0
  292. package/src/agent-tooling/mcp/tools/types.ts +14 -0
  293. package/src/agent-tooling/registry.ts +41 -0
  294. package/src/agent-tooling/types.ts +30 -0
  295. package/src/components/chat-thread.tsx +27 -35
  296. package/src/components/choice-card.tsx +33 -4
  297. package/src/components/component-meta.json +250 -26
  298. package/src/components/confirm-card.tsx +15 -1
  299. package/src/components/dismissed-stub.tsx +80 -0
  300. package/src/components/form.tsx +21 -3
  301. package/src/components/message.tsx +151 -10
  302. package/src/components/response-compare-types.ts +127 -0
  303. package/src/components/response-compare.tsx +431 -0
  304. package/src/components/tasks-card.tsx +33 -3
  305. package/src/components/toast.tsx +398 -0
  306. package/src/components/use-card-resolution.ts +24 -8
  307. package/src/elements/autoloader.ts +89 -0
  308. package/src/elements/chat-types.ts +7 -0
  309. package/src/elements/chat-workspace.tsx +4 -2
  310. package/src/elements/chat.tsx +4 -2
  311. package/src/elements/compare.tsx +88 -0
  312. package/src/elements/compiled.css +1 -1
  313. package/src/elements/define.tsx +7 -1
  314. package/src/elements/element-manifest.json +182 -0
  315. package/src/elements/element-meta.json +191 -68
  316. package/src/elements/element-types.d.ts +43 -6
  317. package/src/elements/framework-usage.json +25 -0
  318. package/src/elements/message.tsx +27 -45
  319. package/src/elements/register-impl.ts +58 -0
  320. package/src/elements/register.ts +26 -51
  321. package/src/elements/remote.tsx +4 -0
  322. package/src/elements/styles.css +19 -0
  323. package/src/elements/toast.tsx +74 -0
  324. package/src/index.ts +30 -0
  325. package/src/primitives/card-contract.ts +14 -5
  326. package/src/primitives/card-recovery.ts +149 -0
  327. package/src/primitives/card-routing.ts +3 -0
  328. package/src/primitives/card-schemas/choice.schema.json +5 -0
  329. package/src/primitives/card-schemas/tasks.schema.json +5 -0
  330. package/src/primitives/message-feedback.ts +123 -0
  331. package/src/primitives/toast-store.ts +235 -0
  332. package/src/ui/tooltip.tsx +8 -2
  333. package/theme.css +4 -2
@@ -0,0 +1,431 @@
1
+ import {
2
+ type JSX,
3
+ type Accessor,
4
+ Show,
5
+ Index,
6
+ splitProps,
7
+ mergeProps,
8
+ createSignal,
9
+ createMemo,
10
+ createEffect,
11
+ on,
12
+ ErrorBoundary,
13
+ createUniqueId,
14
+ } from 'solid-js';
15
+ import { cn } from '../utils/cn';
16
+ import { Button } from '../ui/button';
17
+ import { Card } from './card';
18
+ import { MessageBody } from './message';
19
+ import { TextShimmer } from './text-shimmer';
20
+ import { Check } from 'lucide-solid';
21
+ import {
22
+ type CompareCandidate,
23
+ type ComparePair,
24
+ type CompareSelection,
25
+ type ResponseCompareData,
26
+ buildSelection,
27
+ isAnyStreaming,
28
+ normalizeCandidates,
29
+ } from './response-compare-types';
30
+
31
+ // Re-export the data types + pure helpers so consumers (and the `<kai-compare>`
32
+ // facade) can import everything from a single `response-compare` module.
33
+ export type {
34
+ CompareCandidate,
35
+ ComparePair,
36
+ CompareCollapse,
37
+ ResponseCompareData,
38
+ CompareSelection,
39
+ } from './response-compare-types';
40
+ export { normalizeCandidates, buildSelection, isAnyStreaming } from './response-compare-types';
41
+
42
+ export type CompareLayout = 'auto' | 'columns' | 'tabs';
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Resolution controller — a sibling of `useCardResolution` for `CompareSelection`,
46
+ // which has no `kind` field (so it can't satisfy `R extends CardResolution`). Same
47
+ // precedence: prop (host-driven / re-hydrated) > local optimistic flip > none. A
48
+ // fresh `data` identity clears the local flip, but an explicit prop keeps it
49
+ // resolved. `isOptimistic` is true only for a flip made THIS session (announced via
50
+ // role="status"; silent on re-hydrate).
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+
53
+ export interface ResolvedController<T> {
54
+ value: Accessor<T | undefined>;
55
+ isResolved: Accessor<boolean>;
56
+ isOptimistic: Accessor<boolean>;
57
+ setLocal: (v: T) => void;
58
+ }
59
+
60
+ export function useResolved<T>(opts: {
61
+ prop: Accessor<T | undefined>;
62
+ data: Accessor<unknown>;
63
+ }): ResolvedController<T> {
64
+ const [local, setLocal] = createSignal<T | undefined>(undefined);
65
+ // Clear the optimistic flip on a NEW data identity (deferred so mount doesn't
66
+ // clobber an initial prop). The prop still wins via the memo below.
67
+ createEffect(on(opts.data, () => setLocal(undefined), { defer: true }));
68
+ const value = createMemo(() => opts.prop() ?? local());
69
+ const isResolved = createMemo(() => value() !== undefined);
70
+ const isOptimistic = createMemo(() => opts.prop() === undefined && local() !== undefined);
71
+ return { value, isResolved, isOptimistic, setLocal };
72
+ }
73
+
74
+ // ─────────────────────────────────────────────────────────────────────────────
75
+ // The <ResponseCompare> component.
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ export interface ResponseCompareProps {
79
+ /** The compare definition (prompt + the two candidates). */
80
+ data?: ResponseCompareData;
81
+ /** Stable id correlating every emitted event. */
82
+ compareId?: string;
83
+ /** Re-hydrate / control the selection. Renders the collapsed winner. Set as a
84
+ * JS property: `el.selection = { chosenId, rejectedIds }`. */
85
+ selection?: CompareSelection;
86
+ /** Layout. `'columns'` = side-by-side; `'tabs'` = one candidate at a time with
87
+ * pills to switch; `'auto'` (default) uses a CONTAINER query — columns when the
88
+ * component is ≥640px wide, tabs when narrower (e.g. on a phone). */
89
+ layout?: CompareLayout;
90
+ class?: string;
91
+ /** Fired when the user commits a pick. */
92
+ onSelect?: (sel: CompareSelection) => void;
93
+ /** Fired once both candidates have settled (stopped streaming) and a valid
94
+ * definition is shown. */
95
+ onReady?: () => void;
96
+ /** Fired for an unusable definition (inline error state). */
97
+ onError?: (message: string) => void;
98
+ }
99
+
100
+ /**
101
+ * `ResponseCompare` — a dual-response comparison. Two assistant candidates for the
102
+ * same prompt render side-by-side (or as tabs), each via `MessageBody` so a
103
+ * candidate reads exactly like an assistant message (reasoning + tools +
104
+ * attachments + markdown). The pick is a COMMIT, not a Submit: clicking "Pick this"
105
+ * (or Enter on the focused column) computes `{ chosenId, rejectedIds:[other], at }`,
106
+ * optimistically collapses to the chosen candidate's body, and calls `onSelect` for
107
+ * the consumer to send a `(prompt, chosen, rejected)` preference pair. Single-shot.
108
+ *
109
+ * While EITHER candidate is `streaming`, both pick controls are disabled and the
110
+ * streaming column shows a `TextShimmer`; `onReady` fires once both settle. The two
111
+ * columns are a WAI-ARIA radiogroup with roving tabindex (Arrow keys move A↔B,
112
+ * Enter/Space picks). Emits `onError` for a malformed definition.
113
+ */
114
+ export function ResponseCompare(props: ResponseCompareProps): JSX.Element {
115
+ const merged = mergeProps({ compareId: 'kai-compare', layout: 'auto' as CompareLayout }, props);
116
+ const [local] = splitProps(merged, [
117
+ 'data',
118
+ 'compareId',
119
+ 'selection',
120
+ 'layout',
121
+ 'class',
122
+ 'onSelect',
123
+ 'onReady',
124
+ 'onError',
125
+ ]);
126
+
127
+ const uid = createUniqueId();
128
+
129
+ const normalized = createMemo(() => normalizeCandidates(local.data?.candidates));
130
+ const valid = createMemo(() => normalized().candidates !== null);
131
+ const errorMessage = createMemo(() => normalized().error ?? '');
132
+ const pair = createMemo<ComparePair | null>(() => normalized().candidates);
133
+
134
+ const res = useResolved<CompareSelection>({
135
+ prop: () => local.selection,
136
+ data: () => local.data,
137
+ });
138
+
139
+ // Both candidates settled → enable the pick + announce ready.
140
+ const streaming = createMemo(() => isAnyStreaming(pair()));
141
+
142
+ const [focusIndex, setFocusIndex] = createSignal(0);
143
+ // In tabs mode (and auto when the container is narrow) only one candidate shows
144
+ // at a time; `active` is which. Ignored in columns mode where both render.
145
+ const [active, setActive] = createSignal(0);
146
+ let groupRef: HTMLDivElement | undefined;
147
+
148
+ // Reset the roving tab stop + active tab whenever a NEW definition arrives.
149
+ createEffect(on(() => local.data, () => { setFocusIndex(0); setActive(0); }));
150
+
151
+ // ready / error lifecycle: error on an unusable definition, ready once both
152
+ // candidates have settled (so consumers know the pick is now live).
153
+ createEffect(
154
+ on([valid, streaming], ([ok, isStreaming]) => {
155
+ if (!ok) {
156
+ local.onError?.(errorMessage());
157
+ return;
158
+ }
159
+ if (!isStreaming) local.onReady?.();
160
+ }),
161
+ );
162
+
163
+ const chosenId = createMemo(() => res.value()?.chosenId);
164
+ const chosenCandidate = createMemo<CompareCandidate | undefined>(() => {
165
+ const id = chosenId();
166
+ return id === undefined ? undefined : pair()?.find((c) => c.id === id);
167
+ });
168
+
169
+ // Commit a pick: emit + optimistically flip (single-shot; inert once resolved
170
+ // or while still streaming).
171
+ const pick = (candidate: CompareCandidate): void => {
172
+ if (res.isResolved()) return;
173
+ if (streaming()) return;
174
+ const p = pair();
175
+ if (!p) return;
176
+ const sel = buildSelection(p, candidate.id);
177
+ local.onSelect?.(sel);
178
+ res.setLocal(sel);
179
+ };
180
+
181
+ const focusColumn = (index: number): void => {
182
+ const cols = groupRef?.querySelectorAll<HTMLElement>('[role="radio"]');
183
+ cols?.[index]?.focus();
184
+ };
185
+
186
+ // Two-item roving tabindex (A↔B). Arrows toggle the focused column; Enter/Space
187
+ // picks it. No-op once resolved.
188
+ const onGroupKeyDown = (e: KeyboardEvent): void => {
189
+ if (res.isResolved()) return;
190
+ const p = pair();
191
+ if (!p) return;
192
+ const move = (next: number) => { setFocusIndex(next); setActive(next); focusColumn(next); };
193
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
194
+ e.preventDefault();
195
+ move(focusIndex() === 0 ? 1 : 0);
196
+ } else if (e.key === 'Home') {
197
+ e.preventDefault();
198
+ move(0);
199
+ } else if (e.key === 'End') {
200
+ e.preventDefault();
201
+ move(1);
202
+ } else if (e.key === ' ' || e.key === 'Enter') {
203
+ e.preventDefault();
204
+ const cand = p[focusIndex()];
205
+ if (cand) pick(cand);
206
+ }
207
+ };
208
+
209
+ // `auto` switches by CONTAINER width (Tailwind v4 `@container` + `@[640px]`
210
+ // variants); `@container/compare` lives on the OUTER wrapper (below) so the
211
+ // pill bar + columns both respond to it.
212
+ const wrapperClass = createMemo(() => {
213
+ const l = local.layout;
214
+ if (l === 'columns') return 'grid grid-cols-2 gap-3';
215
+ if (l === 'tabs') return 'grid grid-cols-1 gap-3';
216
+ return 'grid grid-cols-1 gap-3 @[640px]/compare:grid-cols-2'; // auto
217
+ });
218
+ // The tab-pill bar shows in tabs mode, and in auto only while the container is narrow.
219
+ const showTabs = () => local.layout === 'tabs' || local.layout === 'auto';
220
+ const tabBarClass = () => (local.layout === 'auto' ? '@[640px]/compare:hidden' : '');
221
+ // A non-active candidate is hidden in tabs (always) and auto (only while narrow);
222
+ // the active one — and both in columns mode — always render.
223
+ const columnHiddenClass = (index: number): string => {
224
+ const l = local.layout;
225
+ if (l === 'columns' || index === active()) return '';
226
+ return l === 'tabs' ? 'hidden' : 'hidden @[640px]/compare:block';
227
+ };
228
+
229
+ const promptId = `kai-compare-prompt-${uid}`;
230
+ const groupLabel = (): string => local.data?.prompt ?? 'Compare two responses';
231
+
232
+ return (
233
+ <Show
234
+ when={valid()}
235
+ fallback={<Card errorMessage={errorMessage() || "This comparison couldn't be displayed."} />}
236
+ >
237
+ <ErrorBoundary
238
+ fallback={() => {
239
+ local.onError?.('The comparison failed to render.');
240
+ return <Card errorMessage="The comparison failed to render." />;
241
+ }}
242
+ >
243
+ <div class={cn('flex flex-col gap-3', local.layout === 'auto' && '@container/compare', local.class)}>
244
+ <Show when={local.data?.prompt}>
245
+ <p id={promptId} class="text-sm text-foreground">
246
+ {local.data?.prompt}
247
+ </p>
248
+ </Show>
249
+
250
+ <Show
251
+ when={!res.isResolved()}
252
+ fallback={
253
+ <CollapsedWinner
254
+ candidate={chosenCandidate()}
255
+ optimistic={res.isOptimistic()}
256
+ />
257
+ }
258
+ >
259
+ {/* Tab pills — switch the visible candidate in tabs / auto-narrow mode. */}
260
+ <Show when={showTabs()}>
261
+ <div role="tablist" aria-label="Switch response" class={cn('flex gap-1', tabBarClass())}>
262
+ <Index each={pair() ?? []}>
263
+ {(candidate, index) => (
264
+ <button
265
+ type="button"
266
+ role="tab"
267
+ aria-selected={active() === index}
268
+ class={cn(
269
+ 'rounded-md px-3 py-1 text-xs font-medium transition-colors',
270
+ active() === index
271
+ ? 'bg-muted text-foreground'
272
+ : 'text-muted-foreground hover:text-foreground',
273
+ )}
274
+ onClick={() => { setActive(index); setFocusIndex(index); }}
275
+ >
276
+ {candidate().label ?? `Response ${candidate().id}`}
277
+ </button>
278
+ )}
279
+ </Index>
280
+ </div>
281
+ </Show>
282
+
283
+ <div
284
+ ref={groupRef}
285
+ role="radiogroup"
286
+ aria-label={groupLabel()}
287
+ aria-describedby={local.data?.prompt ? promptId : undefined}
288
+ class={wrapperClass()}
289
+ onKeyDown={onGroupKeyDown}
290
+ >
291
+ <Index each={pair() ?? []}>
292
+ {(candidate, index) => (
293
+ <CompareColumn
294
+ candidate={candidate()}
295
+ tabStop={index === focusIndex()}
296
+ disabled={streaming()}
297
+ hiddenClass={() => columnHiddenClass(index)}
298
+ onFocus={() => setFocusIndex(index)}
299
+ onPick={() => {
300
+ setFocusIndex(index);
301
+ pick(candidate());
302
+ }}
303
+ />
304
+ )}
305
+ </Index>
306
+ </div>
307
+ </Show>
308
+ </div>
309
+ </ErrorBoundary>
310
+ </Show>
311
+ );
312
+ }
313
+
314
+ // ─────────────────────────────────────────────────────────────────────────────
315
+ // One candidate column — a plain container with the assistant-styled body and a
316
+ // "Pick this" button that IS the radio (the radiogroup option). Keeping the radio
317
+ // on the button, not the wrapping div, avoids nesting interactive controls (the
318
+ // body itself can contain interactive bits like reasoning/tool toggles).
319
+ // ─────────────────────────────────────────────────────────────────────────────
320
+
321
+ interface ColumnProps {
322
+ candidate: CompareCandidate;
323
+ tabStop: boolean;
324
+ disabled: boolean;
325
+ /** Extra (reactive) class controlling tabs/auto visibility — hidden when this
326
+ * is the non-active candidate in tabs / auto-narrow mode. */
327
+ hiddenClass?: () => string;
328
+ onFocus: () => void;
329
+ onPick: () => void;
330
+ }
331
+
332
+ function CompareColumn(props: ColumnProps): JSX.Element {
333
+ const labelText = (): string =>
334
+ props.candidate.label ?? props.candidate.model ?? props.candidate.id;
335
+ return (
336
+ <div
337
+ class={cn(
338
+ 'flex min-w-0 flex-col gap-3 rounded-lg border border-input bg-background/40 p-3',
339
+ props.hiddenClass?.(),
340
+ )}
341
+ >
342
+ <Show when={props.candidate.label || props.candidate.model}>
343
+ <div class="flex items-center gap-2">
344
+ <Show when={props.candidate.label}>
345
+ <span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
346
+ {props.candidate.label}
347
+ </span>
348
+ </Show>
349
+ <Show when={props.candidate.model}>
350
+ <span class="text-xs text-muted-foreground">{props.candidate.model}</span>
351
+ </Show>
352
+ </div>
353
+ </Show>
354
+
355
+ <div class="min-w-0 flex-1">
356
+ <Show
357
+ when={props.candidate.streaming}
358
+ fallback={
359
+ <MessageBody
360
+ content={props.candidate.content}
361
+ reasoning={props.candidate.reasoning}
362
+ tools={props.candidate.tools}
363
+ attachments={props.candidate.attachments}
364
+ isUser={false}
365
+ markdown={true}
366
+ />
367
+ }
368
+ >
369
+ <TextShimmer class="text-sm">Generating response…</TextShimmer>
370
+ </Show>
371
+ </div>
372
+
373
+ <Button
374
+ type="button"
375
+ role="radio"
376
+ aria-checked={false}
377
+ aria-label={`Pick ${labelText()}`}
378
+ data-candidate-id={props.candidate.id}
379
+ variant="outline"
380
+ size="sm"
381
+ class="mt-auto w-full"
382
+ disabled={props.disabled}
383
+ tabindex={props.tabStop ? 0 : -1}
384
+ onFocus={props.onFocus}
385
+ onClick={(e) => {
386
+ e.stopPropagation();
387
+ props.onPick();
388
+ }}
389
+ >
390
+ Pick this
391
+ </Button>
392
+ </div>
393
+ );
394
+ }
395
+
396
+ // ─────────────────────────────────────────────────────────────────────────────
397
+ // CollapsedWinner — the read-only view after a pick: the chosen candidate's body
398
+ // (reasoning + tools + attachments + markdown), with a "Selected" affordance.
399
+ // role="status" only for an optimistic flip made this session.
400
+ // ─────────────────────────────────────────────────────────────────────────────
401
+
402
+ function CollapsedWinner(props: {
403
+ candidate: CompareCandidate | undefined;
404
+ optimistic: boolean;
405
+ }): JSX.Element {
406
+ return (
407
+ <div
408
+ class="flex flex-col gap-2 rounded-lg border border-input bg-accent/40 p-3"
409
+ role={props.optimistic ? 'status' : undefined}
410
+ >
411
+ <div class="flex items-center gap-2 text-sm font-medium text-accent-foreground">
412
+ <Check size={16} aria-hidden="true" />
413
+ <span>Selected{props.candidate?.label ? `: ${props.candidate.label}` : ''}</span>
414
+ </div>
415
+ <Show when={props.candidate} fallback={<p class="text-sm text-muted-foreground">Response selected.</p>}>
416
+ {(c) => (
417
+ <div class="min-w-0">
418
+ <MessageBody
419
+ content={c().content}
420
+ reasoning={c().reasoning}
421
+ tools={c().tools}
422
+ attachments={c().attachments}
423
+ isUser={false}
424
+ markdown={true}
425
+ />
426
+ </div>
427
+ )}
428
+ </Show>
429
+ </div>
430
+ );
431
+ }
@@ -14,11 +14,12 @@ import {
14
14
  import { cn } from '../utils/cn';
15
15
  import { Button } from '../ui/button';
16
16
  import { Card } from './card';
17
+ import { DismissedStub } from './dismissed-stub';
17
18
  import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
18
19
  import { useCardResolution } from './use-card-resolution';
19
20
  import { emitCardEvent } from '../primitives/card-routing';
20
21
  import { useCardHost } from '../primitives/card-host';
21
- import { Check } from 'lucide-solid';
22
+ import { Check, X } from 'lucide-solid';
22
23
 
23
24
  // ─────────────────────────────────────────────────────────────────────────────
24
25
  // Types (tasks.schema.json) — see src/primitives/card-schemas/tasks.schema.json
@@ -41,6 +42,7 @@ export interface TasksCardData {
41
42
  allowEmpty?: boolean;
42
43
  min?: number;
43
44
  max?: number;
45
+ dismissible?: boolean; // show a close affordance that emits `dismiss`
44
46
  }
45
47
 
46
48
  export interface TasksCardResult {
@@ -251,11 +253,21 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
251
253
  res.setLocal({ kind: 'submit', data: result });
252
254
  };
253
255
 
254
- // Surface the resolved state for host styling.
256
+ // Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
257
+ // the card collapses to its re-openable stub immediately.
258
+ const onDismiss = (): void => {
259
+ if (res.isResolved()) return;
260
+ emit({ kind: 'dismiss', cardId: local.cardId });
261
+ res.setLocal({ kind: 'dismissed' });
262
+ };
263
+ const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
264
+
265
+ // Surface the resolved state for host styling. Only a `submit` resolution is the
266
+ // "submitted" state; a deferred `dismissed` (or `expired`) is not.
255
267
  createEffect(() => {
256
268
  const el = local.hostElement;
257
269
  if (!el) return;
258
- if (res.isResolved()) el.setAttribute('data-kai-resolved', 'submitted');
270
+ if (res.resolution()?.kind === 'submit') el.setAttribute('data-kai-resolved', 'submitted');
259
271
  else el.removeAttribute('data-kai-resolved');
260
272
  });
261
273
 
@@ -280,11 +292,28 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
280
292
  return <Card heading={local.heading} errorMessage="The card failed to render." />;
281
293
  }}
282
294
  >
295
+ <Show
296
+ when={!res.isDeferred()}
297
+ fallback={
298
+ <DismissedStub type={TASKS_CARD_TYPE} title={local.heading ?? local.data?.heading} onReopen={onReopen} />
299
+ }
300
+ >
283
301
  <Card
284
302
  heading={local.heading ?? local.data?.heading}
285
303
  actions={
286
304
  res.isResolved() ? undefined : (
287
305
  <div class="flex w-full flex-wrap items-center justify-between gap-2">
306
+ <Show when={local.data?.dismissible === true}>
307
+ <Button
308
+ type="button"
309
+ variant="ghost"
310
+ size="icon-sm"
311
+ aria-label="Dismiss"
312
+ onClick={onDismiss}
313
+ >
314
+ <X size={16} aria-hidden="true" />
315
+ </Button>
316
+ </Show>
288
317
  <span id={countId} aria-live="polite" class="text-xs text-muted-foreground">
289
318
  {count()} selected
290
319
  </span>
@@ -402,6 +431,7 @@ export function TasksCard(props: TasksCardProps): JSX.Element {
402
431
  </div>
403
432
  </Show>
404
433
  </Card>
434
+ </Show>
405
435
  </ErrorBoundary>
406
436
  </Show>
407
437
  );