@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
@@ -223,6 +223,53 @@ const RULES: Rule[] = [
223
223
  "// Use `integration: 'mock'` in the scaffold tool\n" +
224
224
  '```',
225
225
  },
226
+ {
227
+ // Rule 9 — reduce bundle size / footprint / "how much does @kitn.ai/ui add"
228
+ // Source: dist/elements/<file>.js per-element exports; dist/autoloader.js
229
+ id: 'bundle-footprint',
230
+ test: (t) =>
231
+ /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(
232
+ t,
233
+ ),
234
+ title: 'Reducing bundle footprint — three load modes',
235
+ cause:
236
+ 'The default `import \'@kitn.ai/ui/elements\'` registers every `kai-*` element. ' +
237
+ 'If your page uses only one or two elements, that pulls in the full ~119 KB gz bundle. ' +
238
+ 'Two opt-in modes let you load only what you need.',
239
+ fix:
240
+ '**Mode 1 — register-all (default, SSR-safe):**\n' +
241
+ 'Best for multi-element apps or any SSR/meta-framework. ' +
242
+ 'Load once and every `kai-*` element is available.\n\n' +
243
+ '```js\n' +
244
+ "import '@kitn.ai/ui/elements'; // ~119 KB gz — registers everything\n" +
245
+ '```\n\n' +
246
+ '**Mode 2 — per-element import (tree-shaking, bundler apps):**\n' +
247
+ 'Use `import \'@kitn.ai/ui/elements/<file>\'` to register only one element. ' +
248
+ 'A bundler (Vite, webpack, Rollup) will tree-shake to just its chunks (~73 KB gz for `kai-chat` alone). ' +
249
+ 'Client-only — do not use in SSR entry points.\n\n' +
250
+ '```js\n' +
251
+ "// Registers only <kai-chat> (~73 KB gz vs ~119 KB gz register-all)\n" +
252
+ "import '@kitn.ai/ui/elements/chat';\n\n" +
253
+ "// Other examples:\n" +
254
+ "import '@kitn.ai/ui/elements/code-block'; // <kai-code-block>\n" +
255
+ "import '@kitn.ai/ui/elements/confirm-card'; // <kai-confirm>\n" +
256
+ '```\n\n' +
257
+ 'The file name is the element\'s source basename from `element-manifest.json` ' +
258
+ '(e.g. `kai-chat` → `chat`, `kai-confirm` → `confirm-card`).\n\n' +
259
+ '**Mode 3 — autoloader (no-build / CDN pages only):**\n' +
260
+ 'Watches the DOM and dynamically imports each `kai-*` element\'s module on demand. ' +
261
+ 'A page that uses only `<kai-chat>` never downloads the other elements. ' +
262
+ 'It is a CDN / static-file tool — load it from a `<script type="module">` tag. ' +
263
+ 'It is NOT importable through a bundler: Vite/webpack relocate it and the on-demand imports 404. ' +
264
+ 'Client-only.\n\n' +
265
+ '```html\n' +
266
+ '<script type="module" src="https://cdn.jsdelivr.net/npm/@kitn.ai/ui@<version>/dist/elements/autoloader.js"></script>\n' +
267
+ '```\n\n' +
268
+ 'In a BUNDLED app (Vite/webpack/Next) use Mode 1 or Mode 2 instead — not the autoloader.\n\n' +
269
+ '**SSR note:** use Mode 1 (register-all) in SSR apps — ' +
270
+ 'per-element imports and the autoloader are client-only (they call DOM APIs at module eval). ' +
271
+ 'Modes 1 & 2 are side-effect imports; keep them even if your linter flags them as "unused".',
272
+ },
226
273
  {
227
274
  // Rule 5 — SSR / server component / document is not defined
228
275
  // Source: for-ai-agents.mdx (client-only import); context7.json rule 2 (property rule requires DOM)
@@ -256,6 +303,143 @@ const RULES: Rule[] = [
256
303
  "import { Chat } from '@kitn.ai/ui/react';\n" +
257
304
  '```',
258
305
  },
306
+ {
307
+ // Rule 10 — toast() is the imperative API; there is no <kai-toast> to place
308
+ // Source: src/primitives/toast-store.ts (the `toast` fn + auto-mounted region)
309
+ id: 'toast-imperative',
310
+ test: (t) => {
311
+ // Placing a toast element by hand (the region auto-mounts — you never write it).
312
+ if (/<kai-toast(-region)?\b/.test(t)) return true;
313
+ // Asking how to show / trigger a toast or notification with kai context.
314
+ if (
315
+ /\btoast(s)?\b|notification|snackbar/i.test(t) &&
316
+ /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)
317
+ )
318
+ return true;
319
+ return false;
320
+ },
321
+ title: 'Toast is an imperative call — `toast(\'…\')`, not a `<kai-toast>` you place',
322
+ cause:
323
+ 'Toasts are raised IMPERATIVELY by calling `toast(message)` — there is no ' +
324
+ '`<kai-toast>` element you add to your markup. The first call lazily mounts ONE ' +
325
+ '`<kai-toast-region>` on `document.body` (a real, kit-styled, viewport-positioned ' +
326
+ 'element) and every later toast feeds that same region. Trying to place a toast ' +
327
+ 'element by hand, or looking for a `messages`/`toasts` prop to push into, is the ' +
328
+ 'wrong model.',
329
+ fix:
330
+ 'Import `toast` and call it. It is exported from BOTH the root `@kitn.ai/ui` and ' +
331
+ 'the `@kitn.ai/ui/elements` bundle, so the web-components-only consumer gets it too. ' +
332
+ 'It is SSR-safe (no DOM is touched until the first call on the client).\n\n' +
333
+ '```js\n' +
334
+ "import { toast } from '@kitn.ai/ui/elements'; // or '@kitn.ai/ui'\n\n" +
335
+ "// ✅ Fire-and-forget\n" +
336
+ "toast('Copied to clipboard');\n" +
337
+ "toast.success('Saved');\n\n" +
338
+ "// ✅ With an Undo action + an imperative handle\n" +
339
+ "const t = toast('Item deleted', {\n" +
340
+ " action: { label: 'Undo', onAction: () => restore() },\n" +
341
+ '});\n' +
342
+ "t.update({ message: 'Restored', variant: 'success' });\n" +
343
+ 't.dismiss();\n' +
344
+ '```\n\n' +
345
+ 'The auto-mounted `<kai-toast-region>` carries its own shadow root + kit styles — ' +
346
+ 'do NOT add a `<kai-toast-region>` tag yourself unless you deliberately want a ' +
347
+ 'second, declaratively-controlled region.',
348
+ },
349
+ {
350
+ // Rule 11 — dismissed cards are DEFERRED (reopenable stub), not deleted
351
+ // Source: src/primitives/card-recovery.ts (dismissRecovery) + the dismissed stub
352
+ id: 'card-dismiss-deferred',
353
+ test: (t) => {
354
+ const cardCtx = /\bcard(s)?\b|envelope|kai-card|kai-cards|kai-confirm|kai-choice|kai-tasks|kai-form|generative.?ui|resolution|dismissRecovery/i;
355
+ if (!cardCtx.test(t)) return false;
356
+ // dismiss / reopen / undo / disappear / filter-out signals in a card context.
357
+ return /dismiss|reopen|re-?open|\bundo\b|disappear|remove.*card|card.*(gone|remove|delete|vanish)|filter.*out|stub/i.test(t);
358
+ },
359
+ title: 'Dismissed cards are DEFERRED (a reopenable stub), not deleted',
360
+ cause:
361
+ 'Dismissing a generative-UI card does NOT delete its envelope from history. The ' +
362
+ 'card stamps a `{ kind: \'dismissed\' }` resolution onto its envelope and collapses ' +
363
+ 'to a small reopenable stub ("Proposed: <title> — dismissed · Reopen"). If you ' +
364
+ 'filter `dismissed` envelopes out of your cards array, the stub vanishes and the ' +
365
+ 'user can never reopen it — and you lose the audit trail of what was proposed.',
366
+ fix:
367
+ 'Keep dismissed envelopes in the array. Wire dismiss/reopen with `dismissRecovery()` ' +
368
+ '(from `@kitn.ai/ui`), which builds the `onDismiss`/`onReopen` half of a `CardPolicy` ' +
369
+ 'over your store and can show a "Dismissed · Undo" toast via an injected adapter.\n\n' +
370
+ '```ts\n' +
371
+ "import { dismissRecovery } from '@kitn.ai/ui';\n" +
372
+ "import { toast } from '@kitn.ai/ui/elements';\n\n" +
373
+ '// Adapter: map dismissRecovery\'s toast shape onto the imperative toast().\n' +
374
+ 'const toastAdapter = {\n' +
375
+ ' show: ({ message, action, durationMs }) => {\n' +
376
+ " const handle = toast(message, {\n" +
377
+ ' duration: durationMs,\n' +
378
+ ' action: action && { label: action.label, onAction: action.onClick },\n' +
379
+ ' });\n' +
380
+ ' return { dismiss: handle.dismiss };\n' +
381
+ ' },\n' +
382
+ '};\n\n' +
383
+ 'const { onDismiss, onReopen } = dismissRecovery({\n' +
384
+ ' get: () => cards, // your current envelopes\n' +
385
+ ' set: (next) => setCards(next), // NEW array reference (never mutate in place)\n' +
386
+ ' toast: toastAdapter,\n' +
387
+ '});\n' +
388
+ '// Pass these on the CardPolicy you hand to <kai-cards> / <kai-remote>.\n' +
389
+ '```\n\n' +
390
+ '`onDismiss` writes `dismissed` immutably (Undo restores the prior resolution); ' +
391
+ '`onReopen` clears it back to live (or stamps `expired` when the host says the card ' +
392
+ 'is no longer reopenable). Never mutate the array in place — re-render needs a new ref.',
393
+ },
394
+ {
395
+ // Rule 12 — kai-compare contract: two candidates, JS data prop, stream both, terminal pick
396
+ // Source: src/elements/compare.tsx + src/components/response-compare-types.ts
397
+ id: 'compare-contract',
398
+ test: (t) => {
399
+ if (/<kai-compare\b|kai-compare-select|ResponseCompareData|response.?compare/i.test(t)) return true;
400
+ // "compare two responses / candidates / A vs B" with a kai/UI context.
401
+ if (
402
+ /compar(e|ing|ison)|side.by.side|a\/b|two\s+(responses|candidates|answers|completions)|dual.?response/i.test(t) &&
403
+ /kai-|@kitn|candidate|prefer(ence)?|chosen|reject/i.test(t)
404
+ )
405
+ return true;
406
+ return false;
407
+ },
408
+ title: '`kai-compare` — two candidates, `data` as a JS property, terminal pick',
409
+ cause:
410
+ '`<kai-compare>` shows EXACTLY two assistant candidates for one prompt and lets the ' +
411
+ 'user pick the better one. The `data` value is an array/object, so it must be set as ' +
412
+ 'a JS PROPERTY (never an HTML attribute). Both candidates can stream — but, like ' +
413
+ '`kai-chat`, that needs a NEW `data` reference per chunk (mutating in place will not ' +
414
+ 're-render). The pick is a COMMIT (not a Submit): it fires once and the card collapses.',
415
+ fix:
416
+ 'Set `data` in JS with two candidates, stream by reassigning a fresh `data` object ' +
417
+ 'per chunk, and listen for `kai-compare-select` directly on the element.\n\n' +
418
+ '```ts\n' +
419
+ "import { toast } from '@kitn.ai/ui/elements';\n" +
420
+ "import type { ResponseCompareData, CompareSelection } from '@kitn.ai/ui';\n\n" +
421
+ "const el = document.querySelector('kai-compare')!;\n" +
422
+ '// data is a JS PROPERTY — exactly two candidates, each with a unique id.\n' +
423
+ 'el.data = {\n' +
424
+ " prompt: 'Summarise the report',\n" +
425
+ ' candidates: [\n' +
426
+ " { id: 'a', content: '', streaming: true },\n" +
427
+ " { id: 'b', content: '', streaming: true },\n" +
428
+ ' ],\n' +
429
+ '} satisfies ResponseCompareData;\n\n' +
430
+ '// Stream BOTH columns: replace data with a NEW object per chunk.\n' +
431
+ "el.data = { ...el.data, candidates: [{ ...a, content: aText }, { ...b, content: bText }] };\n" +
432
+ '// Clear `streaming` on a candidate when it settles — the pick stays disabled\n' +
433
+ '// until BOTH have settled, then `kai-ready` fires.\n\n' +
434
+ "// Picking is terminal: emits { chosenId, rejectedIds, at } and collapses.\n" +
435
+ "el.addEventListener('kai-compare-select', (e) => {\n" +
436
+ ' const { chosenId, rejectedIds } = (e as CustomEvent<CompareSelection>).detail;\n' +
437
+ ' recordPreference({ prompt, chosen: chosenId, rejected: rejectedIds });\n' +
438
+ '});\n' +
439
+ '```\n\n' +
440
+ 'A malformed definition (not two candidates, missing/duplicate ids) fires `kai-error` ' +
441
+ 'instead. The event is non-bubbling — listen on the element, not on `document`.',
442
+ },
259
443
  ];
260
444
 
261
445
  function buildText(matched: Rule[]): string {
@@ -1530,7 +1530,139 @@ function compose(
1530
1530
  envLines,
1531
1531
  ].join('\n');
1532
1532
 
1533
- return [header, block1, block2, block3].join('\n\n');
1533
+ // SCAF-16: loading-options note — inform consumers about the two opt-in load modes
1534
+ // (per-element tree-shaking + autoloader) without changing the default import above.
1535
+ // Leads with "the default is right" rather than a size headline; the debug tool
1536
+ // carries the full KB breakdown for developers who ask for it.
1537
+ const block4 = [
1538
+ `=== LOADING OPTIONS ===`,
1539
+ ``,
1540
+ `The scaffold uses \`import '@kitn.ai/ui/elements'\` (register-all) — the right`,
1541
+ `default: it registers every kai-* element and is SSR-safe, so leave it as is.`,
1542
+ `Two opt-in modes load less if a page only ever uses a few elements:`,
1543
+ ``,
1544
+ ` Per-element (bundler apps): import '@kitn.ai/ui/elements/<file>'`,
1545
+ ` Registers just that element; your bundler tree-shakes the rest away.`,
1546
+ ` Example: import '@kitn.ai/ui/elements/chat' (client-only — not for SSR)`,
1547
+ ``,
1548
+ ` Autoloader (no-build / CDN pages): a <script type="module"> tag pointing at`,
1549
+ ` dist/elements/autoloader.js — loads each kai-* element on demand as it`,
1550
+ ` appears in the DOM. A CDN/static-file tool; not importable through a bundler.`,
1551
+ ``,
1552
+ `Run the debug tool with "reduce bundle size" for the full breakdown and sizes.`,
1553
+ ].join('\n');
1554
+
1555
+ // SCAF-17: interaction patterns — small, copy-pasteable snippets for the
1556
+ // toast / card-recovery / preference-capture features. Appended as a reference
1557
+ // section so the consumer can wire confirmations, undo, and A/B preference
1558
+ // capture without leaving the scaffold. Does not change blocks 1–4.
1559
+ const block5 = interactionPatternsBlock();
1560
+
1561
+ return [header, block1, block2, block3, block4, block5].join('\n\n');
1562
+ }
1563
+
1564
+ // ── SCAF-17: reusable interaction-pattern snippets ─────────────────────────────
1565
+
1566
+ /**
1567
+ * The three interaction patterns from the chat-interactions feature set, emitted
1568
+ * as a labeled reference section the scaffolder always appends:
1569
+ * 1. `toast()` confirmation + Undo.
1570
+ * 2. `dismissRecovery()` card-policy wiring (with a toast adapter).
1571
+ * 3. `kai-compare` preference capture (two streams → kai-compare-select →
1572
+ * recordPreference({ prompt, chosen, rejected })).
1573
+ *
1574
+ * Framework-agnostic (plain TS / DOM) so it drops into any of the front-end
1575
+ * targets; the imports are valid from `@kitn.ai/ui` (and `toast` also from
1576
+ * `@kitn.ai/ui/elements`).
1577
+ */
1578
+ function interactionPatternsBlock(): string {
1579
+ const toastPattern = [
1580
+ `--- Pattern: toast() — confirmation + Undo ---`,
1581
+ `// toast is IMPERATIVE — call it; there is no <kai-toast> to place. The first`,
1582
+ `// call auto-mounts one <kai-toast-region> on document.body. Exported from`,
1583
+ `// both '@kitn.ai/ui' and '@kitn.ai/ui/elements'.`,
1584
+ `import { toast } from '@kitn.ai/ui/elements';`,
1585
+ ``,
1586
+ `toast('Copied to clipboard'); // neutral, auto-dismisses`,
1587
+ `toast.success('Saved'); // emerald success variant`,
1588
+ ``,
1589
+ `// Undo affordance: an action floors the duration so there's time to act.`,
1590
+ `const t = toast('Item deleted', {`,
1591
+ ` action: { label: 'Undo', onAction: () => restoreItem() },`,
1592
+ `});`,
1593
+ `// t.update({ message: 'Restored', variant: 'success' }); t.dismiss();`,
1594
+ ``,
1595
+ `// Collapsed (Sonner-style) stacking — toasts pile + expand on hover/focus.`,
1596
+ `// Opt in once at startup, or per-region via <kai-toast-region stack="collapsed">.`,
1597
+ `import { configureToasts } from '@kitn.ai/ui/elements';`,
1598
+ `configureToasts({ stack: 'collapsed' });`,
1599
+ ].join('\n');
1600
+
1601
+ const recoveryPattern = [
1602
+ `--- Pattern: dismissRecovery() — card dismiss + Undo (DEFERRED, not deleted) ---`,
1603
+ `// Dismissing a generative-UI card does NOT delete its envelope — it stamps a`,
1604
+ `// 'dismissed' resolution and collapses to a reopenable stub. Keep dismissed`,
1605
+ `// envelopes in your array; wire the policy with dismissRecovery().`,
1606
+ `import { dismissRecovery } from '@kitn.ai/ui';`,
1607
+ `import { toast } from '@kitn.ai/ui/elements';`,
1608
+ ``,
1609
+ `// Adapter: map dismissRecovery's toast shape onto the imperative toast().`,
1610
+ `const toastAdapter = {`,
1611
+ ` show: ({ message, action, durationMs }) => {`,
1612
+ ` const h = toast(message, {`,
1613
+ ` duration: durationMs,`,
1614
+ ` action: action && { label: action.label, onAction: action.onClick },`,
1615
+ ` });`,
1616
+ ` return { dismiss: h.dismiss };`,
1617
+ ` },`,
1618
+ `};`,
1619
+ ``,
1620
+ `const { onDismiss, onReopen } = dismissRecovery({`,
1621
+ ` get: () => cards, // your current envelopes`,
1622
+ ` set: (next) => setCards(next), // a NEW array reference (never mutate in place)`,
1623
+ ` toast: toastAdapter,`,
1624
+ `});`,
1625
+ `// Hand { onDismiss, onReopen } to the CardPolicy on <kai-cards> / <kai-remote>.`,
1626
+ ].join('\n');
1627
+
1628
+ const preferencePattern = [
1629
+ `--- Pattern: kai-compare — capture an A/B preference pair ---`,
1630
+ `// <kai-compare> shows EXACTLY two candidates for one prompt. data is a JS`,
1631
+ `// PROPERTY; stream both columns with a fresh data ref per chunk; picking is`,
1632
+ `// terminal and fires kai-compare-select { chosenId, rejectedIds }.`,
1633
+ `import type { ResponseCompareData, CompareSelection } from '@kitn.ai/ui';`,
1634
+ ``,
1635
+ `const el = document.querySelector('kai-compare')!;`,
1636
+ `el.data = {`,
1637
+ ` prompt,`,
1638
+ ` candidates: [`,
1639
+ ` { id: 'a', content: '', streaming: true },`,
1640
+ ` { id: 'b', content: '', streaming: true },`,
1641
+ ` ],`,
1642
+ `} satisfies ResponseCompareData;`,
1643
+ ``,
1644
+ `// Stream BOTH: replace data with a NEW object per chunk; clear streaming when`,
1645
+ `// a candidate settles. The pick unlocks once both have settled (kai-ready).`,
1646
+ `// el.data = { ...el.data, candidates: [{ ...a, content: aText }, { ...b, content: bText }] };`,
1647
+ ``,
1648
+ `el.addEventListener('kai-compare-select', (e) => {`,
1649
+ ` const { chosenId, rejectedIds } = (e as CustomEvent<CompareSelection>).detail;`,
1650
+ ` recordPreference({ prompt, chosen: chosenId, rejected: rejectedIds });`,
1651
+ `});`,
1652
+ ].join('\n');
1653
+
1654
+ return [
1655
+ `=== INTERACTION PATTERNS ===`,
1656
+ ``,
1657
+ `Optional snippets for confirmations, card recovery, and preference capture.`,
1658
+ `Drop in the one(s) you need; all imports resolve from @kitn.ai/ui.`,
1659
+ ``,
1660
+ toastPattern,
1661
+ ``,
1662
+ recoveryPattern,
1663
+ ``,
1664
+ preferencePattern,
1665
+ ].join('\n');
1534
1666
  }
1535
1667
 
1536
1668
  // ── error text ────────────────────────────────────────────────────────────────
@@ -1,10 +1,9 @@
1
1
  import { createSignal, For, Show } from 'solid-js';
2
2
  import { ChatConfig, useChatConfig } from '../primitives/chat-config';
3
3
  import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container';
4
- import { Message, MessageAvatar, MessageContent, MessageActionBar } from './message';
5
- import { Reasoning, ReasoningTrigger, ReasoningContent } from './reasoning';
6
- import { Tool } from './tool';
7
- import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from './attachments';
4
+ import { Message, MessageAvatar, MessageBody } from './message';
5
+ import { type AttachmentData } from './attachments';
6
+ import { createMessageFeedback, type MessageActionDetail } from '../primitives/message-feedback';
8
7
  import { ModelSwitcher } from './model-switcher';
9
8
  import { ScrollButton } from './scroll-button';
10
9
  import {
@@ -95,7 +94,7 @@ export interface ChatThreadProps {
95
94
  onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void;
96
95
  onSuggestionClick?: (value: string) => void;
97
96
  onModelChange?: (modelId: string) => void;
98
- onMessageAction?: (detail: { messageId: string; action: string }) => void;
97
+ onMessageAction?: (detail: MessageActionDetail) => void;
99
98
  onSearch?: () => void;
100
99
  onVoice?: () => void;
101
100
  onSlashSelect?: (command: SlashCommandItem) => void;
@@ -104,6 +103,15 @@ export interface ChatThreadProps {
104
103
  export function ChatThread(props: ChatThreadProps) {
105
104
  const outer = useChatConfig();
106
105
  const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
106
+ // Feedback (copy + vote) state lives ABOVE the per-message <For>, so streaming
107
+ // re-renders (a fresh `messages` array ref per chunk) don't wipe it.
108
+ // The copy/feedback toasts scope to the chat (this thread's root) so they appear
109
+ // in-chat rather than at the page top.
110
+ let rootEl: HTMLElement | undefined;
111
+ const feedback = createMessageFeedback({
112
+ emit: (detail) => props.onMessageAction?.(detail),
113
+ target: () => rootEl,
114
+ });
107
115
  const [internal, setInternal] = createSignal(props.value ?? '');
108
116
  const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
109
117
  const current = () => props.value ?? internal();
@@ -122,7 +130,7 @@ export function ChatThread(props: ChatThreadProps) {
122
130
 
123
131
  return (
124
132
  <ChatConfig proseSize={props.proseSize} codeTheme={props.codeTheme} codeHighlight={props.codeHighlight !== false} portalMount={outer.portalMount()}>
125
- <div class={`flex h-full flex-col bg-background ${props.class ?? ''}`}>
133
+ <div ref={(e) => (rootEl = e as HTMLElement)} class={`flex h-full flex-col bg-background ${props.class ?? ''}`}>
126
134
  <Show when={showHeader()}>
127
135
  <header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
128
136
  <div class="flex items-center gap-2">
@@ -168,35 +176,19 @@ export function ChatThread(props: ChatThreadProps) {
168
176
  <For each={props.messages}>
169
177
  {(m) => {
170
178
  const body = (
171
- <>
172
- <Show when={m.reasoning}>
173
- <Reasoning class="mb-2 w-full">
174
- <ReasoningTrigger>{m.reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
175
- <ReasoningContent markdown>{m.reasoning!.text}</ReasoningContent>
176
- </Reasoning>
177
- </Show>
178
- <For each={m.tools ?? []}>{(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}</For>
179
- <Show when={m.attachments?.length}>
180
- <Attachments variant="inline" class={m.role === 'user' ? 'mb-2 justify-end' : 'mb-2'}>
181
- <For each={m.attachments!}>
182
- {(att) => (<Attachment data={att}><AttachmentPreview /><AttachmentInfo /></Attachment>)}
183
- </For>
184
- </Attachments>
185
- </Show>
186
- <MessageContent
187
- markdown={m.role === 'assistant'}
188
- class={m.role === 'user' ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2' : 'bg-transparent p-0'}
189
- >
190
- {m.content}
191
- </MessageContent>
192
- <Show when={m.actions?.length}>
193
- <MessageActionBar
194
- actions={m.actions!}
195
- reveal={reveal()}
196
- onAction={(action) => props.onMessageAction?.({ messageId: m.id, action })}
197
- />
198
- </Show>
199
- </>
179
+ <MessageBody
180
+ content={m.content}
181
+ reasoning={m.reasoning}
182
+ tools={m.tools}
183
+ attachments={m.attachments}
184
+ isUser={m.role === 'user'}
185
+ markdown={m.role === 'assistant'}
186
+ actions={m.actions}
187
+ actionsReveal={reveal()}
188
+ activeFeedback={feedback.resolveFeedback(m)}
189
+ copied={feedback.isCopied(m.id)}
190
+ onAction={(action) => feedback.handleAction(m, action)}
191
+ />
200
192
  );
201
193
  const rowGroup = reveal() === 'hover' ? 'group ' : '';
202
194
  return (
@@ -15,11 +15,12 @@ import { cn } from '../utils/cn';
15
15
  import { Button } from '../ui/button';
16
16
  import { HoverCard } from '../ui/hover-card';
17
17
  import { Card } from './card';
18
+ import { DismissedStub } from './dismissed-stub';
18
19
  import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
19
20
  import { useCardResolution } from './use-card-resolution';
20
21
  import { emitCardEvent } from '../primitives/card-routing';
21
22
  import { useCardHost } from '../primitives/card-host';
22
- import { Check } from 'lucide-solid';
23
+ import { Check, X } from 'lucide-solid';
23
24
 
24
25
  // ─────────────────────────────────────────────────────────────────────────────
25
26
  // Types (choice.schema.json) — see src/primitives/card-schemas/choice.schema.json
@@ -49,6 +50,7 @@ export interface ChoiceCardData {
49
50
  options: ChoiceOption[]; // 1..N
50
51
  allowOther?: ChoiceAllowOther; // free-text escape
51
52
  submitLabel?: string; // label for the Submit button (default 'Submit')
53
+ dismissible?: boolean; // show a close affordance that emits `dismiss`
52
54
  }
53
55
 
54
56
  export type ChoiceCardEnvelope = CardEnvelope<'choice', ChoiceCardData>;
@@ -310,6 +312,15 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
310
312
  });
311
313
  };
312
314
 
315
+ // Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
316
+ // the card collapses to its re-openable stub immediately.
317
+ const onDismiss = (): void => {
318
+ if (res.isResolved()) return;
319
+ emit({ kind: 'dismiss', cardId: local.cardId });
320
+ res.setLocal({ kind: 'dismissed' });
321
+ };
322
+ const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
323
+
313
324
  const focusRadio = (index: number): void => {
314
325
  const radios = groupRef?.querySelectorAll<HTMLElement>('[role="radio"]');
315
326
  radios?.[index]?.focus();
@@ -353,6 +364,10 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
353
364
  return <Card heading={local.heading} errorMessage="The card failed to render." />;
354
365
  }}
355
366
  >
367
+ <Show
368
+ when={!res.isDeferred()}
369
+ fallback={<DismissedStub type={CHOICE_CARD_TYPE} title={local.heading} onReopen={onReopen} />}
370
+ >
356
371
  <Card heading={local.heading}>
357
372
  <div class={cn('flex flex-col gap-3', local.class)}>
358
373
  <Show when={local.data?.prompt}>
@@ -421,12 +436,26 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
421
436
  </div>
422
437
  </Show>
423
438
 
424
- <Button type="button" class="self-end" disabled={!canSubmit()} onClick={submit}>
425
- {submitLabel()}
426
- </Button>
439
+ <div class="flex w-full flex-wrap items-center justify-between gap-2">
440
+ <Show when={local.data?.dismissible === true}>
441
+ <Button
442
+ type="button"
443
+ variant="ghost"
444
+ size="icon-sm"
445
+ aria-label="Dismiss"
446
+ onClick={onDismiss}
447
+ >
448
+ <X size={16} aria-hidden="true" />
449
+ </Button>
450
+ </Show>
451
+ <Button type="button" class="ml-auto" disabled={!canSubmit()} onClick={submit}>
452
+ {submitLabel()}
453
+ </Button>
454
+ </div>
427
455
  </Show>
428
456
  </div>
429
457
  </Card>
458
+ </Show>
430
459
  </ErrorBoundary>
431
460
  </Show>
432
461
  );