@kitn.ai/ui 0.27.0 → 0.28.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 (266) hide show
  1. package/README.md +10 -2
  2. package/dist/{Icon-BnFKqqtY.js → Icon-DObhsRtm.js} +1 -1
  3. package/dist/{action-icons-CBskfBMz.js → action-icons-CcgNN2oT.js} +1 -1
  4. package/dist/arrow-down-CjRU3JgV.js +1 -0
  5. package/dist/arrow-left-Bnpb1UEW.js +1 -0
  6. package/dist/{artifact-CvXEm1lS.js → artifact-Bwt7OgcR.js} +1 -1
  7. package/dist/{attachments-C43ZegEq.js → attachments-Qr4kg-2b.js} +1 -1
  8. package/dist/{audio-visualizer-B-79y1Tj.js → audio-visualizer-DToYdfSc.js} +1 -1
  9. package/dist/{badge-BpUbq8c0.js → badge-CdEu_jn2.js} +1 -1
  10. package/dist/{bell-sn17Ip9v.js → bell-DQAAOpDz.js} +1 -1
  11. package/dist/{button-BH6rN0U-.js → button-DkuOnGLy.js} +1 -1
  12. package/dist/{card-renderer-Cb1gQmth.js → card-renderer-Jx70m6_l.js} +1 -1
  13. package/dist/{card-routing-DifQgS7n.js → card-routing-0YidER_3.js} +1 -1
  14. package/dist/check-njkVdZu2.js +1 -0
  15. package/dist/{checkbox-Dc9Yov_B.js → checkbox-D7iRB3nL.js} +1 -1
  16. package/dist/{checkbox-group-B1kDLLYk.js → checkbox-group-B2_rpoPM.js} +1 -1
  17. package/dist/chevron-down-B72s6PXR.js +1 -0
  18. package/dist/chevron-right-BatuHzUm.js +1 -0
  19. package/dist/{choice-card-BT3c1S8g.js → choice-card-m0BYqqOK.js} +1 -1
  20. package/dist/circle-check-B62pODB3.js +1 -0
  21. package/dist/{circle-x-wobWkoSx.js → circle-x-BPG0iPst.js} +1 -1
  22. package/dist/circle-zx0ByzY9.js +1 -0
  23. package/dist/code-block-ClMLKb7C.js +1 -0
  24. package/dist/{collapsible-CPJbf6FM.js → collapsible-B4DlWicL.js} +1 -1
  25. package/dist/components/chat-thread.d.ts +89 -1
  26. package/dist/components/conversation-item.d.ts +16 -0
  27. package/dist/components/conversation-panel.d.ts +29 -0
  28. package/dist/components/home-panel.d.ts +34 -0
  29. package/dist/components/widget-tab-bar.d.ts +16 -0
  30. package/dist/{composer-CjngLzal.js → composer-sQ0HcvyG.js} +3 -3
  31. package/dist/{confirm-card-DCx_A4bU.js → confirm-card-rQWDQsnC.js} +1 -1
  32. package/dist/construct-cli.es.js +182 -14
  33. package/dist/{context-BY8V-Nlp.js → context-0kg4ZX1J.js} +1 -1
  34. package/dist/controllable-CnGpMF6r.js +1 -0
  35. package/dist/conversation-item-BxwQLaFJ.js +1 -0
  36. package/dist/conversation-list-BCfGN1JU.js +1 -0
  37. package/dist/{copy-B6DO4PqB.js → copy-n9u9rqpi.js} +1 -1
  38. package/dist/{create-tween-CnMHviKB.js → create-tween-C-q-JCM-.js} +1 -1
  39. package/dist/{create-tween-D1aiGZZh.js → create-tween-CRrJXxBd.js} +1 -1
  40. package/dist/{create-tween-BPzaefTp.js → create-tween-DZ3RRDHD.js} +1 -1
  41. package/dist/{create-tween-DUSQUKpr.js → create-tween-uPkArjIG.js} +1 -1
  42. package/dist/custom-elements.json +51 -2
  43. package/dist/{default-input-CucQEOqH.js → default-input-CaMrJkgH.js} +1 -1
  44. package/dist/define-By-uL588.js +1 -0
  45. package/dist/define.js +2 -2
  46. package/dist/define.server.js +2 -2
  47. package/dist/{disclosure-CbLtajUQ.js → disclosure-WUImBbDv.js} +1 -1
  48. package/dist/{dropdown-BwhViJFd.js → dropdown-wZVeJOMd.js} +1 -1
  49. package/dist/elements/agent-card.js +1 -1
  50. package/dist/elements/artifact.js +1 -1
  51. package/dist/elements/attachments.js +1 -1
  52. package/dist/elements/audio-visualizer.js +1 -1
  53. package/dist/elements/avatar.js +1 -1
  54. package/dist/elements/badge.js +1 -1
  55. package/dist/elements/button.js +1 -1
  56. package/dist/elements/card.js +1 -1
  57. package/dist/elements/cards.js +1 -1
  58. package/dist/elements/chain-of-thought.js +1 -1
  59. package/dist/elements/chat-scope-picker.js +1 -1
  60. package/dist/elements/chat-workspace.js +1 -1
  61. package/dist/elements/chat.js +1 -1
  62. package/dist/elements/checkbox-group.js +1 -1
  63. package/dist/elements/checkbox.js +1 -1
  64. package/dist/elements/checkpoint.js +1 -1
  65. package/dist/elements/choice.js +1 -1
  66. package/dist/elements/coachmark.js +1 -1
  67. package/dist/elements/code-block.js +1 -1
  68. package/dist/elements/command.js +1 -1
  69. package/dist/elements/compare.js +1 -1
  70. package/dist/elements/composer.js +1 -1
  71. package/dist/elements/confirm-card.js +1 -1
  72. package/dist/elements/context-meter.js +1 -1
  73. package/dist/elements/conversation-item.js +1 -1
  74. package/dist/elements/conversation-list.js +1 -1
  75. package/dist/elements/dialog.js +1 -1
  76. package/dist/elements/dock.js +3 -3
  77. package/dist/elements/dropdown.js +1 -1
  78. package/dist/elements/editable-label.js +1 -1
  79. package/dist/elements/embed.js +1 -1
  80. package/dist/elements/empty.js +1 -1
  81. package/dist/elements/feedback-bar.js +1 -1
  82. package/dist/elements/file-tree.js +1 -1
  83. package/dist/elements/file-upload.js +1 -1
  84. package/dist/elements/form.js +1 -1
  85. package/dist/elements/hover-card.js +1 -1
  86. package/dist/elements/icon.js +1 -1
  87. package/dist/elements/image.js +1 -1
  88. package/dist/elements/input.js +1 -1
  89. package/dist/elements/kbd.js +1 -1
  90. package/dist/elements/link-preview.js +1 -1
  91. package/dist/elements/loader.js +1 -1
  92. package/dist/elements/markdown.js +1 -1
  93. package/dist/elements/menu.js +1 -1
  94. package/dist/elements/message-skills.js +1 -1
  95. package/dist/elements/message.js +1 -1
  96. package/dist/elements/model-switcher.js +1 -1
  97. package/dist/elements/nav.js +1 -1
  98. package/dist/elements/notice.js +1 -1
  99. package/dist/elements/pane-grid.js +1 -1
  100. package/dist/elements/pane-group.js +1 -1
  101. package/dist/elements/pane.js +1 -1
  102. package/dist/elements/popover.js +1 -1
  103. package/dist/elements/progress-bar.js +1 -1
  104. package/dist/elements/prompt-dock.js +1 -1
  105. package/dist/elements/prompt-input.js +1 -1
  106. package/dist/elements/prompt-suggestions.js +1 -1
  107. package/dist/elements/radio-group.js +1 -1
  108. package/dist/elements/reasoning.js +1 -1
  109. package/dist/elements/remote.js +1 -1
  110. package/dist/elements/resizable.js +1 -1
  111. package/dist/elements/response-stream.js +1 -1
  112. package/dist/elements/screen.js +1 -1
  113. package/dist/elements/scroll-area.js +1 -1
  114. package/dist/elements/scroll-button.js +1 -1
  115. package/dist/elements/search.js +1 -1
  116. package/dist/elements/segmented.js +1 -1
  117. package/dist/elements/select.js +1 -1
  118. package/dist/elements/separator.js +1 -1
  119. package/dist/elements/setting-item.js +1 -1
  120. package/dist/elements/settings-group.js +1 -1
  121. package/dist/elements/skeleton.js +1 -1
  122. package/dist/elements/slider.js +1 -1
  123. package/dist/elements/source.js +1 -1
  124. package/dist/elements/status.js +1 -1
  125. package/dist/elements/switch.js +1 -1
  126. package/dist/elements/tabs.js +1 -1
  127. package/dist/elements/tasks.js +1 -1
  128. package/dist/elements/text-shimmer.js +1 -1
  129. package/dist/elements/thinking-bar.js +1 -1
  130. package/dist/elements/thread.js +1 -1
  131. package/dist/elements/toast.js +1 -1
  132. package/dist/elements/tool.js +1 -1
  133. package/dist/elements/tooltip.js +1 -1
  134. package/dist/elements/voice-input.js +1 -1
  135. package/dist/elements/voice-output.js +1 -1
  136. package/dist/elements.d.ts +20 -4
  137. package/dist/{ellipsis-DFr4Vqr3.js → ellipsis-CbOpJnU3.js} +1 -1
  138. package/dist/{embed-5s_vU4J0.js → embed-CrfjkbUG.js} +1 -1
  139. package/dist/{external-link-u9d85ORQ.js → external-link-ByXAwBBQ.js} +1 -1
  140. package/dist/{file-text-CX7_x42o.js → file-text-DMib2qwt.js} +1 -1
  141. package/dist/{file-tree-D3kBjXI5.js → file-tree-CJqM2aP7.js} +1 -1
  142. package/dist/{folder-V-YdIL8f.js → folder-_BAb929C.js} +1 -1
  143. package/dist/{form-BRNUMCN2.js → form-DSZgZQZk.js} +1 -1
  144. package/dist/{hover-card-DaeiYrTA.js → hover-card-BQbul9eq.js} +1 -1
  145. package/dist/{icon-BpixdZNq.js → icon-CdcfRhYI.js} +1 -1
  146. package/dist/index.d.ts +3 -1
  147. package/dist/index.js +2842 -2703
  148. package/dist/index.server.js +2820 -2681
  149. package/dist/{info-8mFsIjS3.js → info-DsFjGuI_.js} +1 -1
  150. package/dist/{input-d1Yuu-hz.js → input-DKbmkMos.js} +1 -1
  151. package/dist/kai.es.js +1 -1
  152. package/dist/{kbd-961zMPFB.js → kbd-PvdGhX4s.js} +1 -1
  153. package/dist/{link-preview-DIIbV2Sd.js → link-preview-DbM1866N.js} +1 -1
  154. package/dist/{link-DlZZBGaR.js → link-zF8PzWfF.js} +1 -1
  155. package/dist/{loader-YQszHFaW.js → loader-BM4CEz4G.js} +1 -1
  156. package/dist/{markdown-CVk1MPsj.js → markdown-LMvmAyBk.js} +7 -7
  157. package/dist/mcp.es.js +78 -4
  158. package/dist/{message-CwKVEdZg.js → message-BSsgvHvf.js} +1 -1
  159. package/dist/{message-DS8lzHc1.js → message-BYZECyek.js} +1 -1
  160. package/dist/message-circle-DruwObfX.js +1 -0
  161. package/dist/message-square-BG6q6mlm.js +1 -0
  162. package/dist/{minimize-2-UEZmP2QT.js → minimize-2-BQRoBV1V.js} +1 -1
  163. package/dist/{model-switcher-Kk1jEQ5t.js → model-switcher-CMpCOYcB.js} +1 -1
  164. package/dist/{overlay-C1od6LOz.js → overlay-Cb-MkyDa.js} +1 -1
  165. package/dist/{panel-right-ryET0yyn.js → panel-right-i8warFxT.js} +1 -1
  166. package/dist/{paperclip-BDNoZFJP.js → paperclip-ByY3UVO6.js} +1 -1
  167. package/dist/play-BvKKq3fX.js +1 -0
  168. package/dist/primitives/conversation-store.d.ts +54 -0
  169. package/dist/{progress-bar-BvYX0aoy.js → progress-bar-jVAHvMUf.js} +1 -1
  170. package/dist/{prompt-suggestion-BRFu2lcu.js → prompt-suggestion-CD-qtNgC.js} +1 -1
  171. package/dist/{radio-DcL23cNJ.js → radio-BsoBMhkF.js} +1 -1
  172. package/dist/react/index.d.ts +393 -0
  173. package/dist/react.js +20 -20
  174. package/dist/{reasoning-Cackq4Rh.js → reasoning-4kBPtlTx.js} +1 -1
  175. package/dist/register-impl-B1hLkrI8.js +424 -0
  176. package/dist/{resizable-Cp9HUYih.js → resizable-uPmY9VY-.js} +1 -1
  177. package/dist/{rotate-ccw-0DLRDHH5.js → rotate-ccw-B2xn93py.js} +1 -1
  178. package/dist/{rotate-cw-DOB9IGcU.js → rotate-cw-CMVLoCyH.js} +1 -1
  179. package/dist/{scroll-area-emVmWP4K.js → scroll-area-BqLUo8ZZ.js} +1 -1
  180. package/dist/{scroll-button-CeP_gv1x.js → scroll-button-DslXiJyL.js} +1 -1
  181. package/dist/{select-CWmu00tS.js → select-2cLRtPGT.js} +1 -1
  182. package/dist/{separator-CE6vly6z.js → separator-BWQtNJcM.js} +1 -1
  183. package/dist/{settings-CE-78SvZ.js → settings-BBR63jLg.js} +1 -1
  184. package/dist/{settings-group-ZFWNxFQR.js → settings-group-B2ciy-Iw.js} +1 -1
  185. package/dist/{skeleton-BebdP4mf.js → skeleton-CkHI-HgC.js} +1 -1
  186. package/dist/{slider-BAKRYUlz.js → slider-DDR9U-i0.js} +1 -1
  187. package/dist/{solid-BE5ui6sy.js → solid-BzCFdL25.js} +7720 -7058
  188. package/dist/{solid-DFhsce3O.js → solid-DruVOXnf.js} +5864 -5283
  189. package/dist/solid.d.ts +6 -2
  190. package/dist/solid.js +217 -210
  191. package/dist/solid.server.js +217 -210
  192. package/dist/{source-B0FlguSx.js → source-GEnFF528.js} +1 -1
  193. package/dist/{star-CBMZvLuN.js → star-CilHq8GP.js} +1 -1
  194. package/dist/store-Ciyk6eZG.js +1 -0
  195. package/dist/{switch-CViARZCy.js → switch-KkeQIbuH.js} +1 -1
  196. package/dist/{tasks-card-MjCcOhR_.js → tasks-card-BrN4bggJ.js} +1 -1
  197. package/dist/{text-shimmer-DKlwRF2G.js → text-shimmer-Dcj1MHuq.js} +1 -1
  198. package/dist/{textarea-3ay2Me2-.js → textarea-Dpvbngbg.js} +1 -1
  199. package/dist/theme.tokens.css +28 -0
  200. package/dist/{thumbs-up-DQcu2HMO.js → thumbs-up-CsT9Wzb9.js} +1 -1
  201. package/dist/{toast-store-C6yKOhbk.js → toast-store-tgK6MuMv.js} +1 -1
  202. package/dist/{tool-BCYNlfKy.js → tool-DzUjIBT8.js} +1 -1
  203. package/dist/{tooltip-05zI4hjs.js → tooltip-Bkkg_4t0.js} +1 -1
  204. package/dist/{trash-2-BR1RxGky.js → trash-2-jBKxjGGQ.js} +1 -1
  205. package/dist/{triangle-alert-DHohDOCy.js → triangle-alert-DlQ68ppf.js} +1 -1
  206. package/dist/types.d.ts +48 -3
  207. package/dist/ui/dock.d.ts +29 -0
  208. package/dist/{upload-BbGW6A_u.js → upload-TcAHXv9W.js} +1 -1
  209. package/dist/{url-scheme-policy-DHpoTJwB.js → url-scheme-policy-DR5DgvkM.js} +1 -1
  210. package/dist/{use-card-resolution-CJW4grFa.js → use-card-resolution-DaZqAdWf.js} +1 -1
  211. package/dist/{variant-aurora-xiMPty8f.js → variant-aurora-BhLuYxJR.js} +1 -1
  212. package/dist/{variant-aurora-BUOVgPOB.js → variant-aurora-CNmfHhcx.js} +2 -2
  213. package/dist/{variant-aurora-CUsUJvFI.js → variant-aurora-D6GsdTPA.js} +2 -2
  214. package/dist/{variant-aurora-CW2i4bfr.js → variant-aurora-DbW_OWL2.js} +1 -1
  215. package/dist/{variant-custom-D91HjAKz.js → variant-custom-BRvaNHSi.js} +1 -1
  216. package/dist/{variant-custom-BQtOnmeO.js → variant-custom-BvYHR62b.js} +1 -1
  217. package/dist/{variant-custom-CAnzzppW.js → variant-custom-DMTrEO5w.js} +2 -2
  218. package/dist/{variant-custom-CAm9L-E5.js → variant-custom-DV2x254s.js} +2 -2
  219. package/dist/{variant-wave-Ca1Ls4_y.js → variant-wave-BNMGrDfj.js} +2 -2
  220. package/dist/{variant-wave-uY5crU_K.js → variant-wave-BpDd8t3Z.js} +1 -1
  221. package/dist/{variant-wave-CxX39DKd.js → variant-wave-CyBueXj9.js} +2 -2
  222. package/dist/{variant-wave-DYyzIIZv.js → variant-wave-sp-qwsdI.js} +1 -1
  223. package/dist/x-Z5UntkIJ.js +1 -0
  224. package/frameworks/react/index.tsx +14 -4
  225. package/llms-full.txt +7 -2
  226. package/package.json +1 -1
  227. package/src/agent-tooling/README.md +192 -0
  228. package/src/agent-tooling/catalog/invariants.ts +2 -2
  229. package/src/agent-tooling/construct/cli.ts +16 -0
  230. package/src/agent-tooling/construct/codegen.ts +309 -27
  231. package/src/agent-tooling/construct/construct.v1.schema.json +69 -0
  232. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +11 -2
  233. package/src/agent-tooling/construct/schema.ts +81 -0
  234. package/src/components/chat-thread.tsx +532 -36
  235. package/src/components/conversation-item.tsx +23 -0
  236. package/src/components/conversation-panel.tsx +118 -0
  237. package/src/components/home-panel.tsx +159 -0
  238. package/src/components/widget-tab-bar.tsx +73 -0
  239. package/src/elements/chat.tsx +94 -2
  240. package/src/elements/compiled.css +1 -1
  241. package/src/elements/element-meta.json +49 -6
  242. package/src/elements/element-nonscalar.json +2 -0
  243. package/src/elements/element-types.d.ts +20 -4
  244. package/src/index.ts +3 -1
  245. package/src/primitives/conversation-store.ts +248 -0
  246. package/src/solid.ts +10 -2
  247. package/src/types.ts +45 -3
  248. package/src/ui/dock.tsx +47 -1
  249. package/theme.css +28 -0
  250. package/dist/arrow-down-Br0qtpsz.js +0 -1
  251. package/dist/arrow-left-Dj7xgE12.js +0 -1
  252. package/dist/check-DPeeMUYx.js +0 -1
  253. package/dist/chevron-down-CHSWk7ZT.js +0 -1
  254. package/dist/chevron-right-B7l_fMgx.js +0 -1
  255. package/dist/circle-Bb0yOpqM.js +0 -1
  256. package/dist/circle-check-BGZpCiz7.js +0 -1
  257. package/dist/code-block-CUQ5mtv0.js +0 -1
  258. package/dist/controllable-D43BKmtX.js +0 -1
  259. package/dist/conversation-list-D4lLU_yS.js +0 -1
  260. package/dist/define-CGGX-7Vg.js +0 -1
  261. package/dist/message-circle-Xf8Q-vZp.js +0 -1
  262. package/dist/message-square-Cxnwgkns.js +0 -1
  263. package/dist/play-OFIvYXUQ.js +0 -1
  264. package/dist/register-impl-C7rENvPK.js +0 -424
  265. package/dist/store-7eoadydQ.js +0 -1
  266. package/dist/x-C5m1JFgi.js +0 -1
@@ -493,7 +493,8 @@ function emitIndexHtml(c: Construct): string {
493
493
 
494
494
  function emitElement(c: Construct): string {
495
495
  const accent = c.theme?.accent;
496
- if (!accent) {
496
+ const unreadColor = c.theme?.unreadColor;
497
+ if (!accent && !unreadColor) {
497
498
  // `empty` (Task 14) composes straight into ChatThread's own `emptyContent`
498
499
  // prop now (see emitEmptyContentProp's doc) — a plain JSX value passed down
499
500
  // through App, not a Portal onto the host — so the facade needs no `ctx` and
@@ -503,6 +504,35 @@ function emitElement(c: Construct): string {
503
504
  return `import { defineWebComponent } from '@kitn.ai/ui/define';
504
505
  import { App } from './App';
505
506
 
507
+ // The one facade. Interior stays pure Solid (no nested element registrations);
508
+ // the kit CSS is injected into the shadow root by defineWebComponent itself.
509
+ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
510
+ `;
511
+ }
512
+
513
+ // `unreadColor` (owner ruling, 2026-08-26 — unread-indicator round) gets the
514
+ // exact same setProperty treatment as `accent` below — construct-authored/
515
+ // untrusted text, JSON.stringify'd at its one interpolation site, carried via
516
+ // `ctx.element.style.setProperty` rather than interpolated into CSS text (so
517
+ // it can never break out into a new declaration/rule) — but with NO paired
518
+ // -foreground computation: unlike the primary accent, --color-unread never
519
+ // sits behind text, only inside small filled dots (list-row/header/Dock
520
+ // badge), so there is nothing to contrast-pair against. Emitted
521
+ // unconditionally alongside accent's setProperty call when present, even if
522
+ // `accent` itself is absent — this branch is reached whenever EITHER is set.
523
+ const unreadColorSetProperty = unreadColor
524
+ ? `\n ctx.element.style.setProperty('--kai-color-unread', ${JSON.stringify(unreadColor)});`
525
+ : '';
526
+
527
+ if (!accent) {
528
+ // unreadColor-only construct: no accent, so none of the contrast-pairing
529
+ // machinery below applies — just the one setProperty call and `ctx`.
530
+ const facade = `(_props, ctx) => {${unreadColorSetProperty}
531
+ return <App />;
532
+ }`;
533
+ return `import { defineWebComponent } from '@kitn.ai/ui/define';
534
+ import { App } from './App';
535
+
506
536
  // The one facade. Interior stays pure Solid (no nested element registrations);
507
537
  // the kit CSS is injected into the shadow root by defineWebComponent itself.
508
538
  defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
@@ -557,7 +587,7 @@ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' |
557
587
  foregroundCss + `${CONTRAST_COLOR_SUPPORTS} {\n :host { --kai-color-primary-foreground: contrast-color(var(--kai-color-primary)); }\n}`;
558
588
 
559
589
  const facade = `(_props, ctx) => {
560
- ctx.element.style.setProperty('--kai-color-primary', ${JSON.stringify(accent)});
590
+ ctx.element.style.setProperty('--kai-color-primary', ${JSON.stringify(accent)});${unreadColorSetProperty}
561
591
  return (
562
592
  <>
563
593
  <style>{${JSON.stringify(styleText)}}</style>
@@ -582,10 +612,11 @@ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' |
582
612
 
583
613
  function emitApp(c: Construct): string {
584
614
  if (c.layout === 'custom') return emitCustomApp(c);
585
- return `${emitSolidJsImport(c)}import { ChatThread, createKaiChat${emitLayoutImport(c)}${emitCardComponentImport(c)}${emitEmptyComponentImport(c)}${emitHeaderCloseImport(c)} } from '@kitn.ai/ui/solid';
586
- import type { AttachmentData${emitHistoryTypeImport(c)} } from '@kitn.ai/ui/solid';
615
+ return `${emitSolidJsImport(c)}${emitConversationsSignalsImport(c)}import { ChatThread, createKaiChat${emitLayoutImport(c)}${emitCardComponentImport(c)}${emitEmptyComponentImport(c)}${emitHeaderCloseImport(c)} } from '@kitn.ai/ui/solid';
616
+ import type { AttachmentData${emitHistoryTypeImport(c)}${emitConversationsResetTypeImport(c)} } from '@kitn.ai/ui/solid';
587
617
  ${emitProviderImports(c)}
588
618
  ${emitCardsImport(c)}
619
+ ${emitConversationsImport(c)}
589
620
 
590
621
  ${emitProviderSetup(c)}
591
622
  ${emitHistorySetup(c)}
@@ -654,9 +685,26 @@ ${emitHistorySetup(c)}
654
685
  // emitDockCloseVar/emitHeaderEndContentProp's docs. No header means no
655
686
  // row for it to sit in, so that case is untouched and Dock's own built-in
656
687
  // mobile X keeps covering it.
688
+ // - conversations (Task 5): gated via ChatThread's own \`conversations\`/
689
+ // \`store\` props (kit-owned end to end — the prior-conversations list,
690
+ // list/load/save, autosave on every \`chat.messages()\` change). Requires
691
+ // capabilities.history persistence \`local\` or \`endpoint\` (schema
692
+ // superRefine, C-4) and SUBSUMES this file's hand-rolled history effect
693
+ // when on — see emitHistorySetup's own doc for the persistence-ownership
694
+ // decision: the store prop is the ONLY persistence mechanism emitted,
695
+ // never both. ChatThread never mutates \`messages\` itself, so
696
+ // \`onConversationLoad\` is ALSO wired here (\`chat.setMessages(() =>
697
+ // messages)\`) — without it, select/new/mount-restore all update
698
+ // ChatThread's own internal view/list state while the rendered thread
699
+ // never changes (see emitConversationsProps's own doc for the Task 6
700
+ // live-browser bug this fixes).
701
+ // - conversations + widget (owner follow-up): closing the widget while its
702
+ // list view is open must not leave it there for the next open — see
703
+ // widgetHasConversationsChrome/emitDockOnOpenChangeProp's docs. Wired only
704
+ // for \`widget\`, the one layout with something that closes/reopens at all.
657
705
  export function App() {
658
- ${emitDockCloseVar(c, ' ')} return (
659
- ${emitLayoutOpen(c)}${emitSlots(c.slots, ' ')} <ChatThread messages={chat.messages()} loading={chat.loading()} placeholder="Ask anything" onSubmit={submit} webSearch={false} voice={false}${emitHeaderProp(c)}${emitHeaderEndContentProp(c)}${emitAttachProps(c)}${emitStartersProp(c)}${emitReasoningProp(c)}${emitReasoningOpenProp(c)}${emitEmptyContentProp(c)}${emitCardTypesProp(c)} />
706
+ ${emitDockCloseVar(c, ' ')}${emitChatControllerVar(c, ' ')}${emitConversationsSignalsVar(c, ' ')} return (
707
+ ${emitLayoutOpen(c)}${emitSlots(c.slots, ' ')} <ChatThread messages={chat.messages()} loading={chat.loading()} placeholder="Ask anything" onSubmit={submit} webSearch={false} voice={false}${emitHeaderProp(c)}${emitHeaderEndContentProp(c)}${emitAttachProps(c)}${emitStartersProp(c)}${emitReasoningProp(c)}${emitReasoningOpenProp(c)}${emitEmptyContentProp(c)}${emitCardTypesProp(c)}${emitHomeProp(c)}${emitConversationsProps(c)}${emitChatControllerRefProp(c)}${emitChatThreadUnreadProps(c)} />
660
708
  ${emitLayoutClose(c)} );
661
709
  }
662
710
  `;
@@ -683,13 +731,18 @@ ${emitLayoutClose(c)} );
683
731
  * hand, which this format's own rule already commits to (no code-in-JSON).
684
732
  *
685
733
  * Capability gating: only cards are wired here (`Thread` accepts `cardTypes`
686
- * natively, same as `ChatThread`). starters/attachments/reasoning are NOT
687
- * wired for `custom` in v1 — `Thread`/`PromptInput` don't carry the kit's own
688
- * plumbing for those (they live inside `ChatThread`'s composer), and
689
- * hand-rolling a second copy here is exactly the restatement this file
690
- * avoids everywhere else. Decided loudly in the emitted comment below, not
691
- * silently: this is the eject artifact, so a construct author who needs one
692
- * of them on `custom` adds it directly to the plain Solid file they now own.
734
+ * natively, same as `ChatThread`). starters/attachments/reasoning/
735
+ * reasoningOpen/header.title/empty/conversations are NOT wired for `custom`
736
+ * in v1 `Thread`/`PromptInput` don't carry the kit's own plumbing for
737
+ * those (they live inside `ChatThread`'s composer, or for conversations
738
+ * ChatThread itself owns the list/load/save wiring), and hand-rolling a
739
+ * second copy here is exactly the restatement this file avoids everywhere
740
+ * else. Decided loudly in the emitted comment below, not silently: this is
741
+ * the eject artifact, so a construct author who needs one of them on
742
+ * `custom` adds it directly to the plain Solid file they now own.
743
+ * capabilities.history persistence itself IS still honored on `custom` (the
744
+ * hand-rolled effect below), independent of whether conversations is also
745
+ * set — see emitHistorySetup's own doc for why `custom` keeps it.
693
746
  */
694
747
  function emitCustomApp(c: Construct): string {
695
748
  const slots = c.slots ?? [];
@@ -708,8 +761,8 @@ ${emitHistorySetup(c)}
708
761
  // layout: custom — minimal chrome, no ChatThread/Dock/PaneGroup. The bare
709
762
  // spine (Thread + PromptInput) plus the declared slots, positioned by hand so
710
763
  // YOU own the surrounding DOM. Capabilities beyond the spine (starters,
711
- // attachments, reasoning display-mode, reasoningOpen, header.title, empty)
712
- // are NOT wired here in v1 — this file is the eject artifact; add them the
764
+ // attachments, reasoning display-mode, reasoningOpen, header.title, empty,
765
+ // conversations) are NOT wired here in v1 — this file is the eject artifact; add them the
713
766
  // way ChatThread composes them (components/chat-thread.tsx in the kit's own
714
767
  // source) if this construct needs them on a custom layout.
715
768
  export function App() {
@@ -820,6 +873,116 @@ function widgetHasHeaderClose(c: Construct): boolean {
820
873
  return c.layout === 'widget' && !!c.header?.title;
821
874
  }
822
875
 
876
+ /** `widget` layout with `capabilities.conversations` on: whether the widget
877
+ * needs the close-resets-the-list-view wiring below (owner follow-up,
878
+ * 2026-08-26 — closing the widget while its conversations list was open
879
+ * left `ChatThread`'s internal `view` state at `'list'`, so the NEXT open
880
+ * landed back on the list instead of the default chat screen). Only
881
+ * `widget` has a `Dock` to close/reopen at all; every other layout renders
882
+ * ChatThread inline with nothing that ever hides it, so the regression
883
+ * can't occur there and nothing is emitted for them.
884
+ *
885
+ * ALSO gates unread indicators (owner round, 2026-08-26): the FAB's `Dock
886
+ * unread` badge and `ChatThread`'s own `hostOpen` prop both need the exact
887
+ * same `dockOpen` tracking this reset wiring already needs, and both need
888
+ * the same "is there even a Dock to reflect this on" condition — one gate,
889
+ * reused, rather than two nearly-identical predicates drifting apart.
890
+ *
891
+ * Widened (Task 5) to also fire on `home` alone, with no `conversations`
892
+ * capability: closing the widget must return the view to Home exactly like
893
+ * it must return to the default chat screen — same regression class, same
894
+ * reset wiring, and `closeConversationsList()` is a safe no-op when there
895
+ * never was a conversations list to close. */
896
+ function widgetHasConversationsChrome(c: Construct): boolean {
897
+ return c.layout === 'widget' && (!!c.capabilities?.conversations || !!c.home);
898
+ }
899
+
900
+ /** Declares the closure `emitChatControllerRefProp`/`emitDockOnOpenChangeProp`
901
+ * share: `ChatThread`'s own `controllerRef` (chat-thread.tsx) hands back a
902
+ * `ChatThreadController` — the existing imperative seam, not a new one — and
903
+ * this captures it so `Dock`'s `onOpenChange` (a sibling prop on a sibling
904
+ * element, not a ChatThread descendant) can call
905
+ * `closeConversationsList()` on it. Declared inside `App()`, not at module
906
+ * scope, for the same instance-isolation reason as `emitDockCloseVar`. */
907
+ function emitChatControllerVar(c: Construct, indent: string): string {
908
+ return widgetHasConversationsChrome(c) ? `${indent}let chatController: ChatThreadController | undefined;\n` : '';
909
+ }
910
+
911
+ /** Threads `emitChatControllerVar`'s closure onto `<ChatThread controllerRef>`. */
912
+ function emitChatControllerRefProp(c: Construct): string {
913
+ return widgetHasConversationsChrome(c) ? ' controllerRef={(api) => (chatController = api)}' : '';
914
+ }
915
+
916
+ /** `Dock`'s own `onOpenChange` (ui/dock.tsx) already fires on EVERY close
917
+ * path — the header X, the launcher toggle, and Escape all resolve through
918
+ * its single `setOpen` — so wiring it once here covers all three with no
919
+ * per-path duplication (the alternative `emitDockCloseVar`/`dockClose`
920
+ * closure above only covers the header-X path, which is why that one isn't
921
+ * reused for this). The reset half fires ONLY on the close transition
922
+ * (`!open`) — opening needs no action there, the list stays gone until the
923
+ * visitor taps the toggle again, and a mid-open reset call would fight
924
+ * anyone still viewing the list. `setDockOpen(open)` fires on BOTH
925
+ * transitions unconditionally — it's a plain mirror of Dock's own state
926
+ * into a signal `ChatThread`'s `hostOpen` prop can read (see
927
+ * `emitConversationsSignalsVar`'s doc for why a signal, not a variable). */
928
+ function emitDockOnOpenChangeProp(c: Construct): string {
929
+ return widgetHasConversationsChrome(c)
930
+ ? ' onOpenChange={(open) => { setDockOpen(open); if (!open) chatController?.closeConversationsList(); }}'
931
+ : '';
932
+ }
933
+
934
+ /** `ChatThreadController` is needed only when `emitChatControllerVar` above
935
+ * declares a variable of that type — appended onto the same `import type`
936
+ * statement as `AttachmentData`/`ChatMessage`, never a second import
937
+ * statement for the module (same convention as `emitHistoryTypeImport`). */
938
+ function emitConversationsResetTypeImport(c: Construct): string {
939
+ return widgetHasConversationsChrome(c) ? ', ChatThreadController' : '';
940
+ }
941
+
942
+ /** `createSignal` import for `emitConversationsSignalsVar`'s two signals.
943
+ * Deliberately its OWN `import { createSignal } from 'solid-js'` line, not
944
+ * folded onto `emitSolidJsImport`'s: that one imports `createEffect` for the
945
+ * hand-rolled history-restore effect, which is UNEMITTED whenever
946
+ * `capabilities.conversations` is on (conversations subsumes persistence —
947
+ * see that function's own doc) — so for every construct this function
948
+ * actually fires for, `emitSolidJsImport` is already returning `''` and
949
+ * there's nothing to fold onto. */
950
+ function emitConversationsSignalsImport(c: Construct): string {
951
+ return widgetHasConversationsChrome(c) ? `import { createSignal } from 'solid-js';\n` : '';
952
+ }
953
+
954
+ /** Declares the two signals `Dock`'s `onOpenChange` (above) writes and
955
+ * `ChatThread`'s `hostOpen`/`onUnreadChange` props (below) read/write:
956
+ * `dockOpen` mirrors whether the panel is currently open — a SIGNAL, not a
957
+ * plain closure variable like `dockClose`/`chatController` above, because
958
+ * `ChatThread` reads it REACTIVELY every render (`hostOpen={dockOpen()}`),
959
+ * not just calls it imperatively on an event; `anyUnread` is the reverse
960
+ * direction, `ChatThread` writing outward via `onUnreadChange={setAnyUnread}`
961
+ * so `Dock`'s own `unread` prop (ui/dock.tsx — already exists, already
962
+ * tested, no change needed there) can mirror it onto the FAB.
963
+ * `dockOpen`'s initial value matches `widget.defaultOpen` (mirroring
964
+ * `emitDockDefaultOpen`'s own read of the same field) — a widget that opens
965
+ * by default has to start "seen", or a message arriving before the first
966
+ * `onOpenChange` fire would look like it happened while closed. */
967
+ function emitConversationsSignalsVar(c: Construct, indent: string): string {
968
+ if (!widgetHasConversationsChrome(c)) return '';
969
+ const w = c.layout === 'widget' ? c.widget : undefined;
970
+ const defaultOpen = w?.defaultOpen === true ? 'true' : 'false';
971
+ return `${indent}const [dockOpen, setDockOpen] = createSignal(${defaultOpen});\n${indent}const [anyUnread, setAnyUnread] = createSignal(false);\n`;
972
+ }
973
+
974
+ /** Threads the two signals above onto `<ChatThread hostOpen>`/
975
+ * `<ChatThread onUnreadChange>`. */
976
+ function emitChatThreadUnreadProps(c: Construct): string {
977
+ return widgetHasConversationsChrome(c) ? ' hostOpen={dockOpen()} onUnreadChange={setAnyUnread}' : '';
978
+ }
979
+
980
+ /** Threads `anyUnread` onto `Dock`'s own (pre-existing, unchanged) `unread`
981
+ * prop — the FAB's badge while closed. */
982
+ function emitDockUnreadProp(c: Construct): string {
983
+ return widgetHasConversationsChrome(c) ? ' unread={anyUnread()}' : '';
984
+ }
985
+
823
986
  /** The local closure `emitHeaderEndContentProp`/`emitDockControllerRef` share:
824
987
  * Dock's `controllerRef` hands back `{ open, setOpen }` (ui/dock.tsx) — the
825
988
  * existing imperative seam, not a new one — and this captures `setOpen`
@@ -916,22 +1079,106 @@ function emitReasoningOpenProp(c: Construct): string {
916
1079
  return c.capabilities?.reasoningOpen === true ? ' reasoningOpen={true}' : '';
917
1080
  }
918
1081
 
1082
+ /** capabilities.conversations -> ChatThread's own `conversations`/`store`
1083
+ * props (C-8: this is the ONLY logic codegen contributes — everything else
1084
+ * is kit code behind ChatThread). `local` persistence wires
1085
+ * localStorageStore(name[, userId]); `endpoint` wires fetchStore(url[,
1086
+ * userId]) — both re-exported from @kitn.ai/ui/solid, never hand-rolled
1087
+ * here (composition-over-reauthoring, same discipline as every other
1088
+ * capability in this file). `c.name` is validated against TAG_RE
1089
+ * (schema.ts — lowercase, hyphenated, a valid custom-element tag), so like
1090
+ * the other `'${c.name}'` interpolation sites in this file (defineWebComponent,
1091
+ * the preview HTML's element tag) it is embedded directly rather than
1092
+ * JSON.stringify'd. userId/url ARE construct-authored/untrusted free text,
1093
+ * JSON.stringify'd at their one interpolation site like theme.accent. The
1094
+ * schema's superRefine (C-4) guarantees history is present with persistence
1095
+ * 'local' or 'endpoint' whenever conversations is true, so no further guard
1096
+ * is needed here.
1097
+ *
1098
+ * BUG FIX (Task 6 live-browser demo, both defects traced to one root cause):
1099
+ * `conversations`/`store` alone only drive ChatThread's OWN internal
1100
+ * view/list/activeId state machine — select/new/mount-restore all resolve
1101
+ * through `store.load()`/a cleared array, but ChatThread never mutates
1102
+ * `props.messages` itself (doc comment on `onConversationLoad`, chat-thread.tsx
1103
+ * line ~144: "this component does not mutate props.messages itself"). Without
1104
+ * `onConversationLoad` wired back to this app's own `chat` store, every
1105
+ * load/new/restore updated ChatThread's internal bookkeeping (active id, list
1106
+ * entry) while the actually-rendered `chat.messages()` never changed — the
1107
+ * exact silent-drop class this codebase's CLAUDE.md calls out ("decide
1108
+ * loudly"), except here the drop was an OMITTED wire, not a decision: "+ New
1109
+ * conversation" appeared to do nothing (same messages kept rendering) and a
1110
+ * reload's mount-time auto-restore updated the list's active row but left the
1111
+ * chat view on the empty/welcome screen. `chat.setMessages(() => messages)`
1112
+ * closes the loop — ChatThread already hands back a FRESH array reference on
1113
+ * every call (`[...messages]` at both call sites in chat-thread.tsx), so no
1114
+ * extra clone is needed here; the updater-returns-new-array form is what the
1115
+ * reactivity contract (CLAUDE.md) requires and is what emitHistorySetup's
1116
+ * own hand-rolled local/endpoint restore already did before conversations
1117
+ * subsumed it. */
1118
+ /** `home` -> ChatThread's own `home` prop (chat-thread.tsx, Task 1-4), plain
1119
+ * data threaded straight through as ONE JSON.stringify'd object literal —
1120
+ * vocabulary-never-logic (this format's binding rule): the construct only
1121
+ * ever DECLARES the home screen's content, never wires a handler for it.
1122
+ * `<kai-chat>`'s own `kai-home-link` CustomEvent (Task 2-3) is how a
1123
+ * consumer of the EMITTED app would react to a link click; codegen never
1124
+ * emits an `onHomeLink` listener here for the same reason it never emits
1125
+ * handlers for any other capability in this file. Omitted entirely when no
1126
+ * `home` is declared — the same off-by-default gating as `emitHeaderProp`/
1127
+ * `emitEmptyContentProp` above. */
1128
+ function emitHomeProp(c: Construct): string {
1129
+ if (!c.home) return '';
1130
+ return ` home={${JSON.stringify(c.home)}}`;
1131
+ }
1132
+
1133
+ function emitConversationsProps(c: Construct): string {
1134
+ if (!c.capabilities?.conversations) return '';
1135
+ const history = c.capabilities.history;
1136
+ const storeCall =
1137
+ history?.persistence === 'endpoint'
1138
+ ? `fetchStore(${JSON.stringify(history.url)}${c.userId ? `, ${JSON.stringify(c.userId)}` : ''})`
1139
+ : `localStorageStore('${c.name}'${c.userId ? `, ${JSON.stringify(c.userId)}` : ''})`;
1140
+ return ` conversations={true} store={${storeCall}} onConversationLoad={(messages) => chat.setMessages(() => messages)}`;
1141
+ }
1142
+
1143
+ /** A dedicated named import for whichever store constructor
1144
+ * emitConversationsProps used — its own statement (mirrors emitCardsImport's
1145
+ * own `import { cards } from './cards'` line) rather than spliced onto the
1146
+ * ChatThread import list, so noUnusedLocals never trips when conversations
1147
+ * is absent. */
1148
+ function emitConversationsImport(c: Construct): string {
1149
+ if (!c.capabilities?.conversations) return '';
1150
+ const name = c.capabilities.history?.persistence === 'endpoint' ? 'fetchStore' : 'localStorageStore';
1151
+ return `import { ${name} } from '@kitn.ai/ui/solid';`;
1152
+ }
1153
+
919
1154
  /** capabilities.history -> whether the App module needs `createEffect`
920
1155
  * (both persisted variants react to `chat.messages()` changing; `none`/
921
1156
  * absent needs no extra Solid import at all, matching the off-by-default
922
- * gating everywhere else in this file). */
1157
+ * gating everywhere else in this file).
1158
+ *
1159
+ * capabilities.conversations SUBSUMES this on every layout EXCEPT `custom`
1160
+ * (CU-1: conversations is one of the capabilities excluded from the custom
1161
+ * layout's escape hatch, so custom never wires the store-based path and
1162
+ * must keep this hand-rolled one). Where it applies, ChatThread's own
1163
+ * conversations feature owns persistence entirely through the `store` prop
1164
+ * (see emitHistorySetup's doc for the full decision) and this file's
1165
+ * hand-rolled effect is never emitted, so it needs no `createEffect` import
1166
+ * either — gate the same way. */
923
1167
  function emitSolidJsImport(c: Construct): string {
924
1168
  const history = c.capabilities?.history;
925
- if (!history || history.persistence === 'none') return '';
1169
+ if (!history || history.persistence === 'none' || (c.layout !== 'custom' && c.capabilities?.conversations)) return '';
926
1170
  return `import { createEffect } from 'solid-js';\n`;
927
1171
  }
928
1172
 
929
1173
  /** capabilities.history -> whether the AttachmentData type import also needs
930
1174
  * ChatMessage (only the persisted variants read/write full ChatMessage[]
931
- * arrays). */
1175
+ * arrays). Gated off when capabilities.conversations subsumes persistence
1176
+ * (see emitSolidJsImport's doc — same `layout !== 'custom'` condition) for
1177
+ * the same reason: the hand-rolled block that used this type is never
1178
+ * emitted there. */
932
1179
  function emitHistoryTypeImport(c: Construct): string {
933
1180
  const history = c.capabilities?.history;
934
- if (!history || history.persistence === 'none') return '';
1181
+ if (!history || history.persistence === 'none' || (c.layout !== 'custom' && c.capabilities?.conversations)) return '';
935
1182
  // The enclosing statement is already `import type { ... }` (AttachmentData),
936
1183
  // so this must NOT repeat the `type` modifier inside the braces — `import
937
1184
  // type { AttachmentData, type ChatMessage }` is a TS syntax error.
@@ -992,6 +1239,28 @@ function emitHistorySetup(c: Construct): string {
992
1239
  const history = c.capabilities?.history;
993
1240
  if (!history || history.persistence === 'none') return '';
994
1241
 
1242
+ // PERSISTENCE-OWNERSHIP DECISION (Task 5): capabilities.conversations
1243
+ // SUBSUMES this hand-rolled block entirely. ChatThread's own conversations
1244
+ // feature (chat-thread.tsx) already autosaves the current thread on every
1245
+ // `chat.messages()` change through whatever ConversationStore its `store`
1246
+ // prop wires — localStorageStore(name) for `local`, fetchStore(url) for
1247
+ // `endpoint` (see emitConversationsProps below), the SAME "keyed by
1248
+ // construct name, local browser storage or a fetch endpoint" mechanism this
1249
+ // block hand-rolls for a single anonymous thread via THREAD_KEY/fetch. With
1250
+ // conversations on there is a real conversation id to save under (the store
1251
+ // contract), so the store path is strictly more correct, not just
1252
+ // redundant. Emitting both would persist through two independent
1253
+ // mechanisms on every change — not corrupting, but wasteful, confusing to
1254
+ // read, and exactly the kind of restatement this file avoids elsewhere
1255
+ // (see emitApp's header comment) — so this function contributes NOTHING
1256
+ // when capabilities.conversations is set: the store prop is the ONE
1257
+ // persistence mechanism in the emitted App. This does NOT apply to the
1258
+ // `custom` layout — CU-1 excludes conversations from custom's escape
1259
+ // hatch (emitCustomApp never calls emitConversationsProps), so custom
1260
+ // must keep this hand-rolled block as its only persistence mechanism even
1261
+ // when capabilities.conversations is set on the construct.
1262
+ if (c.layout !== 'custom' && c.capabilities?.conversations) return '';
1263
+
995
1264
  if (history.persistence === 'local') {
996
1265
  const key = JSON.stringify(c.userId ? `kai:${c.name}:${c.userId}:thread` : `kai:${c.name}:thread`);
997
1266
  return `
@@ -1159,7 +1428,7 @@ async function submit(detail: { value: string; attachments: AttachmentData[] })
1159
1428
  function emitLayoutImport(c: Construct): string {
1160
1429
  switch (c.layout) {
1161
1430
  case 'widget':
1162
- return ', Dock';
1431
+ return `, Dock${hasLauncherIcon(c) ? ', DockLauncherImage' : ''}`;
1163
1432
  case 'split':
1164
1433
  return ', WorkspaceShell';
1165
1434
  case 'fullscreen':
@@ -1240,15 +1509,28 @@ function emitDockPosition(c: Construct): string {
1240
1509
  return w?.position ? ` position="${w.position}"` : '';
1241
1510
  }
1242
1511
 
1243
- /** widget.launcherIcon -> Dock's `launcher` prop, replacing the built-in
1244
- * closed-state glyph with an <img>. launcherIcon is construct-authored/
1245
- * untrusted text (like theme.accent/provider.url elsewhere in this file),
1246
- * so it is JSON.stringify'd into a real JS string-literal expression, never
1247
- * interpolated into a raw JSX attribute string. */
1512
+ /** Whether `widgetHasLauncherIcon` (see below) needs the extra import — its
1513
+ * own named predicate so `emitLayoutImport`'s `widget` case reads plainly. */
1514
+ function hasLauncherIcon(c: Construct): boolean {
1515
+ return c.layout === 'widget' && !!c.widget?.launcherIcon;
1516
+ }
1517
+
1518
+ /** widget.launcherIcon -> Dock's `launcher` prop, via `DockLauncherImage`
1519
+ * (ui/dock.tsx) rather than a hand-rolled `<img>`: a construct-authored URL
1520
+ * is exactly as capable of 404ing as any other network fetch (this is what
1521
+ * the fix report found — `kai dev`'s own `owner-widget` fixture pinned a
1522
+ * `https://example.com/logo.png` placeholder that never resolved, so the
1523
+ * FAB rendered a permanently broken image), and `DockLauncherImage` is the
1524
+ * kit's own tested graceful-degradation component for exactly that,
1525
+ * falling back to the built-in glyph on load failure. launcherIcon is
1526
+ * construct-authored/untrusted text (like theme.accent/provider.url
1527
+ * elsewhere in this file), so it is JSON.stringify'd into a real JS
1528
+ * string-literal expression, never interpolated into a raw JSX attribute
1529
+ * string. */
1248
1530
  function emitDockLauncher(c: Construct): string {
1249
1531
  const w = c.layout === 'widget' ? c.widget : undefined;
1250
1532
  if (!w?.launcherIcon) return '';
1251
- return ` launcher={<img src={${JSON.stringify(w.launcherIcon)}} alt="" style={{ width: '24px', height: '24px', 'border-radius': '9999px' }} />}`;
1533
+ return ` launcher={<DockLauncherImage src={${JSON.stringify(w.launcherIcon)}} />}`;
1252
1534
  }
1253
1535
 
1254
1536
  /** widget.defaultOpen -> Dock's own `defaultOpen` prop. Only `true` costs a
@@ -1262,7 +1544,7 @@ function emitDockDefaultOpen(c: Construct): string {
1262
1544
  function emitLayoutOpen(c: Construct): string {
1263
1545
  switch (c.layout) {
1264
1546
  case 'widget':
1265
- return ` <Dock label="${c.name}"${emitDockPosition(c)}${emitDockLauncher(c)}${emitDockDefaultOpen(c)}${emitDockHideClose(c)}${emitDockControllerRef(c)}>\n`;
1547
+ return ` <Dock label="${c.name}"${emitDockPosition(c)}${emitDockLauncher(c)}${emitDockDefaultOpen(c)}${emitDockHideClose(c)}${emitDockControllerRef(c)}${emitDockOnOpenChangeProp(c)}${emitDockUnreadProp(c)}>\n`;
1266
1548
  case 'fullscreen':
1267
1549
  return ` <div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>\n`;
1268
1550
  case 'aside':
@@ -74,6 +74,9 @@
74
74
  "accent": {
75
75
  "type": "string"
76
76
  },
77
+ "unreadColor": {
78
+ "type": "string"
79
+ },
77
80
  "mode": {
78
81
  "default": "system",
79
82
  "type": "string",
@@ -120,6 +123,68 @@
120
123
  ],
121
124
  "additionalProperties": false
122
125
  },
126
+ "home": {
127
+ "type": "object",
128
+ "properties": {
129
+ "greeting": {
130
+ "type": "object",
131
+ "properties": {
132
+ "title": {
133
+ "type": "string",
134
+ "minLength": 1
135
+ },
136
+ "subtitle": {
137
+ "type": "string",
138
+ "minLength": 1
139
+ }
140
+ },
141
+ "additionalProperties": false
142
+ },
143
+ "recentConversation": {
144
+ "type": "boolean",
145
+ "const": true
146
+ },
147
+ "newConversation": {
148
+ "type": "object",
149
+ "properties": {
150
+ "label": {
151
+ "type": "string",
152
+ "minLength": 1
153
+ }
154
+ },
155
+ "additionalProperties": false
156
+ },
157
+ "links": {
158
+ "type": "array",
159
+ "items": {
160
+ "type": "object",
161
+ "properties": {
162
+ "label": {
163
+ "type": "string",
164
+ "minLength": 1
165
+ },
166
+ "href": {
167
+ "type": "string",
168
+ "minLength": 1
169
+ },
170
+ "description": {
171
+ "type": "string",
172
+ "minLength": 1
173
+ },
174
+ "icon": {
175
+ "type": "string",
176
+ "minLength": 1
177
+ }
178
+ },
179
+ "required": [
180
+ "label"
181
+ ],
182
+ "additionalProperties": false
183
+ }
184
+ }
185
+ },
186
+ "additionalProperties": false
187
+ },
123
188
  "capabilities": {
124
189
  "type": "object",
125
190
  "properties": {
@@ -180,6 +245,10 @@
180
245
  },
181
246
  "reasoningOpen": {
182
247
  "type": "boolean"
248
+ },
249
+ "conversations": {
250
+ "type": "boolean",
251
+ "const": true
183
252
  }
184
253
  },
185
254
  "additionalProperties": false
@@ -5,19 +5,28 @@
5
5
  "provider": { "mode": "mock" },
6
6
  "userId": "user_123",
7
7
  "header": { "title": "Acme Support" },
8
+ "theme": { "unreadColor": "#38BDF8" },
8
9
  "empty": {
9
10
  "title": "Hi, we're Acme Support",
10
11
  "description": "Ask us about orders, refunds, and more."
11
12
  },
13
+ "home": {
14
+ "greeting": { "title": "Hi from Acme 👋", "subtitle": "Orders, refunds, anything." },
15
+ "recentConversation": true,
16
+ "links": [
17
+ { "label": "Help center", "href": "https://ui.kitn.ai", "description": "Guides and FAQs", "icon": "book-open" },
18
+ { "label": "Talk to sales", "description": "We reply fast", "icon": "message-circle" }
19
+ ]
20
+ },
12
21
  "widget": {
13
22
  "position": "top-start",
14
- "launcherIcon": "https://example.com/logo.png",
15
23
  "defaultOpen": true
16
24
  },
17
25
  "capabilities": {
18
26
  "attachments": { "accept": ["image/*", "application/pdf"] },
19
27
  "history": { "persistence": "local" },
20
28
  "starters": ["Where's my order?", "Request a refund"],
21
- "reasoningOpen": true
29
+ "reasoningOpen": true,
30
+ "conversations": true
22
31
  }
23
32
  }