@kitn.ai/ui 0.26.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.
- package/README.md +25 -6
- package/bin/mcp.js +16 -3
- package/bin/route.js +23 -0
- package/bin/route.test.js +58 -0
- package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-DObhsRtm.js} +1 -1
- package/dist/action-icons-CcgNN2oT.js +1 -0
- package/dist/arrow-down-CjRU3JgV.js +1 -0
- package/dist/arrow-left-Bnpb1UEW.js +1 -0
- package/dist/artifact-Bwt7OgcR.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-Qr4kg-2b.js +1 -0
- package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-DToYdfSc.js} +1 -1
- package/dist/badge-CdEu_jn2.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-DQAAOpDz.js +1 -0
- package/dist/{elements/chunks/button-flUbeufF.js → button-DkuOnGLy.js} +1 -1
- package/dist/card-renderer-Jx70m6_l.js +1 -0
- package/dist/card-routing-0YidER_3.js +1 -0
- package/dist/check-njkVdZu2.js +1 -0
- package/dist/checkbox-D7iRB3nL.js +1 -0
- package/dist/checkbox-group-B2_rpoPM.js +1 -0
- package/dist/chevron-down-B72s6PXR.js +1 -0
- package/dist/chevron-right-BatuHzUm.js +1 -0
- package/dist/choice-card-m0BYqqOK.js +1 -0
- package/dist/circle-check-B62pODB3.js +1 -0
- package/dist/circle-x-BPG0iPst.js +1 -0
- package/dist/circle-zx0ByzY9.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/code-block-ClMLKb7C.js +1 -0
- package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-B4DlWicL.js} +1 -1
- package/dist/components/attachment-types.d.ts +7 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
- package/dist/components/card-renderer.d.ts +13 -0
- package/dist/components/card.d.ts +4 -0
- package/dist/components/chat-thread.d.ts +135 -2
- package/dist/components/conversation-item.d.ts +35 -7
- package/dist/components/conversation-list.d.ts +17 -5
- package/dist/components/conversation-panel.d.ts +29 -0
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +48 -1
- package/dist/components/home-panel.d.ts +34 -0
- package/dist/components/message.d.ts +25 -3
- package/dist/components/reasoning.d.ts +6 -0
- package/dist/components/scroll-button.d.ts +11 -0
- package/dist/components/thread.d.ts +5 -0
- package/dist/components/tool-types.d.ts +9 -0
- package/dist/components/widget-tab-bar.d.ts +16 -0
- package/dist/composer-sQ0HcvyG.js +106 -0
- package/dist/confirm-card-rQWDQsnC.js +1 -0
- package/dist/construct-cli.es.js +1337 -0
- package/dist/{elements/chunks/context-VLIL3sJ-.js → context-0kg4ZX1J.js} +1 -1
- package/dist/controllable-CnGpMF6r.js +1 -0
- package/dist/conversation-item-BxwQLaFJ.js +1 -0
- package/dist/conversation-list-BCfGN1JU.js +1 -0
- package/dist/copy-n9u9rqpi.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-BFT6c-Y8.js → create-tween-C-q-JCM-.js} +1 -1
- package/dist/{create-tween-CJNlzoaO.js → create-tween-CRrJXxBd.js} +1 -1
- package/dist/{create-tween-CQED4T0g.js → create-tween-DZ3RRDHD.js} +1 -1
- package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-uPkArjIG.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +1433 -202
- package/dist/default-input-CaMrJkgH.js +1 -0
- package/dist/define-By-uL588.js +1 -0
- package/dist/define.d.ts +4 -0
- package/dist/define.js +510 -0
- package/dist/define.server.d.ts +5 -0
- package/dist/define.server.js +506 -0
- package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-WUImBbDv.js} +1 -1
- package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-wZVeJOMd.js} +1 -1
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkbox-group.d.ts +1 -0
- package/dist/elements/checkbox-group.js +1 -0
- package/dist/elements/checkbox.d.ts +1 -0
- package/dist/elements/checkbox.js +1 -0
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-item.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/define-entry.d.ts +12 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.js +102 -8
- package/dist/elements/dropdown.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-data-types.d.ts +37 -0
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-grid.d.ts +1 -0
- package/dist/elements/pane-grid.js +1 -0
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +1 -0
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/select.d.ts +1 -0
- package/dist/elements/select.js +1 -0
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +1 -0
- package/dist/elements/slots.d.ts +1 -1
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +380 -26
- package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-CbOpJnU3.js} +1 -1
- package/dist/embed-CrfjkbUG.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-ByXAwBBQ.js} +1 -1
- package/dist/file-text-DMib2qwt.js +1 -0
- package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-CJqM2aP7.js} +1 -1
- package/dist/folder-_BAb929C.js +1 -0
- package/dist/form-DSZgZQZk.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-BQbul9eq.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-CdcfRhYI.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7474 -5857
- package/dist/index.server.js +5965 -4762
- package/dist/info-DsFjGuI_.js +1 -0
- package/dist/input-DKbmkMos.js +1 -0
- package/dist/javascript-C25yR2R2.js +1 -6
- package/dist/json-DxJze_jm.js +1 -6
- package/dist/kai.es.js +1 -1
- package/dist/kbd-PvdGhX4s.js +1 -0
- package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DbM1866N.js} +1 -1
- package/dist/link-zF8PzWfF.js +1 -0
- package/dist/loader-BM4CEz4G.js +1 -0
- package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-LMvmAyBk.js} +7 -7
- package/dist/mcp.es.js +1361 -81
- package/dist/message-BSsgvHvf.js +1 -0
- package/dist/message-BYZECyek.js +1 -0
- package/dist/message-circle-DruwObfX.js +1 -0
- package/dist/message-square-BG6q6mlm.js +1 -0
- package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-BQRoBV1V.js} +1 -1
- package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-CMpCOYcB.js} +1 -1
- package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-Cb-MkyDa.js} +1 -1
- package/dist/panel-right-i8warFxT.js +1 -0
- package/dist/paperclip-ByY3UVO6.js +1 -0
- package/dist/play-BvKKq3fX.js +1 -0
- package/dist/primitives/card-data-types.d.ts +49 -0
- package/dist/primitives/card-routing.d.ts +1 -33
- package/dist/primitives/card-validate.d.ts +2 -2
- package/dist/primitives/chat-config.d.ts +29 -2
- package/dist/primitives/conversation-store.d.ts +54 -0
- package/dist/primitives/field-mask.d.ts +96 -0
- package/dist/primitives/field-semantics.d.ts +36 -0
- package/dist/primitives/input-mask.d.ts +46 -0
- package/dist/primitives/url-scheme-policy.d.ts +34 -0
- package/dist/primitives/use-resize-observer.d.ts +2 -2
- package/dist/progress-bar-jVAHvMUf.js +1 -0
- package/dist/prompt-suggestion-CD-qtNgC.js +1 -0
- package/dist/radio-BsoBMhkF.js +1 -0
- package/dist/react/index.d.ts +586 -19
- package/dist/react.js +223 -187
- package/dist/reasoning-4kBPtlTx.js +1 -0
- package/dist/register-impl-B1hLkrI8.js +424 -0
- package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-uPmY9VY-.js} +1 -1
- package/dist/rotate-ccw-B2xn93py.js +1 -0
- package/dist/rotate-cw-CMVLoCyH.js +1 -0
- package/dist/schemas/form.schema.json +21 -1
- package/dist/schemas/tool-defs.d.ts +11 -1
- package/dist/schemas.js +81 -81
- package/dist/scroll-area-BqLUo8ZZ.js +1 -0
- package/dist/scroll-button-DslXiJyL.js +1 -0
- package/dist/select-2cLRtPGT.js +1 -0
- package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-BWQtNJcM.js} +1 -1
- package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-BBR63jLg.js} +1 -1
- package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-B2ciy-Iw.js} +1 -1
- package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-CkHI-HgC.js} +1 -1
- package/dist/slider-DDR9U-i0.js +1 -0
- package/dist/{solid-BtdqNZI7.js → solid-BzCFdL25.js} +9771 -7594
- package/dist/{solid-CnavwBye.js → solid-DruVOXnf.js} +7893 -6183
- package/dist/solid.d.ts +16 -2
- package/dist/solid.js +256 -243
- package/dist/solid.server.js +256 -243
- package/dist/source-GEnFF528.js +1 -0
- package/dist/star-CilHq8GP.js +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/mock.d.ts +32 -2
- package/dist/state.js +133 -108
- package/dist/store-Ciyk6eZG.js +1 -0
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-KkeQIbuH.js +1 -0
- package/dist/tasks-card-BrN4bggJ.js +1 -0
- package/dist/text-shimmer-Dcj1MHuq.js +1 -0
- package/dist/textarea-Dpvbngbg.js +1 -0
- package/dist/theme.tokens.css +110 -13
- package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-CsT9Wzb9.js} +1 -1
- package/dist/toast-store-tgK6MuMv.js +1 -0
- package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-DzUjIBT8.js} +1 -1
- package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-Bkkg_4t0.js} +1 -1
- package/dist/trash-2-jBKxjGGQ.js +1 -0
- package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DlQ68ppf.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- package/dist/types.d.ts +48 -3
- package/dist/typescript-RycA9KXf.js +1 -6
- package/dist/ui/checkbox-group.d.ts +83 -0
- package/dist/ui/checkbox.d.ts +40 -0
- package/dist/ui/dock.d.ts +51 -0
- package/dist/ui/icon.d.ts +3 -0
- package/dist/ui/input.d.ts +48 -2
- package/dist/ui/radio.d.ts +85 -0
- package/dist/ui/select.d.ts +71 -0
- package/dist/ui/slider.d.ts +65 -0
- package/dist/ui/switch.d.ts +9 -2
- package/dist/upload-TcAHXv9W.js +1 -0
- package/dist/url-scheme-policy-DR5DgvkM.js +1 -0
- package/dist/use-card-resolution-DaZqAdWf.js +1 -0
- package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-BhLuYxJR.js} +1 -1
- package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-CNmfHhcx.js} +2 -2
- package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-D6GsdTPA.js} +2 -2
- package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-DbW_OWL2.js} +1 -1
- package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-BRvaNHSi.js} +1 -1
- package/dist/{variant-custom-L16JLLZy.js → variant-custom-BvYHR62b.js} +1 -1
- package/dist/{variant-custom-BdvYC7af.js → variant-custom-DMTrEO5w.js} +2 -2
- package/dist/{variant-custom-C714mqKl.js → variant-custom-DV2x254s.js} +2 -2
- package/dist/{variant-wave-ecwykeyL.js → variant-wave-BNMGrDfj.js} +2 -2
- package/dist/{variant-wave-2S4La2Ka.js → variant-wave-BpDd8t3Z.js} +1 -1
- package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CyBueXj9.js} +2 -2
- package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-sp-qwsdI.js} +1 -1
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/x-Z5UntkIJ.js +1 -0
- package/frameworks/react/index.tsx +210 -22
- package/llms-full.txt +1184 -22
- package/llms.txt +6 -3
- package/package.json +17 -4
- package/src/agent-tooling/README.md +192 -0
- package/src/agent-tooling/catalog/invariants.ts +3 -3
- package/src/agent-tooling/catalog/surfaces.ts +72 -0
- package/src/agent-tooling/construct/cli-entry.ts +8 -0
- package/src/agent-tooling/construct/cli.ts +147 -0
- package/src/agent-tooling/construct/codegen.ts +1635 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +319 -0
- package/src/agent-tooling/construct/dev.ts +148 -0
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +32 -0
- package/src/agent-tooling/construct/schema.ts +452 -0
- package/src/agent-tooling/mcp/server.ts +2 -1
- package/src/agent-tooling/mcp/tools/construct.ts +130 -0
- package/src/agent-tooling/mcp/tools/debug.ts +126 -4
- package/src/agent-tooling/mcp/tools/reference.ts +104 -3
- package/src/agent-tooling/recipes/composed-thread.ts +714 -0
- package/src/agent-tooling/recipes/index.ts +22 -0
- package/src/agent-tooling/recipes/types.ts +32 -0
- package/src/agent-tooling/registry.ts +5 -0
- package/src/components/attachment-types.ts +7 -0
- package/src/components/audio-visualizer/fit-scale.ts +1 -1
- package/src/components/card-renderer.tsx +39 -8
- package/src/components/card.tsx +10 -2
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +602 -43
- package/src/components/choice-card.tsx +102 -37
- package/src/components/coachmark.tsx +1 -1
- package/src/components/code-block.tsx +2 -2
- package/src/components/composer.tsx +89 -15
- package/src/components/confirm-card.tsx +20 -4
- package/src/components/conversation-item.tsx +56 -9
- package/src/components/conversation-list.tsx +25 -10
- package/src/components/conversation-panel.tsx +118 -0
- package/src/components/empty.tsx +14 -3
- package/src/components/file-tree.tsx +5 -1
- package/src/components/form-widgets.tsx +257 -195
- package/src/components/form.tsx +297 -9
- package/src/components/home-panel.tsx +159 -0
- package/src/components/loader.tsx +12 -5
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +71 -11
- package/src/components/prompt-input.tsx +7 -1
- package/src/components/prompt-suggestion.tsx +4 -1
- package/src/components/reasoning.tsx +30 -3
- package/src/components/response-compare.tsx +2 -2
- package/src/components/scroll-button.tsx +56 -4
- package/src/components/source.tsx +6 -1
- package/src/components/tasks-card.tsx +58 -16
- package/src/components/thread.tsx +10 -1
- package/src/components/toast.tsx +1 -1
- package/src/components/tool-types.ts +9 -0
- package/src/components/widget-tab-bar.tsx +73 -0
- package/src/elements/attachments.tsx +3 -1
- package/src/elements/chat-workspace.tsx +1 -1
- package/src/elements/chat.tsx +115 -5
- package/src/elements/checkbox-group.tsx +164 -0
- package/src/elements/checkbox.tsx +134 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/composer.tsx +12 -1
- package/src/elements/conversation-item.tsx +62 -22
- package/src/elements/conversation-list.tsx +2 -2
- package/src/elements/define-entry.ts +12 -0
- package/src/elements/dock.tsx +11 -0
- package/src/elements/element-data-types.ts +40 -0
- package/src/elements/element-manifest.json +24 -0
- package/src/elements/element-meta.json +767 -34
- package/src/elements/element-nonscalar.json +17 -0
- package/src/elements/element-types.d.ts +380 -26
- package/src/elements/feedback-bar.tsx +5 -0
- package/src/elements/icon-names.json +29 -0
- package/src/elements/input.tsx +312 -23
- package/src/elements/message.tsx +2 -0
- package/src/elements/pane-grid.tsx +111 -0
- package/src/elements/prompt-input.tsx +8 -2
- package/src/elements/radio-group.tsx +129 -0
- package/src/elements/register-impl.ts +6 -0
- package/src/elements/scroll-button.tsx +54 -6
- package/src/elements/select.tsx +165 -0
- package/src/elements/slider.tsx +171 -0
- package/src/elements/slots.ts +1 -1
- package/src/elements/styles.css +89 -3
- package/src/elements/thread.tsx +2 -0
- package/src/elements/toast.tsx +14 -1
- package/src/index.ts +5 -2
- package/src/primitives/card-data-types.ts +49 -0
- package/src/primitives/card-host.tsx +4 -2
- package/src/primitives/card-routing.ts +7 -56
- package/src/primitives/card-schemas/form.schema.json +21 -1
- package/src/primitives/card-validate.ts +10 -2
- package/src/primitives/chat-config.tsx +33 -6
- package/src/primitives/conversation-store.ts +248 -0
- package/src/primitives/field-mask.ts +256 -0
- package/src/primitives/field-semantics.ts +115 -0
- package/src/primitives/input-mask.ts +853 -0
- package/src/primitives/toast-store.ts +66 -4
- package/src/primitives/url-scheme-policy.ts +70 -0
- package/src/primitives/use-resize-observer.ts +2 -2
- package/src/schemas/tool-defs.ts +12 -2
- package/src/solid.ts +20 -2
- package/src/state/index.ts +1 -1
- package/src/state/mock.ts +92 -6
- package/src/types.ts +45 -3
- package/src/ui/agent-card.tsx +2 -2
- package/src/ui/avatar.tsx +1 -1
- package/src/ui/checkbox-group.tsx +153 -0
- package/src/ui/checkbox.tsx +63 -0
- package/src/ui/dock.tsx +192 -7
- package/src/ui/icon.tsx +53 -0
- package/src/ui/input.tsx +249 -9
- package/src/ui/kbd.tsx +1 -1
- package/src/ui/nav.tsx +1 -1
- package/src/ui/pane-group.tsx +2 -2
- package/src/ui/radio.tsx +150 -0
- package/src/ui/select.tsx +168 -0
- package/src/ui/slider.tsx +178 -0
- package/src/ui/switch.tsx +49 -18
- package/src/utils/cn.ts +11 -5
- package/theme.css +112 -13
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
- package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
- package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
- package/dist/elements/chunks/bash-InADTalH.js +0 -1
- package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
- package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
- package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
- package/dist/elements/chunks/code-block-BdLiTitd.js +0 -1
- package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
- package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
- package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
- package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
- package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
- package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
- package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
- package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
- package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
- package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
- package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
- package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
- package/dist/elements/chunks/form-CQirdC4W.js +0 -1
- package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
- package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
- package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
- package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
- package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
- package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
- package/dist/elements/chunks/json-DxJze_jm.js +0 -1
- package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
- package/dist/elements/chunks/link-2My69BqT.js +0 -1
- package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
- package/dist/elements/chunks/message-VMZohrXx.js +0 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
- package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
- package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
- package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
- package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
- package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
- package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
- package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
- package/dist/elements/chunks/source-UWCvmVak.js +0 -1
- package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
- package/dist/elements/chunks/store-Cjq0qwYN.js +0 -1
- package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
- package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
- package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
- package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
- package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
- package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
- package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
- package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
- package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
- package/dist/llms/llms-full.txt +0 -2949
- package/dist/llms/llms.txt +0 -156
- package/dist/register-impl-DpE7icIb.js +0 -293
- package/dist/ui/stat.d.ts +0 -16
- package/src/ui/stat.tsx +0 -41
- /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
- /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
- /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal, createEffect, createMemo, For, Show, onMount } from 'solid-js';
|
|
1
|
+
import { createSignal, createEffect, createComputed, createMemo, For, Show, Switch, Match, onMount, untrack } from 'solid-js';
|
|
2
2
|
import { ChatConfig, useChatConfig } from '../primitives/chat-config';
|
|
3
3
|
import { type ComposerDoc, normalizeValue, serializeToText } from '../primitives/composer-model';
|
|
4
4
|
import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container';
|
|
@@ -19,6 +19,17 @@ import type { ProseSize } from '../primitives/chat-config';
|
|
|
19
19
|
import type { ModelOption } from '../types';
|
|
20
20
|
import type { CardComponentMap } from '../primitives/card-registry';
|
|
21
21
|
import type { CardSchemaMap } from './card-renderer';
|
|
22
|
+
import type { JSX } from 'solid-js';
|
|
23
|
+
import type { ConversationStore } from '../primitives/conversation-store';
|
|
24
|
+
import { byRecency } from '../primitives/conversation-store';
|
|
25
|
+
import { ConversationPanel } from './conversation-panel';
|
|
26
|
+
import { isConversationUnread } from './conversation-item';
|
|
27
|
+
import type { ConversationSummary } from '../types';
|
|
28
|
+
import { MessagesSquare, ArrowLeft } from 'lucide-solid';
|
|
29
|
+
import { Button } from '../ui/button';
|
|
30
|
+
import { HomePanel } from './home-panel';
|
|
31
|
+
import { WidgetTabBar } from './widget-tab-bar';
|
|
32
|
+
import type { HomeConfig, HomeLinkEntry } from '../types';
|
|
22
33
|
|
|
23
34
|
export interface ChatThreadContextUsage {
|
|
24
35
|
usedTokens: number;
|
|
@@ -45,6 +56,11 @@ export interface ChatThreadProps {
|
|
|
45
56
|
* the kit checks its own seven built-ins and leaves your own card type
|
|
46
57
|
* unvalidated. A schema here WINS over a built-in of the same name. */
|
|
47
58
|
cardSchemas?: CardSchemaMap;
|
|
59
|
+
/** The custom-element host node to emit card events off when no `CardProvider`
|
|
60
|
+
* is present, forwarded through `MessageBody` to `CardRenderer`. The
|
|
61
|
+
* element facades pass their own host element so card events leave as the
|
|
62
|
+
* bubbling `kai-card` CustomEvent. */
|
|
63
|
+
cardHostElement?: HTMLElement;
|
|
48
64
|
/** Value of the input. A **string** is controlled (the host owns the text and
|
|
49
65
|
* updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that
|
|
50
66
|
* pre-populates pills; the user then edits freely. Leave unset for uncontrolled. */
|
|
@@ -73,6 +89,19 @@ export interface ChatThreadProps {
|
|
|
73
89
|
/** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
|
|
74
90
|
* `<pre>` blocks (lighter, no highlighter load). Default true. */
|
|
75
91
|
codeHighlight?: boolean;
|
|
92
|
+
/** How `reasoning` parts render across the thread. `'full'` (default) is the
|
|
93
|
+
* current collapsible-disclosure behavior; `'compact'` shows only a shimmer
|
|
94
|
+
* loader while a reasoning part streams and nothing once it settles (no
|
|
95
|
+
* expandable detail); `'off'` renders reasoning parts not at all. Forwarded
|
|
96
|
+
* to every `MessageBody` as `reasoningMode`. */
|
|
97
|
+
reasoning?: 'full' | 'compact' | 'off';
|
|
98
|
+
/** Seeds the reasoning disclosure open AND keeps it tracking the stream
|
|
99
|
+
* (open while streaming, closes when it settles): the pre-Task-19f `full`
|
|
100
|
+
* behavior. Default false/absent: the panel starts closed (just the
|
|
101
|
+
* "Thinking" shimmer chip) and only opens on click, the current default
|
|
102
|
+
* (owner ruling, 2026-08-26). Meaningless when `reasoning` is `'compact'`
|
|
103
|
+
* or `'off'`. Forwarded to every `MessageBody` as `reasoningDefaultOpen`. */
|
|
104
|
+
reasoningOpen?: boolean;
|
|
76
105
|
/** Optional header title shown on the left of the header. */
|
|
77
106
|
chatTitle?: string;
|
|
78
107
|
/** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
|
|
@@ -90,6 +119,90 @@ export interface ChatThreadProps {
|
|
|
90
119
|
headerStart?: boolean;
|
|
91
120
|
/** Whether the host has `slot="header-end"` content (right of the controls). */
|
|
92
121
|
headerEnd?: boolean;
|
|
122
|
+
/** Extra content rendered in the header-end region, AFTER `slot="header-end"`.
|
|
123
|
+
* This is a JSX escape hatch for a caller composing `ChatThread` directly as a
|
|
124
|
+
* Solid component (no shadow-DOM host, so there is no light-DOM node to slot) —
|
|
125
|
+
* the `kai-dock`-docked construct widget is the motivating case: it needs its
|
|
126
|
+
* own close affordance to sit IN the header row, sharing it with the title
|
|
127
|
+
* instead of floating as a second control with no visible relationship to the
|
|
128
|
+
* chat surface. Renders alongside the named slot rather than replacing it, so a
|
|
129
|
+
* real `slot="header-end"` consumer and this prop can both be present. Counts
|
|
130
|
+
* toward `showHeader()` the same as `headerEnd`, so a construct with no title
|
|
131
|
+
* and only this content still gets a header row to sit in. */
|
|
132
|
+
headerEndContent?: JSX.Element;
|
|
133
|
+
/** Turns on the prior-conversations list: a list-toggle button appears in
|
|
134
|
+
* the header row and the panel gains a second, list, view (C-1 — one
|
|
135
|
+
* panel, two states, never a persistent sidebar). Off by default, same
|
|
136
|
+
* convention as every other capability in this file. Requires BOTH
|
|
137
|
+
* `store` AND `onConversationLoad` — the second is the only path a caller
|
|
138
|
+
* has to actually receive a loaded conversation's messages back (this
|
|
139
|
+
* component never mutates `props.messages` itself). Set with either
|
|
140
|
+
* missing, the feature decides loudly (one `console.error` on mount) and
|
|
141
|
+
* stays visually off rather than throwing or silently going inert: a
|
|
142
|
+
* `store` with no `onConversationLoad` would otherwise make row-select/
|
|
143
|
+
* new/restore fire and do nothing visible, and mount's own auto-restore
|
|
144
|
+
* would still stamp an active conversation id that the save effect could
|
|
145
|
+
* then clobber with whatever `props.messages` the caller drives in next. */
|
|
146
|
+
conversations?: boolean;
|
|
147
|
+
/** The adapter this thread persists through when `conversations` is on:
|
|
148
|
+
* `list()` on mount and on every list-view open, `load(id)` on row select,
|
|
149
|
+
* `save(id, messages)` on every message-array change for the active
|
|
150
|
+
* conversation. A kit-owned INTERFACE (C-3) — the dev owns invocation,
|
|
151
|
+
* transport, auth and retention entirely; `localStorageStore`/`fetchStore`
|
|
152
|
+
* (`@kitn.ai/ui`'s `primitives/conversation-store`) are the shipped
|
|
153
|
+
* built-ins. Set as a JS property; never expressible as an attribute (an
|
|
154
|
+
* adapter is a live object of functions, not scalar data). */
|
|
155
|
+
store?: ConversationStore;
|
|
156
|
+
/** Fires whenever `load(id)` resolves and this thread's `messages` are
|
|
157
|
+
* about to be replaced with that conversation's history — the hook a
|
|
158
|
+
* caller uses to actually own and re-render `messages` (this component
|
|
159
|
+
* does not mutate `props.messages` itself; C-8 keeps the state machine
|
|
160
|
+
* here but the message ARRAY stays the caller's own state, matching every
|
|
161
|
+
* other prop in this file). The second argument is the conversation's id
|
|
162
|
+
* — `undefined` for the "new conversation" case (C-6: no id exists until
|
|
163
|
+
* the first message mints one). Required whenever `conversations` is on;
|
|
164
|
+
* see that prop's own doc for the guard this component runs without it. */
|
|
165
|
+
onConversationLoad?: (messages: ChatMessage[], id?: string) => void;
|
|
166
|
+
/** Whether the surrounding chrome that HOSTS this thread is currently
|
|
167
|
+
* VISIBLE to the visitor — e.g. a docked widget's open/closed state.
|
|
168
|
+
* `ChatThread` has no knowledge of whatever hosts it (a `Dock`, a plain
|
|
169
|
+
* page, anything — same boundary `closeConversationsList` documents), so
|
|
170
|
+
* this is the seam: a host that can hide itself sets it, everyone else
|
|
171
|
+
* leaves it unset.
|
|
172
|
+
*
|
|
173
|
+
* Meaningful only when `conversations` is on, where it's the third leg of
|
|
174
|
+
* "seen" (owner round, 2026-08-26 — unread indicators): the active
|
|
175
|
+
* conversation counts as seen, and gets `store.markRead` called for it,
|
|
176
|
+
* only while it's ALSO the active conversation AND the chat view (not the
|
|
177
|
+
* list) is showing AND this is true. Undeclared/`true` (the default) means
|
|
178
|
+
* "always visible" — correct for every layout with no show/hide concept at
|
|
179
|
+
* all (fullscreen/aside/split/custom) and for any widget consumer that
|
|
180
|
+
* doesn't wire it, which just means unread never distinguishes "closed"
|
|
181
|
+
* from "open" for them (a smaller inaccuracy than the alternative: without
|
|
182
|
+
* this leg, a message arriving to the active conversation while the
|
|
183
|
+
* widget is actually closed would get silently marked read behind the
|
|
184
|
+
* visitor's back, purely because it happened to be the active id). */
|
|
185
|
+
hostOpen?: boolean;
|
|
186
|
+
/** Fires whenever "is any OTHER conversation (not the active one) unread"
|
|
187
|
+
* changes — the value this thread already renders as a dot on its own
|
|
188
|
+
* header toggle, reported outward so a sibling control with no view into
|
|
189
|
+
* `ChatThread`'s internal conversation-summary state (a `Dock`'s own
|
|
190
|
+
* `unread` prop, say) can mirror it. Only meaningful with `conversations`
|
|
191
|
+
* on; never fires otherwise. */
|
|
192
|
+
onUnreadChange?: (unread: boolean) => void;
|
|
193
|
+
/** Turns on the widget home screen (Intercom-pattern, H-1/H-2): the panel
|
|
194
|
+
* boots into a `home` view — greeting, most-recent-conversation card, a
|
|
195
|
+
* "new conversation" CTA, and host-defined links — with a Home/Messages
|
|
196
|
+
* tab bar beneath the content area. The prior-conversations list moves
|
|
197
|
+
* from the header toggle onto the Messages tab (H-2); a drilled-into chat
|
|
198
|
+
* (from a list row, the recent card, or "new conversation") hides the tab
|
|
199
|
+
* bar and shows a back arrow in the header instead. Off by default: unset,
|
|
200
|
+
* the widget renders byte-for-byte as it does today. */
|
|
201
|
+
home?: HomeConfig;
|
|
202
|
+
/** Fires when a `home.links` entry with no `href` is activated (an
|
|
203
|
+
* `href`-bearing entry navigates as a real anchor instead — see
|
|
204
|
+
* `HomePanelProps.onLink`). Meaningful only when `home` is set. */
|
|
205
|
+
onHomeLink?: (entry: HomeLinkEntry) => void;
|
|
93
206
|
// ── Composition slots ─────────────────────────────────────────────────────
|
|
94
207
|
// Each flag below is set by the `<kai-chat>` facade when matching light-DOM
|
|
95
208
|
// `slot="…"` content is projected, and gates one composition slot. Two kinds:
|
|
@@ -106,6 +219,17 @@ export interface ChatThreadProps {
|
|
|
106
219
|
sidebar?: boolean;
|
|
107
220
|
/** REPLACE: custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). */
|
|
108
221
|
empty?: boolean;
|
|
222
|
+
/** REPLACE, JSX form: the empty-state content itself, for a caller composing
|
|
223
|
+
* `ChatThread` directly as a Solid component rather than through the `<kai-chat>`
|
|
224
|
+
* shadow-DOM boundary that `empty`/`slot="empty"` targets. Renders INSIDE this
|
|
225
|
+
* component's own tree — so a caller passing the kit's own `<Empty>` composition
|
|
226
|
+
* (`components/empty.tsx`) gets it fully styled by the adopted stylesheet, unlike
|
|
227
|
+
* `slot="empty"`: that slot only ever receives LIGHT-DOM children of the shadow
|
|
228
|
+
* HOST, and light-DOM nodes are outside the shadow root's adopted stylesheets, so
|
|
229
|
+
* Tailwind-utility-class content projected there renders bare. Takes priority
|
|
230
|
+
* over `empty`/`slot="empty"` when both are set — the two are alternate delivery
|
|
231
|
+
* mechanisms for the same region, not additive like `headerEndContent`. */
|
|
232
|
+
emptyContent?: JSX.Element;
|
|
109
233
|
/** REPLACE: full custom composer in place of the built-in prompt input. The
|
|
110
234
|
* projected content wires its own submit (the data-flow boundary). */
|
|
111
235
|
composer?: boolean;
|
|
@@ -120,6 +244,10 @@ export interface ChatThreadProps {
|
|
|
120
244
|
accept?: MediaTypeFilter;
|
|
121
245
|
/** Files the composer refused because `accept` excluded them. */
|
|
122
246
|
onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
|
|
247
|
+
/** When `false`, hides the built-in paperclip attach button. Defaults to
|
|
248
|
+
* `true` (undeclared keeps today's behavior: attach visible), matching
|
|
249
|
+
* `DefaultPromptInput`'s own default: only an explicit `false` hides it. */
|
|
250
|
+
attach?: boolean;
|
|
123
251
|
/** Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. */
|
|
124
252
|
webSearch?: boolean;
|
|
125
253
|
/** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
|
|
@@ -154,6 +282,20 @@ export interface ChatThreadController {
|
|
|
154
282
|
clear(): void;
|
|
155
283
|
send(): void;
|
|
156
284
|
scrollToBottom(behavior?: ScrollBehavior): void;
|
|
285
|
+
/** Force the widget back to its default landing view — `'home'` when the
|
|
286
|
+
* `home` prop is set (H-5), `'chat'` otherwise (a no-op if already there,
|
|
287
|
+
* or if neither `home` nor `conversations` is on). ChatThread has no knowledge of
|
|
288
|
+
* whatever chrome hosts it — a `Dock`, a plain page, anything — so it
|
|
289
|
+
* cannot know when that host closes. The seam is this one imperative
|
|
290
|
+
* call: a host that can go from visible to hidden and back (the `kai-dock`
|
|
291
|
+
* widget being the motivating case) calls it on every hide, so the NEXT
|
|
292
|
+
* open always lands on the chat view rather than wherever the list was
|
|
293
|
+
* left (owner: reopening the widget should show the default screen, not
|
|
294
|
+
* a stale list view). `Dock`'s own `onOpenChange` already fires on every
|
|
295
|
+
* close path — header X, the launcher toggle, and Escape — so a single
|
|
296
|
+
* `onOpenChange={(open) => !open && controller.closeConversationsList()}`
|
|
297
|
+
* at the call site covers all three with no per-path wiring. */
|
|
298
|
+
closeConversationsList(): void;
|
|
157
299
|
}
|
|
158
300
|
|
|
159
301
|
/**
|
|
@@ -233,6 +375,288 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
233
375
|
});
|
|
234
376
|
const [internal, setInternal] = createSignal<string | ComposerDoc>(props.value ?? '');
|
|
235
377
|
const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
|
|
378
|
+
// ── Conversations (C-1..C-9) ────────────────────────────────────────────
|
|
379
|
+
// ── Home screen (H-1..H-6, Task 3) ──────────────────────────────────────
|
|
380
|
+
// `home` set adds a THIRD view (the list moves from the header toggle onto
|
|
381
|
+
// the Messages tab — H-2) and boots into it instead of 'chat'. `chatEntry`
|
|
382
|
+
// tracks whether the current 'chat' view is a DRILLED chat (entered from
|
|
383
|
+
// home/list — hides the tab bar, shows a back arrow) or a ROOT chat (the
|
|
384
|
+
// Messages tab with conversations off — tab bar stays, no back arrow).
|
|
385
|
+
const homeEnabled = () => props.home != null;
|
|
386
|
+
type WidgetView = 'home' | 'chat' | 'list';
|
|
387
|
+
const [view, setView] = createSignal<WidgetView>(props.home != null ? 'home' : 'chat');
|
|
388
|
+
const [chatEntry, setChatEntry] = createSignal<'home' | 'list' | null>(null);
|
|
389
|
+
const tabBarVisible = () => homeEnabled() && (view() !== 'chat' || chatEntry() === null);
|
|
390
|
+
const activeTab = (): 'home' | 'messages' => (view() === 'home' ? 'home' : 'messages');
|
|
391
|
+
// The initial `view` signal value freezes the home-landing decision at
|
|
392
|
+
// MOUNT — but the `kai-` contract has consumers set object props (like
|
|
393
|
+
// `home`) as JS properties AFTER the element is appended/upgraded (the
|
|
394
|
+
// React wrapper's `useLayoutEffect` runs post-mount by construction), so
|
|
395
|
+
// `props.home` is routinely still `undefined` on this component's first
|
|
396
|
+
// render. Without this, `<kai-chat>` then `el.home = {...}` renders a
|
|
397
|
+
// tab bar (props.home is set by the time JSX reads it reactively) but
|
|
398
|
+
// stays on `view() === 'chat'` forever — H-5 violated on a primary
|
|
399
|
+
// consumer path. React to the RISING edge instead of relying on the
|
|
400
|
+
// initial value: `home` turning on from off lands on 'home', but only
|
|
401
|
+
// from the untouched default ('chat' with no drilled entry) — a late
|
|
402
|
+
// toggle must never yank a visitor OUT of a chat they're already in
|
|
403
|
+
// (root or drilled). `{ defer: true }` skips the run this effect's own
|
|
404
|
+
// creation would otherwise trigger.
|
|
405
|
+
// Falling edge (symmetric): `home` turning OFF while sitting on the
|
|
406
|
+
// 'home' view leaves a default-configured HomePanel with no tab bar to
|
|
407
|
+
// navigate away from — reset to 'chat', the only view a home-less widget
|
|
408
|
+
// has.
|
|
409
|
+
//
|
|
410
|
+
// NOT `on(homeEnabled, fn, { defer: true })`: Solid's `on()` skips calling
|
|
411
|
+
// `fn` on its first real invocation but does NOT capture the deferred
|
|
412
|
+
// read as `prevInput` — the first non-deferred call always sees `prev ===
|
|
413
|
+
// undefined` (confirmed against `solid-js/dist/solid.js`'s `on()`, not
|
|
414
|
+
// just inferred), so `!isOn && wasOn` can never be true and the falling
|
|
415
|
+
// edge silently never fires. Track the previous value by hand instead: a
|
|
416
|
+
// closure variable seeded with `homeEnabled()`'s value BEFORE the
|
|
417
|
+
// computed exists (so the computed's own first run sees `isOn === wasOn`
|
|
418
|
+
// and no-ops, the same "skip the initial run" behavior `defer` was for)
|
|
419
|
+
// and updated at the end of every run.
|
|
420
|
+
let wasHomeEnabled = untrack(homeEnabled);
|
|
421
|
+
createComputed(() => {
|
|
422
|
+
const isOn = homeEnabled();
|
|
423
|
+
const wasOn = wasHomeEnabled;
|
|
424
|
+
if (isOn !== wasOn) {
|
|
425
|
+
if (isOn && view() === 'chat' && chatEntry() === null) setView('home');
|
|
426
|
+
else if (!isOn && view() === 'home') setView('chat');
|
|
427
|
+
}
|
|
428
|
+
wasHomeEnabled = isOn;
|
|
429
|
+
});
|
|
430
|
+
const [conversationSummaries, setConversationSummaries] = createSignal<ConversationSummary[]>([]);
|
|
431
|
+
const [activeConversationId, setActiveConversationId] = createSignal<string | undefined>(undefined);
|
|
432
|
+
const conversationsReady = () => props.conversations === true && props.store != null && props.onConversationLoad != null;
|
|
433
|
+
// Set right before a load/restore hands its messages to the caller via
|
|
434
|
+
// `onConversationLoad`, and consumed (read-then-cleared) by the very next
|
|
435
|
+
// run of the save effect below. Plain closure variable, not a signal: it
|
|
436
|
+
// gates a single effect run rather than driving any render, so it needs no
|
|
437
|
+
// reactivity of its own. See the save effect's own comment for why this
|
|
438
|
+
// exists (IMPORTANT-1, 2026-08-26 final review).
|
|
439
|
+
let suppressNextSave = false;
|
|
440
|
+
|
|
441
|
+
onMount(() => {
|
|
442
|
+
if (props.conversations && !props.store) {
|
|
443
|
+
console.error('ChatThread: `conversations` is true but no `store` was provided — the conversations feature needs a ConversationStore to persist to. Staying in chat-only mode.');
|
|
444
|
+
} else if (props.conversations && props.store && !props.onConversationLoad) {
|
|
445
|
+
console.error('ChatThread: `conversations` is true but no `onConversationLoad` handler was provided — row-select, "new conversation", and mount auto-restore would have nowhere to deliver the loaded messages, leaving row-tap/new/restore inert (and mount\'s auto-restore would still stamp an active conversation id the save effect could then clobber). Staying in chat-only mode.');
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const refreshConversations = async () => {
|
|
450
|
+
if (!conversationsReady()) return;
|
|
451
|
+
try {
|
|
452
|
+
// Fresh array reference every refresh (reactivity contract) — the
|
|
453
|
+
// adapter already returns a new array per call, so no extra clone is
|
|
454
|
+
// needed here; the signal write is what notifies <ConversationPanel>.
|
|
455
|
+
setConversationSummaries(await props.store!.list());
|
|
456
|
+
} catch (err) {
|
|
457
|
+
// Note this can run in the BACKGROUND, not just from `openList()`: the
|
|
458
|
+
// save effect below calls this after every save() to keep the badge
|
|
459
|
+
// cache fresh. Only bail OUT of the list view here — a transient
|
|
460
|
+
// list() blip mid-drilled-chat (or on the root/home views) must stay a
|
|
461
|
+
// harmless no-op, not teleport the visitor off whatever they're doing;
|
|
462
|
+
// the list view itself has nothing to show without a summaries array,
|
|
463
|
+
// so THAT is the one view this failure has to evacuate.
|
|
464
|
+
console.warn('ChatThread: conversations list() failed; staying in chat-only mode.', err);
|
|
465
|
+
if (view() === 'list') {
|
|
466
|
+
setChatEntry(null);
|
|
467
|
+
setView(homeEnabled() ? 'home' : 'chat');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const openList = () => { setView('list'); void refreshConversations(); };
|
|
473
|
+
// Messages-tab entry point (H-2): the list moved off the header toggle onto
|
|
474
|
+
// this tab, so it routes through 'list' when the store is wired and 'chat'
|
|
475
|
+
// (the root chat, ambiguity 1 — no back arrow, `chatEntry` stays null)
|
|
476
|
+
// otherwise.
|
|
477
|
+
const openMessagesTab = () => {
|
|
478
|
+
setChatEntry(null);
|
|
479
|
+
if (conversationsReady()) { setView('list'); void refreshConversations(); }
|
|
480
|
+
else setView('chat');
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const selectConversation = async (id: string) => {
|
|
484
|
+
if (!conversationsReady()) return;
|
|
485
|
+
try {
|
|
486
|
+
const messages = await props.store!.load(id);
|
|
487
|
+
setActiveConversationId(id);
|
|
488
|
+
setView('chat');
|
|
489
|
+
// IMPORTANT-1 (2026-08-26 final review): the caller is about to hand
|
|
490
|
+
// this same content straight back in as a new `props.messages`
|
|
491
|
+
// reference (the reactivity contract requires a fresh array on every
|
|
492
|
+
// change, load included). Without this flag the save effect below
|
|
493
|
+
// reads that as "the thread changed," stamps a fresh `updatedAt`, and
|
|
494
|
+
// calls `store.save()` — phantom-unreading a fully-read conversation
|
|
495
|
+
// on every reload and, for `fetchStore`, firing a full-thread PUT on
|
|
496
|
+
// every load for no actual change. Set BEFORE the callback so it's
|
|
497
|
+
// armed no matter how synchronously the caller re-renders.
|
|
498
|
+
suppressNextSave = true;
|
|
499
|
+
// A genuinely new array (never a mutated one) — the reactivity
|
|
500
|
+
// contract's array-reference rule, satisfied at the caller boundary.
|
|
501
|
+
props.onConversationLoad?.([...messages], id);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.warn(`ChatThread: conversations load(${id}) failed.`, err);
|
|
504
|
+
// Defensive: a failed load must not leave a stale drilled-chat
|
|
505
|
+
// back-target around from whatever set it before calling this.
|
|
506
|
+
setChatEntry(null);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const startNewConversation = () => {
|
|
511
|
+
// C-6: lazy id — no id, no save, until the first message. Clearing the
|
|
512
|
+
// active id and the message list (via onConversationLoad) is enough; the
|
|
513
|
+
// id itself is minted the first time the save-on-change effect below
|
|
514
|
+
// sees a non-empty thread with no active id. No `suppressNextSave` here:
|
|
515
|
+
// an empty thread already short-circuits the save effect on its own
|
|
516
|
+
// (`messages.length === 0`), so there is nothing to suppress.
|
|
517
|
+
setActiveConversationId(undefined);
|
|
518
|
+
setView('chat');
|
|
519
|
+
props.onConversationLoad?.([], undefined);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
createEffect(() => {
|
|
523
|
+
if (!conversationsReady()) return;
|
|
524
|
+
const messages = props.messages;
|
|
525
|
+
if (messages.length === 0) return; // C-6: nothing persists until the first message
|
|
526
|
+
// IMPORTANT-1: skip exactly the one save that is this same effect firing
|
|
527
|
+
// on the `props.messages` bounce a load/restore itself caused — see
|
|
528
|
+
// `suppressNextSave`'s own comment at `selectConversation`. Consumed
|
|
529
|
+
// (cleared) unconditionally so only that one run is skipped; a genuine
|
|
530
|
+
// edit right after a load still saves normally.
|
|
531
|
+
if (suppressNextSave) { suppressNextSave = false; return; }
|
|
532
|
+
// `untrack` the read: this effect also WRITES activeConversationId (minting
|
|
533
|
+
// the lazy id below), so a tracked read makes the effect depend on a signal
|
|
534
|
+
// it sets itself — the write re-triggers the same run and store.save() fires
|
|
535
|
+
// twice with identical args for a conversation's first message. Reading it
|
|
536
|
+
// untracked keeps the dependency set to `conversationsReady`/`props.messages`
|
|
537
|
+
// only; the mint-then-set below still updates the signal (for the toggle
|
|
538
|
+
// button/list UI) without feeding back into this effect's own re-run.
|
|
539
|
+
let id = untrack(activeConversationId);
|
|
540
|
+
if (!id) {
|
|
541
|
+
id = crypto.randomUUID();
|
|
542
|
+
setActiveConversationId(id);
|
|
543
|
+
}
|
|
544
|
+
props.store!.save(id, messages)
|
|
545
|
+
// Re-fetch the summary list after every save (not just on mount/list-open):
|
|
546
|
+
// `anyUnread`/the badge read `conversationSummaries()`, a CACHE that
|
|
547
|
+
// otherwise only refreshes on those two paths, so a message arriving on
|
|
548
|
+
// the active conversation while the host is closed (hostOpen === false —
|
|
549
|
+
// `seenNow` below is false, so `markRead` correctly does NOT fire) would
|
|
550
|
+
// update the persisted record's `updatedAt` but leave the cached summary
|
|
551
|
+
// stale, and the badge would never appear until the visitor happened to
|
|
552
|
+
// open the list. `refreshConversations` already no-ops safely if this
|
|
553
|
+
// component unmounts mid-flight (`conversationsReady` guard) and handles
|
|
554
|
+
// its own list() failure (console.warn, degrades to chat-only) — no new
|
|
555
|
+
// error path here.
|
|
556
|
+
.then(() => refreshConversations())
|
|
557
|
+
.catch((err) => {
|
|
558
|
+
// Decide loudly (spec's degradation section): the thread stays usable,
|
|
559
|
+
// the failure is surfaced, never a silent no-op.
|
|
560
|
+
console.error(`ChatThread: conversations save(${id}) failed; the conversation is not persisted for this change.`, err);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// "Seen" (owner round, 2026-08-26, revised in the follow-up review): the
|
|
565
|
+
// active conversation, AND the chat view (not the list) is showing, AND
|
|
566
|
+
// the host is open (`hostOpen`, defaulting true — see that prop's doc).
|
|
567
|
+
// ONE memo, shared by both consumers below, so they cannot drift: the
|
|
568
|
+
// markRead effect gates on it directly, and `anyUnread` uses it to decide
|
|
569
|
+
// whether the active conversation is still exempt from the "unread"
|
|
570
|
+
// badge. Before this fix the two were separately-written conditions that
|
|
571
|
+
// happened to look alike; `anyUnread` excluded the active id
|
|
572
|
+
// UNCONDITIONALLY, so a message arriving on the active conversation while
|
|
573
|
+
// the host was CLOSED (hostOpen === false) — the canonical "agent replied
|
|
574
|
+
// while your box was shut" case — was dropped from the badge too, even
|
|
575
|
+
// though it was never marked read (markRead's own gate correctly skips it
|
|
576
|
+
// while closed). The active id is only a legitimate exclusion when it is
|
|
577
|
+
// ALSO currently being marked seen.
|
|
578
|
+
const seenNow = createMemo(
|
|
579
|
+
() => activeConversationId() !== undefined && view() === 'chat' && props.hostOpen !== false,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Every dependency read here is TRACKED — unlike the save effect above,
|
|
583
|
+
// nothing in this effect writes any of them, so there's no feedback loop
|
|
584
|
+
// to guard against with `untrack`. Re-running on every `props.messages`
|
|
585
|
+
// change is exactly "on new messages arriving while seen holds";
|
|
586
|
+
// re-running on `activeConversationId`/`view`/`hostOpen` changes is "on
|
|
587
|
+
// select/restore/host-visibility-change" (the first two resolve through
|
|
588
|
+
// `selectConversation`, which sets both).
|
|
589
|
+
createEffect(() => {
|
|
590
|
+
if (!conversationsReady()) return;
|
|
591
|
+
const id = activeConversationId();
|
|
592
|
+
const seen = seenNow();
|
|
593
|
+
void props.messages; // tracked on purpose: see the comment above
|
|
594
|
+
if (!seen || id === undefined) return;
|
|
595
|
+
props.store!.markRead?.(id).catch((err) => {
|
|
596
|
+
// Decide loudly, matching the save effect above — this degrades to
|
|
597
|
+
// "unread never clears for this conversation," not a crash.
|
|
598
|
+
console.error(`ChatThread: conversations markRead(${id}) failed.`, err);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// The header toggle's own dot (rendered below) AND the value reported
|
|
603
|
+
// outward via onUnreadChange are the SAME computation — a Dock (or any
|
|
604
|
+
// other sibling control with no view into this thread's internal
|
|
605
|
+
// conversation-summary state) mirrors it through the callback rather than
|
|
606
|
+
// reaching in. Excludes the active conversation by id — but ONLY while
|
|
607
|
+
// `seenNow()` holds. When it doesn't (list view open, or the host is
|
|
608
|
+
// closed), the active conversation is exactly as unread-eligible as any
|
|
609
|
+
// other row: nothing has marked it seen, so a message landing on it must
|
|
610
|
+
// still surface here. `undefined` never matches a real conversation id, so
|
|
611
|
+
// the exclusion is simply inert (matches nothing) while not seen, rather
|
|
612
|
+
// than needing a second branch. Also excludes the active id (not by its
|
|
613
|
+
// possibly-stale lastReadAt) so switching `activeConversationId` — e.g.
|
|
614
|
+
// selecting a previously-unread row — clears that row's contribution to
|
|
615
|
+
// this badge immediately, without waiting on a fresh list() round-trip.
|
|
616
|
+
const anyUnread = createMemo(() =>
|
|
617
|
+
conversationSummaries().some(
|
|
618
|
+
(c) => c.id !== (seenNow() ? activeConversationId() : undefined) && isConversationUnread(c),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
createEffect(() => props.onUnreadChange?.(anyUnread()));
|
|
622
|
+
|
|
623
|
+
// Visitor continuity (C-7's whole point): a plain-history construct
|
|
624
|
+
// auto-restored the visitor's thread on mount, so upgrading to
|
|
625
|
+
// `conversations` must not regress that — their most recent conversation
|
|
626
|
+
// (migrated legacy thread included) has to reappear without an extra tap
|
|
627
|
+
// into the list. Guarded three ways: only when `list()` actually returned
|
|
628
|
+
// something (a failed list() already degrades to chat-only inside
|
|
629
|
+
// `refreshConversations`, and an empty store is a brand-new visitor who
|
|
630
|
+
// gets the empty-chat welcome screen); only when nothing is active yet
|
|
631
|
+
// (never fights `startNewConversation`/a prior `selectConversation`); and
|
|
632
|
+
// only when `props.messages` is still empty (a parent that seeded its own
|
|
633
|
+
// thread owns that choice — never clobber it). Reuses `selectConversation`
|
|
634
|
+
// so the fresh-array/`onConversationLoad` contract is the same single path
|
|
635
|
+
// as an explicit row click, and stays in `view() === 'chat'` throughout
|
|
636
|
+
// (this never opens the list). One more guard: `view()` is re-checked
|
|
637
|
+
// right before selecting, still 'chat' — mount's own list() and an
|
|
638
|
+
// impatient caller's own `openList()`/manual navigation racing it (both
|
|
639
|
+
// just microtask chains over the same synchronous localStorage) can
|
|
640
|
+
// interleave, and `selectConversation` unconditionally snaps `view` back
|
|
641
|
+
// to 'chat'; without this a visitor who opened the list before this
|
|
642
|
+
// resolved would get yanked back out of it.
|
|
643
|
+
onMount(() => {
|
|
644
|
+
if (!conversationsReady()) return;
|
|
645
|
+
void (async () => {
|
|
646
|
+
await refreshConversations();
|
|
647
|
+
if (untrack(activeConversationId) !== undefined) return;
|
|
648
|
+
if (props.messages.length !== 0) return;
|
|
649
|
+
if (view() !== 'chat') return;
|
|
650
|
+
const summaries = conversationSummaries();
|
|
651
|
+
if (summaries.length === 0) return;
|
|
652
|
+
// Most-recently-updated first. `list()`'s own ordering isn't a
|
|
653
|
+
// contract (`localStorageStore` returns insertion order, not recency),
|
|
654
|
+
// so sort defensively by `updatedAt` rather than trusting index [0];
|
|
655
|
+
// an unparsable/missing date sorts last rather than throwing.
|
|
656
|
+
const newest = [...summaries].sort(byRecency)[0];
|
|
657
|
+
await selectConversation(newest.id);
|
|
658
|
+
})();
|
|
659
|
+
});
|
|
236
660
|
// A string `value` is controlled; a ComposerDoc `value` is a one-time seed that
|
|
237
661
|
// lives in `internal` so the user's (string) edits replace it without a fight.
|
|
238
662
|
const current = (): string | ComposerDoc =>
|
|
@@ -252,7 +676,23 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
252
676
|
if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
|
|
253
677
|
else { props.onSubmit?.({ value: v, attachments: attachments() }); afterSubmit(); }
|
|
254
678
|
};
|
|
255
|
-
const showHeader = () => !!(
|
|
679
|
+
const showHeader = () => !!(
|
|
680
|
+
props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd
|
|
681
|
+
|| props.headerEndContent || conversationsReady()
|
|
682
|
+
// The back arrow on a drilled chat needs the header row even with no
|
|
683
|
+
// title/models/context/store — a home-only construct (no `conversations`)
|
|
684
|
+
// whose "new conversation" card drills into chat still needs somewhere
|
|
685
|
+
// to put it.
|
|
686
|
+
|| (homeEnabled() && view() === 'chat' && chatEntry() !== null)
|
|
687
|
+
);
|
|
688
|
+
// Recent-conversation card (H-1): only when explicitly opted into
|
|
689
|
+
// (`home.recentConversation === true`), summaries are actually hydrated,
|
|
690
|
+
// and at least one exists — the newest by the shared recency rule (#335).
|
|
691
|
+
const recentSummary = createMemo(() => {
|
|
692
|
+
if (!homeEnabled() || props.home?.recentConversation !== true || !conversationsReady()) return undefined;
|
|
693
|
+
const summaries = conversationSummaries();
|
|
694
|
+
return summaries.length ? [...summaries].sort(byRecency)[0] : undefined;
|
|
695
|
+
});
|
|
256
696
|
// Suggestions are conversation starters: show only on an empty thread unless
|
|
257
697
|
// the host opts into persisting them.
|
|
258
698
|
const visibleSuggestions = () =>
|
|
@@ -272,6 +712,7 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
272
712
|
const vp = rootEl?.querySelector<HTMLElement>('.overflow-y-auto');
|
|
273
713
|
vp?.scrollTo({ top: vp.scrollHeight, behavior: behavior ?? 'smooth' });
|
|
274
714
|
},
|
|
715
|
+
closeConversationsList: () => { setChatEntry(null); setView(homeEnabled() ? 'home' : 'chat'); },
|
|
275
716
|
});
|
|
276
717
|
});
|
|
277
718
|
|
|
@@ -325,9 +766,71 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
325
766
|
</ContextContent>
|
|
326
767
|
</Context>
|
|
327
768
|
</Show>
|
|
769
|
+
{/* A chat-bubble glyph, not the "menu that does nothing" the retrofit
|
|
770
|
+
shipped (owner feedback: it "looks like nothing and isn't even
|
|
771
|
+
the same size as the X"). Same `Button variant="ghost"
|
|
772
|
+
size="icon-sm"` component AND the same 24px icon size as the
|
|
773
|
+
widget's own close X (`codegen.ts`'s `headerEndContent` button,
|
|
774
|
+
which renders `DockCloseGlyph` = `<X size={24} />`) — identical
|
|
775
|
+
hit-area and optical weight, so the two read as siblings.
|
|
776
|
+
Swaps to a back arrow while the list is open, returning to chat. */}
|
|
777
|
+
{/* Back arrow for a DRILLED chat (H-5): entered from home, the
|
|
778
|
+
recent card, or a list row/new-conversation pill while `home`
|
|
779
|
+
is set. Returns to whichever surface it was entered from. */}
|
|
780
|
+
<Show when={homeEnabled() && view() === 'chat' && chatEntry() !== null}>
|
|
781
|
+
<Button
|
|
782
|
+
variant="ghost"
|
|
783
|
+
size="icon-sm"
|
|
784
|
+
data-kai-home-back
|
|
785
|
+
aria-label="Back"
|
|
786
|
+
onClick={() => { const target = chatEntry()!; setChatEntry(null); setView(target); }}
|
|
787
|
+
>
|
|
788
|
+
<ArrowLeft size={24} aria-hidden="true" />
|
|
789
|
+
</Button>
|
|
790
|
+
</Show>
|
|
791
|
+
{/* H-2: with `home` set, the prior-conversations list moved off this
|
|
792
|
+
header toggle onto the Messages tab — the toggle no longer renders
|
|
793
|
+
at all (H-3). */}
|
|
794
|
+
<Show when={props.conversations && props.store && !homeEnabled()}>
|
|
795
|
+
<Button
|
|
796
|
+
variant="ghost"
|
|
797
|
+
size="icon-sm"
|
|
798
|
+
class="relative"
|
|
799
|
+
data-kai-conversations-toggle
|
|
800
|
+
// `aria-label` WINS over any descendant text for a button's
|
|
801
|
+
// accessible name, so "unread" has to be folded in here rather
|
|
802
|
+
// than in a sr-only sibling span (which AT would never reach).
|
|
803
|
+
aria-label={
|
|
804
|
+
view() === 'list'
|
|
805
|
+
? 'Back to chat'
|
|
806
|
+
: anyUnread() && view() === 'chat'
|
|
807
|
+
? 'Conversations (unread)'
|
|
808
|
+
: 'Conversations'
|
|
809
|
+
}
|
|
810
|
+
onClick={() => (view() === 'list' ? setView('chat') : openList())}
|
|
811
|
+
>
|
|
812
|
+
<Show when={view() === 'list'} fallback={<MessagesSquare size={24} aria-hidden="true" />}>
|
|
813
|
+
<ArrowLeft size={24} aria-hidden="true" />
|
|
814
|
+
</Show>
|
|
815
|
+
{/* Unread badge (owner round, 2026-08-26): ANY conversation other
|
|
816
|
+
than the active one is unread. Only over the chat-bubble glyph
|
|
817
|
+
— once the list is open showing the back arrow, the visitor is
|
|
818
|
+
already looking at the rows themselves, each carrying its own
|
|
819
|
+
dot (ConversationPanel), so a second badge on the arrow would be
|
|
820
|
+
redundant chrome. */}
|
|
821
|
+
<Show when={anyUnread() && view() === 'chat'}>
|
|
822
|
+
<span data-kai-conversations-unread aria-hidden="true" class="absolute right-0.5 top-0.5 size-1.5 rounded-full bg-unread" />
|
|
823
|
+
</Show>
|
|
824
|
+
</Button>
|
|
825
|
+
</Show>
|
|
328
826
|
{/* Consumer-injected trailing controls (share, settings, …).
|
|
329
827
|
Projects light-DOM `slot="header-end"` children of <kai-chat>. */}
|
|
330
828
|
<slot name="header-end" />
|
|
829
|
+
{/* JSX escape hatch for a Solid-composed caller with no shadow-DOM
|
|
830
|
+
host to slot into (see the prop doc) — a docked widget's own
|
|
831
|
+
close affordance is the motivating case, sharing this row with
|
|
832
|
+
the title instead of floating as an unrelated second control. */}
|
|
833
|
+
{props.headerEndContent}
|
|
331
834
|
</div>
|
|
332
835
|
</header>
|
|
333
836
|
</Show>
|
|
@@ -336,13 +839,19 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
336
839
|
<header part="header" class="shrink-0"><slot name="header" /></header>
|
|
337
840
|
</Show>
|
|
338
841
|
<div class="relative flex-1 overflow-hidden">
|
|
339
|
-
<
|
|
842
|
+
<Switch
|
|
843
|
+
fallback={
|
|
844
|
+
<ChatContainer class="h-full px-4 py-3">
|
|
340
845
|
<ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
|
|
341
|
-
{/* REPLACE — custom empty-state
|
|
846
|
+
{/* REPLACE — custom empty-state content, shown only while the thread is
|
|
342
847
|
empty. The component still owns WHEN it shows (data state); the
|
|
343
|
-
consumer owns WHAT it looks like.
|
|
344
|
-
|
|
345
|
-
|
|
848
|
+
consumer owns WHAT it looks like. `emptyContent` (JSX, rendered
|
|
849
|
+
in-tree and fully styled) wins over `empty`/`slot="empty"`
|
|
850
|
+
(light-DOM projection) when both are set — see the prop doc. */}
|
|
851
|
+
<Show when={(props.empty || props.emptyContent) && props.messages.length === 0}>
|
|
852
|
+
<Show when={props.emptyContent} fallback={<slot name="empty" />}>
|
|
853
|
+
{props.emptyContent}
|
|
854
|
+
</Show>
|
|
346
855
|
</Show>
|
|
347
856
|
{/* Keyed by message id (see the note above this component), so a
|
|
348
857
|
streaming delta updates the row instead of replacing it. */}
|
|
@@ -361,11 +870,17 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
361
870
|
parts={m().parts}
|
|
362
871
|
/* F-21: streaming-ness = the thread's ONE existing
|
|
363
872
|
loading signal + being the last message and an
|
|
364
|
-
assistant turn. No second streaming source
|
|
365
|
-
reasoning disclosure auto-opens on it
|
|
873
|
+
assistant turn. No second streaming source. The
|
|
874
|
+
reasoning disclosure no longer auto-opens on it
|
|
875
|
+
by default (Task 19f, owner ruling 2026-08-26) —
|
|
876
|
+
only the trigger's shimmer reflects streaming
|
|
877
|
+
unless `reasoningOpen` opts back in. */
|
|
366
878
|
isStreaming={props.loading === true && m().role === 'assistant' && i() === props.messages.length - 1}
|
|
879
|
+
reasoningMode={props.reasoning}
|
|
880
|
+
reasoningDefaultOpen={props.reasoningOpen}
|
|
367
881
|
cardTypes={props.cardTypes}
|
|
368
882
|
cardSchemas={props.cardSchemas}
|
|
883
|
+
cardHostElement={props.cardHostElement}
|
|
369
884
|
isUser={m().role === 'user'}
|
|
370
885
|
markdown={m().role === 'assistant'}
|
|
371
886
|
actions={m().actions}
|
|
@@ -408,45 +923,89 @@ export function ChatThread(props: ChatThreadProps) {
|
|
|
408
923
|
</ChatContainerContent>
|
|
409
924
|
<Show when={showScrollButton()}>
|
|
410
925
|
<div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
|
|
411
|
-
|
|
926
|
+
{/* The button now owns its elevation (kai-elevation); a `shadow-sm`
|
|
927
|
+
here would set box-shadow a second time and the winner would
|
|
928
|
+
be stylesheet order, not this call site. */}
|
|
929
|
+
<ScrollButton />
|
|
412
930
|
</div>
|
|
413
931
|
</Show>
|
|
414
|
-
|
|
932
|
+
</ChatContainer>
|
|
933
|
+
}
|
|
934
|
+
>
|
|
935
|
+
<Match when={view() === 'home'}>
|
|
936
|
+
<HomePanel
|
|
937
|
+
greeting={props.home?.greeting}
|
|
938
|
+
recent={recentSummary()}
|
|
939
|
+
newChatLabel={props.home?.newConversation?.label}
|
|
940
|
+
links={props.home?.links}
|
|
941
|
+
onSelectRecent={(id) => { setChatEntry('home'); void selectConversation(id); }}
|
|
942
|
+
onNewChat={() => { setChatEntry('home'); startNewConversation(); }}
|
|
943
|
+
onLink={(entry) => props.onHomeLink?.(entry)}
|
|
944
|
+
/>
|
|
945
|
+
</Match>
|
|
946
|
+
<Match when={view() === 'list' && conversationsReady()}>
|
|
947
|
+
<ConversationPanel
|
|
948
|
+
conversations={conversationSummaries()}
|
|
949
|
+
activeId={activeConversationId()}
|
|
950
|
+
onSelect={(id) => { setChatEntry(homeEnabled() ? 'list' : null); void selectConversation(id); }}
|
|
951
|
+
onNewChat={() => { setChatEntry(homeEnabled() ? 'list' : null); startNewConversation(); }}
|
|
952
|
+
/>
|
|
953
|
+
</Match>
|
|
954
|
+
</Switch>
|
|
415
955
|
</div>
|
|
416
|
-
{/*
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
956
|
+
{/* The list view TAKES OVER the full content area (owner: "if we are
|
|
957
|
+
looking at the conversations, i don't think i would see the
|
|
958
|
+
suggestions nor the prompt input... the convo list would be taking
|
|
959
|
+
over the full content area"). The retrofit only swapped the thread;
|
|
960
|
+
this hides the composer-actions row, the composer itself and the
|
|
961
|
+
footer too — nothing below the header renders except the panel. */}
|
|
962
|
+
<Show when={view() === 'chat'}>
|
|
963
|
+
{/* INJECT — accessory row above the composer (extra actions/toolbar). */}
|
|
964
|
+
<Show when={props.composerActions}>
|
|
965
|
+
<div class="shrink-0 px-4">
|
|
966
|
+
<div class="mx-auto flex max-w-3xl items-center gap-2 pb-2"><slot name="composer-actions" /></div>
|
|
967
|
+
</div>
|
|
968
|
+
</Show>
|
|
969
|
+
<div class="shrink-0 px-4 pb-4">
|
|
970
|
+
<div class="mx-auto max-w-3xl">
|
|
971
|
+
{/* REPLACE — a full `composer` slot stands in for the built-in input.
|
|
972
|
+
The slotted content owns its own submit/loading wiring. */}
|
|
973
|
+
<Show
|
|
974
|
+
when={props.composer}
|
|
975
|
+
fallback={
|
|
976
|
+
<DefaultPromptInput
|
|
977
|
+
value={current()} placeholder={props.placeholder} loading={props.loading === true}
|
|
978
|
+
suggestions={visibleSuggestions()} attachments={attachments()}
|
|
979
|
+
accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
|
|
980
|
+
attach={props.attach} webSearch={props.webSearch === true} voice={props.voice === true}
|
|
981
|
+
triggers={props.triggers} kindIcons={props.kindIcons}
|
|
982
|
+
onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
|
|
983
|
+
onAttachmentsChange={(a) => { setAttachments(a); props.onAttachmentsChange?.(a); }}
|
|
984
|
+
onWebSearch={() => props.onWebSearch?.()} onVoice={() => props.onVoice?.()}
|
|
985
|
+
/>
|
|
986
|
+
}
|
|
987
|
+
>
|
|
988
|
+
<slot name="composer" />
|
|
989
|
+
</Show>
|
|
990
|
+
</div>
|
|
420
991
|
</div>
|
|
992
|
+
{/* INJECT: footer row below the composer. */}
|
|
993
|
+
<Show when={props.footer}>
|
|
994
|
+
<div part="footer" class="shrink-0 px-4 pb-3">
|
|
995
|
+
<div class="mx-auto max-w-3xl text-center text-xs text-muted-foreground"><slot name="footer" /></div>
|
|
996
|
+
</div>
|
|
997
|
+
</Show>
|
|
421
998
|
</Show>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
|
|
433
|
-
webSearch={props.webSearch === true} voice={props.voice === true}
|
|
434
|
-
triggers={props.triggers} kindIcons={props.kindIcons}
|
|
435
|
-
onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
|
|
436
|
-
onAttachmentsChange={(a) => { setAttachments(a); props.onAttachmentsChange?.(a); }}
|
|
437
|
-
onWebSearch={() => props.onWebSearch?.()} onVoice={() => props.onVoice?.()}
|
|
438
|
-
/>
|
|
439
|
-
}
|
|
440
|
-
>
|
|
441
|
-
<slot name="composer" />
|
|
442
|
-
</Show>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
{/* INJECT: footer row below the composer. */}
|
|
446
|
-
<Show when={props.footer}>
|
|
447
|
-
<div part="footer" class="shrink-0 px-4 pb-3">
|
|
448
|
-
<div class="mx-auto max-w-3xl text-center text-xs text-muted-foreground"><slot name="footer" /></div>
|
|
449
|
-
</div>
|
|
999
|
+
{/* Home/Messages tab bar (H-2/H-6): shown on the 'home' and 'list'
|
|
1000
|
+
views and on the ROOT chat (Messages tab, conversations off —
|
|
1001
|
+
ambiguity 1), hidden on a DRILLED chat so the back arrow above
|
|
1002
|
+
is the only way back. */}
|
|
1003
|
+
<Show when={tabBarVisible()}>
|
|
1004
|
+
<WidgetTabBar
|
|
1005
|
+
active={activeTab()}
|
|
1006
|
+
onChange={(tab) => (tab === 'home' ? setView('home') : openMessagesTab())}
|
|
1007
|
+
unread={anyUnread()}
|
|
1008
|
+
/>
|
|
450
1009
|
</Show>
|
|
451
1010
|
</div>
|
|
452
1011
|
</div>
|