@kitn.ai/ui 0.16.0 → 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 (194) hide show
  1. package/README.md +28 -13
  2. package/dist/components/chat-thread.d.ts +2 -4
  3. package/dist/components/choice-card.d.ts +1 -0
  4. package/dist/components/dismissed-stub.d.ts +22 -0
  5. package/dist/components/message.d.ts +59 -2
  6. package/dist/components/response-compare-types.d.ts +66 -0
  7. package/dist/components/response-compare.d.ts +51 -0
  8. package/dist/components/tasks-card.d.ts +1 -0
  9. package/dist/components/toast.d.ts +48 -0
  10. package/dist/components/use-card-resolution.d.ts +10 -6
  11. package/dist/custom-elements.json +260 -11
  12. package/dist/elements/artifact.js +1 -0
  13. package/dist/elements/attachments.js +1 -0
  14. package/dist/elements/autoloader.d.ts +2 -0
  15. package/dist/elements/autoloader.js +1 -0
  16. package/dist/elements/card.js +1 -0
  17. package/dist/elements/cards.d.ts +1 -0
  18. package/dist/elements/cards.js +1 -0
  19. package/dist/elements/chain-of-thought.d.ts +0 -12
  20. package/dist/elements/chain-of-thought.js +1 -0
  21. package/dist/elements/chat-scope-picker.js +1 -0
  22. package/dist/elements/chat-workspace.js +1 -0
  23. package/dist/elements/chat.js +1 -0
  24. package/dist/elements/checkpoint.js +1 -0
  25. package/dist/elements/choice.js +1 -0
  26. package/dist/elements/chunks/Icon-D_Zmwmwh.js +1 -0
  27. package/dist/elements/chunks/action-icons-PVfIvcuG.js +1 -0
  28. package/dist/elements/chunks/attachments-Dp6vqtrc.js +1 -0
  29. package/dist/elements/chunks/bash-InADTalH.js +1 -0
  30. package/dist/elements/chunks/button-CKyYD9G4.js +1 -0
  31. package/dist/elements/chunks/card-Cjho-PYk.js +1 -0
  32. package/dist/elements/chunks/card-routing-C986KuUA.js +1 -0
  33. package/dist/elements/chunks/chat-thread-RT808iPf.js +1 -0
  34. package/dist/elements/chunks/check-CDYaMYrM.js +1 -0
  35. package/dist/elements/chunks/chevron-down-3SBiq8HF.js +1 -0
  36. package/dist/elements/chunks/chevron-right-k2UFlIVK.js +1 -0
  37. package/dist/elements/chunks/cn-Dogu6PbV.js +1 -0
  38. package/dist/elements/chunks/code-block-BYUmfFSd.js +1 -0
  39. package/dist/elements/chunks/collapsible-BiNwdOn6.js +1 -0
  40. package/dist/elements/chunks/context-18g6oi4J.js +1 -0
  41. package/dist/elements/chunks/conversation-list-D9Y0NVdo.js +1 -0
  42. package/dist/elements/chunks/core-AYMC6_lb.js +12 -0
  43. package/dist/elements/chunks/css-M7EaDHN_.js +1 -0
  44. package/dist/elements/chunks/default-input-CSAoYYMd.js +1 -0
  45. package/dist/elements/chunks/download-D8K0Je8r.js +1 -0
  46. package/dist/elements/chunks/dropdown-bUs2iTia.js +1 -0
  47. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +141 -0
  48. package/dist/elements/chunks/external-link-6mA0wxab.js +1 -0
  49. package/dist/elements/chunks/file-text-DnaYk3Dm.js +1 -0
  50. package/dist/elements/chunks/file-tree-CgtRLlxK.js +1 -0
  51. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +1 -0
  52. package/dist/elements/chunks/github-light-JYsPkUQd.js +1 -0
  53. package/dist/elements/chunks/hover-card-CIGw62gP.js +1 -0
  54. package/dist/elements/chunks/html-CPZ3oZQ7.js +1 -0
  55. package/dist/elements/chunks/index-CTBEJeo6.js +1 -0
  56. package/dist/elements/chunks/javascript-C25yR2R2.js +1 -0
  57. package/dist/elements/chunks/json-DxJze_jm.js +1 -0
  58. package/dist/elements/chunks/link-BlGSeoPy.js +1 -0
  59. package/dist/elements/chunks/loader-DvhR4qa0.js +1 -0
  60. package/dist/elements/chunks/markdown-CMTGx9r8.js +64 -0
  61. package/dist/elements/chunks/message-CDZwXjhL.js +1 -0
  62. package/dist/elements/chunks/message-feedback-CYx81QiA.js +1 -0
  63. package/dist/elements/chunks/model-switcher-O_tUhoOT.js +1 -0
  64. package/dist/elements/chunks/overlay-DydXYMQW.js +1 -0
  65. package/dist/elements/chunks/prompt-suggestion-ldlWV5kd.js +1 -0
  66. package/dist/elements/chunks/reasoning-2PUnHlmr.js +1 -0
  67. package/dist/elements/chunks/resizable-B5WNbSeb.js +1 -0
  68. package/dist/elements/chunks/star-t9tI-P0q.js +1 -0
  69. package/dist/elements/chunks/store-C6rIooZz.js +1 -0
  70. package/dist/elements/chunks/svelte--5p79yCD.js +1 -0
  71. package/dist/elements/chunks/text-shimmer-uh9V_KUh.js +1 -0
  72. package/dist/elements/chunks/textarea-3nm6Ia9U.js +1 -0
  73. package/dist/elements/chunks/thumbs-up-BBtguGld.js +1 -0
  74. package/dist/elements/chunks/toast-store-D8cL3fWl.js +1 -0
  75. package/dist/elements/chunks/tool-GJ3vwp1U.js +1 -0
  76. package/dist/elements/chunks/tooltip-DwKAz4XR.js +1 -0
  77. package/dist/elements/chunks/triangle-alert-Ct1zt2JL.js +1 -0
  78. package/dist/elements/chunks/tsx-B8rCNbgL.js +1 -0
  79. package/dist/elements/chunks/typescript-RycA9KXf.js +1 -0
  80. package/dist/elements/chunks/use-card-resolution-D-_0Alzy.js +1 -0
  81. package/dist/elements/chunks/video-mVYiM99-.js +1 -0
  82. package/dist/elements/chunks/vue-BmIZj4XD.js +1 -0
  83. package/dist/elements/chunks/x-CzxL27Ch.js +1 -0
  84. package/dist/elements/code-block.js +1 -0
  85. package/dist/elements/compare.js +1 -0
  86. package/dist/elements/confirm-card.js +1 -0
  87. package/dist/elements/context-meter.js +1 -0
  88. package/dist/elements/conversation-list.d.ts +1 -11
  89. package/dist/elements/conversation-list.js +1 -0
  90. package/dist/elements/embed.js +1 -0
  91. package/dist/elements/empty.js +1 -0
  92. package/dist/elements/feedback-bar.js +1 -0
  93. package/dist/elements/file-tree.js +1 -0
  94. package/dist/elements/file-upload.js +1 -0
  95. package/dist/elements/form.js +1 -0
  96. package/dist/elements/image.js +1 -0
  97. package/dist/elements/link-preview.js +1 -0
  98. package/dist/elements/loader.js +1 -0
  99. package/dist/elements/markdown.js +1 -0
  100. package/dist/elements/message-skills.d.ts +0 -16
  101. package/dist/elements/message-skills.js +1 -0
  102. package/dist/elements/message.js +1 -0
  103. package/dist/elements/model-switcher.d.ts +1 -11
  104. package/dist/elements/model-switcher.js +1 -0
  105. package/dist/elements/popover.js +1 -0
  106. package/dist/elements/prompt-input.d.ts +1 -9
  107. package/dist/elements/prompt-input.js +1 -0
  108. package/dist/elements/prompt-suggestions.d.ts +0 -6
  109. package/dist/elements/prompt-suggestions.js +1 -0
  110. package/dist/elements/reasoning.js +1 -0
  111. package/dist/elements/resizable.d.ts +1 -17
  112. package/dist/elements/resizable.js +1 -0
  113. package/dist/elements/response-stream.js +14 -0
  114. package/dist/elements/scroll-button.js +1 -0
  115. package/dist/elements/source.d.ts +0 -17
  116. package/dist/elements/source.js +1 -0
  117. package/dist/elements/switch.js +1 -0
  118. package/dist/elements/tasks.js +1 -0
  119. package/dist/elements/text-shimmer.js +1 -0
  120. package/dist/elements/thinking-bar.js +1 -0
  121. package/dist/elements/toast.d.ts +1 -0
  122. package/dist/elements/toast.js +1 -0
  123. package/dist/elements/tool.js +1 -0
  124. package/dist/elements/voice-input.js +1 -0
  125. package/dist/elements.d.ts +41 -5
  126. package/dist/index.d.ts +12 -0
  127. package/dist/index.js +11832 -9790
  128. package/dist/kitn-chat.es.js +1 -1
  129. package/dist/llms/llms-full.txt +57 -12
  130. package/dist/llms/llms.txt +3 -3
  131. package/dist/mcp.es.js +157 -1
  132. package/dist/primitives/card-contract.d.ts +20 -4
  133. package/dist/primitives/card-recovery.d.ts +55 -0
  134. package/dist/primitives/message-feedback.d.ts +35 -0
  135. package/dist/primitives/toast-store.d.ts +88 -0
  136. package/dist/react/index.d.ts +83 -3
  137. package/dist/react.js +90 -80
  138. package/dist/register-JyTglR6m.js +1 -0
  139. package/dist/register-impl-C6wlSsts.js +77 -0
  140. package/dist/schemas/choice.schema.json +5 -0
  141. package/dist/schemas/tasks.schema.json +5 -0
  142. package/dist/ui/tooltip.d.ts +1 -0
  143. package/frameworks/react/index.tsx +63 -11
  144. package/llms-full.txt +57 -12
  145. package/llms.txt +3 -3
  146. package/package.json +15 -6
  147. package/src/agent-tooling/mcp/tools/debug.ts +184 -0
  148. package/src/agent-tooling/mcp/tools/scaffold.ts +133 -1
  149. package/src/components/chat-thread.tsx +27 -35
  150. package/src/components/choice-card.tsx +33 -4
  151. package/src/components/component-meta.json +250 -26
  152. package/src/components/confirm-card.tsx +15 -1
  153. package/src/components/dismissed-stub.tsx +80 -0
  154. package/src/components/form.tsx +21 -3
  155. package/src/components/message.tsx +151 -10
  156. package/src/components/response-compare-types.ts +127 -0
  157. package/src/components/response-compare.tsx +431 -0
  158. package/src/components/tasks-card.tsx +33 -3
  159. package/src/components/toast.tsx +398 -0
  160. package/src/components/use-card-resolution.ts +24 -8
  161. package/src/elements/autoloader.ts +89 -0
  162. package/src/elements/chat-types.ts +7 -0
  163. package/src/elements/chat-workspace.tsx +4 -2
  164. package/src/elements/chat.tsx +4 -2
  165. package/src/elements/compare.tsx +88 -0
  166. package/src/elements/compiled.css +1 -1
  167. package/src/elements/element-manifest.json +182 -0
  168. package/src/elements/element-meta.json +191 -68
  169. package/src/elements/element-types.d.ts +41 -5
  170. package/src/elements/framework-usage.json +25 -0
  171. package/src/elements/message.tsx +27 -45
  172. package/src/elements/register-impl.ts +3 -0
  173. package/src/elements/register.ts +9 -0
  174. package/src/elements/remote.tsx +4 -0
  175. package/src/elements/styles.css +19 -0
  176. package/src/elements/toast.tsx +74 -0
  177. package/src/index.ts +30 -0
  178. package/src/primitives/card-contract.ts +14 -5
  179. package/src/primitives/card-recovery.ts +149 -0
  180. package/src/primitives/card-routing.ts +3 -0
  181. package/src/primitives/card-schemas/choice.schema.json +5 -0
  182. package/src/primitives/card-schemas/tasks.schema.json +5 -0
  183. package/src/primitives/message-feedback.ts +123 -0
  184. package/src/primitives/toast-store.ts +235 -0
  185. package/src/ui/tooltip.tsx +8 -2
  186. package/dist/elements/chat-types.d.ts +0 -41
  187. package/dist/elements/css.d.ts +0 -1
  188. package/dist/elements/default-input.d.ts +0 -40
  189. package/dist/elements/define.d.ts +0 -42
  190. package/dist/elements/register-impl.d.ts +0 -0
  191. package/dist/elements/register.d.ts +0 -3
  192. package/dist/register-impl-wCsFPoh9.js +0 -77
  193. package/dist/register-ujhr1nBS.js +0 -1
  194. /package/dist/elements/{remote.d.ts → compare.d.ts} +0 -0
@@ -1 +1 @@
1
- import{c as i,i as g}from"./register-ujhr1nBS.js";export{i as configureCodeHighlighting,g as isCodeHighlightingEnabled};
1
+ import{A as s,B as a,q as i,v as o}from"./register-JyTglR6m.js";export{s as configureCodeHighlighting,a as configureToasts,i as isCodeHighlightingEnabled,o as toast};
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @kitn.ai/ui
5
5
 
6
- > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 44 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
6
+ > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 46 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
7
7
 
8
8
  ## Install
9
9
 
@@ -37,7 +37,7 @@ Drop an element into any framework (React, Vue, plain HTML). Data in via JS prop
37
37
  - `<kai-prompt-input>` — standalone composer with send button.
38
38
 
39
39
  **Layer 2 — composable primitives** (`import { … } from '@kitn.ai/ui'`):
40
- All 44 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
40
+ All 46 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
41
41
 
42
42
  ## Key rules for the web components
43
43
 
@@ -99,7 +99,7 @@ For Tailwind builds: `@import "@kitn.ai/ui/theme.css"` in your CSS.
99
99
 
100
100
  ## Docs
101
101
 
102
- - Full element reference (all 44 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
102
+ - Full element reference (all 46 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
103
103
  - Machine-readable Custom Elements Manifest: https://unpkg.com/@kitn.ai/ui/dist/custom-elements.json
104
104
  - Working examples: https://github.com/kitn-ai/ui/tree/main/examples
105
105
  - Storybook: https://storybook.kitn.dev
@@ -173,7 +173,7 @@ The same rule applies to every array/object property (`models`, `context`, `sugg
173
173
 
174
174
  ---
175
175
 
176
- ## Element reference (44 elements, generated from custom-elements.json)
176
+ ## Element reference (46 elements, generated from custom-elements.json)
177
177
 
178
178
  Every element also accepts the `theme` attribute. Array/object properties are marked with a `—` attribute: they must be set as JS properties.
179
179
 
@@ -253,9 +253,9 @@ _No events._
253
253
 
254
254
  | Property | Attribute | Type | Description |
255
255
  |---|---|---|---|
256
- | `cards` | — | `undefined \| { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } }[]` | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. |
256
+ | `cards` | — | `undefined \| { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }[]` | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. |
257
257
  | `types` | — | `undefined \| Record<string, string>` | Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. |
258
- | `policy` | — | `undefined \| { onSubmit?: undefined \| (cardId: string, data: unknown) => void; onAction?: undefined \| (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined \| (text: string, opts: { mode: "compose" \| "send"; context?: unknown; }) => void; onOpen?: undefined \| (url: string, target: "tab" \| "artifact") => void; onState?: undefined \| (cardId: string, patch: unknown) => void; onDismiss?: undefined \| (cardId: string) => void; onError?: undefined \| (cardId: string, message: string) => void; maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. |
258
+ | `policy` | — | `undefined \| { onSubmit?: undefined \| (cardId: string, data: unknown) => void; onAction?: undefined \| (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined \| (text: string, opts: { mode: "compose" \| "send"; context?: unknown; }) => void; onOpen?: undefined \| (url: string, target: "tab" \| "artifact") => void; onState?: undefined \| (cardId: string, patch: unknown) => void; onDismiss?: undefined \| (cardId: string) => void; onReopen?: undefined \| (cardId: string) => void; onError?: undefined \| (cardId: string, message: string) => void; maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. |
259
259
 
260
260
  _No events._
261
261
 
@@ -279,7 +279,7 @@ _No events._
279
279
 
280
280
  | Property | Attribute | Type | Description |
281
281
  |---|---|---|---|
282
- | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string } }[]` | The full message thread to render, newest last. Each entry carries its role, content, and optional reasoning/tools/attachments/actions. Set as a JS property (`el.messages = [...]`). |
282
+ | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]` | The full message thread to render, newest last. Each entry carries its role, content, and optional reasoning/tools/attachments/actions. Set as a JS property (`el.messages = [...]`). |
283
283
  | `value` | `value` | `undefined \| string` | Controlled value of the input. When set, the host owns the input text and must update it on `kai-value-change`; leave unset for uncontrolled behavior. |
284
284
  | `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown in the empty input. |
285
285
  | `loading` | `loading` | `undefined \| false \| true` | When true, shows the loading/streaming state and disables submit (use while awaiting the assistant's reply). |
@@ -307,7 +307,7 @@ _No events._
307
307
 
308
308
  | Event | `detail` type | Description |
309
309
  |---|---|---|
310
- | `kai-message-action` | `CustomEvent<{ messageId: string; action: string }>` | An action button on a message was clicked. `action` is the built-in name or custom id. |
310
+ | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
311
311
  | `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. |
312
312
  | `kai-search` | `CustomEvent<Record<string, never>>` | The Search button was clicked. |
313
313
  | `kai-slash-select` | `CustomEvent<{ command: { id: string; label: string; description?: undefined \| string; category?: undefined \| string } }>` | A slash command was chosen from the palette. |
@@ -368,6 +368,30 @@ _No events._
368
368
 
369
369
  ---
370
370
 
371
+ ### `kai-compare` / `Compare`
372
+
373
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
374
+
375
+ | Property | Attribute | Type | Description |
376
+ |---|---|---|---|
377
+ | `data` | — | `undefined \| Record<string, unknown>` | The compare definition (prompt + the two candidates). Set as a JS PROPERTY: `el.data = { prompt, candidates: [A, B], collapse? }`. Import `ResponseCompareData` from `@kitn.ai/ui` for the full shape. |
378
+ | `compareId` | `compare-id` | `undefined \| string` | Stable id correlating every emitted event. Attribute: `compare-id`. |
379
+ | `selection` | — | `undefined \| Record<string, unknown>` | Re-hydrate / control the user's pick. Set as a JS PROPERTY: `el.selection = { chosenId, rejectedIds }`. Renders the collapsed winner. |
380
+ | `layout` | `layout` | `undefined \| "auto" \| "columns" \| "tabs"` | Layout: `'auto'` (default — columns when wide, tabs when narrow, by CONTAINER width) \| `'columns'` (side-by-side) \| `'tabs'` (pills to switch). Attribute: `layout`. |
381
+ | `proseSize` | `prose-size` | `undefined \| "xs" \| "sm" \| "base" \| "lg"` | Prose/text size for the rendered candidates. Attribute: `prose-size`. |
382
+ | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme for code blocks in the candidates. Attribute: `code-theme`. |
383
+ | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Whether code blocks are syntax-highlighted. Attribute: `code-highlight`. |
384
+
385
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
386
+
387
+ | Event | `detail` type | Description |
388
+ |---|---|---|
389
+ | `kai-compare-select` | `CustomEvent<{ chosenId: string; rejectedIds: string[]; at?: undefined \| number }>` | The user committed a pick. `detail` = `{ chosenId, rejectedIds, at }`. |
390
+ | `kai-error` | `CustomEvent<{ compareId: string; message: string }>` | The definition was unusable. |
391
+ | `kai-ready` | `CustomEvent<{ compareId: string }>` | Both candidates have settled and the pick is live. |
392
+
393
+ ---
394
+
371
395
  ### `kai-confirm` / `Confirm`
372
396
 
373
397
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -587,7 +611,7 @@ _No events._
587
611
 
588
612
  | Property | Attribute | Type | Description |
589
613
  |---|---|---|---|
590
- | `message` | — | `undefined \| { id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string } }` | The full message object. Set as a JS property. |
614
+ | `message` | — | `undefined \| { id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }` | The full message object. Set as a JS property. |
591
615
  | `role` | `role` | `undefined \| "user" \| "assistant"` | Convenience for simple cases when not passing a `message` object. |
592
616
  | `content` | `content` | `undefined \| string` | Convenience content (used when `message` is not set). |
593
617
  | `markdown` | `markdown` | `undefined \| false \| true` | Force markdown on/off. Defaults to on for assistant, off for user. |
@@ -602,7 +626,7 @@ _No events._
602
626
 
603
627
  | Event | `detail` type | Description |
604
628
  |---|---|---|
605
- | `kai-message-action` | `CustomEvent<{ messageId: string; action: string }>` | An action button was clicked. `action` is the built-in name or custom id. |
629
+ | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
606
630
 
607
631
  ---
608
632
 
@@ -933,6 +957,27 @@ _No events._
933
957
 
934
958
  ---
935
959
 
960
+ ### `kai-toast-region` / `ToastRegion`
961
+
962
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
963
+
964
+ | Property | Attribute | Type | Description |
965
+ |---|---|---|---|
966
+ | `toasts` | — | `{ id: string; message: string; variant?: undefined \| "neutral" \| "success"; action?: undefined \| { label: string; onAction: () => void \| false }; duration?: undefined \| number; dismissible?: undefined \| false \| true; target?: undefined \| HTMLElement }[]` | The toasts to render. Newest is shown on top. Set as a JS property (array); pass a new array reference to update. |
967
+ | `position` | `position` | `undefined \| "top-center" \| "top-right" \| "top-left" \| "bottom-center" \| "bottom-right" \| "bottom-left"` | Stack anchor: `'top-center'` (default), `'top-right'`, `'bottom-center'`, … |
968
+ | `max` | `max` | `undefined \| number` | Max simultaneously-visible toasts; the rest queue. Defaults to `3`. |
969
+ | `stack` | `stack` | `undefined \| "expanded" \| "collapsed"` | Stacking: 'expanded' (default, full column) \| 'collapsed' (Sonner-style pile that expands on hover/focus). Attribute: stack. |
970
+ | `target` | — | `undefined \| HTMLElement` | Container element to anchor this region to (JS property). Set by the store for a scoped region; unset = the global viewport region. |
971
+
972
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
973
+
974
+ | Event | `detail` type | Description |
975
+ |---|---|---|
976
+ | `kai-action` | `CustomEvent<{ id: string; label: string }>` | A toast's action button was pressed. |
977
+ | `kai-dismiss` | `CustomEvent<{ id: string; reason: "action" \| "timeout" \| "close" }>` | A toast left the stack. `reason` is `'timeout' \| 'close' \| 'action'`. |
978
+
979
+ ---
980
+
936
981
  ### `kai-tool` / `Tool`
937
982
 
938
983
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -973,7 +1018,7 @@ _No events._
973
1018
  | `groups` | — | `{ id: string; userId?: undefined \| string; teamId?: undefined \| string; name: string; sortOrder: number; createdAt: string }[]` | Pre-bucketed conversation groups for the sidebar. Set as a JS property. |
974
1019
  | `conversations` | — | `{ id: string; title: string; groupId?: undefined \| string; scope: { type: "document" \| "collection"; documentId?: undefined \| string; filters?: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }; messageCount: number; lastMessageAt: string; updatedAt: string }[]` | Flat conversation list (auto-bucketed if `groups` is empty). Set as a JS property. |
975
1020
  | `activeId` | `active-id` | `undefined \| string` | Id of the open conversation, highlighted in the sidebar. |
976
- | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string } }[]` | The active conversation's message thread, newest last. Set as a JS property. |
1021
+ | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]` | The active conversation's message thread, newest last. Set as a JS property. |
977
1022
  | `value` | `value` | `undefined \| string` | |
978
1023
  | `placeholder` | `placeholder` | `undefined \| string` | |
979
1024
  | `loading` | `loading` | `undefined \| false \| true` | |
@@ -1003,7 +1048,7 @@ _No events._
1003
1048
  | Event | `detail` type | Description |
1004
1049
  |---|---|---|
1005
1050
  | `kai-conversation-select` | `CustomEvent<{ id: string }>` | A conversation was selected in the sidebar. |
1006
- | `kai-message-action` | `CustomEvent<{ messageId: string; action: string }>` | An action button on a message was clicked. |
1051
+ | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button on a message was clicked. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
1007
1052
  | `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. |
1008
1053
  | `kai-new-chat` | `CustomEvent<Record<string, never>>` | The "New chat" button was clicked. |
1009
1054
  | `kai-search` | `CustomEvent<Record<string, never>>` | The Search button was clicked. |
@@ -1,7 +1,7 @@
1
1
  <!-- AUTO-GENERATED by scripts/gen-llms.mjs — do not edit by hand. Run `npm run build`. -->
2
2
  # @kitn.ai/ui
3
3
 
4
- > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 44 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
4
+ > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 46 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
5
5
 
6
6
  ## Install
7
7
 
@@ -35,7 +35,7 @@ Drop an element into any framework (React, Vue, plain HTML). Data in via JS prop
35
35
  - `<kai-prompt-input>` — standalone composer with send button.
36
36
 
37
37
  **Layer 2 — composable primitives** (`import { … } from '@kitn.ai/ui'`):
38
- All 44 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
38
+ All 46 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
39
39
 
40
40
  ## Key rules for the web components
41
41
 
@@ -97,7 +97,7 @@ For Tailwind builds: `@import "@kitn.ai/ui/theme.css"` in your CSS.
97
97
 
98
98
  ## Docs
99
99
 
100
- - Full element reference (all 44 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
100
+ - Full element reference (all 46 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
101
101
  - Machine-readable Custom Elements Manifest: https://unpkg.com/@kitn.ai/ui/dist/custom-elements.json
102
102
  - Working examples: https://github.com/kitn-ai/ui/tree/main/examples
103
103
  - Storybook: https://storybook.kitn.dev
package/dist/mcp.es.js CHANGED
@@ -1678,7 +1678,111 @@ function compose(archetype, integration, placement, framework, suggestions, audi
1678
1678
  `Env vars to set:`,
1679
1679
  envLines
1680
1680
  ].join("\n");
1681
- return [header, block1, block2, block3].join("\n\n");
1681
+ const block4 = [
1682
+ `=== LOADING OPTIONS ===`,
1683
+ ``,
1684
+ `The scaffold uses \`import '@kitn.ai/ui/elements'\` (register-all) — the right`,
1685
+ `default: it registers every kai-* element and is SSR-safe, so leave it as is.`,
1686
+ `Two opt-in modes load less if a page only ever uses a few elements:`,
1687
+ ``,
1688
+ ` Per-element (bundler apps): import '@kitn.ai/ui/elements/<file>'`,
1689
+ ` Registers just that element; your bundler tree-shakes the rest away.`,
1690
+ ` Example: import '@kitn.ai/ui/elements/chat' (client-only — not for SSR)`,
1691
+ ``,
1692
+ ` Autoloader (no-build / CDN pages): a <script type="module"> tag pointing at`,
1693
+ ` dist/elements/autoloader.js — loads each kai-* element on demand as it`,
1694
+ ` appears in the DOM. A CDN/static-file tool; not importable through a bundler.`,
1695
+ ``,
1696
+ `Run the debug tool with "reduce bundle size" for the full breakdown and sizes.`
1697
+ ].join("\n");
1698
+ const block5 = interactionPatternsBlock();
1699
+ return [header, block1, block2, block3, block4, block5].join("\n\n");
1700
+ }
1701
+ function interactionPatternsBlock() {
1702
+ const toastPattern = [
1703
+ `--- Pattern: toast() — confirmation + Undo ---`,
1704
+ `// toast is IMPERATIVE — call it; there is no <kai-toast> to place. The first`,
1705
+ `// call auto-mounts one <kai-toast-region> on document.body. Exported from`,
1706
+ `// both '@kitn.ai/ui' and '@kitn.ai/ui/elements'.`,
1707
+ `import { toast } from '@kitn.ai/ui/elements';`,
1708
+ ``,
1709
+ `toast('Copied to clipboard'); // neutral, auto-dismisses`,
1710
+ `toast.success('Saved'); // emerald success variant`,
1711
+ ``,
1712
+ `// Undo affordance: an action floors the duration so there's time to act.`,
1713
+ `const t = toast('Item deleted', {`,
1714
+ ` action: { label: 'Undo', onAction: () => restoreItem() },`,
1715
+ `});`,
1716
+ `// t.update({ message: 'Restored', variant: 'success' }); t.dismiss();`,
1717
+ ``,
1718
+ `// Collapsed (Sonner-style) stacking — toasts pile + expand on hover/focus.`,
1719
+ `// Opt in once at startup, or per-region via <kai-toast-region stack="collapsed">.`,
1720
+ `import { configureToasts } from '@kitn.ai/ui/elements';`,
1721
+ `configureToasts({ stack: 'collapsed' });`
1722
+ ].join("\n");
1723
+ const recoveryPattern = [
1724
+ `--- Pattern: dismissRecovery() — card dismiss + Undo (DEFERRED, not deleted) ---`,
1725
+ `// Dismissing a generative-UI card does NOT delete its envelope — it stamps a`,
1726
+ `// 'dismissed' resolution and collapses to a reopenable stub. Keep dismissed`,
1727
+ `// envelopes in your array; wire the policy with dismissRecovery().`,
1728
+ `import { dismissRecovery } from '@kitn.ai/ui';`,
1729
+ `import { toast } from '@kitn.ai/ui/elements';`,
1730
+ ``,
1731
+ `// Adapter: map dismissRecovery's toast shape onto the imperative toast().`,
1732
+ `const toastAdapter = {`,
1733
+ ` show: ({ message, action, durationMs }) => {`,
1734
+ ` const h = toast(message, {`,
1735
+ ` duration: durationMs,`,
1736
+ ` action: action && { label: action.label, onAction: action.onClick },`,
1737
+ ` });`,
1738
+ ` return { dismiss: h.dismiss };`,
1739
+ ` },`,
1740
+ `};`,
1741
+ ``,
1742
+ `const { onDismiss, onReopen } = dismissRecovery({`,
1743
+ ` get: () => cards, // your current envelopes`,
1744
+ ` set: (next) => setCards(next), // a NEW array reference (never mutate in place)`,
1745
+ ` toast: toastAdapter,`,
1746
+ `});`,
1747
+ `// Hand { onDismiss, onReopen } to the CardPolicy on <kai-cards> / <kai-remote>.`
1748
+ ].join("\n");
1749
+ const preferencePattern = [
1750
+ `--- Pattern: kai-compare — capture an A/B preference pair ---`,
1751
+ `// <kai-compare> shows EXACTLY two candidates for one prompt. data is a JS`,
1752
+ `// PROPERTY; stream both columns with a fresh data ref per chunk; picking is`,
1753
+ `// terminal and fires kai-compare-select { chosenId, rejectedIds }.`,
1754
+ `import type { ResponseCompareData, CompareSelection } from '@kitn.ai/ui';`,
1755
+ ``,
1756
+ `const el = document.querySelector('kai-compare')!;`,
1757
+ `el.data = {`,
1758
+ ` prompt,`,
1759
+ ` candidates: [`,
1760
+ ` { id: 'a', content: '', streaming: true },`,
1761
+ ` { id: 'b', content: '', streaming: true },`,
1762
+ ` ],`,
1763
+ `} satisfies ResponseCompareData;`,
1764
+ ``,
1765
+ `// Stream BOTH: replace data with a NEW object per chunk; clear streaming when`,
1766
+ `// a candidate settles. The pick unlocks once both have settled (kai-ready).`,
1767
+ `// el.data = { ...el.data, candidates: [{ ...a, content: aText }, { ...b, content: bText }] };`,
1768
+ ``,
1769
+ `el.addEventListener('kai-compare-select', (e) => {`,
1770
+ ` const { chosenId, rejectedIds } = (e as CustomEvent<CompareSelection>).detail;`,
1771
+ ` recordPreference({ prompt, chosen: chosenId, rejected: rejectedIds });`,
1772
+ `});`
1773
+ ].join("\n");
1774
+ return [
1775
+ `=== INTERACTION PATTERNS ===`,
1776
+ ``,
1777
+ `Optional snippets for confirmations, card recovery, and preference capture.`,
1778
+ `Drop in the one(s) you need; all imports resolve from @kitn.ai/ui.`,
1779
+ ``,
1780
+ toastPattern,
1781
+ ``,
1782
+ recoveryPattern,
1783
+ ``,
1784
+ preferencePattern
1785
+ ].join("\n");
1682
1786
  }
1683
1787
  function rejectIntegration(id) {
1684
1788
  const valid = listIntegrations().map((i) => `${i.id} (${i.title})`).join(", ");
@@ -2053,6 +2157,17 @@ const RULES = [
2053
2157
  cause: "A plain Vite/CRA React SPA has no server — there are no `/api` routes. A scaffolded Next.js route handler (`export async function POST`) does not run there, so `fetch('/api/chat')` 404s.",
2054
2158
  fix: "Either run the backend somewhere real, or skip it entirely for local dev:\n\n```ts\n// Option A — use Next.js where route handlers are supported\n// app/api/chat/route.ts: export async function POST(req) { ... }\n\n// Option B — add a Vite dev-server middleware/proxy\n// vite.config.ts: server: { proxy: { '/api': 'http://localhost:3001' } }\n\n// Option C — run a separate Express/Hono server\n// framework: 'express' in your harness config\n\n// Option D — zero-config local dev with mock integration (no backend needed)\n// Use `integration: 'mock'` in the scaffold tool\n```"
2055
2159
  },
2160
+ {
2161
+ // Rule 9 — reduce bundle size / footprint / "how much does @kitn.ai/ui add"
2162
+ // Source: dist/elements/<file>.js per-element exports; dist/autoloader.js
2163
+ id: "bundle-footprint",
2164
+ test: (t) => /bundle\s*size|footprint|tree.?shak|how\s+much.*does.*@kitn|reduce.*import|import.*only.*element|per.?element\s+import|autoload|cdn.*no.?build|no.?build.*cdn/i.test(
2165
+ t
2166
+ ),
2167
+ title: "Reducing bundle footprint — three load modes",
2168
+ cause: "The default `import '@kitn.ai/ui/elements'` registers every `kai-*` element. If your page uses only one or two elements, that pulls in the full ~119 KB gz bundle. Two opt-in modes let you load only what you need.",
2169
+ fix: "**Mode 1 — register-all (default, SSR-safe):**\nBest for multi-element apps or any SSR/meta-framework. Load once and every `kai-*` element is available.\n\n```js\nimport '@kitn.ai/ui/elements'; // ~119 KB gz — registers everything\n```\n\n**Mode 2 — per-element import (tree-shaking, bundler apps):**\nUse `import '@kitn.ai/ui/elements/<file>'` to register only one element. A bundler (Vite, webpack, Rollup) will tree-shake to just its chunks (~73 KB gz for `kai-chat` alone). Client-only — do not use in SSR entry points.\n\n```js\n// Registers only <kai-chat> (~73 KB gz vs ~119 KB gz register-all)\nimport '@kitn.ai/ui/elements/chat';\n\n// Other examples:\nimport '@kitn.ai/ui/elements/code-block'; // <kai-code-block>\nimport '@kitn.ai/ui/elements/confirm-card'; // <kai-confirm>\n```\n\nThe file name is the element's source basename from `element-manifest.json` (e.g. `kai-chat` → `chat`, `kai-confirm` → `confirm-card`).\n\n**Mode 3 — autoloader (no-build / CDN pages only):**\nWatches the DOM and dynamically imports each `kai-*` element's module on demand. A page that uses only `<kai-chat>` never downloads the other elements. It is a CDN / static-file tool — load it from a `<script type=\"module\">` tag. It is NOT importable through a bundler: Vite/webpack relocate it and the on-demand imports 404. Client-only.\n\n```html\n<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@kitn.ai/ui@<version>/dist/elements/autoloader.js\"><\/script>\n```\n\nIn a BUNDLED app (Vite/webpack/Next) use Mode 1 or Mode 2 instead — not the autoloader.\n\n**SSR note:** use Mode 1 (register-all) in SSR apps — per-element imports and the autoloader are client-only (they call DOM APIs at module eval). Modes 1 & 2 are side-effect imports; keep them even if your linter flags them as \"unused\"."
2170
+ },
2056
2171
  {
2057
2172
  // Rule 5 — SSR / server component / document is not defined
2058
2173
  // Source: for-ai-agents.mdx (client-only import); context7.json rule 2 (property rule requires DOM)
@@ -2065,6 +2180,47 @@ const RULES = [
2065
2180
  title: "SSR / server-side rendering — element requires the browser DOM",
2066
2181
  cause: '`kai-*` elements are client-side web components. They require `document` and `customElements` to register and render. Importing them in a server component (Next.js App Router server component, Nuxt SSR, etc.) throws "document is not defined" or silently produces no output.',
2067
2182
  fix: "Register the element on the client only. Use your framework's \"client-only\" / island / dynamic-import pattern.\n\n```js\n// ✅ Plain HTML / vanilla — import in a <script type=\"module\">\nimport '@kitn.ai/ui/elements';\n\n// ✅ Next.js App Router — mark the component with \"use client\"\n'use client';\nimport '@kitn.ai/ui/elements';\n\n// ✅ Next.js — dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\nconst KaiChat = dynamic(() => import('@kitn.ai/ui/elements').then(() => 'kai-chat'), { ssr: false });\n\n// ✅ React wrapper (already client-safe)\nimport { Chat } from '@kitn.ai/ui/react';\n```"
2183
+ },
2184
+ {
2185
+ // Rule 10 — toast() is the imperative API; there is no <kai-toast> to place
2186
+ // Source: src/primitives/toast-store.ts (the `toast` fn + auto-mounted region)
2187
+ id: "toast-imperative",
2188
+ test: (t) => {
2189
+ if (/<kai-toast(-region)?\b/.test(t)) return true;
2190
+ if (/\btoast(s)?\b|notification|snackbar/i.test(t) && /how.*(show|raise|trigger|fire|display)|show.*toast|raise.*toast|trigger.*toast|toast.*(not|isn'?t|won'?t).*(show|appear|render)|where.*toast|add.*toast|kai-|@kitn/i.test(t))
2191
+ return true;
2192
+ return false;
2193
+ },
2194
+ title: "Toast is an imperative call — `toast('…')`, not a `<kai-toast>` you place",
2195
+ cause: "Toasts are raised IMPERATIVELY by calling `toast(message)` — there is no `<kai-toast>` element you add to your markup. The first call lazily mounts ONE `<kai-toast-region>` on `document.body` (a real, kit-styled, viewport-positioned element) and every later toast feeds that same region. Trying to place a toast element by hand, or looking for a `messages`/`toasts` prop to push into, is the wrong model.",
2196
+ fix: "Import `toast` and call it. It is exported from BOTH the root `@kitn.ai/ui` and the `@kitn.ai/ui/elements` bundle, so the web-components-only consumer gets it too. It is SSR-safe (no DOM is touched until the first call on the client).\n\n```js\nimport { toast } from '@kitn.ai/ui/elements'; // or '@kitn.ai/ui'\n\n// ✅ Fire-and-forget\ntoast('Copied to clipboard');\ntoast.success('Saved');\n\n// ✅ With an Undo action + an imperative handle\nconst t = toast('Item deleted', {\n action: { label: 'Undo', onAction: () => restore() },\n});\nt.update({ message: 'Restored', variant: 'success' });\nt.dismiss();\n```\n\nThe auto-mounted `<kai-toast-region>` carries its own shadow root + kit styles — do NOT add a `<kai-toast-region>` tag yourself unless you deliberately want a second, declaratively-controlled region."
2197
+ },
2198
+ {
2199
+ // Rule 11 — dismissed cards are DEFERRED (reopenable stub), not deleted
2200
+ // Source: src/primitives/card-recovery.ts (dismissRecovery) + the dismissed stub
2201
+ id: "card-dismiss-deferred",
2202
+ test: (t) => {
2203
+ const cardCtx = /\bcard(s)?\b|envelope|kai-card|kai-cards|kai-confirm|kai-choice|kai-tasks|kai-form|generative.?ui|resolution|dismissRecovery/i;
2204
+ if (!cardCtx.test(t)) return false;
2205
+ return /dismiss|reopen|re-?open|\bundo\b|disappear|remove.*card|card.*(gone|remove|delete|vanish)|filter.*out|stub/i.test(t);
2206
+ },
2207
+ title: "Dismissed cards are DEFERRED (a reopenable stub), not deleted",
2208
+ cause: "Dismissing a generative-UI card does NOT delete its envelope from history. The card stamps a `{ kind: 'dismissed' }` resolution onto its envelope and collapses to a small reopenable stub (\"Proposed: <title> — dismissed · Reopen\"). If you filter `dismissed` envelopes out of your cards array, the stub vanishes and the user can never reopen it — and you lose the audit trail of what was proposed.",
2209
+ fix: "Keep dismissed envelopes in the array. Wire dismiss/reopen with `dismissRecovery()` (from `@kitn.ai/ui`), which builds the `onDismiss`/`onReopen` half of a `CardPolicy` over your store and can show a \"Dismissed · Undo\" toast via an injected adapter.\n\n```ts\nimport { dismissRecovery } from '@kitn.ai/ui';\nimport { toast } from '@kitn.ai/ui/elements';\n\n// Adapter: map dismissRecovery's toast shape onto the imperative toast().\nconst toastAdapter = {\n show: ({ message, action, durationMs }) => {\n const handle = toast(message, {\n duration: durationMs,\n action: action && { label: action.label, onAction: action.onClick },\n });\n return { dismiss: handle.dismiss };\n },\n};\n\nconst { onDismiss, onReopen } = dismissRecovery({\n get: () => cards, // your current envelopes\n set: (next) => setCards(next), // NEW array reference (never mutate in place)\n toast: toastAdapter,\n});\n// Pass these on the CardPolicy you hand to <kai-cards> / <kai-remote>.\n```\n\n`onDismiss` writes `dismissed` immutably (Undo restores the prior resolution); `onReopen` clears it back to live (or stamps `expired` when the host says the card is no longer reopenable). Never mutate the array in place — re-render needs a new ref."
2210
+ },
2211
+ {
2212
+ // Rule 12 — kai-compare contract: two candidates, JS data prop, stream both, terminal pick
2213
+ // Source: src/elements/compare.tsx + src/components/response-compare-types.ts
2214
+ id: "compare-contract",
2215
+ test: (t) => {
2216
+ if (/<kai-compare\b|kai-compare-select|ResponseCompareData|response.?compare/i.test(t)) return true;
2217
+ if (/compar(e|ing|ison)|side.by.side|a\/b|two\s+(responses|candidates|answers|completions)|dual.?response/i.test(t) && /kai-|@kitn|candidate|prefer(ence)?|chosen|reject/i.test(t))
2218
+ return true;
2219
+ return false;
2220
+ },
2221
+ title: "`kai-compare` — two candidates, `data` as a JS property, terminal pick",
2222
+ cause: "`<kai-compare>` shows EXACTLY two assistant candidates for one prompt and lets the user pick the better one. The `data` value is an array/object, so it must be set as a JS PROPERTY (never an HTML attribute). Both candidates can stream — but, like `kai-chat`, that needs a NEW `data` reference per chunk (mutating in place will not re-render). The pick is a COMMIT (not a Submit): it fires once and the card collapses.",
2223
+ fix: "Set `data` in JS with two candidates, stream by reassigning a fresh `data` object per chunk, and listen for `kai-compare-select` directly on the element.\n\n```ts\nimport { toast } from '@kitn.ai/ui/elements';\nimport type { ResponseCompareData, CompareSelection } from '@kitn.ai/ui';\n\nconst el = document.querySelector('kai-compare')!;\n// data is a JS PROPERTY — exactly two candidates, each with a unique id.\nel.data = {\n prompt: 'Summarise the report',\n candidates: [\n { id: 'a', content: '', streaming: true },\n { id: 'b', content: '', streaming: true },\n ],\n} satisfies ResponseCompareData;\n\n// Stream BOTH columns: replace data with a NEW object per chunk.\nel.data = { ...el.data, candidates: [{ ...a, content: aText }, { ...b, content: bText }] };\n// Clear `streaming` on a candidate when it settles — the pick stays disabled\n// until BOTH have settled, then `kai-ready` fires.\n\n// Picking is terminal: emits { chosenId, rejectedIds, at } and collapses.\nel.addEventListener('kai-compare-select', (e) => {\n const { chosenId, rejectedIds } = (e as CustomEvent<CompareSelection>).detail;\n recordPreference({ prompt, chosen: chosenId, rejected: rejectedIds });\n});\n```\n\nA malformed definition (not two candidates, missing/duplicate ids) fires `kai-error` instead. The event is non-bubbling — listen on the element, not on `document`."
2068
2224
  }
2069
2225
  ];
2070
2226
  function buildText(matched) {
@@ -60,6 +60,9 @@ export type CardEvent = {
60
60
  } | {
61
61
  kind: 'dismiss';
62
62
  cardId: string;
63
+ } | {
64
+ kind: 'reopen';
65
+ cardId: string;
63
66
  } | {
64
67
  kind: 'error';
65
68
  cardId: string;
@@ -67,10 +70,13 @@ export type CardEvent = {
67
70
  };
68
71
  export type CardEventKind = CardEvent['kind'];
69
72
  /** How a card was resolved by the user — the re-hydration channel for the chromed
70
- * read-only state. Mirrors the two terminal CardEvents (minus `cardId`): the
71
- * resolution is just the event that resolved the card. `at` is optional ISO-8601
72
- * provenance (data only; never rendered). Additivedoes not bump the contract
73
- * version. */
73
+ * read-only state. The terminal kinds (`action`/`submit`) mirror the two terminal
74
+ * CardEvents (minus `cardId`): the resolution is just the event that resolved the
75
+ * card. `dismissed` is a DEFERRED (non-terminal) statethe user set the card
76
+ * aside; it can be re-opened. `expired` is terminal — a dismissed card the host
77
+ * decided can no longer be re-opened (e.g. the agent proceeded, or it went stale).
78
+ * `at` is optional ISO-8601 provenance (data only; never rendered). Additive —
79
+ * does not bump the contract version. */
74
80
  export type CardResolution = {
75
81
  kind: 'action';
76
82
  action: string;
@@ -80,6 +86,13 @@ export type CardResolution = {
80
86
  kind: 'submit';
81
87
  data: unknown;
82
88
  at?: string;
89
+ } | {
90
+ kind: 'dismissed';
91
+ at?: string;
92
+ } | {
93
+ kind: 'expired';
94
+ reason?: string;
95
+ at?: string;
83
96
  };
84
97
  /** What every card is handed (via native context or the iframe bridge). */
85
98
  export interface CardHost {
@@ -97,6 +110,9 @@ export interface CardPolicy {
97
110
  onOpen?: (url: string, target: 'tab' | 'artifact') => void;
98
111
  onState?: (cardId: string, patch: unknown) => void;
99
112
  onDismiss?: (cardId: string) => void;
113
+ /** A dismissed card asked to be re-opened. The host decides whether it can come
114
+ * back (clear the resolution → live again) or has expired (stamp `expired`). */
115
+ onReopen?: (cardId: string) => void;
100
116
  onError?: (cardId: string, message: string) => void;
101
117
  /** Cap on send-prompt: 'compose' (default) forbids silent sends. 'send' to allow. */
102
118
  maxSendPromptMode?: 'compose' | 'send';
@@ -0,0 +1,55 @@
1
+ import { CardEnvelope, CardPolicy, CardResolution } from './card-contract';
2
+ /** The minimal toast surface this helper needs. Inject a real adapter (e.g. one
3
+ * backed by `kai-toast-region`) — this module never imports the toast itself. */
4
+ export interface RecoveryToast {
5
+ show(opts: {
6
+ message: string;
7
+ action?: {
8
+ label: string;
9
+ onClick: () => void;
10
+ };
11
+ durationMs?: number;
12
+ }): {
13
+ dismiss(): void;
14
+ };
15
+ }
16
+ /** Context handed to a custom `isReopenable` predicate. */
17
+ export interface ReopenEnv {
18
+ cardId: string;
19
+ /** The card's current resolution at reopen time (should be `dismissed`). */
20
+ resolution: CardResolution | undefined;
21
+ /** `Date.now()` at reopen time (injected for testability). */
22
+ now: number;
23
+ }
24
+ export interface DismissRecoveryOptions {
25
+ /** Read the current cards array. */
26
+ get: () => CardEnvelope[];
27
+ /** Write the next cards array (a NEW reference). */
28
+ set: (next: CardEnvelope[]) => void;
29
+ /** Optional injected toast adapter for the "Dismissed · Undo" affordance. */
30
+ toast?: RecoveryToast;
31
+ /** Override the default re-openable rule. Return false to expire instead. */
32
+ isReopenable?: (env: ReopenEnv) => boolean;
33
+ /** A dismissed card older than this (ms since `dismissed.at`) is no longer
34
+ * re-openable (→ expired). Omit/Infinity = never stale. */
35
+ staleAfterMs?: number;
36
+ /** Auto-dismiss the Undo toast after this long. Default 6000ms. */
37
+ undoMs?: number;
38
+ /** Injected clock (defaults to Date.now); keeps the helper testable. */
39
+ now?: () => number;
40
+ }
41
+ /** Default re-openable rule: a card can come back unless it carries a terminal
42
+ * resolution OR it has gone stale (`staleAfterMs` elapsed since `dismissed.at`).
43
+ * A `dismissed` card with no/invalid `at` is always re-openable. */
44
+ export declare function defaultIsReopenable(env: ReopenEnv, staleAfterMs?: number): boolean;
45
+ /**
46
+ * Build the `{ onDismiss, onReopen }` policy handlers for the card dismiss/recovery
47
+ * flow over a host store (`get`/`set`).
48
+ *
49
+ * - `onDismiss(cardId)` — stamps `{ kind:'dismissed', at }` immutably and, when a
50
+ * `toast` is injected, shows "Dismissed" with an Undo that restores the card's
51
+ * prior resolution (live again when there was none).
52
+ * - `onReopen(cardId)` — clears the resolution (live) when `isReopenable`, else
53
+ * stamps `{ kind:'expired', at }`.
54
+ */
55
+ export declare function dismissRecovery(opts: DismissRecoveryOptions): Pick<CardPolicy, 'onDismiss' | 'onReopen'>;
@@ -0,0 +1,35 @@
1
+ import { ChatMessage, FeedbackVote } from '../elements/chat-types';
2
+ /** Detail shape emitted by the action row. `state` is present only for the
3
+ * toggleable feedback votes: `'on'` when a vote is set, `'off'` when cleared.
4
+ * Copy / regenerate / edit / custom actions omit it. */
5
+ export interface MessageActionDetail {
6
+ messageId: string;
7
+ action: string;
8
+ state?: 'on' | 'off';
9
+ }
10
+ export interface MessageFeedbackOptions {
11
+ /** Emit the `kai-message-action` detail (facade maps this to `dispatch` /
12
+ * `onMessageAction`). */
13
+ emit: (detail: MessageActionDetail) => void;
14
+ /** How long the copy check stays before auto-clearing. Defaults to 2000ms. */
15
+ copiedDuration?: number;
16
+ /** Container the copy/feedback toasts should be scoped to (the chat, by
17
+ * default) so they appear in-chat rather than at the page top. */
18
+ target?: () => HTMLElement | undefined;
19
+ }
20
+ export interface MessageFeedback {
21
+ /** Resolve the active vote for a message: a controlled `m.feedback` wins,
22
+ * else the facade's own optimistic map. */
23
+ resolveFeedback: (m: Pick<ChatMessage, 'id' | 'feedback'>) => FeedbackVote | undefined;
24
+ /** Whether a message's copy button should currently show its check. */
25
+ isCopied: (id: string) => boolean;
26
+ /** Route a clicked action through copy / vote / passthrough handling. */
27
+ handleAction: (m: Pick<ChatMessage, 'id' | 'content' | 'feedback'>, action: string) => void;
28
+ }
29
+ /**
30
+ * Create the action-row feedback controller for one message list (or one
31
+ * standalone message). Owns the optimistic `feedbackMap` + transient
32
+ * `copiedIds`, resolves the controlled-wins vote, and turns clicks into
33
+ * clipboard writes, toasts, and `kai-message-action` emissions.
34
+ */
35
+ export declare function createMessageFeedback(opts: MessageFeedbackOptions): MessageFeedback;
@@ -0,0 +1,88 @@
1
+ import { ToastPosition } from '../components/toast';
2
+ export type ToastVariant = 'neutral' | 'success';
3
+ export interface ToastConfig {
4
+ stack?: 'expanded' | 'collapsed';
5
+ position?: ToastPosition;
6
+ max?: number;
7
+ }
8
+ /**
9
+ * Configure the imperative `toast()` singleton — call once at app start.
10
+ * `toast.success('…')` has no element to set a prop on, so this is how you opt
11
+ * the auto-mounted region into collapsed stacking / a position / a max. Updates
12
+ * any already-mounted regions too, so call order doesn't matter.
13
+ */
14
+ export declare function configureToasts(config: ToastConfig): void;
15
+ /** An action button rendered inside the toast. Returning `false` from
16
+ * `onAction` keeps the toast open (e.g. to show a follow-up); any other
17
+ * return value dismisses it. */
18
+ export interface ToastAction {
19
+ label: string;
20
+ onAction: () => void | false;
21
+ }
22
+ export interface ToastItem {
23
+ id: string;
24
+ message: string;
25
+ variant?: ToastVariant;
26
+ action?: ToastAction;
27
+ /** Auto-dismiss delay in ms. `0` = sticky (never auto-dismisses). When an
28
+ * `action` is present the effective floor is 4000ms so it stays long enough
29
+ * to act on. Defaults to 2000ms. */
30
+ duration?: number;
31
+ /** Whether the × close affordance is shown. Defaults to `true`. */
32
+ dismissible?: boolean;
33
+ /** Container to scope this toast WITHIN — it floats anchored to that element's
34
+ * bounds instead of the viewport. Omit for a global, viewport-anchored toast.
35
+ * The chat targets itself by default so its copy/feedback toasts stay in-chat. */
36
+ target?: HTMLElement;
37
+ }
38
+ /** Options accepted by `toast()` — everything but the message. Pass `id` to
39
+ * update an existing toast in place. */
40
+ export interface ToastOptions {
41
+ id?: string;
42
+ variant?: ToastVariant;
43
+ action?: ToastAction;
44
+ duration?: number;
45
+ dismissible?: boolean;
46
+ /** Scope this toast to a container's bounds (e.g. a chat) instead of the
47
+ * viewport. Omit for a global, viewport-anchored toast. */
48
+ target?: HTMLElement;
49
+ }
50
+ /** Handle returned from `toast()` for imperative control. */
51
+ export interface ToastHandle {
52
+ id: string;
53
+ dismiss: () => void;
54
+ update: (patch: Partial<Omit<ToastItem, 'id'>>) => void;
55
+ }
56
+ /** Default auto-dismiss delay. Long enough to read + reach before it leaves. */
57
+ export declare const DEFAULT_TOAST_DURATION = 5000;
58
+ /** Minimum auto-dismiss delay when the toast carries an action (e.g. Undo) — it
59
+ * has to stay up long enough to actually act on. */
60
+ export declare const ACTION_TOAST_FLOOR = 7000;
61
+ /** The live toast list (reactive). The region facade binds to this. */
62
+ export declare function getToasts(): ToastItem[];
63
+ /** Resolve the effective duration, applying the action floor. */
64
+ declare function resolveDuration(item: Pick<ToastItem, 'duration' | 'action'>): number;
65
+ export declare function ensureMounted(target?: HTMLElement | null): HTMLElement | undefined;
66
+ /** Test-only: whether `ensureMounted` has run. */
67
+ export declare function isToastRegionMounted(): boolean;
68
+ export interface ToastFn {
69
+ (message: string, opts?: ToastOptions): ToastHandle;
70
+ /** Raise a success (emerald check) toast. */
71
+ success: (message: string, opts?: ToastOptions) => ToastHandle;
72
+ /** Dismiss a toast by id. */
73
+ dismiss: (id: string) => void;
74
+ /** Dismiss every active toast. */
75
+ clear: () => void;
76
+ }
77
+ /**
78
+ * Raise a transient toast. Returns a `{ id, dismiss, update }` handle.
79
+ *
80
+ * ```ts
81
+ * toast('Copied to clipboard');
82
+ * toast.success('Saved');
83
+ * const t = toast('Working…', { duration: 0 });
84
+ * t.update({ message: 'Done', variant: 'success', duration: 2000 });
85
+ * ```
86
+ */
87
+ export declare const toast: ToastFn;
88
+ export { resolveDuration };